From f2ef3b397d7121fd7ba75d283bff94ee4ed3bf26 Mon Sep 17 00:00:00 2001 From: Leo Date: Thu, 21 Dec 2023 18:02:13 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 33 + pom.xml | 118 ++ .../cn/van333/mt2ql/Mt2QlApplication.java | 17 + .../cn/van333/mt2ql/wxMessage/WXListener.java | 67 + .../wxMessage/config/ThreadPoolConfig.java | 60 + .../mt2ql/wxMessage/enums/EventType.java | 99 ++ .../mt2ql/wxMessage/enums/FromType.java | 87 + .../van333/mt2ql/wxMessage/enums/IEnum.java | 14 + .../mt2ql/wxMessage/enums/MsgTypeEnum.java | 83 + .../mt2ql/wxMessage/model/WxMessage.java | 32 + .../mt2ql/wxMessage/utils/RedisCache.java | 256 +++ .../van333/mt2ql/wxMessage/utils/Threads.java | 81 + .../cn/van333/mt2ql/wxMessage/utils/Util.java | 1562 +++++++++++++++++ .../wxMessage/utils/WxMessageConsumer.java | 289 +++ src/main/resources/application-dev.yaml | 25 + src/main/resources/application-prod.yaml | 22 + src/main/resources/application.yaml | 5 + src/main/resources/static/index.html | 6 + .../van333/mt2ql/Mt2QlApplicationTests.java | 13 + 19 files changed, 2869 insertions(+) create mode 100644 .gitignore create mode 100644 pom.xml create mode 100644 src/main/java/cn/van333/mt2ql/Mt2QlApplication.java create mode 100644 src/main/java/cn/van333/mt2ql/wxMessage/WXListener.java create mode 100644 src/main/java/cn/van333/mt2ql/wxMessage/config/ThreadPoolConfig.java create mode 100644 src/main/java/cn/van333/mt2ql/wxMessage/enums/EventType.java create mode 100644 src/main/java/cn/van333/mt2ql/wxMessage/enums/FromType.java create mode 100644 src/main/java/cn/van333/mt2ql/wxMessage/enums/IEnum.java create mode 100644 src/main/java/cn/van333/mt2ql/wxMessage/enums/MsgTypeEnum.java create mode 100644 src/main/java/cn/van333/mt2ql/wxMessage/model/WxMessage.java create mode 100644 src/main/java/cn/van333/mt2ql/wxMessage/utils/RedisCache.java create mode 100644 src/main/java/cn/van333/mt2ql/wxMessage/utils/Threads.java create mode 100644 src/main/java/cn/van333/mt2ql/wxMessage/utils/Util.java create mode 100644 src/main/java/cn/van333/mt2ql/wxMessage/utils/WxMessageConsumer.java create mode 100644 src/main/resources/application-dev.yaml create mode 100644 src/main/resources/application-prod.yaml create mode 100644 src/main/resources/application.yaml create mode 100644 src/main/resources/static/index.html create mode 100644 src/test/java/cn/van333/mt2ql/Mt2QlApplicationTests.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..c234b20 --- /dev/null +++ b/pom.xml @@ -0,0 +1,118 @@ + + + 4.0.0 + cn.van333 + MT2QL + 0.0.1-SNAPSHOT + MT2QL + MT2QL + + 1.8 + UTF-8 + UTF-8 + 2.6.13 + + + + org.springframework.boot + spring-boot-starter-data-redis + + + org.springframework.boot + spring-boot-starter-web + + + + + + + + + + + + com.alibaba.fastjson2 + fastjson2 + 2.0.14 + + + org.projectlombok + lombok + + + org.apache.commons + commons-lang3 + + + mysql + mysql-connector-java + + + org.springframework.boot + spring-boot-starter-data-redis + + + com.baomidou + mybatis-plus-boot-starter + 3.5.4 + + + cn.hutool + hutool-all + 5.8.8 + + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 1.8 + 1.8 + UTF-8 + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + cn.van333.mt2ql.Mt2QlApplication + true + + + + repackage + + repackage + + + + + + + + diff --git a/src/main/java/cn/van333/mt2ql/Mt2QlApplication.java b/src/main/java/cn/van333/mt2ql/Mt2QlApplication.java new file mode 100644 index 0000000..a0e0d22 --- /dev/null +++ b/src/main/java/cn/van333/mt2ql/Mt2QlApplication.java @@ -0,0 +1,17 @@ +package cn.van333.mt2ql; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableAsync; + +@SpringBootApplication +@EnableAsync +@MapperScan("cn.van333.mt2ql.wxMessage.mapper") +public class Mt2QlApplication { + + public static void main(String[] args) { + SpringApplication.run(Mt2QlApplication.class, args); + } + +} diff --git a/src/main/java/cn/van333/mt2ql/wxMessage/WXListener.java b/src/main/java/cn/van333/mt2ql/wxMessage/WXListener.java new file mode 100644 index 0000000..29703a8 --- /dev/null +++ b/src/main/java/cn/van333/mt2ql/wxMessage/WXListener.java @@ -0,0 +1,67 @@ +package cn.van333.mt2ql.wxMessage; + +import cn.van333.mt2ql.wxMessage.model.WxMessage; +import cn.van333.mt2ql.wxMessage.utils.WxMessageConsumer; +import com.alibaba.fastjson2.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author Leo + * @version 1.0 + * @create 2023/12/18 0018 下午 05:12 + * @description: + */ +@Controller +@RequestMapping("/wx") +@RestController +public class WXListener { + + private static final Logger logger = LoggerFactory.getLogger(WXListener.class); + + @Autowired + private WxMessageConsumer wxMessageConsumer; + + + + /** + * { + * "event": 10009, + * "wxid": "wxid_kr145nk7l0an31", + * "data": { + * "type": "D0003",* "des": "鏀跺埌娑堟伅", + * "data": { + * "timeStamp": "1702951964728", + * "fromType": 1, + * "msgType": 1, + * "msgSource": 0, + * "fromWxid": "wxid_ytpc72mdoskt22", + * "finalFromWxid": "", + * "atWxidList": [], + * "silence": 0, + * "membercount": 0, + * "signature": "v1_MllZwZMZ", + * "msg": "785", + * "msgBase64": "Nzg1" + * }, + * "timestamp": "1702951964740", + * "wxid": "wxid_kr145nk7l0an31", + * "port": 16888, + * "pid": 10468, + * "flag": "7777" + * } + * } + **/ + @RequestMapping("/message") + public String message(@RequestBody String requestBody) { + WxMessage message = JSONObject.parseObject(requestBody, WxMessage.class); + wxMessageConsumer.consume(message); + return "OK"; + } + +} diff --git a/src/main/java/cn/van333/mt2ql/wxMessage/config/ThreadPoolConfig.java b/src/main/java/cn/van333/mt2ql/wxMessage/config/ThreadPoolConfig.java new file mode 100644 index 0000000..1f5bed3 --- /dev/null +++ b/src/main/java/cn/van333/mt2ql/wxMessage/config/ThreadPoolConfig.java @@ -0,0 +1,60 @@ +package cn.van333.mt2ql.wxMessage.config; + +import cn.van333.mt2ql.wxMessage.utils.Threads; +import org.apache.commons.lang3.concurrent.BasicThreadFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadPoolExecutor; + +/** + * @author Leo + * @version 1.0 + * @create 2023/12/19 0019 上午 10:58 + * @description: + */ +@Configuration +public class ThreadPoolConfig { + // 核心线程池大小 + private final int corePoolSize = 50; + + // 最大可创建的线程数 + private final int maxPoolSize = 200; + + // 队列最大长度 + private final int queueCapacity = 1000; + + // 线程池维护线程所允许的空闲时间 + private final int keepAliveSeconds = 300; + + @Bean(name = "threadPoolTaskExecutor") + public ThreadPoolTaskExecutor threadPoolTaskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setMaxPoolSize(maxPoolSize); + executor.setCorePoolSize(corePoolSize); + executor.setQueueCapacity(queueCapacity); + executor.setKeepAliveSeconds(keepAliveSeconds); + // 线程池对拒绝任务(无线程可用)的处理策略 + executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + return executor; + } + + /** + * 执行周期性或定时任务 + */ + @Bean(name = "scheduledExecutorService") + protected ScheduledExecutorService scheduledExecutorService() { + return new ScheduledThreadPoolExecutor(corePoolSize, + new BasicThreadFactory.Builder().namingPattern("schedule-pool-%d").daemon(true).build(), + new ThreadPoolExecutor.CallerRunsPolicy()) { + @Override + protected void afterExecute(Runnable r, Throwable t) { + super.afterExecute(r, t); + Threads.printException(r, t); + } + }; + } +} diff --git a/src/main/java/cn/van333/mt2ql/wxMessage/enums/EventType.java b/src/main/java/cn/van333/mt2ql/wxMessage/enums/EventType.java new file mode 100644 index 0000000..c4f073f --- /dev/null +++ b/src/main/java/cn/van333/mt2ql/wxMessage/enums/EventType.java @@ -0,0 +1,99 @@ +package cn.van333.mt2ql.wxMessage.enums; + +/** + * @author Leo + * @version 1.0 + * @create 2023/12/19 0019 上午 10:27 + * @description: + */ + +import com.fasterxml.jackson.annotation.JsonValue; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public enum EventType implements IEnum { + /** + * 账号变动事件 (10014) + * 收到群聊消息 (10008) + * 收到私聊消息 (10009) + * 自己发出消息 (10010) + * 转账事件 (10006) + * 撤回事件 (10013) + * 好友请求 (10011) + * 支付事件 (10007) + */ + ACCOUNT_CHANGE(10014, "账号变动事件"), + SELF_MESSAGE(10010, "自己发出消息"), + TRANSFER_EVENT(10006, "转账事件"), + PAY_EVENT(10007, "支付事件"), + GROUP_MESSAGE(10008, "收到群聊消息"), + PRIVATE_MESSAGE(10009, "收到私聊消息"), + REVOKE_EVENT(10013, "撤回事件"), + FRIEND_REQUEST(10011, "好友请求"); + private final int key; + + private final String name; + + EventType(int key, String name) { + this.key = key; + this.name = name; + } + + public static EventType get(int key) { + for (EventType e : EventType.values()) { + if (e.getKey() == key) { + return e; + } + } + return null; + } + + public static String getName(Integer key) { + //if (Object.isNotEmpty(key)) { + EventType[] items = EventType.values(); + for (EventType item : items) { + if (item.getKey() == key) { + return item.getName(); + } + } + //} + return ""; + } + + public static Map getKeyVlue() { + Map map = new HashMap<>(); + EventType[] items = EventType.values(); + for (EventType item : items) { + map.put(item.getKey() + "", item.getName()); + } + return map; + } + + public static List> getSelectItems() { + List> result = new ArrayList>(); + EventType[] items = EventType.values(); + for (EventType item : items) { + Map map = new HashMap<>(); + map.put("label", item.getName()); + map.put("value", item.getKey()); + result.add(map); + } + return result; + } + + @Override + @JsonValue + public Integer getKey() { + return key; + } + + @Override + public String getName() { + return name; + } + +} + diff --git a/src/main/java/cn/van333/mt2ql/wxMessage/enums/FromType.java b/src/main/java/cn/van333/mt2ql/wxMessage/enums/FromType.java new file mode 100644 index 0000000..240ed61 --- /dev/null +++ b/src/main/java/cn/van333/mt2ql/wxMessage/enums/FromType.java @@ -0,0 +1,87 @@ +package cn.van333.mt2ql.wxMessage.enums; + +/** + * @author Leo + * @version 1.0 + * @create 2023/12/19 0019 上午 10:27 + * @description: + */ + +import com.fasterxml.jackson.annotation.JsonValue; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public enum FromType implements IEnum { + /** + * fromType 来源类型:1|私聊 2|群聊 3|公众号 + */ + PRIVATE(1, "私聊"), + GROUP(2, "群聊"), + MP(3, "公众号"); + private final int key; + + private final String name; + + FromType(int key, String name) { + this.key = key; + this.name = name; + } + + public static FromType get(int key) { + for (FromType e : FromType.values()) { + if (e.getKey() == key) { + return e; + } + } + return null; + } + + public static String getName(Integer key) { + //if (Object.isNotEmpty(key)) { + FromType[] items = FromType.values(); + for (FromType item : items) { + if (item.getKey() == key) { + return item.getName(); + } + } + //} + return ""; + } + + public static Map getKeyVlue() { + Map map = new HashMap<>(); + FromType[] items = FromType.values(); + for (FromType item : items) { + map.put(item.getKey() + "", item.getName()); + } + return map; + } + + public static List> getSelectItems() { + List> result = new ArrayList>(); + FromType[] items = FromType.values(); + for (FromType item : items) { + Map map = new HashMap<>(); + map.put("label", item.getName()); + map.put("value", item.getKey()); + result.add(map); + } + return result; + } + + @Override + @JsonValue + public Integer getKey() { + return key; + } + + @Override + public String getName() { + return name; + } + +} + diff --git a/src/main/java/cn/van333/mt2ql/wxMessage/enums/IEnum.java b/src/main/java/cn/van333/mt2ql/wxMessage/enums/IEnum.java new file mode 100644 index 0000000..18aba0b --- /dev/null +++ b/src/main/java/cn/van333/mt2ql/wxMessage/enums/IEnum.java @@ -0,0 +1,14 @@ +package cn.van333.mt2ql.wxMessage.enums; + +/** + * @author Leo + * @version 1.0 + * @create 2023/12/19 0019 上午 10:33 + * @description: + */ +public interface IEnum { + + Integer getKey(); + + String getName(); +} diff --git a/src/main/java/cn/van333/mt2ql/wxMessage/enums/MsgTypeEnum.java b/src/main/java/cn/van333/mt2ql/wxMessage/enums/MsgTypeEnum.java new file mode 100644 index 0000000..fb63d8f --- /dev/null +++ b/src/main/java/cn/van333/mt2ql/wxMessage/enums/MsgTypeEnum.java @@ -0,0 +1,83 @@ +package cn.van333.mt2ql.wxMessage.enums; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author Leo + * @version 1.0 + * @create 2023/12/19 0019 下午 02:31 + * @description: + */ +public enum MsgTypeEnum implements IEnum { + /** + msgType 消息类型:1|文本 3|图片 34|语音 42|名片 43|视频 47|动态表情 48|地理位置 49|分享链接或附件 2001|红包 2002|小程序 2003|群邀请 10000|系统消息 + * */ + TEXT(1, "文本"), + IMAGE(3, "图片"), + VOICE(34, "语音"), + VIDEO(43, "视频"), + SHARE(49, "分享"), + LOCATION(48, "位置"), + REDPACKET(2001, "红包"), + MINIPROGRAM(2002, "小程序"), + GROUP_INVITE(2003, "群邀请"), + SYSTEM(10000, "系统消息"), + ; + private final int key; + + private final String name; + + MsgTypeEnum(int key, String name) { + this.key = key; + this.name = name; + } + + public static String getName(Integer key) { + for (MsgTypeEnum msgTypeEnum : MsgTypeEnum.values()) { + if (msgTypeEnum.key == key) { + return msgTypeEnum.name; + } + } + return null; + } + + public static Map getKeyVlue() { + Map map = new HashMap<>(); + for (MsgTypeEnum msgTypeEnum : MsgTypeEnum.values()) { + map.put(msgTypeEnum.key + "", msgTypeEnum.name); + } + return map; + } + + public static List> getSelectItems() { + List> list = new ArrayList<>(); + for (MsgTypeEnum msgTypeEnum : MsgTypeEnum.values()) { + Map map = new HashMap<>(); + map.put("key", msgTypeEnum.key); + map.put("value", msgTypeEnum.name); + list.add(map); + } + return list; + } + + public static MsgTypeEnum get(int key) { + for (MsgTypeEnum msgTypeEnum : MsgTypeEnum.values()) { + if (msgTypeEnum.key == key) { + return msgTypeEnum; + } + } + return null; + } + + public Integer getKey() { + return key; + } + + public String getName() { + return name; + } + +} diff --git a/src/main/java/cn/van333/mt2ql/wxMessage/model/WxMessage.java b/src/main/java/cn/van333/mt2ql/wxMessage/model/WxMessage.java new file mode 100644 index 0000000..9aec95c --- /dev/null +++ b/src/main/java/cn/van333/mt2ql/wxMessage/model/WxMessage.java @@ -0,0 +1,32 @@ +package cn.van333.mt2ql.wxMessage.model; + +import com.alibaba.fastjson2.JSONObject; +import lombok.Data; + +/** + * @author Leo + * @version 1.0 + * @create 2023/12/18 0018 下午 05:52 + * @description: + */ +@Data +public class WxMessage { + + //{ + // "event": 10014, //事件的id(可用来区分是什么事件) + // "wxid": "wxid_3sq4tklb6c3121", //收到这条事件的微信 + // "data": {} //事件的主要内容,不同事件,具体对象参数也不尽相同 + //} + private Integer event; + private String wxid; + private JSONObject data; + + public WxMessage() { + } + + public WxMessage(Integer event, String wxid, JSONObject data) { + this.event = event; + this.wxid = wxid; + this.data = data; + } +} diff --git a/src/main/java/cn/van333/mt2ql/wxMessage/utils/RedisCache.java b/src/main/java/cn/van333/mt2ql/wxMessage/utils/RedisCache.java new file mode 100644 index 0000000..26593f6 --- /dev/null +++ b/src/main/java/cn/van333/mt2ql/wxMessage/utils/RedisCache.java @@ -0,0 +1,256 @@ +package cn.van333.mt2ql.wxMessage.utils; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.BoundSetOperations; +import org.springframework.data.redis.core.HashOperations; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.stereotype.Component; + +import java.util.*; +import java.util.concurrent.TimeUnit; + +/** + * spring redis 工具类 + * + * @author ruoyi + **/ +@SuppressWarnings(value = {"unchecked", "rawtypes"}) +@Component +public class RedisCache { + @Autowired + public RedisTemplate redisTemplate; + + /** + * 缓存基本的对象,Integer、String、实体类等 + * + * @param key 缓存的键值 + * @param value 缓存的值 + */ + public void setCacheObject(final String key, final T value) { + redisTemplate.opsForValue().set(key, value); + } + + /** + * 缓存基本的对象,Integer、String、实体类等 + * + * @param key 缓存的键值 + * @param value 缓存的值 + * @param timeout 时间 + * @param timeUnit 时间颗粒度 + */ + public void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit) { + redisTemplate.opsForValue().set(key, value, timeout, timeUnit); + } + + /** + * 设置有效时间 + * + * @param key Redis键 + * @param timeout 超时时间 + * @return true=设置成功;false=设置失败 + */ + public boolean expire(final String key, final long timeout) { + return expire(key, timeout, TimeUnit.SECONDS); + } + + /** + * 设置有效时间 + * + * @param key Redis键 + * @param timeout 超时时间 + * @param unit 时间单位 + * @return true=设置成功;false=设置失败 + */ + public boolean expire(final String key, final long timeout, final TimeUnit unit) { + return redisTemplate.expire(key, timeout, unit); + } + + /** + * 获取有效时间 + * + * @param key Redis键 + * @return 有效时间 + */ + public long getExpire(final String key) { + return redisTemplate.getExpire(key); + } + + /** + * 判断 key是否存在 + * + * @param key 键 + * @return true 存在 false不存在 + */ + public Boolean hasKey(String key) { + return redisTemplate.hasKey(key); + } + + /** + * 获得缓存的基本对象。 + * + * @param key 缓存键值 + * @return 缓存键值对应的数据 + */ + public T getCacheObject(final String key) { + ValueOperations operation = redisTemplate.opsForValue(); + return operation.get(key); + } + + /** + * 删除单个对象 + * + * @param key + */ + public boolean deleteObject(final String key) { + return redisTemplate.delete(key); + } + + /** + * 删除集合对象 + * + * @param collection 多个对象 + * @return + */ + public boolean deleteObject(final Collection collection) { + return redisTemplate.delete(collection) > 0; + } + + /** + * 缓存List数据 + * + * @param key 缓存的键值 + * @param dataList 待缓存的List数据 + * @return 缓存的对象 + */ + public long setCacheList(final String key, final List dataList) { + Long count = redisTemplate.opsForList().rightPushAll(key, dataList); + return count == null ? 0 : count; + } + + /** + * 重新set缓存List数据 + * + * @param key 缓存的键值 + * @param dataList 待缓存的List数据 + * @return 缓存的对象 + */ + public long reSetCacheList(final String key, final List dataList) { + this.deleteObject(key); + Long count = redisTemplate.opsForList().rightPushAll(key, dataList); + return count == null ? 0 : count; + } + + /** + * 获得缓存的list对象 + * + * @param key 缓存的键值 + * @return 缓存键值对应的数据 + */ + public List getCacheList(final String key) { + return redisTemplate.opsForList().range(key, 0, -1); + } + + /** + * 缓存Set + * + * @param key 缓存键值 + * @param dataSet 缓存的数据 + * @return 缓存数据的对象 + */ + public BoundSetOperations setCacheSet(final String key, final Set dataSet) { + BoundSetOperations setOperation = redisTemplate.boundSetOps(key); + Iterator it = dataSet.iterator(); + while (it.hasNext()) { + setOperation.add(it.next()); + } + return setOperation; + } + + /** + * 获得缓存的set + * + * @param key + * @return + */ + public Set getCacheSet(final String key) { + return redisTemplate.opsForSet().members(key); + } + + /** + * 缓存Map + * + * @param key + * @param dataMap + */ + public void setCacheMap(final String key, final Map dataMap) { + if (dataMap != null) { + redisTemplate.opsForHash().putAll(key, dataMap); + } + } + + /** + * 获得缓存的Map + * + * @param key + * @return + */ + public Map getCacheMap(final String key) { + return redisTemplate.opsForHash().entries(key); + } + + /** + * 往Hash中存入数据 + * + * @param key Redis键 + * @param hKey Hash键 + * @param value 值 + */ + public void setCacheMapValue(final String key, final String hKey, final T value) { + redisTemplate.opsForHash().put(key, hKey, value); + } + + /** + * 获取Hash中的数据 + * + * @param key Redis键 + * @param hKey Hash键 + * @return Hash中的对象 + */ + public T getCacheMapValue(final String key, final String hKey) { + HashOperations opsForHash = redisTemplate.opsForHash(); + return opsForHash.get(key, hKey); + } + + /** + * 获取多个Hash中的数据 + * + * @param key Redis键 + * @param hKeys Hash键集合 + * @return Hash对象集合 + */ + public List getMultiCacheMapValue(final String key, final Collection hKeys) { + return redisTemplate.opsForHash().multiGet(key, hKeys); + } + + /** + * 删除Hash中的某条数据 + * + * @param key Redis键 + * @param hKey Hash键 + * @return 是否成功 + */ + public boolean deleteCacheMapValue(final String key, final String hKey) { + return redisTemplate.opsForHash().delete(key, hKey) > 0; + } + + /** + * 获得缓存的基本对象列表 + * + * @param pattern 字符串前缀 + * @return 对象列表 + */ + public Collection keys(final String pattern) { + return redisTemplate.keys(pattern); + } +} diff --git a/src/main/java/cn/van333/mt2ql/wxMessage/utils/Threads.java b/src/main/java/cn/van333/mt2ql/wxMessage/utils/Threads.java new file mode 100644 index 0000000..3dc2d12 --- /dev/null +++ b/src/main/java/cn/van333/mt2ql/wxMessage/utils/Threads.java @@ -0,0 +1,81 @@ +package cn.van333.mt2ql.wxMessage.utils; + +/** + * @author Leo + * @version 1.0 + * @create 2023/12/19 0019 上午 11:01 + * @description: + */ + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.*; + +/** + * 线程相关工具类. + * + * @author ruoyi + */ +public class Threads { + private static final Logger logger = LoggerFactory.getLogger(Threads.class); + + /** + * sleep等待,单位为毫秒 + */ + public static void sleep(long milliseconds) { + try { + Thread.sleep(milliseconds); + } catch (InterruptedException e) { + return; + } + } + + /** + * 停止线程池 + * 先使用shutdown, 停止接收新任务并尝试完成所有已存在任务. + * 如果超时, 则调用shutdownNow, 取消在workQueue中Pending的任务,并中断所有阻塞函数. + * 如果仍然超時,則強制退出. + * 另对在shutdown时线程本身被调用中断做了处理. + */ + public static void shutdownAndAwaitTermination(ExecutorService pool) { + if (pool != null && !pool.isShutdown()) { + pool.shutdown(); + try { + if (!pool.awaitTermination(120, TimeUnit.SECONDS)) { + pool.shutdownNow(); + if (!pool.awaitTermination(120, TimeUnit.SECONDS)) { + logger.info("Pool did not terminate"); + } + } + } catch (InterruptedException ie) { + pool.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + } + + /** + * 打印线程异常信息 + */ + public static void printException(Runnable r, Throwable t) { + if (t == null && r instanceof Future) { + try { + Future future = (Future) r; + if (future.isDone()) { + future.get(); + } + } catch (CancellationException ce) { + t = ce; + } catch (ExecutionException ee) { + t = ee.getCause(); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + } + } + if (t != null) { + logger.error(t.getMessage(), t); + } + } +} + diff --git a/src/main/java/cn/van333/mt2ql/wxMessage/utils/Util.java b/src/main/java/cn/van333/mt2ql/wxMessage/utils/Util.java new file mode 100644 index 0000000..4f19c69 --- /dev/null +++ b/src/main/java/cn/van333/mt2ql/wxMessage/utils/Util.java @@ -0,0 +1,1562 @@ +package cn.van333.mt2ql.wxMessage.utils; + +/** + * @author Leo + * @version 1.0 + * @create 2023/12/21 0021 上午 11:27 + * @description: 旗云的 Util 用习惯了 用一下 + */ + +import org.apache.ibatis.jdbc.SQL; +import org.apache.tomcat.util.codec.binary.Base64; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.util.HtmlUtils; + +import javax.activation.FileDataSource; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.*; +import java.math.BigDecimal; +import java.util.*; + +/** + * 最基础的工具类 + */ +public class +Util { + + private static final Logger log = LoggerFactory.getLogger(Util.class); + + /** + * byte数组倒序 + * + * @param bytes + * @return + */ + public static final byte[] byteArrayDesc(byte[] bytes) { + if (Util.isEmpty(bytes)) { + return new byte[0]; + } + int len = bytes.length; + byte[] tmp = new byte[len]; + for (int i = len - 1; i >= 0; i--) { + tmp[len - i - 1] = bytes[i]; + } + return tmp; + + } + + /** + * 打印程序耗时 + * + * @param msg + */ + public static final void consume(long start, long end, String msg) { + System.err.println(String.format("%s consume times: %s=%s-%s", msg, (end - start), end, start)); + } + + /** + * 将非法且不可见的XML字符转为空格(空格的的ascii码是32) + * + * @param bts byte[] + * @return 转换后的 byte[] + */ + public static final byte[] convertInvalidInvisibleToSpace(byte[] bytes) { + if (Util.isEmpty(bytes)) { + return new byte[0]; + } + byte[] tmp = new byte[bytes.length]; + for (int i = 0; i < bytes.length; i++) { + byte b = bytes[i]; + if (b >= 0 && b <= 8 || b >= 14 && b <= 31 || b == 11 || b == 12 || b == 127) { + tmp[i] = 32; + } else { + tmp[i] = bytes[i]; + } + } + return tmp; + } + + /** + * 将非法且不可见的XML字符转为空格 + */ + public static final String convertInvalidInvisibleToSpace(String str) { + if (Util.isEmpty(str)) { + return ""; + } + return new String(Util.convertInvalidInvisibleToSpace(str.getBytes())); + } + + /** + * 将非法且不可见的XML字符转为空格 + * + * @param bts byte[] + * @return 转换后的 byte[] + */ + public static final byte[] convertInvalidInvisibleXml(byte[] bytes) { + if (Util.isEmpty(bytes)) { + return new byte[0]; + } + byte[] tmp = new byte[bytes.length]; + for (int i = 0; i < bytes.length; i++) { + byte b = bytes[i]; + if (b >= 0 && b <= 8)// \\x00-\\x08 + { + tmp[i] = 32; + } else if (b >= 14 && b <= 31)// \\x0e-\\x1f + { + tmp[i] = 32; + } else if (b == 11 || b == 12 || b == 127)// \\x0b-\\x0c and \\x7f + { + tmp[i] = 32; + } else { + tmp[i] = bytes[i]; + } + } + return tmp; + } + + /** + * 删除临时文件. + * + * @param dir + */ + public static final void deleteTempFile(String dir) { + File tf = new File(dir); + String[] childs = tf.list(); + for (String child : childs) { + new File(dir + File.separator + child).delete(); + } + } + + /** + * 读取文件路径,返回DataSource容器 + * + * @param attFiles + * @return + */ + //public static final List fileDirsToDatasource(List attFiles) { + // DataSource dc = null; + // List dcs = new ArrayList(); + // if (isNotEmpty(attFiles)) { + // for (File file : attFiles) { + // dc = new FileDataSource(file); + // dcs.add(dc); + // } + // } + // return dcs; + //} + + /** + * 得到数组中非空元素的数量 + * + * @param arr + * @return + */ + public static final int getArraySizeWithoutEmptyElement(String[] arr) { + if (Util.isEmpty(arr)) { + return 0; + } + int count = 0; + for (String s : arr) { + if (!Util.isEmpty(s)) { + count++; + } + } + return count; + } + + /** + * 指定cookie的key,获取对应的value。 + * + * @param request + * @param key cookie key + * @return cookie value + */ + //public static final String getCookieValue(HttpServletRequest request, String key) { + // if (Util.isAnyEmpty(key, request)) { + // return ""; + // } + // Cookie[] cookies = request.getCookies(); + // if (Util.isNotEmpty(cookies)) { + // for (Cookie c : cookies) { + // if (key.equals(c.getName())) { + // if (Util.isNotEmpty(c.getValue())) { + // return c.getValue(); + // } + // break; + // } + // } + // } + // return ""; + //} + + /** + * @param response + * @param key 键 + * @param value 值 + * @param maxAge 存活时间,默认-1 + * @param path 有效域,默认/ + */ + //public static void setCookie(HttpServletResponse response, String key, String value, Integer maxAge, String path) { + // Cookie cookie = new Cookie(key, value); + // + // // 设置浏览器的cookie保留时间,单位秒。负值标识浏览器关闭即删除cookie,0表示马上删除。 + // // 这里设置成一年,如果想控制登录超时,可以用ADMIN_SESSION_TIMEOUT在t_parameter中设置redis的超时时间,统一后台控制。 + // if (Util.isEmpty(maxAge)) { + // cookie.setMaxAge(-1); + // } else { + // cookie.setMaxAge(maxAge); + // } + // + // if (Util.isEmpty(path)) { + // cookie.setPath("/"); + // } else { + // cookie.setPath(path); + // } + // + // // 这两个参数是因为应付漏洞扫描加入的。 + // // 不允许前端javaScript显性读取 + // // cookie.setHttpOnly(true); + // // 只允许https请求时提交给服务器,也就是说http(测试环境)服务器端读取不到cookie。 + // // cookie.setSecure(true); + // + // response.addCookie(cookie); + //} + + /** + * 将InputStream流写入一个File对象中返回 + * + * @param inputStream + * @return + * @throws IOException + */ + //public static final File inputStreamToFile(InputStream in, String name) throws IOException { + // if (Util.isEmpty(in)) { + // return null; + // } + // if (Util.isEmpty(name)) { + // name = Util.times() + ""; + // } + // File f = new File(name); + // FileUtils.copyInputStreamToFile(in, f); + // return f; + //} + + /** + * 判断送入的字符串是否全部为中文 + * + * @param name 需判断的字符串 + * @return 判断结果返回 true为中文 false为非中文 + */ + public static final boolean isChinese(String str) { + if (Util.isEmpty(str)) { + return false; + } + str = str.replaceAll("[\u4e00-\u9FFF]", ""); + if (Util.isEmpty(str)) { + return true; + } + return false; + } + + /** + * 判断一个对象是否为空或者长度为0时返回true,否则返回false String:null或length=0返回true + * List,Set,Map:null或size=0返回true 其他null返回true + * + * @param o + * @return + */ + @SuppressWarnings("rawtypes") + public static final boolean isEmpty(Object o) { + if (null == o) { + return true; + } + if (o instanceof String) { + return (0 == ((String) o).trim().length()); + } + if (o instanceof List) { + return (0 == ((List) o).size()); + } + if (o instanceof Set) { + return (0 == ((Set) o).size()); + } + if (o instanceof Map) { + return (0 == ((Map) o).size()); + } + if (o instanceof Object[]) { + return (0 == ((Object[]) o).length); + } + return false; + } + + /** + * 判断一个对象不为null,并且长度不为0时返回true,否则返回false 规则参考Util.isEmpty() + * + * @param o + * @return + * @see #isEmpty(Object o) + */ + public static final boolean isNotEmpty(Object o) { + return !isEmpty(o); + } + + /** + * 获取非空对象,若空返回对象类型默认值 + * + * @param t + * @return + */ + public static final String getNotNull(String t) { + return isEmpty(t) ? new String("") : t; + } + + /** + * 获取非空对象,若空返回对象类型默认值 + * + * @param t + * @return + */ + public static final Character getNotNull(Character t) { + return isEmpty(t) ? new Character('\0') : t; + } + + /** + * 获取非空对象 + * + * @param t + * @return + */ + public static final Byte getNotNull(Byte t) { + byte b = 0; + return isEmpty(t) ? b : t; + } + + /** + * 获取非空对象,若空返回对象类型默认值 + * + * @param t + * @return + */ + public static final Short getNotNull(Short t) { + short s = 0; + return isEmpty(t) ? s : t; + } + + /** + * 获取非空对象,若空返回对象类型默认值 + * + * @param t + * @return + */ + public static final Integer getNotNull(Integer t) { + return isEmpty(t) ? new Integer(0) : t; + } + + /** + * 获取非空对象,若空返回对象类型默认值 + * + * @param t + * @return + */ + public static final Long getNotNull(Long t) { + return isEmpty(t) ? new Long(0) : t; + } + + /** + * 获取非空对象,若空返回对象类型默认值 + * + * @param t + * @return + */ + public static final Float getNotNull(Float t) { + return isEmpty(t) ? new Float(0) : t; + } + + /** + * 获取非空对象,若空返回对象类型默认值 + * + * @param t + * @return + */ + public static final Double getNotNull(Double t) { + return isEmpty(t) ? new Double(0) : t; + } + + /** + * 获取非空对象,若空返回对象类型默认值 + * + * @param t + * @return + */ + public static BigDecimal getNotNull(BigDecimal t) { + return isEmpty(t) ? new BigDecimal(0) : t; + } + + /** + * 倒序行读文件内容(确保文件为UTF-8编码,才能显示中文)。 + * + * @param filename 文件完整路径(确保文件必须存在并且是文件而不是路径) + * @param seekIndex 从该字节位置往前读取,初始为0,输入null时从最后一个字节往前读取 + * @param howManyLineToRead 读取行数 + * @param isNewline 返回的内容是否换行 + * @return 返回指针位置(下标0 , 用于下次继续读取 ) _读取内容 ( 下标1) + * @throws BusinessException + */ + //public static final List readLineDesc(String filename, Long seekIndex, int howManyLineToRead, + // Boolean isNewline) throws BusinessException { + // + // if (Util.isEmpty(filename)) { + // return null; + // } + // + // if (seekIndex != null && seekIndex <= 0 || howManyLineToRead <= 0) { + // return null; + // } + // + // RandomAccessFile rf = null; + // StringBuffer result = new StringBuffer(""); + // List temResult = new ArrayList(); + // int lineCount = 0; + // List seekIndex_resultStr = null; + // String r = ""; + // if (isNewline) { + // r = "\n"; + // } + // + // try { + // rf = new RandomAccessFile(filename, "r"); + // // 计算文件指针位置,默认是文件字节数-1,即从最后一个字节开始 + // long nextend = 0L; + // if (seekIndex == null) { + // // 得到文件最后一个字节的指针,指向该字节下方 + // nextend = rf.length() - 1; + // } else { + // nextend = seekIndex; + // } + // + // int c = -1; + // ByteArrayOutputStream out = new ByteArrayOutputStream(); + // while (nextend >= 0 && howManyLineToRead > lineCount) { + // rf.seek(nextend); + // // 读取指针指向的字节 + // c = rf.read(); + // // 如果该字节的ascii码与换行符的ascii码相等 + // if ((c == '\n') && nextend != 0) { + // lineCount++; + // String line = Util.trim(new String(byteArrayDesc(out.toByteArray()))); + // if (Util.isNotEmpty(line)) { + // temResult.add(line + r); + // } + // out = new ByteArrayOutputStream(); + // } else { + // out.write(c); + // } + // if (nextend == 0) {// 当文件指针退至文件开始处,输出第一行 + // String line = Util.trim(new String(byteArrayDesc(out.toByteArray()))); + // if (Util.isNotEmpty(line)) { + // temResult.add(line + r); + // } + // log.info("Have seek to the first byte."); + // break; + // } + // nextend--; + // } + // seekIndex_resultStr = new ArrayList(); + // seekIndex_resultStr.add(nextend + ""); + // for (int i = temResult.size() - 1; i >= 0; i--) { + // if (i == 0) { + // result.append(temResult.get(i).trim()); + // } else { + // result.append(temResult.get(i)); + // } + // } + // seekIndex_resultStr.add(result.toString()); + // return seekIndex_resultStr; + // } catch (FileNotFoundException e) { + // throw new BusinessException("Can't locate file " + filename + ",", e); + // } catch (IOException e) { + // throw new BusinessException("Read data from file " + filename + " failed,", e); + // } finally { + // try { + // if (rf != null) { + // rf.close(); + // } + // } catch (IOException e) { + // throw new BusinessException("Close file " + filename + " failed,", e); + // } + // } + //} + + /** + * 获取当前时间毫秒数 + * + * @return + */ + public static final long times() { + return System.currentTimeMillis(); + } + + /** + * 强化trim() + * + * @param val + * @return + */ + public static final String trim(String val) { + if (isEmpty(val)) { + return ""; + } else { + return val.trim(); + } + } + + /** + * 将File f对象写到服务器本地 + * + * @param f + * @param dir 服务器本地路径,可以是一个目录路径,也可以是一个文件路径 如果是目录路径,此目录若不存在,程序自动创建 + * 如果是文件路径,程序会自动创建此文件,包括不存在的目录,并将File + * f写入此文件中,注意,文件路径必须有文件后缀,否则被认为是目录路径 如果是目录路径,文件名是f.getName(); + * 如果是文件路径,文件名为路径中指定的文件名; + * 路径如果不存在,创建的时候,如果此路径为绝对路径,那么创建相应的绝对路径,如果是相对路径,那么相对于用户的user目录,参考File + * getAbsolutePath()方法 + * @throws IOException + * @throws BusinessException + */ + //public static final void writeFileToLocalDir(File f, String dir) throws IOException, BusinessException { + // if (Util.isEmpty(f)) { + // throw new BusinessException("Util.writeFileToLocalDir()'s File f is null."); + // } + // if (Util.isEmpty(dir)) { + // throw new BusinessException("Util.writeFileToLocalDir()'s File dir is empty."); + // } + // File dirf = new File(dir); + // if (!dirf.exists()) { + // String[] dirArr = dirf.getAbsolutePath().split("\\" + File.separator); + // String lastDir = dirArr[dirArr.length - 1]; + // if (!lastDir.contains(".")) { + // dirf = new File(dirf.getAbsolutePath() + File.separator + f.getName()); + // } + // } else if (dirf.exists() && dirf.isDirectory()) { + // dirf = new File(dirf.getAbsolutePath() + File.separator + f.getName()); + // } + // FileUtils.writeByteArrayToFile(dirf, FileUtils.readFileToByteArray(f)); + //} + + /** + * 将字符串的首字母变小写 + * + * @param s + * @return + */ + public static final String lowerCaseFirstChar(String s) { + if (Util.isEmpty(s)) { + return ""; + } + return s.substring(0, 1).toString().toLowerCase() + s.substring(1).toString(); + } + + /** + * 将字符串的首字母变大写 + * + * @param s + * @return + */ + public static final String upperCaseFirstChar(String s) { + if (Util.isEmpty(s)) { + return ""; + } + return s.substring(0, 1).toString().toUpperCase() + s.substring(1).toString(); + } + + /** + * 只要其中一个对象是empty,返回true,否则返回false + * + * @param o + * @return + */ + public static final boolean isAnyEmpty(Object... o) { + for (int i = 0; i < o.length; i++) { + Object obj = o[i]; + if (Util.isEmpty(obj)) { + return true; + } + } + return false; + } + + /** + * 所有对象都不为empty,返回true,否则返回false + * + * @param o + * @return + */ + public static final boolean isNotAnyEmpty(Object... o) { + return !isAnyEmpty(o); + } + + /** + * 获取UUID,纳秒+uuid,用纳秒开头是为了让记录可以使用这个uuid进行排序 + */ + public static final String getUUID() { + return System.nanoTime() + UUID.randomUUID().toString().replaceAll("\\-", ""); + } + + /** + * 使用不可见字符串Constant.unsee0x01将字符串隔开,重新组成一串字符串 + * + * @param strs + * @return + */ + public static final String joinStringWithUnseeChar(Collection strs) { + return joinStringWithSplitor(strs, String.valueOf((char) 0x01)); + } + + /** + * 用任何分隔符将字符串容器中的字符串组成一串字符串。 + * + * @param strs + * @param splitor + * @return + */ + public static final String joinStringWithSplitor(Collection strs, String splitor) { + if (Util.isEmpty(strs)) { + return ""; + } + StringBuffer res = new StringBuffer(""); + for (String s : strs) { + res.append(s + splitor); + } + if (Util.isNotEmpty(res.toString())) { + return res.toString().substring(0, res.toString().length() - 1); + } + return res.toString(); + } + + /** + * 使用不可见字符串Constant.unsee0x01将字符串隔开,重新组成一串字符串 + * + * @param strs + * @return + */ + public static final String joinStringWithUnseeChar(String[] strs) { + return joinStringWithSplitor(strs, String.valueOf((char) 0x01)); + } + + /** + * 用任何分隔符将字符串数组中的字符串组成一串字符串。 + * + * @param strs + * @param splitor + * @return + */ + public static final String joinStringWithSplitor(String[] strs, String splitor) { + if (Util.isEmpty(strs)) { + return ""; + } + StringBuffer res = new StringBuffer(""); + for (String s : strs) { + res.append(s + splitor); + } + if (Util.isNotEmpty(res.toString())) { + return res.toString().substring(0, res.toString().length() - 1); + } + return res.toString(); + } + + /** + * 去除字符串数组中的空串 + * + * @param split + * @return + */ + public static final String[] removeEmptyElements(String[] split) { + List l = new ArrayList(); + for (String s : split) { + if (Util.isNotEmpty(s)) { + l.add(s); + } + } + String[] res = new String[l.size()]; + return l.toArray(res); + } + + /** + * 去除队列中为null的元素 + * + * @param list + * @return + */ + @SuppressWarnings("unchecked") + public static List removeNull(List list) { + if (Util.isEmpty(list)) { + return (List) list; + } + list.removeAll(Collections.singleton(null)); + List res = (List) list; + return res; + } + + /** + * 去除队列中为null的元素 + * + * @param list + * @return + */ + @SuppressWarnings("unchecked") + public static List removeEmpty(List list) { + if (Util.isEmpty(list)) { + return (List) list; + } + List newList = new ArrayList(); + for (T t : list) { + if (Util.isNotEmpty(t)) { + newList.add(t); + } + } + return newList; + } + + /** + * 获取文件后缀 + * + * @param fileName 文件名,File对象调用getName(); + * @return + */ + public static final String getSuffix(String fileName) { + if (Util.isEmpty(fileName)) { + return ""; + } + fileName = getRealFileName(fileName); + return fileName.substring(fileName.lastIndexOf(".")); + } + + /** + * 获取文件后缀(连后缀的最开始的“.”都不要了) + * + * @param fileName 文件名,File对象调用getName(); + * @return + */ + public static final String getSuffixWithoutDot(String fileName) { + if (Util.isEmpty(fileName)) { + return ""; + } + fileName = getRealFileName(fileName); + return fileName.substring(fileName.lastIndexOf(".") + 1); + } + + /** + * 获取除掉后缀的文件名 + * + * @param fileName 文件名,File对象调用getName(); + * @return + */ + public static final String getFileNameIgnoreSuffix(String fileName) { + if (Util.isEmpty(fileName)) { + return ""; + } + fileName = getRealFileName(fileName); + return fileName.substring(0, fileName.lastIndexOf(".")); + } + + /** + * 获取文件名,有些文件名是一串路径,有些是正确的文件名,这个方法做兼容。 + * 比如IE上传到后台接收到的MultipartFile的getOriginalFilename就是一串路径。 + * + * @return 最终的文件名 + */ + public static final String getRealFileName(String fileName) { + if (fileName.indexOf("/") != -1) { + fileName = fileName.substring(fileName.lastIndexOf("/") + 1); + } + if (fileName.indexOf("\\") != -1) { + fileName = fileName.substring(fileName.lastIndexOf("\\") + 1); + } + return fileName; + } + + /** + * 删除第一个字符以及最后一个字符 + * + * @return + */ + public static final String removeFirstLastChar(String s) { + if (Util.isEmpty(s)) { + return ""; + } + return s.substring(1, s.length() - 1); + } + + /** + * spring mvc专用下载 + * + * @param response + * @param request + * @param name 客户端附件下载的名字 + * @param b 下载数据 + * @throws IOException + * @throws BusinessException + */ + //public static final ResponseEntity write(String name, byte[] b) throws IOException, BusinessException { + // try { + // HttpHeaders headers = new HttpHeaders(); + // headers.setContentType(MediaType.APPLICATION_OCTET_STREAM); + // String downloadFileName = new String(name.getBytes(Constant.ENCODING), "iso-8859-1");// 设置编码 + // headers.setContentDispositionFormData("attachment", downloadFileName); + // return new ResponseEntity(b, headers, HttpStatus.CREATED); + // } catch (Exception e) { + // throw new BusinessException(e); + // } + //} + + /** + * 将Map的value中的String[]变成以Constant.unsee0x01隔开的字符串。 原因是Controller不能传递Map的形式到WS层方法, 因为不想做那么复杂,当前只支持用j_m_m_xxx传递Map + * 的变量,所以当分页的时候,需要传递的searchParams就是Map + * 形式的,所以需要将Map里面的String[]变成字符串,从而整个变量就能使用j_map_xxx来传递 + * + * @param j_map_searchParams + * @return + */ + //public static Map convertMapArrayToStirng(Map j_map_searchParams) { + // Map res = new HashMap(); + // if (Util.isEmpty(j_map_searchParams)) { + // return res; + // } + // for (Entry en : j_map_searchParams.entrySet()) { + // if (Util.isNotEmpty(en.getValue())) { + // if (en.getValue() instanceof String[]) { + // res.put(en.getKey(), Util.joinStringWithUnseeChar((String[]) en.getValue())); + // } else { + // res.put(en.getKey(), (String) en.getValue()); + // } + // } + // } + // return res; + //} + + /** + * 格式化文件的大小,如果小于1024kb,直接显示kb,如果大于1024kb,换算成mb进行显示,以此类推。 + * + * @param size 传一个b为单位的数字 + * @return + */ + public static String formatFileSize(long size) { + String fileSize = ""; + double kbNum = new BigDecimal(size).divide(new BigDecimal(1024), 2, BigDecimal.ROUND_HALF_UP).doubleValue(); + if (1024 < kbNum) { + double mbNum = new BigDecimal(kbNum).divide(new BigDecimal(1024), 2, BigDecimal.ROUND_HALF_UP) + .doubleValue(); + if (1024 < mbNum) { + double gbNum = new BigDecimal(mbNum).divide(new BigDecimal(1024), 2, BigDecimal.ROUND_HALF_UP) + .doubleValue(); + fileSize = gbNum + "GB"; + } else { + fileSize = mbNum + "MB"; + } + } else { + fileSize = kbNum + "KB"; + } + return fileSize; + } + + /** + * 判断字符串中字母的个数 + */ + public static int getAlphaCharLength(String str) { + int count = 0; + for (int i = 0; i < str.length(); i++) { + if (str.charAt(i) * 1 >= 65 && str.charAt(i) * 1 <= 90 + || str.charAt(i) * 1 >= 97 && str.charAt(i) * 1 <= 122) { + count++; + } + } + return count; + } + + /** + * 判断字符串中数字个数 + */ + public static int getNumLength(String str) { + int count = 0; + for (int i = 0; i < str.length(); i++) { + if (str.charAt(i) * 1 >= 48 && str.charAt(i) * 1 <= 57) { + count++; + } + } + return count; + } + + ; + + /** + * 判断字符串中特殊字符个数。 + */ + public static int getSpecialCharLength(String str) { + int count = 0; + for (int i = 0; i < str.length(); i++) { + if (str.charAt(i) * 1 < 48 || str.charAt(i) * 1 > 57 && str.charAt(i) * 1 < 65 + || str.charAt(i) * 1 > 90 && str.charAt(i) * 1 < 97 || str.charAt(i) * 1 > 122) { + count++; + } + } + return count; + } + + ; + + /** + * 判断字符串是否包含大小写。 + */ + public static boolean containBigAndSmall(String str) { + boolean big = false; + boolean small = false; + for (int i = 0; i < str.length(); i++) { + if (str.charAt(i) * 1 >= 65 && str.charAt(i) * 1 <= 90) { + big = true; + break; + } + } + for (int i = 0; i < str.length(); i++) { + if (str.charAt(i) * 1 >= 97 && str.charAt(i) * 1 <= 122) { + small = true; + break; + } + } + if (big && small) { + return true; + } + return false; + } + + /** + * 简单混淆字符串中字符。 当前是0位和1位调换,最后两位字符调换。 + * + * @param str + * @return + */ + public static String confusionStr(String str) { + char[] c = str.toCharArray(); + char a = c[1]; + c[1] = c[0]; + c[0] = a; + a = c[c.length - 1]; + c[c.length - 1] = c[c.length - 2]; + c[c.length - 2] = a; + return new String(c); + } + + /** + * 获取某个范围的随机数。 如获取0-5的随机数,minRang:0,maxRang:5, 如获取5-50的随机数,minRang:5,maxRang:50 + * + * @param minRang + * @param maxRang + * @return + */ + public static int getRandomIntInRang(int minRang, int maxRang) { + Random rand = new Random(); + int randNum = rand.nextInt(maxRang + 1) + minRang; + return randNum; + } + + /** + * 随机获取a-z,0-9中的n个字符作为验证码。 + * + * @return + */ + public static String getVerifyCodeChars(int n) { + char[] c = new char[]{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', + 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0'}; + int randNum = -1; + char[] resc = new char[n]; + for (int i = 0; i < n; i++) { + randNum = getRandomIntInRang(0, 35); + try { + resc[i] = c[randNum]; + } catch (Exception e) { + resc[i] = 'p'; + continue; + } + } + return new String(resc); + } + + /** + * 随机获取0-9中的n个字符作为验证码。一般用作短信验证码时。 + * + * @return + */ + public static String getVerifyCodeCharsOnliNum(int n) { + char[] c = new char[]{'1', '2', '3', '4', '5', '6', '7', '8', '9', '0'}; + int randNum = -1; + char[] resc = new char[n]; + for (int i = 0; i < n; i++) { + randNum = getRandomIntInRang(0, 9); + try { + resc[i] = c[randNum]; + } catch (Exception e) { + resc[i] = '8'; + continue; + } + } + return new String(resc); + } + + /** + * 去除空元素后再join + * 和javaScript的数组的join方法作用一样,主要用于将数组的元素用指定的连接字符相连成一行字符串, + * 放在searchParams中的IN参数中进行查询,如["a", "b", 1]→"a,b,1" + * + * @param array + * @param joinStr 链接字符串,可以是单个字符,也可以是多个字符的字符串,也可以是空串。 + * @return + */ + public static String joinIgnoreEmpty(List array, String joinStr) { + array = removeEmpty(array); + return join(array, joinStr); + } + + public static void main(String[] args) { + List s = new ArrayList(); + s.add("a"); + s.add("b"); + s.add(""); + s.add("d"); + System.out.println(joinIgnoreEmpty(s, ",")); + } + + /** + * 和javaScript的数组的join方法作用一样,主要用于将数组的元素用指定的连接字符相连成一行字符串, + * 放在searchParams中的IN参数中进行查询,如["a", "b", 1]→"a,b,1" + * + * @param array + * @param joinStr 链接字符串,可以是单个字符,也可以是多个字符的字符串,也可以是空串。 + * @return + */ + public static String join(List array, String joinStr) { + if (Util.isEmpty(array)) { + return ""; + } else { + StringBuffer sb = new StringBuffer(""); + for (T o : array) { + String val = ""; + if (Util.isNotEmpty(o)) { + val = o.toString(); + } + sb.append(val + joinStr); + } + if (Util.isNotEmpty(sb.toString())) { + return sb.toString().substring(0, sb.toString().length() - joinStr.length()); + } + } + return ""; + } + + /** + * 将字符串按照正则匹配分割 + */ + public static List cut(String str, String regex) { + List list = new ArrayList(); + if (Util.isEmpty(regex)) { + return list; + } + String[] ss = str.split(regex); + for (String s : ss) { + list.add(s); + } + return list; + } + + /** + * 将字符串按指定分割符切分成多段并转成List + * + * @param string + * @param splitStr + * @return + */ + public static List convertToList(String string, String splitStr) { + List list = new ArrayList(); + if (Util.isEmpty(string)) { + return list; + } + String[] strs = string.split(splitStr); + if (Util.isEmpty(strs)) { + return list; + } + for (String str : strs) { + list.add(str); + } + return list; + } + + /** + * HTML字符转义 + * + * @return String 过滤后的字符串 + * @see 对输入参数中的敏感字符进行过滤替换,防止用户利用JavaScript等方式输入恶意代码 + * @see String input = + * @see HtmlUtils.htmlEscape(input); //from spring.jar + * @see StringEscapeUtils.escapeHtml(input); //from commons-lang.jar + * @see 尽管Spring和Apache都提供了字符转义的方法,但Apache的StringEscapeUtils功能要更强大一些 + * @see StringEscapeUtils提供了对HTML,Java,JavaScript, SQL ,XML等字符的转义和反转义 + * @see 但二者在转义HTML字符时,都不会对单引号和空格进行转义,而本方法则提供了对它们的转义 + */ + public static String htmlEscape(String input) { + if (isEmpty(input)) { + return input; + } + input = input.replaceAll("&", "&"); + input = input.replaceAll("<", "<"); + input = input.replaceAll(">", ">"); + input = input.replaceAll(" ", " "); + input = input.replaceAll("'", "'"); // IE暂不支持单引号的实体名称,而支持单引号的实体编号,故单引号转义成实体编号,其它字符转义成实体名称 + input = input.replaceAll("\"", """); // 双引号也需要转义,所以加一个斜线对其进行转义 + input = input.replaceAll("\n", "
"); // 不能把\n的过滤放在前面,因为还要对<和>过滤,这样就会导致
失效了 + return input; + } + + /** + * Controller端获取searchParams + * + * @param request + * @return + */ + //public static Map getSearchParams(HttpServletRequest request) { + // Map searchParams = WebUtil.getParametersStartingWith(request, WebConstant.search); + // return searchParams; + //} + + /** + * Controller端获取searchParams (value not empty) + * + * @param request + * @return + */ + //public static Map getSearchParamsNotEmpty(HttpServletRequest request) { + // Map searchParams = getSearchParams(request); + // Map searchParamsCopy = new HashMap(searchParams); + // Set> entrys = searchParamsCopy.entrySet(); + // for (Entry entry : entrys) { + // if (Util.isEmpty(entry.getValue())) { + // searchParams.remove(entry.getKey()); + // } + // } + // return searchParams; + //} + + /** + * Controller端获取sortParams + * + * @param request + * @return + */ + //public static Map getSortParams(HttpServletRequest request) { + // Map sortParams = WebUtil.getParametersStartingWith(request, WebConstant.sort); + // return sortParams; + //} + + /** + * Controller端获取sortParams (value not empty) + * + * @param request + * @return + */ + //public static Map getSortParamsNotEmpty(HttpServletRequest request) { + // Map sortParams = getSortParams(request); + // Map sortParamsCopy = new HashMap(sortParams); + // Set> entrys = sortParamsCopy.entrySet(); + // for (Entry entry : entrys) { + // if (Util.isEmpty(entry.getValue())) { + // sortParams.remove(entry.getKey()); + // } + // } + // return sortParams; + //} + + /** + * java通用下载。如果使用spring mvc框架,请用spring mvc的专用下载。 + * 将数据写到客户端,供客户下载,比如客户端各种文件附件的下载,以及客户端图片的显示都是用这个方法输出到客户端。 + * + * @param response + * @param request + * @param name 客户端附件下载的名字 + * @param b 下载数据 + * @throws IOException + * @throws BusinessException + * @see #write(String, byte[]) + */ + //public static final void write(HttpServletRequest request, HttpServletResponse response, String name, byte[] b) + // throws IOException, BusinessException { + // OutputStream out = null; + // try { + // out = response.getOutputStream(); + // response.setContentType("APPLICATION/OCTET-STREAM"); + // response.setCharacterEncoding(Constant.ENCODING); + // response.setHeader("Pragma", "public"); + // response.setHeader("Cache-Control", "max-age=0"); + // String agent = (String) request.getHeader("USER-AGENT"); + // if (agent != null && agent.indexOf("MSIE") == -1) { + // // FF + // String enableFileName = "=?" + Constant.ENCODING + "?B?" + // + (new String(Base64.encodeBase64(name.getBytes(Constant.ENCODING)))) + "?="; + // response.setHeader("Content-Disposition", "attachment; filename=" + enableFileName); + // } else { + // // IE + // String enableFileName = new String(name.getBytes("GBK"), "ISO-8859-1"); + // response.setHeader("Content-Disposition", "attachment; filename=" + enableFileName); + // } + // out.write(b); + // } catch (Exception e) { + // throw new BusinessException(e); + // } finally { + // IOUtil.flush(out); + // IOUtil.close(out); + // } + //} + + /** + * 从字节数组中的start下标开始(包括start)下标,获取len个元素,返回一个新的数组。 + * + * @param b + * @param start + * @param len + * @return + */ + public static byte[] getTargetBytes(byte[] b, int start, int len) { + byte[] newB = new byte[len]; + int i = 0; + if (Util.isNotEmpty(b)) { + while (true) { + if (i == len) { + break; + } + newB[i] = b[start]; + i++; + start++; + } + } + return newB; + } + + /** + * 辅助方法 数组元素 + 前缀 + 分隔符 然后拼接起来 如 参数分别是 [AA,BB,CC], TDD, -- 返回结果是 + * TDDAA--TDDBB--TDDCC + * + * @param deviceIds + * @param prefix + * @return + */ + public static String join(List array, String prefix, String joinStr) { + if (array == null || prefix == null || joinStr == null) { + return ""; + } + StringBuilder buff = new StringBuilder(); + for (int i = 0; i < array.size(); i++) { + if (i == array.size() - 1) { + buff.append(prefix + array.get(i)); + } else { + buff.append(prefix + array.get(i) + joinStr); + } + } + return buff.toString(); + } + + /** + * 拼接sql中的in参数 + * + * @param + * @param array + * @param joinStr + * @return + */ + public static String joinInStr(List array) { + if (Util.isAnyEmpty(array)) { + return ""; + } + StringBuilder buff = new StringBuilder(); + for (int i = 0; i < array.size(); i++) { + if (Util.isNotEmpty(array.get(i))) { + if (i == array.size() - 1) { + buff.append("'" + array.get(i) + "'"); + } else { + buff.append("'" + array.get(i) + "',"); + } + } + } + return buff.toString(); + } + + /** + * 辅助方法 数组元素 + 前缀 然后返回 如 参数分别是 [AA,BB,CC], TDD 返回结果是 [TDDAA,TDDBB,TDDCC] + * + * @param deviceIds + * @param prefix + * @return + */ + @SuppressWarnings("unchecked") + public static List joinPrefix(List array, String prefix) { + if (array == null || prefix == null) { + return new ArrayList<>(); + } + List newArray = new ArrayList(array.size()); + for (T t : array) { + newArray.add((T) (prefix + t)); + } + return newArray; + } + + /** + * 从0度开始为正北方 + *

+ * Title: degrees + *

+ *

+ * Description:根据角度转化成中文风向,16位陆地 + *

+ * + * @param degrees + * @return + * @author dorry + */ + public static String degrees(Double degrees) { + if (348.75 <= degrees && degrees <= 360) { + return "北"; + } else if (0 <= degrees && degrees <= 11.25) { + return "北"; + } else if (11.25 < degrees && degrees <= 33.75) { + return "东北偏北"; + } else if (33.75 < degrees && degrees <= 56.25) { + return "东北"; + } else if (56.25 < degrees && degrees <= 78.75) { + return "东北偏东"; + } else if (78.75 < degrees && degrees <= 101.25) { + return "东"; + } else if (101.25 < degrees && degrees <= 123.75) { + return "东南偏东"; + } else if (123.75 < degrees && degrees <= 146.25) { + return "东南"; + } else if (146.25 < degrees && degrees <= 168.75) { + return "东南偏南"; + } else if (168.75 < degrees && degrees <= 191.25) { + return "南"; + } else if (191.25 < degrees && degrees <= 213.75) { + return "西南偏南"; + } else if (213.75 < degrees && degrees <= 236.25) { + return "西南"; + } else if (236.25 < degrees && degrees <= 258.75) { + return "西南偏西"; + } else if (258.75 < degrees && degrees <= 281.25) { + return "西"; + } else if (281.25 < degrees && degrees <= 303.75) { + return "西北偏西"; + } else if (303.75 < degrees && degrees <= 326.25) { + return "西北"; + } else if (326.25 < degrees && degrees < 348.75) { + return "西北偏北"; + } else { + return "无风"; + } + } + + /** + * velocity模板得到字符串,模板为绝对路径文件 + * + * @param data + * @param templateDir + * @return + */ + //public static final String getStringFromVelocityEngineByAbsuluteDir(Map data, String templateDir) { + // File templateFile = new File(templateDir); + // if (!templateFile.exists()) { + // return ""; + // } + // VelocityEngine ve = new VelocityEngine(); + // Properties p = new Properties(); + // p.put(Velocity.FILE_RESOURCE_LOADER_PATH, templateFile.getParent()); + // ve.init(p); + // Template t = ve.getTemplate(templateFile.getName(), Constant.ENCODING); + // return getStringFromVelocityEngine(data, t, templateFile); + //} + + /** + * velocity模板得到字符串,模板为相对路径文件 + * + * @param data + * @param templateDir + * @return + */ + //public static final String getStringFromVelocityEngineByRelationDir(Map data, String templateDir) { + // File templateFile = new File(templateDir); + // if (!templateFile.exists()) { + // return ""; + // } + // VelocityEngine ve = new VelocityEngine(); + // Template t = ve.getTemplate(templateFile.getPath(), Constant.ENCODING); + // return getStringFromVelocityEngine(data, t, templateFile); + //} + + /** + * velocity模板得到字符串,实现 + * + * @param data + * @param t + * @param templateFile + * @return + */ + //private static final String getStringFromVelocityEngine(Map data, Template t, File templateFile) { + // VelocityContext context = new VelocityContext(); + // if (!Util.isEmpty(data)) { + // for (Entry e : data.entrySet()) { + // context.put(e.getKey(), e.getValue()); + // } + // } + // StringWriter writer = new StringWriter(); + // t.merge(context, writer); + // log.debug(String.format("Velocity string generate for template[%s]:\n%s", templateFile.getAbsolutePath(), + // writer.toString())); + // return writer.toString(); + //} + + /** + * 读取配置文件中的值 + * 前提是确认Application.java中读取配置文件的编码是ISO8859-1 + * + * @param env + * @param key + * @return + * @throws UnsupportedEncodingException + */ + //public static String getProperty(Environment env, String key) throws UnsupportedEncodingException { + // return new String(env.getProperty(key).getBytes("ISO8859-1"), "UTF-8"); + //} + + /** + * 过滤前端参数中的javaScript脚本和html标签,防止xss攻击 + * + * @param value + * @return + */ + public static String filterDangerString(String value) { + if (Util.isEmpty(value)) { + return ""; + } + value = value.replaceAll("<", "<"); + value = value.replaceAll(">", ">"); + return value; + } + + /** + * 随机生成密钥 + * + * @return + */ + public static String getAESRandomKey(int length) { + + ArrayList strList = new ArrayList(); + + int begin = 97; + // 生成小写字母,并加入集合 + for (int i = begin; i < begin + 26; i++) { + strList.add((char) i + ""); + } + // 生成大写字母,并加入集合 + begin = 65; + for (int i = begin; i < begin + 26; i++) { + strList.add((char) i + ""); + } + // 将0-9的数字加入集合 + for (int i = 0; i < 10; i++) { + strList.add(i + ""); + } + + Random random = new Random(); + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < length; i++) { + int size = strList.size(); + String randomStr = strList.get(random.nextInt(size)); + sb.append(randomStr); + } + return sb.toString(); + } + + /** + * 将字符串转Integer + * + * @return + */ + public static Integer getInteger(String val) { + if (Util.isEmpty(val)) { + return 0; + } + + val = val.replaceAll("[^\\-0-9]", ""); + + if (!isNumber(val)) { + return 0; + } + + try { + return Integer.parseInt(val); + } catch (Exception e) { + return 0; + } + } + + /** + * 判断一个字符串是不是数值型字符串,包括正负小数 + * + * @param content + * @return + */ + public static boolean isNumber(String content) { + if (Util.isEmpty(content)) { + return false; + } + + String pattern = "^[\\+\\-]?[\\d]+(\\.[\\d|E]+)?$"; + return content.matches(pattern); + } + + /** + * 把输入流的内容转化成字符串 + * + * @param is + * @return + * @throws IOException + */ + public static String readInputStream(InputStream is) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + int length = 0; + byte[] buffer = new byte[1024]; + while ((length = is.read(buffer)) != -1) { + baos.write(buffer, 0, length); + } + is.close(); + baos.close(); + return baos.toString(); + } + + /** + * hibernate返回的list map中的map是不能直接修改的,如果想修改,需要重新new一个新的list map + * + * @param result + * @return + */ + public static List> newListMap(List> result) { + List> tempList = new ArrayList>(); + if (Util.isNotEmpty(result)) { + for (Map item : result) { + tempList.add(new HashMap(item)); + } + } + return tempList; + } + +} + diff --git a/src/main/java/cn/van333/mt2ql/wxMessage/utils/WxMessageConsumer.java b/src/main/java/cn/van333/mt2ql/wxMessage/utils/WxMessageConsumer.java new file mode 100644 index 0000000..72543f6 --- /dev/null +++ b/src/main/java/cn/van333/mt2ql/wxMessage/utils/WxMessageConsumer.java @@ -0,0 +1,289 @@ +package cn.van333.mt2ql.wxMessage.utils; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.hutool.http.HttpRequest; +import cn.van333.mt2ql.wxMessage.enums.EventType; +import cn.van333.mt2ql.wxMessage.model.WxMessage; +import cn.van333.mt2ql.wxMessage.model.WxMessageDataForChat; +import cn.van333.mt2ql.wxMessage.model.WxUser; +import cn.van333.mt2ql.wxMessage.service.WxMessageDataForChatService; +import cn.van333.mt2ql.wxMessage.service.WxUserService; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +import static cn.hutool.core.thread.ThreadUtil.sleep; + +/** + * @author Leo + * @version 1.0 + * @create 2023/12/19 0019 上午 11:03 + * @description: + */ +@Component +public class WxMessageConsumer { + public static final String QL_BASE_URL = "http://134.175.126.60:45700"; + public static final String GET_TOKEN = QL_BASE_URL + "/open/auth/token"; + + //client_id + public static final String CLIENT_ID = "Ouv_S9gk5LpV"; + //client_secret + public static final String CLIENT_SECRET = "1pLjAIfBBzu1_UA9q-hOj778"; + public static final String QL_TOKEN_KEY = "QL_TOKEN_KEY"; + + /**/ + private static final Logger logger = LoggerFactory.getLogger(WxMessageConsumer.class); + + private static final RedisCache redisCache = SpringUtil.getBean(RedisCache.class); + /** + * 临时参数 + * 每次扣费 + */ + private static final BigDecimal priceOfMT20 = new BigDecimal("0.2"); + private final WxMessageDataForChatService wxMessageDataForChatService; + private final WxUserService wxUserService; + + @Autowired + public WxMessageConsumer(@Lazy WxMessageDataForChatService wxMessageDataForChatService, @Lazy WxUserService wxUserService) { + this.wxMessageDataForChatService = wxMessageDataForChatService; + this.wxUserService = wxUserService; + } + + @Async("threadPoolTaskExecutor") + public void consume(WxMessage wxMessage) { + logger.info("接收到消息 : {}", wxMessage); + if (wxMessage.getEvent() == null) { + return; + } + /** + * 需要处理 私聊 和 转账消息 + * 其他消息暂时不处理 + * 私聊需要解析是否美团领券 + * 转账需要对接会员系统 + * + * */ + Integer event = wxMessage.getEvent(); + if (event.equals(EventType.PRIVATE_MESSAGE.getKey())) { + handlePrivateMessage(wxMessage); + } + + } + + /** + * 处理私聊消息 + * + * @param wxMessage + */ + private void handlePrivateMessage(WxMessage wxMessage) { + /** + * 接收到消息 : WxMessage(event=10009, wxid=wxid_kr145nk7l0an31, data={"type":"D0003","des":"收到消息","data":{"timeStamp":"1703128368100","fromType":1,"msgT两次ype":1,"msgSource":0,"fromWxid":"wxid_ytpc72mdoskt22","finalFromWxid":"","atWxidList":[],"silence":0,"membercount":0,"signature":"v1_vXrWK/iB","msg":"嗨鲁个迷紫123","msgBase64":"5Zeo6bKB5Liq6L+357SrMTIz"},"timestamp":"1703128368112","wxid":"wxid_kr145nk7l0an31","port":16888,"pid":10468,"flag":"7777"}) + * 需要get 两次 data 字段*/ + JSONObject data = wxMessage.getData().getJSONObject("data"); + if (data == null) { + return; + } + + + System.out.println("+++++++++++"+getToken()); + + /**{"type":"D0003","des":"收到消息","data":{"timeStamp":"1702957325031","fromType":1,"msgType":1,"msgSource":0,"fromWxid":"wxid_ytpc72mdoskt22","finalFromWxid":"","atWxidList":[],"silence":0,"membercount":0,"signature":"v1_OJXJYpvM","msg":"在不","msgBase64":"5Zyo5LiN"},"timestamp":"1702957325041","wxid":"wxid_kr145nk7l0an31","port":16888,"pid":10468,"flag":"7777"} + * */ + WxMessageDataForChat wxMessageDataForChat = data.to(WxMessageDataForChat.class); + + // 做业务处理 + logger.info("处理消息: {}", wxMessageDataForChat.toString()); + + /** + * timeStamp 收到这条消息的13位现行时间戳 + * fromType 来源类型:1|私聊 2|群聊 3|公众号 + * msgType 消息类型:1|文本 3|图片 34|语音 42|名片 43|视频 47|动态表情 48|地理位置 49|分享链接或附件 2001|红包 2002|小程序 2003|群邀请 10000|系统消息 + * msgSource 消息来源:0|别人发送 1|自己手机发送 + * fromWxid fromType=1时为好友wxid,fromType=2时为群wxid,fromType=3时公众号wxid + * finalFromWxid 仅fromType=2时有效,为群内发言人wxid + * atWxidList 仅fromType=2,且msgSource=0时有效,为消息中艾特人wxid列表 + * silence 仅fromType=2时有效,0 + * membercount 仅fromType=2时有效,群成员数量 + * signature 消息签名 + * msg 消息内容 + * msgBase64 消息内容的Base64 + * */ + if (Util.isAnyEmpty(wxMessageDataForChat.getMsg(), wxMessageDataForChat.getFromwxid(), wxMessageDataForChat.getFromtype())) { + logger.info("消息内容为空,不处理"); + return; + } + + String msg = wxMessageDataForChat.getMsg(); + /** + * https://i.meituan.com/mttouch/page/account + * ?userId=3822095266 + * &token=AgHdIkm2tAGHc9SQSiG7M8xCx1LbTue9D2HPOAun2eYl3ou7BeEw1uGrGZH-DxmEiUgsbA1v9SM4DQAAAAC6HAAAz0rTXmkB_CIHin08hCu68mFv5k6nUc2q6_CfZqEdBcngRK_xD8Sx5fE4rfdq-yAJ + * */ + if (msg.startsWith("美团 20-7 ")) { + logger.info("处理美团的消息"); + if (msg.contains("https://i.meituan.com/mttouch/page/account")) { + String wxid = null; + if (wxMessageDataForChat.getFromtype() == 1) { + wxid = wxMessageDataForChat.getFromwxid(); + } else if (wxMessageDataForChat.getFromtype() == 2) { + wxid = wxMessageDataForChat.getFinalfromwxid(); + } + String token = msg.substring(msg.indexOf("token=") + 6, msg.indexOf("&")); + String userId = msg.substring(msg.indexOf("userId=") + 7); + logger.info("token: {}, userId: {}", token, userId); + mt20(wxid, userId, token); + //if (runResult) { + // logger.info("领券成功"); + // String newMsg = msg.replace("https://i.meituan.com/mttouch/page/account", "https://i.meituan.com/mttouch/page/account?userId=" + userId + "&token=" + token); + // wxMessageDataForChat.setMsg(newMsg); + // wxMessageDataForChatService.updateById(wxMessageDataForChat); + //} else { + // logger.info("领券失败"); + //} + } + } + + wxMessageDataForChatService.save(wxMessageDataForChat); + + } + + /** + * @param userId + * @param token + * @return + * @throws + * @description + */ + private String mt20(String wxid, String userId, String token) { + /** + * 1 查询用户余额 + * 2 调用青龙的添加环境变量 + * 3 执行美团领券 + * 4 删除环境变量 + * 5 改写返回的消息内容返回给用户 + * */ + logger.info("查询用户余额"); + HashMap checkYuE = checkYuE(wxid); + Boolean isRun = (Boolean) checkYuE.get("isRun"); + String info = (String) checkYuE.get("info"); + BigDecimal yuE = (BigDecimal) checkYuE.get("yuE"); + + // 余额可以支持一次扣费 + if (isRun) { + // 调用青龙 成功 + return runQL(token, wxid, 1); + + } else { + // 调用青龙 失败 + logger.info("调用青龙失败"); + return info; + } + + } + + /** + * @param wxid + * @return + * @throws + * @description 根据 wxid 查询余额 + */ + private HashMap checkYuE(String wxid) { + + HashMap result = new HashMap<>(); + BigDecimal yuE = BigDecimal.ZERO; + String info = ""; + Boolean isRun = false; + WxUser wxUser = wxUserService.getOne(Wrappers.query(new WxUser()).eq("wxid", wxid)); + if (Util.isEmpty(wxUser)) { + info = "未进行过充值,请先充值后使用。"; + } + // 如果余额小于等于零 + if (wxUser.getMoneyShengyu().compareTo(BigDecimal.ZERO) <= 0) { + info = "账户余额不足,请先充值后使用。"; + } + + if (wxUser.getMoneyShengyu().compareTo(priceOfMT20) < 0) { + info = "剩余余额不足以支持本次扣费,请先充值后使用。"; + } else { + isRun = true; + } + // 返回结果 + result.put("yuE", yuE); + result.put("info", info); + result.put("isRun", isRun); + return result; + } + + /** + * @param remark + * @param time 调用次数,后期可以改成包月还是一次 ,目前都是 1 + * @param token + * @return + * @throws + * @description + */ + private String runQL(String token, String remark, Integer time) { + + /** + * 1. 在系统设置 -> 应用设置 -> 添加应用,权限目前支持5个模块,可以选择多个模块。选择一个模块之后,可读写此模块的所有接口。 + * 2. 使用生成的 client_id 和 client_secret 请求获取token接口 http://localhost:5700/open/auth/token?client_id=xxxxxx&client_secret=xxxxxxxx + * 3. 上面接口返回的token有效期为30天,可用于请求青龙的接口 curl 'http://localhost:5700/open/envs?searchValue=&t=1630032278171' -H 'Authorization: Bearer + * 接口返回的token' + * 4. openapi的接口与系统正常接口的区别就是青龙里的是/api/envs,openapi是/open/envs,即就是青龙接口中的api换成open + * */ + + + //String responseStr = HttpRequest.post(QL_BASE_URL + getToken()) + // .body(JSON.toJSONString(jsonMap))//头信息,多个头信息多次调用此方法即可 + // .execute().body(); + + System.out.println("+++++++++++"+getToken()); + return null; + + } + + private String getToken() { + + String token = redisCache.getCacheObject(QL_TOKEN_KEY); + if (StrUtil.isNotEmpty(token)) { + return token; + } else { + //HashMap map = new HashMap<>(); + //map.put("client_id", CLIENT_ID); + //map.put("client_secret", CLIENT_SECRET); + //String jsonStr = JSON.toJSONString(map); + for (int i = 0; i < 3; i++) { + //* 2. 使用生成的 client_id 和 client_secret 请求获取token接口 http://localhost:5700/open/auth/token?client_id=xxxxxx&client_secret=xxxxxxxx + String responseStr = HttpRequest.get(GET_TOKEN+"?client_id="+CLIENT_ID+"&client_secret="+CLIENT_SECRET) + .execute().body(); + if (ObjectUtil.isNotEmpty(responseStr)) { + //{"code":200,"data":{"token":"950e3060-d714-4f6a-9839-c098a116f0a8","token_type":"Bearer","expiration":1705743778}} + JSONObject jsonObject = JSON.parseObject(responseStr); + + System.out.println(jsonObject.getString("code")); + if (Objects.equals(String.valueOf(jsonObject.getString("code")), "200")) { + JSONObject response = jsonObject.getJSONObject("data"); + redisCache.setCacheObject(QL_TOKEN_KEY, (String)response.get("token"), (int)response.get("expiration"), TimeUnit.SECONDS); + return (String) response.get("token"); + } + } + sleep(500); + } + return null; + } + } + + +} diff --git a/src/main/resources/application-dev.yaml b/src/main/resources/application-dev.yaml new file mode 100644 index 0000000..7c0f88a --- /dev/null +++ b/src/main/resources/application-dev.yaml @@ -0,0 +1,25 @@ +server: + port: 5700 +spring: + application: + name: MT2QL + #数据源配置 + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://134.175.126.60:43306/MT2QL?characterEncoding=utf-8&useSSL=true&serverTimezone=GMT%2B8 + username: root + password: mysql_xWbMcG + #redis配置 + redis: + host: 134.175.126.60 + port: 46379 + database: 0 + timeout: 1800000 + lettuce: + pool: + max-active: 20 + #最大阻塞等待时间(负数表示没限制) + max-wait: -1 + max-idle: 5 + min-idle: 0 + password: jhkdjhkjdhsIUTYURTU_HQw7tC # 文件上传 diff --git a/src/main/resources/application-prod.yaml b/src/main/resources/application-prod.yaml new file mode 100644 index 0000000..3b5f901 --- /dev/null +++ b/src/main/resources/application-prod.yaml @@ -0,0 +1,22 @@ +server: + port: 18080 +spring: + application: + name: MT2QL + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://192.168.31.88:3306/MT2QL?characterEncoding=utf-8&useSSL=true&serverTimezone=GMT%2B8 + username: root + password: mysql_xWbMcG + redis: + host: 192.168.31.88 + port: 6379 + database: 2 + timeout: 1800000 + lettuce: + pool: + max-active: 20 + max-wait: -1 + max-idle: 5 + min-idle: 0 + password: jhkdjhkjdhsIUTYURTU_HQw7tC diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml new file mode 100644 index 0000000..de01aca --- /dev/null +++ b/src/main/resources/application.yaml @@ -0,0 +1,5 @@ +spring: + application: + name: MT2QL + profiles: + active: dev diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html new file mode 100644 index 0000000..f67da0a --- /dev/null +++ b/src/main/resources/static/index.html @@ -0,0 +1,6 @@ + + +

hello word!!!

+

this is a html page

+ + diff --git a/src/test/java/cn/van333/mt2ql/Mt2QlApplicationTests.java b/src/test/java/cn/van333/mt2ql/Mt2QlApplicationTests.java new file mode 100644 index 0000000..7f31ed8 --- /dev/null +++ b/src/test/java/cn/van333/mt2ql/Mt2QlApplicationTests.java @@ -0,0 +1,13 @@ +package cn.van333.mt2ql; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class Mt2QlApplicationTests { + + @Test + void contextLoads() { + } + +}