feat(bot): 增加图片处理功能并优化日志系统

- 新增 textToImage 和 imageToText 功能,实现文本与图片的相互转换
- 优化日志系统,使用 log4j2 实现动态日志记录- 重构 BaiduGPTManager 类,增加多线程支持和错误处理
- 更新 MessageHandleBuild 类,支持 message_id 参数
- 修复部分功能的逻辑错误,提高系统稳定性
This commit is contained in:
Yutou 2025-02-04 17:13:48 +08:00
parent 237c9273ca
commit 1041dfa909
15 changed files with 679 additions and 109 deletions

38
pom.xml
View File

@ -164,6 +164,44 @@
<artifactId>jsoup</artifactId>
<version>1.17.2</version>
</dependency>
<dependency>
<groupId>com.baidubce</groupId>
<artifactId>qianfan</artifactId>
<version>0.1.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.24.1</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-bom</artifactId>
<version>2.24.1</version>
<scope>import</scope>
<type>pom</type>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.24.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
</dependencies>
<build>

View File

@ -3,6 +3,7 @@ package com.yutou.napcat.handle;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.yutou.okhttp.BaseBean;
import com.yutou.qqbot.utlis.StringUtils;
import lombok.Data;
import java.util.ArrayList;
@ -13,6 +14,7 @@ public class MessageHandleBuild {
private long qq;
private boolean autoEscape;
private boolean isGroup;
private String messageId;
public static MessageHandleBuild create() {
return new MessageHandleBuild();
@ -41,7 +43,10 @@ public class MessageHandleBuild {
isGroup = group;
return this;
}
public MessageHandleBuild setMessageId(String messageId) {
this.messageId = messageId;
return this;
}
public JSONObject build() {
JSONObject json=new JSONObject();
if(isGroup){
@ -51,6 +56,9 @@ public class MessageHandleBuild {
}
json.put("auto_escape", autoEscape);
json.put("message", msgList);
if(!StringUtils.isEmpty(messageId)){
json.put("message_id",messageId);
}
return json;
}
}

View File

@ -44,9 +44,9 @@ public interface MessageAPI {
@Field("message_id") long messageId
);
@FormUrlEncoded
@POST("/get_msg")
Call<HttpBody<MessageBean>> getMessage(
@Field("message_id") long messageId
@Body
JSONObject message
);
}

View File

@ -22,9 +22,7 @@ public class Message {
public static Message create(String message, boolean isGTP) {
Message msg = new Message();
msg.content = message;
if (isGTP) {
msg.role = "assistant";
}
msg.role = isGTP ? "assistant" : "user";
return msg;
}
}

View File

@ -4,6 +4,7 @@ import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.yutou.napcat.enums.MessageEnum;
import com.yutou.napcat.event.MessageEvent;
import com.yutou.napcat.handle.MessageHandleBuild;
import com.yutou.napcat.handle.OtherHandle;
import com.yutou.napcat.handle.Reply;
import com.yutou.napcat.http.NapCatApi;
@ -78,7 +79,7 @@ public class BiliVideo extends Model {
private String onAIVideo(long id) {
try {
Response<HttpBody<MessageBean>> execute = NapCatApi.getMessageApi().getMessage(id).execute();
Response<HttpBody<MessageBean>> execute = NapCatApi.getMessageApi().getMessage(MessageHandleBuild.create().setMessageId(String.valueOf(id)).build()).execute();
if(execute.body()==null){
return "省流失败";
}

View File

@ -1,20 +1,24 @@
package com.yutou.qqbot.models.Commands;
import com.yutou.napcat.QQDatabase;
import com.yutou.napcat.handle.At;
import com.yutou.napcat.handle.BaseHandle;
import com.yutou.napcat.handle.Text;
import com.yutou.napcat.handle.*;
import com.yutou.napcat.http.NapCatApi;
import com.yutou.qqbot.Annotations.UseModel;
import com.yutou.qqbot.QQBotApplication;
import com.yutou.qqbot.QQBotManager;
import com.yutou.qqbot.data.baidu.Message;
import com.yutou.qqbot.data.baidu.ResponseMessage;
import com.yutou.qqbot.interfaces.DownloadInterface;
import com.yutou.qqbot.models.Model;
import com.yutou.qqbot.utlis.BaiduGPTManager;
import com.yutou.napcat.event.MessageEvent;
import com.yutou.qqbot.utlis.ConfigTools;
import com.yutou.qqbot.utlis.HttpTools;
import com.yutou.qqbot.utlis.StringUtils;
import lombok.val;
import org.apache.catalina.valves.JsonErrorReportValve;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
@ -42,16 +46,6 @@ public class BaiduGPT extends Model {
@Override
public void onMessage(Long qq, MessageEvent event, boolean isGroup) {
super.onMessage(qq, event, isGroup);
String version = ConfigTools.load(ConfigTools.CONFIG, ConfigTools.BAIDU_GPT_VERSION, String.class);
if (StringUtils.isEmpty(version)) {
version = "3.5";
BaiduGPTManager.getManager().setModelFor35();
}
if ("3.5".equals(version)) {
BaiduGPTManager.getManager().setModelFor35();
} else if ("4.0".equals(version)) {
BaiduGPTManager.getManager().setModelFor40();
}
if (event.getTextMessage().equals(QQGroupCommands.GPT_CLEAR)) {
BaiduGPTManager.getManager().clear();
QQBotManager.getInstance().sendMessage(event.isUser(), qq, new Text("已经失忆捏"));
@ -87,17 +81,52 @@ public class BaiduGPT extends Model {
QQBotManager.getInstance().sendMessage(event.isUser(), qq, list);
}
return;
}else if(event.getTextMessage().contains("画画")){
val file = BaiduGPTManager.getManager().textToImage(String.valueOf(qq), event.getTextMessage().replace("@" + QQDatabase.getMe().getUserId(), "").replace("画画", "").trim());
if(file==null){
QQBotManager.getInstance().sendMessage(event.isUser(), qq, new Text("画不出"));
}else{
QQBotManager.getInstance().sendMessage(file,qq, event.getMessageId().toString(), "好嘞");
}
ResponseMessage message = BaiduGPTManager.getManager().sendMessage(
return;
}
if(checkImage()) {
parseImage(event, qq);
return;
}
Message message = BaiduGPTManager.getManager().sendMessage(
String.valueOf(qq),
event.getTextMessage().replace("@" + QQDatabase.getMe().getUserId(), "").trim());
String sb = "调用版本:" +
BaiduGPTManager.getManager().getGPTVersion() +
"\n" +
message.getResult();
message.getContent();
QQBotManager.getInstance().sendMessage(event.isUser(), qq, new Text(sb));
}
}
private void parseImage(MessageEvent event, Long qq) {
Image imageHandle = event.findType(Image.class);
val replyHandle = event.findType(Reply.class);
if (replyHandle != null &&imageHandle==null) {
imageHandle = getReply(replyHandle.getData().getId()).findType(Image.class);
}
if(imageHandle==null){
return;
}
HttpTools.download(imageHandle.getData().getUrl(), "gpt_parse_image.png", new DownloadInterface() {
@Override
public void onDownload(File file) {
super.onDownload(file);
if(file==null){
return;
}
val text = BaiduGPTManager.getManager().imageToText(String.valueOf(qq), file);
QQBotManager.getInstance().sendMessage(event.isUser(),qq,new Reply(event.getMessageId()),new Text(text));
}
});
}
public static void main(String[] args) {
System.out.println(ConfigTools.load(ConfigTools.CONFIG, ConfigTools.BAIDU_GPT_VERSION));

View File

@ -2,12 +2,18 @@ package com.yutou.qqbot.models;
import com.yutou.napcat.event.GroupMessageEvent;
import com.yutou.napcat.event.MessageEvent;
import com.yutou.napcat.handle.At;
import com.yutou.napcat.handle.*;
import com.yutou.napcat.http.NapCatApi;
import com.yutou.napcat.model.MessageBean;
import com.yutou.okhttp.HttpBody;
import com.yutou.qqbot.QQNumberManager;
import com.yutou.qqbot.interfaces.ModelInterface;
import com.yutou.qqbot.utlis.ConfigTools;
import com.yutou.qqbot.utlis.Log;
import lombok.val;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
@ -129,6 +135,34 @@ public abstract class Model implements ModelInterface {
public boolean isAt() {
return msg.contains("@" + ConfigTools.load(ConfigTools.CONFIG, ConfigTools.QQ_NUMBER, String.class));
}
public boolean checkImage(){
val image = event.findType(Image.class);
if(image!=null){
return true;
}
val reply = event.findType(Reply.class);
if(reply!=null){
val replyEvent = getReply(reply.getData().getId());
if(replyEvent!=null){
return replyEvent.findType(Image.class)!=null;
}
}
return false;
}
public MessageEvent getReply(long replyId){
HttpBody<MessageBean> body = null;
try {
body = NapCatApi.getMessageApi().getMessage(MessageHandleBuild.create().setMessageId(String.valueOf(replyId)).build()).execute().body();
if (body == null) {
return null;
}
return MessageEvent.parseHandleHttp(body.getSrc());
} catch (IOException e) {
Log.e(e);
}
return null;
}
public boolean isAdmin() {
return user == 583819556L;

View File

@ -7,10 +7,7 @@ import com.alibaba.fastjson2.JSONObject;
import com.yutou.napcat.QQDatabase;
import com.yutou.napcat.enums.MessageEnum;
import com.yutou.napcat.event.GroupMessageEvent;
import com.yutou.napcat.handle.At;
import com.yutou.napcat.handle.BaseHandle;
import com.yutou.napcat.handle.Image;
import com.yutou.napcat.handle.Text;
import com.yutou.napcat.handle.*;
import com.yutou.napcat.http.NapCatApi;
import com.yutou.napcat.model.MessageBean;
import com.yutou.okhttp.HttpBody;
@ -70,7 +67,9 @@ public class QQSetu extends Model {
RedisTools.set(db_print, redisKey, json.toJSONString());
JSONObject info = setu.getJSONObject("info");
JSONObject score = setu.getJSONObject("score");
HttpBody<MessageBean> body = NapCatApi.getMessageApi().getMessage(info.getLong("id")).execute().body();
HttpBody<MessageBean> body = NapCatApi.getMessageApi().getMessage(
MessageHandleBuild.create().setMessageId(String.valueOf(info.getLong("id"))).build()
).execute().body();
List<BaseHandle<?>> sendList = new ArrayList<>();
if (body == null) {
sendList.add(new Text("[图片获取失败]"));

View File

@ -1,104 +1,225 @@
package com.yutou.qqbot.utlis;
import com.alibaba.fastjson2.JSONObject;
import com.baidubce.qianfan.Qianfan;
import com.baidubce.qianfan.model.chat.ChatResponse;
import com.baidubce.qianfan.model.image.Image2TextResponse;
import com.baidubce.qianfan.model.image.Text2ImageResponse;
import com.yutou.qqbot.data.baidu.Message;
import com.yutou.qqbot.data.baidu.ResponseMessage;
import lombok.val;
import java.nio.charset.StandardCharsets;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Base64;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
public class BaiduGPTManager {
private static int MAX_MESSAGE = 5;
private static BaiduGPTManager manager;
private static final String url_3_5 = "https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/completions";
//4.0
private static final String url_4_0 = "https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/completions_pro";
private static String url = url_3_5;
private static final AtomicInteger MAX_MESSAGE = new AtomicInteger(20);
private static final String AppID = ConfigTools.load(ConfigTools.CONFIG, ConfigTools.BAIDU_GPT_APPID, String.class);
private static final String ApiKey = ConfigTools.load(ConfigTools.CONFIG, ConfigTools.BAIDU_GPT_API_KEY, String.class);
//ConfigTools.load操作可以确保获取到相关参数所以无需关心
private static final String AccessKey = ConfigTools.load(ConfigTools.CONFIG, ConfigTools.BAIDU_GPT_ACCESS_KEY, String.class);
private static final String SecretKey = ConfigTools.load(ConfigTools.CONFIG, ConfigTools.BAIDU_GPT_SECRET_KEY, String.class);
private static Map<String, List<Message>> msgMap;
private final ConcurrentHashMap<String, List<Message>> msgMap;
private final static String modelFor40 = "ERNIE-4.0-8K";
private final static String modelFor35 = "ERNIE-3.5-8K";
private String model = modelFor35;
// 新增锁映射表
private final ConcurrentHashMap<String, AtomicBoolean> userLocks = new ConcurrentHashMap<>();
private final Qianfan qianfan;
private BaiduGPTManager() {
msgMap = new HashMap<>();
msgMap = new ConcurrentHashMap<>();
qianfan = new Qianfan(AccessKey, SecretKey);
String savedVersion = ConfigTools.load(ConfigTools.CONFIG, ConfigTools.BAIDU_GPT_VERSION, String.class);
if (StringUtils.isEmpty(savedVersion) || (!"3.5".equals(savedVersion) && !"4.0".equals(savedVersion))) {
savedVersion = "3.5";
ConfigTools.save(ConfigTools.CONFIG, ConfigTools.BAIDU_GPT_VERSION, savedVersion);
}
model = "3.5".equals(savedVersion) ? modelFor35 : modelFor40;
}
private static volatile BaiduGPTManager manager;
public static BaiduGPTManager getManager() {
if (manager == null) {
synchronized (BaiduGPTManager.class) {
if (manager == null) {
manager = new BaiduGPTManager();
}
}
}
return manager;
}
public int setMaxMessageCount(int count) {
MAX_MESSAGE = count;
return MAX_MESSAGE;
MAX_MESSAGE.set(count);
return count;
}
public void setModelFor40() {
url = url_4_0;
public synchronized void setModelFor40() {
model = modelFor40;
ConfigTools.save(ConfigTools.CONFIG, ConfigTools.BAIDU_GPT_VERSION, "4.0");
}
public void setModelFor35() {
url = url_3_5;
public synchronized void setModelFor35() {
model = modelFor35;
ConfigTools.save(ConfigTools.CONFIG, ConfigTools.BAIDU_GPT_VERSION, "3.5");
}
public void clear() {
/**
* 这里确实是需要清空所有数据
*/
public synchronized void clear() { // 添加同步
msgMap.clear();
for (AtomicBoolean value : userLocks.values()) {
value.set(false);
}
userLocks.forEachValue(1, atomicBoolean -> atomicBoolean.set(false));
userLocks.clear();
}
private String getToken() {
String _url = String.format("https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=%s&client_secret=%s"
, ApiKey
, SecretKey
);
String get = HttpTools.get(_url);
JSONObject response = JSONObject.parseObject(get);
return response.getString("access_token");
// 这个是官方的示例代码表示连续对话
private static void exampleChat() {
Qianfan qianfan = new Qianfan();
ChatResponse response = qianfan.chatCompletion()
// 设置需要使用的模型与endpoint同时只能设置一种
.model("ERNIE-Bot")
// 通过传入历史对话记录来实现多轮对话
.addMessage("user", "你好!你叫什么名字?")
.addMessage("assistant", "你好我是文心一言英文名是ERNIE Bot。")
// 传入本轮对话的用户输入
.addMessage("user", "刚刚我的问题是什么?")
.execute();
System.out.println("输出内容:" + response.getResult());
}
public ResponseMessage sendMessage(String user, String message) {
List<Message> messages = msgMap.getOrDefault(user, new ArrayList<>());
if (messages.size() > MAX_MESSAGE * 2) {
messages.remove(0);
messages.remove(1);
public Message sendMessage(String user, String message) {
// 获取或创建用户锁
AtomicBoolean lock = userLocks.computeIfAbsent(user, k -> new AtomicBoolean(false));
// 尝试加锁如果已被锁定则立即返回提示
if (!lock.compareAndSet(false, true)) {
return Message.create("您有请求正在处理中,请稍后再试", true);
}
messages.add(Message.create(message));
JSONObject json = new JSONObject();
json.put("messages", messages);
System.out.println("json = " + json);
Map<String, String> map = new HashMap<>();
map.put("Content-Type", "application/json");
map.put("Content-Length", String.valueOf(json.toJSONString().getBytes(StandardCharsets.UTF_8).length));
String post = HttpTools.http_post(url + "?access_token=" + getToken()
, json.toJSONString().getBytes(StandardCharsets.UTF_8), 0, map);
System.out.println("post = " + post);
if (StringUtils.isEmpty(post)) {
clear();
return sendMessage(user, message);
try {
List<Message> list = msgMap.computeIfAbsent(user, k -> Collections.synchronizedList(new ArrayList<>()));
// 限制历史消息的最大数量
synchronized (list) {
if (list.size() >= MAX_MESSAGE.get()) {
int removeCount = list.size() - MAX_MESSAGE.get() + 1; // 腾出空间给新消息
list.subList(0, removeCount).clear();
}
ResponseMessage response = JSONObject.parseObject(post, ResponseMessage.class);
messages.add(Message.create(response.getResult(), true));
msgMap.put(user, messages);
System.out.println("\n\n");
list.add(Message.create(message));
}
val builder = qianfan.chatCompletion()
.model(model);
for (Message msg : list) {
builder.addMessage(msg.getRole(), msg.getContent());
}
ChatResponse chatResponse = builder.execute();
Message response = Message.create(chatResponse.getResult(), true);
synchronized (list) {
list.add(response);
if (list.size() > MAX_MESSAGE.get()) {
int overflow = list.size() - MAX_MESSAGE.get();
list.subList(0, overflow).clear();
}
}
// msgMap.put(user, list);
return response;
} catch (Exception e) {
Log.e(e, message);
return Message.create("请求失败,请重试", true);
} finally {
lock.set(false);
userLocks.remove(user, lock);
}
}
/**
* 将文本转换为图像文件
* 该方法使用预训练的AI模型将给定的文本转换为图像并将其保存为文件
*
* @param user 用户标识符用于为生成的图像文件命名
* @param text 要转换为图像的文本
* @return 返回生成的图像文件对象如果转换过程中发生错误则返回null
*/
public File textToImage(String user, String text) {
// 使用QianFan的text2Image方法将文本转换为图像数据
Text2ImageResponse response = qianfan.text2Image()
.prompt(text)
.execute();
// 获取转换后的图像数据以Base64编码的图像字符串形式
val b64Image = response.getData().get(0).getB64Image();
// 将Base64编码的图像数据转换为图像文件
// 创建一个临时目录下的图像文件文件名包含用户标识符和当前时间戳以确保唯一性
val imageFile = new File("tmp" + File.separator + user + "_" + System.currentTimeMillis() + ".png");
try (val inputStream = new ByteArrayInputStream(Base64.getDecoder().decode(b64Image))) {
// 将解码后的图像数据复制到图像文件中替换现有文件
Files.copy(inputStream, imageFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
return imageFile;
} catch (Exception e) {
// 如果在图像文件生成过程中发生错误记录错误信息
Log.e(e);
}
// 如果发生错误返回null
return null;
}
/**
* 将图片转换为文本描述
*
* @param user 使用该功能的用户标识
* @param file 要转换的图片文件
* @return 转换后的文本描述如果转换失败则返回null
*/
public String imageToText(String user, File file) {
// 将file文件转换成base64的代码
try {
// 读取文件内容并转换为Base64编码
val base64 = Base64.getEncoder().encodeToString(Files.readAllBytes(file.toPath()));
// 调用图像转文本的API
Image2TextResponse response = qianfan.image2Text()
.image(base64)
.prompt("分析图片,如果图中有文本则加上文本内容")
.execute();
// 获取API返回的结果
String result = response.getResult();
// 如果结果不是中文通过sendMessage函数尝试将其翻译成中文
result = sendMessage("bot", "你是一个语言翻译专家,如果这段内容不是中文,请翻译成中文,如果已经是中文则不需要翻译:" + result).getContent();
// 返回最终的中文描述结果
return result;
} catch (Exception e) {
// 异常处理记录错误日志
Log.e(e);
}
// 如果发生异常返回null
return null;
}
public String getGPTVersion() {
return (url.equals(url_3_5) ? "3.5" : "4.0");
return (model.equals(modelFor35) ? "3.5" : "4.0");
}
public static void main(String[] args) throws Exception {
ResponseMessage message = BaiduGPTManager.getManager().sendMessage("test", "现在假设小猪等于1,小猴等于2");
System.out.println(message.getResult());
message = BaiduGPTManager.getManager().sendMessage("test", "那么小猪加上小猴等于多少?");
System.out.println(message.getResult());
// BaiduGPTManager.getManager().textToImage("user","画一个猫娘,用二次元动画画风,她是粉色头发,坐在地上");
// BaiduGPTManager.getManager().imageToText("user",new File("test.png"));
// Message message = BaiduGPTManager.getManager().sendMessage("user", "现在假设小猪等于1,小猴等于2");
// System.out.println(message.getContent());
// message = BaiduGPTManager.getManager().sendMessage("user", "那么小猪加上小猴等于多少?");
// System.out.println(message.getContent());
System.out.println(BaiduGPTManager.getManager().sendMessage("user", "分析这个网页链接的页面内容,而非链接本身:https://www.bilibili.com/video/BV1TTf5YrESz/").getContent());
}
}

View File

@ -30,6 +30,7 @@ public class ConfigTools {
public static final String BAIDU_GPT_VERSION = "baidu.gpt.version";
public static final String BAIDU_GPT_APPID = "baidu.gpt.appid";
public static final String BAIDU_GPT_API_KEY = "baidu.gpt.apikey";
public static final String BAIDU_GPT_ACCESS_KEY = "baidu.gpt.accessKey";
public static final String BAIDU_GPT_SECRET_KEY = "baidu.gpt.SecretKey";
public static final String TURNIP_PROPHET_SERVER = "turnip.server";
public static final String TURNIP_PROPHET_SEND_TMP_GROUP = "turnip.send.tmp.group";

View File

@ -0,0 +1,134 @@
package com.yutou.qqbot.utlis;
import org.springframework.util.StringUtils;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class DateFormatUtils {
private final Map<String, ThreadLocal<SimpleDateFormat>> formats = new ConcurrentHashMap<>();
private static volatile DateFormatUtils utils;
public static final String DEFAULT_PATTERN = "yyyy-MM-dd HH:mm:ss.SSS";
private DateFormatUtils() {}
public static DateFormatUtils getInstance() {
if (utils == null) {
synchronized (DateFormatUtils.class) {
if (utils == null) {
utils = new DateFormatUtils();
}
}
}
return utils;
}
public String format(Date date, String format) {
getFormat(format).applyPattern(format);
return getFormat(format).format(date);
}
public String format(long time, String format) {
return getFormat(format).format(new Date(time));
}
public String format(long time) {
if (time < 1000000000) {
time *= 1000;
}
return getFormat().format(new Date(time));
}
public String format(Date date) {
return getFormat().format(date);
}
public String format() {
return getFormat().format(new Date());
}
public Date parseTimer(String date) {
return parse(date, "HH:mm:ss");
}
public Date parse(String date, String format) {
try {
if(date.startsWith("1")){
return new Date(Long.parseLong(date));
}
return getFormat(format).parse(date);
} catch (ParseException e) {
System.err.println("Error parsing date: " + e.getMessage());
return null;
}
}
public String parseString(String date, String format) {
Date time = parse(date, format);
return format(time, format);
}
public String convertSeconds(long totalSeconds) {
// 计算总小时数
long hours = totalSeconds / 3600;
// 剩余的秒数
long remainingSecondsAfterHours = totalSeconds % 3600;
// 计算分钟数
long minutes = remainingSecondsAfterHours / 60;
// 最后剩余的秒数
long seconds = remainingSecondsAfterHours % 60;
return String.format("%d小时%d分%d秒", hours, minutes, seconds);
}
public String formatMillis(long millis) {
Duration duration = Duration.ofMillis(millis);
int seconds = (int) (duration.getSeconds() % 60);
int minutes = (int) ((duration.getSeconds() / 60) % 60);
int hours = (int) (duration.getSeconds() / 3600);
return String.format("%02d:%02d:%02d", hours, minutes, seconds);
}
public boolean checkTime(List<String> weeks, String recordDate) {
if (!StringUtils.hasText(recordDate)) {
recordDate = "00:00:00 - 23:59:59";
}
String[] parts = recordDate.split(" - ");
LocalTime startTime = LocalTime.parse(parts[0], DateTimeFormatter.ofPattern("HH:mm:ss"));
LocalTime endTime = LocalTime.parse(parts[1], DateTimeFormatter.ofPattern("HH:mm:ss"));
// 获取当前时间
LocalTime currentTime = LocalTime.now();
LocalDate currentDate = LocalDate.now();
// 获取当前日期对应的星期几1-7分别对应周一到周日
int currentWeekDay = currentDate.getDayOfWeek().getValue();
// 判断当前日期是否在指定的星期列表中
boolean isSpecifiedWeekDay;
if (weeks == null) {
isSpecifiedWeekDay = true;
} else {
isSpecifiedWeekDay = weeks.contains(String.valueOf(currentWeekDay));
}
// 判断当前时间是否在指定的时间范围内
boolean isWithinRange = (currentTime.isAfter(startTime) || currentTime.equals(startTime)) &&
(currentTime.isBefore(endTime) || currentTime.equals(endTime));
return isWithinRange && isSpecifiedWeekDay;
}
private SimpleDateFormat getFormat() {
return getFormat(DEFAULT_PATTERN);
}
private SimpleDateFormat getFormat(String format) {
return formats.computeIfAbsent(format, key -> ThreadLocal.withInitial(() -> new SimpleDateFormat(key))).get();
}
}

View File

@ -0,0 +1,124 @@
package com.yutou.qqbot.utlis;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.core.Appender;
import org.apache.logging.log4j.core.Layout;
import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.appender.RollingFileAppender;
import org.apache.logging.log4j.core.appender.rolling.CompositeTriggeringPolicy;
import org.apache.logging.log4j.core.appender.rolling.SizeBasedTriggeringPolicy;
import org.apache.logging.log4j.core.appender.rolling.TimeBasedTriggeringPolicy;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.layout.PatternLayout;
import java.util.Date;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
public class DynamicLogFile {
// 创建一个缓存用于存储Logger对象最大容量为1000过期时间为10分钟
static Cache<String, Logger> cache = CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterAccess(10, TimeUnit.MINUTES)
.removalListener(it -> {
if (it.wasEvicted()) {
if (it.getKey() != null) {
String loggerName = (String) it.getKey();
remove(loggerName, true);
}
}
})
.build();
// 根据loggerName获取Logger对象如果缓存中不存在则创建一个新的Logger对象并放入缓存
public static Logger getLogger(String loggerName) {
try {
return cache.get(loggerName, () -> {
configureLogger(loggerName);
return LogManager.getLogger(loggerName);
});
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
}
// 配置Logger对象
private static void configureLogger(String loggerName) {
LoggerContext context = (LoggerContext) LogManager.getContext(false);
Configuration config = context.getConfiguration();
// 创建日志格式
Layout<String> layout = PatternLayout.newBuilder()
.withPattern("%d{yyyy-MM-dd'T'HH:mm:ss.SSSXXX} %-5p [%thread] (%F:%L) : %m%n")
.build();
// 创建时间触发策略
TimeBasedTriggeringPolicy timePolicy = TimeBasedTriggeringPolicy.newBuilder()
.build();
// 创建文件大小触发策略
SizeBasedTriggeringPolicy sizePolicy = SizeBasedTriggeringPolicy.createPolicy("100 MB");
// 创建组合触发策略
CompositeTriggeringPolicy triggeringPolicy = CompositeTriggeringPolicy.createPolicy(timePolicy, sizePolicy);
// 创建滚动文件Appender
RollingFileAppender appender = RollingFileAppender.newBuilder()
.setName(loggerName)
.withFileName("logs" + "/" + DateFormatUtils.getInstance().format(new Date(), "yyyy-MM-dd") + "/" + loggerName + ".log")
.withFilePattern("logs" + "/" + "%d{yyyy-MM-dd}" + "/" + loggerName + "-%i.log.gz")
.setLayout(layout)
.setImmediateFlush(true)
.withAppend(true)
.setIgnoreExceptions(false)
.withPolicy(triggeringPolicy)
.build();
appender.start();
config.addAppender(appender);
// 获取Logger对象
org.apache.logging.log4j.core.Logger coreLogger = context.getLogger(loggerName);
if (coreLogger == null) {
throw new IllegalStateException("Logger with name " + loggerName + " does not exist.");
}
// 将Appender添加到Logger对象中
coreLogger.addAppender(appender);
coreLogger.setLevel(Level.ALL);
coreLogger.setAdditive(false);
// 更新Logger对象
context.updateLoggers();
}
// 移除Logger对象
public static void remove(String loggerName) {
remove(loggerName, false);
}
// 私有方法移除Logger对象isAuto参数用于判断是否是自动移除
private static void remove(String loggerName, boolean isAuto) {
if (!isAuto) {
cache.invalidate(loggerName);
}
LoggerContext context = (LoggerContext) LogManager.getContext(false);
Configuration config = context.getConfiguration();
org.apache.logging.log4j.core.Logger coreLogger = context.getLogger(loggerName);
Appender appender = config.getAppender(loggerName);
if (appender != null) {
appender.stop();
coreLogger.removeAppender(appender);
}
config.getAppenders().remove(loggerName);
context.updateLoggers();
}
}

View File

@ -1,33 +1,74 @@
package com.yutou.qqbot.utlis;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.util.StackLocatorUtil;
public class Log {
public static void i(String tag, Object log) {
i('[' + tag + ']' + log);
public static void i() {
System.out.println();
}
public static void i(Object log) {
if (ConfigTools.load(ConfigTools.CONFIG, ConfigTools.SERVICE_LOG, boolean.class, false)) {
System.out.printf("[%s]%s%n",
AppTools.getToDayNowTimeToString(),
log
);
public static Logger getDynamicLogger(String loggerName) {
return DynamicLogFile.getLogger(loggerName);
}
public static void removeDynamicLogger(String loggerName) {
DynamicLogFile.remove(loggerName);
}
public static void e(String tag, Exception e) {
System.err.printf("[%s]%s - %s%n",
AppTools.getToDayNowTimeToString(),
tag,
AppTools.getExceptionString(e)
);
public static void i(Object... log) {
if (!((boolean) ConfigTools.load(ConfigTools.CONFIG, ConfigTools.SERVICE_LOG))) {
return;
}
public static void i(Object tag, Object log) {
if (tag instanceof String) {
i("[" + tag + "]" + log);
} else {
i(tag.getClass().getSimpleName(), log);
LogManager.getLogger(getStackTrace()).info(buildLog(log));
}
public static void e(Object... log) {
if (!ConfigTools.load(ConfigTools.CONFIG, ConfigTools.SERVICE_LOG, Boolean.class)) {
return;
}
LogManager.getLogger(getStackTrace()).error(buildLog(log));
}
public static void e(Throwable e, Object... log) {
if (!ConfigTools.load(ConfigTools.CONFIG, ConfigTools.SERVICE_LOG, Boolean.class)) {
return;
}
LogManager.getLogger(getStackTrace()).error(buildLog(log), e);
}
public static void e(Throwable e) {
if (!ConfigTools.load(ConfigTools.CONFIG, ConfigTools.SERVICE_LOG, Boolean.class)) {
return;
}
LogManager.getLogger().error(getStackTrace(), e);
}
public static void d(Object... log) {
if (!ConfigTools.load(ConfigTools.CONFIG, ConfigTools.SERVICE_LOG, Boolean.class)) {
return;
}
LogManager.getLogger().debug(buildLog(log));
}
private static String getStackTrace() {
StackTraceElement element = StackLocatorUtil.getStackTraceElement(3);
return "(" + element.getFileName() + ":" + element.getLineNumber() + ")";
}
private static String buildLog(Object... log) {
StringBuilder sb = new StringBuilder();
for (Object obj : log) {
if (!sb.isEmpty()) {
sb.append("|");
}
sb.append(obj);
}
return sb.toString();
}
}

View File

@ -1,3 +1,6 @@
server.port=8002
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=100MB
logging.config=classpath:log4j2.xml
logging.file.path=./logs
logging.level.com.log.controller = trace

View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<Appenders>
<!-- 控制台输出 -->
<Console name="ConsoleAppender" target="SYSTEM_OUT">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} %-5p [%thread] (%F:%L) : %m%n%throwable"/>
</Console>
<!-- 动态路由日志文件 -->
<Routing name="RoutingAppender">
<Routes pattern="$${date:yyyy-MM-dd}">
<Route>
<RollingFile name="Rolling-${date:yyyy-MM-dd}" fileName="logs/${date:yyyy-MM-dd}/system.log"
filePattern="logs/%d{yyyy-MM-dd}/system-%i.log.gz">
<PatternLayout>
<pattern>%d{yyyy-MM-dd'T'HH:mm:ss.SSSXXX} %-5p [%thread] (%F:%L) : %m%n%throwable</pattern>
</PatternLayout>
<Policies>
<TimeBasedTriggeringPolicy />
<SizeBasedTriggeringPolicy size="1024 MB"/>
</Policies>
</RollingFile>
</Route>
</Routes>
</Routing>
</Appenders>
<Loggers>
<!-- 屏蔽 org.apache.hc.client5 包下的所有日志 -->
<Logger name="org.apache.hc.client5" level="OFF"/>
<!-- 根日志记录器 -->
<Root level="info">
<AppenderRef ref="ConsoleAppender"/>
<AppenderRef ref="RoutingAppender"/>
</Root>
</Loggers>
</Configuration>