这个方法就是手动完成验证,基本上能解决90%的登录问题。登录完成后请备份device.json,下次直接套用这个device.json就行了
wssy001 发布的帖子
-
RE: 整合SpringBoot无法登录的问题
-
RE: 整合SpringBoot无法登录的问题
@xiaojiuc 在 整合SpringBoot无法登录的问题 中说:
先看一下这边的文档,添加参数“-Dmirai.slider.captcha.supported”
有安卓手机不?有的话去这边下载一个手动滑动验证的APP https://github.com/mzdluo123/TxCaptchaHelper/releases
使用步骤:
打开那个APP,输入Mirai显示的Captcha link(验证码地址)到APP中间的输入框,点击下一步,完成滑动验证。之后APP会有一个URL,你复制后输入到Mirai中,按回车键就能完成登录验证。 -
RE: Java + Springboot 2.X+ mirai最新RELEASE の采坑记录
@liuzheng2000 个人觉得对springboot熟悉的人看我写的示例应该知道怎么整合mirai core。如有疑问可以发到这儿或者论坛其他地方。
-
RE: 非Spring环境使用Mybatis Plus
旧版Mybatis Plus Generator
// 需要构建一个 代码自动生成器 对象 AutoGenerator mpg = new AutoGenerator(); // 配置策略 // 1、全局配置 GlobalConfig gc = new GlobalConfig(); String projectPath = System.getProperty("user.dir"); gc.setOutputDir(projectPath + "/src/main/java"); gc.setAuthor("Author"); gc.setOpen(false); // 是否覆盖 gc.setFileOverride(false); // 去Service的I前缀 gc.setServiceName("%sService"); gc.setMapperName("%sDao"); gc.setIdType(IdType.ASSIGN_ID); gc.setDateType(DateType.ONLY_DATE); // Swagger 2 API Doc相关注解,Knife4j亦可使用 // gc.setSwagger2(true); mpg.setGlobalConfig(gc); //2、设置数据源 DataSourceConfig dsc = new DataSourceConfig(); dsc.setUrl("jdbc:mysql://localhost:3306/back_up?useSSL=false"); // 驱动包 dsc.setDriverName("com.mysql.cj.jdbc.Driver"); dsc.setUsername("root"); dsc.setPassword("123456"); // 数据库类型 dsc.setDbType(DbType.MYSQL); mpg.setDataSource(dsc); //3、包的配置 PackageConfig pc = new PackageConfig(); pc.setModuleName("test"); pc.setParent("org.test"); pc.setEntity("entity"); pc.setMapper("dao"); pc.setService("service"); pc.setController("controller"); mpg.setPackageInfo(pc); //4、策略配置 StrategyConfig strategy = new StrategyConfig(); // 设置要映射的表名 strategy.setInclude( "t_image", "t_plain_text" ); strategy.setNaming(NamingStrategy.underline_to_camel); strategy.setColumnNaming(NamingStrategy.underline_to_camel); // 自动lombok; strategy.setEntityLombokModel(true); // 逻辑删除 strategy.setLogicDeleteFieldName("enable"); // 自动填充配置 TableFill gmtCreate = new TableFill("create_time", FieldFill.INSERT); TableFill gmtModified = new TableFill("update_time", FieldFill.INSERT_UPDATE); ArrayList<TableFill> tableFills = new ArrayList<>(); tableFills.add(gmtCreate); tableFills.add(gmtModified); strategy.setTableFillList(tableFills); // 乐观锁 // strategy.setVersionFieldName("version"); // Restful 风格 strategy.setRestControllerStyle(true); strategy.setControllerMappingHyphenStyle(true); mpg.setStrategy(strategy); mpg.execute();
-
非Spring环境使用Mybatis Plus
理论上Mirai Console也是可以使用的。IService那些由于适配了spring的事务,所以无spring环境用会有点困难。个人建议对于Iservice提供的快捷方法 如:saveOrUpdate 还是通过手写xml实现吧。
传送门:
Mybatis Plus Generator(3.5.1以下版本)
Mybatis Plus Generator(3.5.1及以上版本 ) 目标楼层末尾
pon.xml<dependencies> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.3</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.31</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus</artifactId> <version>3.4.3</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-extension</artifactId> <version>3.4.3</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.23</version> </dependency> <dependency> <groupId>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> <version>4.0.3</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>3.3.0</version> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-core</artifactId> <version>5.7.17</version> </dependency> <dependency> <groupId>org.apache.velocity</groupId> <artifactId>velocity-engine-core</artifactId> <version>2.2</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.16</version> <scope>compile</scope> </dependency> </dependencies>
新建一个SqlSessionConfig
public class SqlSessionConfig { public static SqlSession session = init(); private static SqlSession init() { SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder(); MybatisConfiguration configuration = new MybatisConfiguration(); initConfiguration(configuration); configuration.addInterceptor(initInterceptor()); //扫描DAO/Mapper所在包 configuration.addMappers("org.test.mybatis.entity.dao"); //配置日志实现 configuration.setLogImpl(Slf4jImpl.class); //构建mybatis-plus需要的globalconfig GlobalConfig globalConfig = new GlobalConfig(); //此参数会自动生成实现baseMapper的基础方法映射 globalConfig.setSqlInjector(new DefaultSqlInjector()); //设置id生成器 globalConfig.setIdentifierGenerator(new DefaultIdentifierGenerator()); //设置超类mapper globalConfig.setSuperMapperClass(BaseMapper.class); // 这里可以配置逻辑删除 globalConfig.setDbConfig(initDBConfig()); //给configuration注入GlobalConfig里面的配置 GlobalConfigUtils.setGlobalConfig(configuration, globalConfig); //设置数据源 Environment environment = new Environment("1", new JdbcTransactionFactory(), initDataSource()); configuration.setEnvironment(environment); // 设置xxxDAO.xml所在的目录 // registryMapperXml(configuration, "dao"); //构建sqlSessionFactory SqlSessionFactory sqlSessionFactory = builder.build(configuration); return sqlSessionFactory.openSession(true); } private static void initConfiguration(MybatisConfiguration configuration) { configuration.setMapUnderscoreToCamelCase(true); configuration.setUseGeneratedKeys(true); } private static DataSource initDataSource() { HikariDataSource dataSource = new HikariDataSource(); dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/back_up?useSSL=false&autoReconnect=true&serverTimezone=GMT%2B8&rewriteBatchedStatements=true"); dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver"); dataSource.setUsername("root"); dataSource.setPassword("123456"); // 数据源设置,请按照当前DB参数设置 dataSource.setIdleTimeout(60000); dataSource.setAutoCommit(true); dataSource.setMaximumPoolSize(5); dataSource.setMinimumIdle(1); dataSource.setMaxLifetime(60000 * 10); dataSource.setConnectionTestQuery("SELECT 1"); return dataSource; } private static Interceptor initInterceptor() { //创建mybatis-plus插件对象 MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); //构建分页插件 PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(); paginationInnerInterceptor.setDbType(DbType.MYSQL); paginationInnerInterceptor.setOverflow(true); paginationInnerInterceptor.setMaxLimit(500L); interceptor.addInnerInterceptor(paginationInnerInterceptor); return interceptor; } private static GlobalConfig.DbConfig initDBConfig() { GlobalConfig.DbConfig dbConfig = new GlobalConfig.DbConfig(); // 逻辑删除字段 dbConfig.setLogicDeleteField("enable"); dbConfig.setLogicDeleteValue("0"); dbConfig.setLogicNotDeleteValue("1"); return dbConfig; } private static void registryMapperXml(MybatisConfiguration configuration, String classPath) throws IOException { ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); Enumeration<URL> mapper = contextClassLoader.getResources(classPath); while (mapper.hasMoreElements()) { URL url = mapper.nextElement(); if (url.getProtocol().equals("file")) { String path = url.getPath(); File file = new File(path); File[] files = file.listFiles(); for (File f : files) { FileInputStream in = new FileInputStream(f); XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(in, configuration, f.getPath(), configuration.getSqlFragments()); xmlMapperBuilder.parse(); in.close(); } } else { JarURLConnection urlConnection = (JarURLConnection) url.openConnection(); JarFile jarFile = urlConnection.getJarFile(); Enumeration<JarEntry> entries = jarFile.entries(); while (entries.hasMoreElements()) { JarEntry jarEntry = entries.nextElement(); if (jarEntry.getName().endsWith(".xml")) { InputStream in = jarFile.getInputStream(jarEntry); XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(in, configuration, jarEntry.getName(), configuration.getSqlFragments()); xmlMapperBuilder.parse(); in.close(); } } } } } }
用法:
SqlSession session = SqlSessionConfig.session; TImageDao tImageDao = session.getMapper(TImageDao.class); List<TImage> tImages = tImageDao.selectList(null); log.info("******{}", tImages);
-
RE: 整合SpringBoot无法登录的问题
@xiaojiuc 你还是把报错信息贴上来吧。我都是用fileBasedDeviceInfo("deviceInfo.json");让Mirai生成在工作目录。由于都是单体springboot项目,打包运行后Mirai生成的deviceInfo.json(A)和xxx.jar放在一个目录下。我把项目部署到Docker中偶尔会出现什么滑动验证,碰到这个问题后我会导出Mirai Android生成的deviceInfo.json(B),把deviceInfo.json(A)内的数据全部清空,再把deviceInfo.json(B)内的数据全部复制到deviceInfo.json(A),再次运行xxx.jar,基本是直接登录的。至于什么不常用设备,老老实实挂个一礼拜再说。如果运行环境再国外,那就看运气了。
-
RE: Spring Cloud Alibaba + Mirai Core の使用蕉♂流
第二版
写在开头
第二版主要是为项目加入了RocketMQ和DB。使用RocketMQ可以缓解高并发给DB(特指Mysql)带来的压力、提供Bot消费时突然宕机的基本解决方案,不过避免重复消费目前仍然需要借助Redis缓存消息ID来实现。DB方面我将采用MongoDB(Spring Data MongoDB Reactive)和Mysql(Mybatis Plus)作为示例。
之前在群里问了下,发现大部分人对DB的事务了解得不多、也有问我事务是啥,是否美味……所以本次更新暂未整合Seata,若有需要可以留言,我会尽快更新。
P.S.目前更新了Mysql版,MongoDB版稍后奉献。一些代码实际应用中会有坑,我正在看源码,未来会改正。代码更新完毕,文档更新略慢。目录
- 主要组件版本控制
- 与第一版的文件变动
- Mysql
3.1 引入Mybatis Plus
3.2 扫描Dao - MongoDB
4.1 [引入Spring Data MongoDB Reactive](#引入Spring Data MongoDB Reactive)
4.2 编写entity - RocketMQ
5.1 配置RocketMQ
5.2 GroupMessageHandler
5.3 ImageMessageSorter
5.4 PlainTextMessageSorter
5.5 UnhandledSendMessageSorter
5.6 ImageMessageDBConsumer
5.7 PlainTextMessageDBConsumer
5.8 UnhandledPlainTextMessageConsumer - DTO
- 其他
- 参考配置
6.1 MybatisGeneratorTest
第二版主要组件版本控制:
Mybatis Plus Mybatis Plus Generator Velocity Engine Core Rocket MQ 3.4.3 3.5.1 2.2 2.2.1
与第一版的文件变动
删除了:RepetitiveGroup(entity)
删除了:MemberJoinHandler、MemberLeaveHandler(handler)
删除了:RepetitiveGroupService(service)修改了:RobotService#handleStoredHttpRequest(service)
Mysql
Mybatis Plus,互联网人都爱它。
引入Mybatis Plus
主POM.xml引入Mybatis Plus、Mybatis Plus Generator以及模板引擎,这里用Velocity
<properties> <mybatis.plus.version>3.4.3</mybatis.plus.version> <mybatis.plus.generator.version>3.5.1</mybatis.plus.generator.version> <velocity.version>2.2</velocity.version> <rocketMQ.version>2.2.1</rocketMQ.version> </properties> <!-- ORM--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>${mybatis.plus.version}</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>${mybatis.plus.generator.version}</version> </dependency> <dependency> <groupId>org.apache.velocity</groupId> <artifactId>velocity-engine-core</artifactId> <version>${velocity.version}</version> </dependency>
模块中引入
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.apache.velocity</groupId> <artifactId>velocity-engine-core</artifactId> </dependency>
连接池我喜欢用hikari(号称全球最快的),所以使用默认设置。然后利用Mybatis Plus Generator生成entity,dao,service……相关类即可,参看配置可以见目录 -> 配置参考。
扫描Dao
这块做法有俩,一:在每个Dao上添加@Mapper注解,Mybatis Plus将会自动扫描。二:配置类上使用@MapperScan("cyou.wssy001.cloud.bot.dao"),Mybatis Plus将会扫描cyou.wssy001.cloud.bot.dao下所有Dao。
Mybatis Plus相关的配置到此结束
MongoDB
听说用了MongoDB,妈妈再也不用担心分库分表了。
引入Spring Data MongoDB Reactive
废话不多说,直接上GAV
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-mongodb-reactive</artifactId> </dependency>
application.yml
spring: data: mongodb: uri: mongodb://root:wssy001@10.60.64.66:27017 database: back_up # 项目中没有Dao,所以选none repositories: type: none
编写entity
MongoDB里每张表都自带一个_id字段代表id,不过默认是String类型,当然我们可以自己实现数值类型的自增id。
大坑
MongoDB里没有Mysql的时区概念(一律UTC),如果要存储时间需要特殊处理,否则会发生以下问题(以java.util.Date,北京时间为例)(存储一个time为2021-12-26 20:00:00、id为“111”的对象,存入MongoDB后会存在一条id为"111",time为2021-12-26 12:00:00的记录;从MongoDB获取id为“111”的记录存入本地,打印显示该对象的time为2021-12-26 12:00:00)。当时网上搜了不少解决方案,但没有生效的,后来想到了LocalDateTime。
@Data @Builder @Document @AllArgsConstructor public class TImage { /** * ID */ @Id private String id; /** * mirai消息ID */ private Integer miraiId; /** * url */ private String url; /** * 本地路径 */ private String path; /** * 机器人账号 */ private Integer botNumber; /** * 群号 */ private Integer groupNumber; /** * 好友账号 */ private Integer friendNumber; /** * 创建时间 */ private LocalDateTime createTime; /** * 更新时间 */ private LocalDateTime updateTime; /** * 是否启用 */ private Boolean enable; @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; TImage image = (TImage) o; return Objects.equals(id, image.id) && Objects.equals(miraiId, image.miraiId); } @Override public int hashCode() { return Objects.hash(id, miraiId); } }
Spring Data MongoDB Reactive相关的配置到此结束
RocketMQ
本教程不涉及RocketMQ的安装,仅涉及与Mirai相关的适配设置。
使用RocketMQ而不是Kafka的主要原因就是前者有事务消息,只是目前不适配Seata。配置RocketMQ
引入RocketMQ
主POM<properties> <rocketMQ.version>2.2.1</rocketMQ.version> </properties> <!-- RocketMQ--> <dependency> <groupId>org.apache.rocketmq</groupId> <artifactId>rocketmq-spring-boot-starter</artifactId> <version>${rocketMQ.version}</version> </dependency>
模块
<dependency> <groupId>org.apache.rocketmq</groupId> <artifactId>rocketmq-spring-boot-starter</artifactId> </dependency>
向application.yml增加配置
rocketmq: name-server: localhost:39876 producer: group: group1
由于我是用的是RocketMQ Spring Boot Starter,翻看DefaultRocketMQListenerContainer.DefaultMessageListenerConcurrently#consumeMessage 源码可以得知:使用@RocketMQMessageListener不太好实现批量拉取并消费。但为了网络优化,我们可以注入一个DefaultMQPushConsumer。
编写RocketMQConfig
@Slf4j @Setter @Configuration @RequiredArgsConstructor public class RocketMQConfig { @Value("${rocketmq.name-server}") private String nameServer; private final ImageMessageDBConsumer imageMessageDBConsumer; private final PlainTextMessageDBConsumer plainTextMessageDBConsumer; private final UnhandledSendMessageSorter unhandledSendMessageSorter; @Bean public DefaultMQPushConsumer imageMessageDBBatchConsumer() { DefaultMQPushConsumer consumer = getDefaultBatchConsumer(); consumer.setNamesrvAddr(nameServer); consumer.registerMessageListener(imageMessageDBConsumer); consumer.setConsumerGroup("image-db"); try { consumer.subscribe("image-message", ""); consumer.start(); } catch (Exception e) { log.info("******Exception:{}", e.getMessage()); } return consumer; } @Bean public DefaultMQPushConsumer plainTextMessageDBBatchConsumer() { DefaultMQPushConsumer consumer = getDefaultBatchConsumer(); consumer.registerMessageListener(plainTextMessageDBConsumer); consumer.setConsumerGroup("plain-text-db"); try { consumer.subscribe("plain-text-message", ""); consumer.start(); } catch (Exception e) { log.info("******Exception:{}", e.getMessage()); } return consumer; } @Bean public DefaultMQPushConsumer unhandledGroupMessageConsumer() { DefaultMQPushConsumer consumer = getDefaultBatchConsumer(); consumer.registerMessageListener(unhandledSendMessageSorter); consumer.setConsumerGroup("group-message"); try { consumer.subscribe("unhandled-group-message", ""); consumer.start(); } catch (Exception e) { log.info("******Exception:{}", e.getMessage()); } return consumer; } private DefaultMQPushConsumer getDefaultBatchConsumer() { DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(); consumer.setNamesrvAddr(nameServer); consumer.setPullInterval(2000); // 设置拉取消息的线程最大,最小值 consumer.setConsumeThreadMax(2); consumer.setConsumeThreadMin(1); consumer.setPullBatchSize(16); consumer.setConsumeMessageBatchMaxSize(16); return consumer; } }
LogSendCallbackService
MQ我喜欢用异步发送,因此需要自己编写一个回调方法,这里贴上一个LogSendCallbackService,但是异步发送时只有消费成功才会给相关消息的信息。
@Slf4j @Service public class LogSendCallbackService implements SendCallback { @Override public void onSuccess(SendResult sendResult) { log.info("******发送成功!,消息ID:{}", sendResult.getMsgId()); } @Override public void onException(Throwable e) { log.info("******发送失败!,错误信息:{}", e.getMessage()); } }
GroupMessageHandler
GroupMessageHandler,单纯的producer。获取所有的群消息,封装后发送至MQ,交给后续的Sorter分拣。
rocketMQTemplate.asyncSend()方法中可以直接传入一个Bean,但是会自动生成消息Id,故我选择手动封装一个消息,自定义消息Id更适合业务场景。@Component @RequiredArgsConstructor public class GroupMessageHandler { @Resource private ReactiveRedisOperations<String, MessageChainDto> reactiveRedisOperations; private final RocketMQTemplate rocketMQTemplate; private final LogSendCallbackService logSendCallbackService; public void handle(@NotNull GroupMessageEvent event) { String miraiCode = MiraiCode.serializeToMiraiCode(event.getMessage().iterator()); StringBuffer stringBuffer = new StringBuffer(); int[] ids = event.getSource().getIds(); Arrays.stream(ids) .forEach(stringBuffer::append); String key = stringBuffer.toString(); MessageChainDto messageChainDto = new MessageChainDto(); messageChainDto.setIds(ids); messageChainDto.setBotAccount(event.getBot().getId()); messageChainDto.setGroupNumber(event.getGroup().getId()); messageChainDto.setMiraiCode(miraiCode); Boolean nonExist = reactiveRedisOperations.opsForValue() .setIfAbsent(key, messageChainDto) .share() .block(); if (!nonExist) return; Duration ttl = Duration.ofSeconds(5); reactiveRedisOperations.expire(key, ttl); Message<MessageChainDto> message = MessageBuilder.withPayload(messageChainDto) .setHeader("KEYS", "GroupMessage_" + key) .build(); rocketMQTemplate.asyncSend("group-message", message, logSendCallbackService); } }
ImageMessageSorter
ImageMessageSorter,监听GroupMessageEvent中发送的QQ群消息,从中获取Image相关的,封装处理后发送至DBConsumer。
@Component @RequiredArgsConstructor @RocketMQMessageListener(topic = "group-message", consumerGroup = "image-message") public class ImageMessageSorter implements RocketMQListener<String> { private final RocketMQTemplate rocketMQTemplate; private final LogSendCallbackService logSendCallbackService; @Override public void onMessage(String message) { MessageChainDto messageChainDto = JSON.parseObject(message, MessageChainDto.class); int[] ids = messageChainDto.getIds(); MessageChain messageChain = MiraiCode.deserializeMiraiCode(messageChainDto.getMiraiCode()); ImageDto imageDto = new ImageDto(); imageDto.setBotAccount(messageChainDto.getBotAccount()); imageDto.setGroupNumber(messageChainDto.getGroupNumber()); List<ImageDto> imageDtoList = new ArrayList<>(); for (int i = 0; i < messageChain.size(); i++) { SingleMessage singleMessage = messageChain.get(i); if (singleMessage instanceof Image) imageDtoList.add(saveImage(imageDto, (Image) singleMessage, ids[i])); } if (imageDtoList.isEmpty()) return; send(imageDtoList); } // 存储图片 private ImageDto saveImage(ImageDto imageDto, Image image, Integer miraiId) { imageDto.setMiraiId(miraiId); String url = Image.queryUrl(image); return imageDto; } @SneakyThrows private void send(List<ImageDto> imageDtoList) { List<Message<ImageDto>> messageList = imageDtoList.parallelStream() .map(v -> MessageBuilder.withPayload(v) .setHeader("KEYS", "ImageDto_" + v.getId()) .build()) .collect(Collectors.toList()); rocketMQTemplate.asyncSend("image-message", messageList, logSendCallbackService); } }
PlainTextMessageSorter
PlainTextMessageSorter,监听GroupMessageEvent中发送的QQ群消息,从中获取Plain Text相关的,封装处理后发送至DBConsumer。
@Component @RequiredArgsConstructor @RocketMQMessageListener(topic = "group-message", consumerGroup = "plain-text-message") public class PlainTextMessageSorter implements RocketMQListener<String> { private final RocketMQTemplate rocketMQTemplate; private final LogSendCallbackService logSendCallbackService; @Override public void onMessage(String message) { MessageChainDto messageChainDto = JSON.parseObject(message, MessageChainDto.class); int[] ids = messageChainDto.getIds(); MessageChain messageChain = MiraiCode.deserializeMiraiCode(messageChainDto.getMiraiCode()); PlainTextDto plainTextDto = new PlainTextDto(); plainTextDto.setBotAccount(messageChainDto.getBotAccount()); plainTextDto.setGroupNumber(messageChainDto.getGroupNumber()); List<PlainTextDto> plainTextDtoList = new ArrayList<>(); for (int i = 0; i < messageChain.size(); i++) { SingleMessage singleMessage = messageChain.get(i); if (singleMessage instanceof PlainText) plainTextDtoList.add(box(plainTextDto, (PlainText) singleMessage, ids[i])); } if (plainTextDtoList.isEmpty()) return; send(plainTextDtoList); } private PlainTextDto box(PlainTextDto plainTextDto, PlainText plainText, Integer miraiCode) { plainTextDto.setMiraiId(miraiCode); plainTextDto.setText(plainText.getContent()); return plainTextDto; } @SneakyThrows private void send(List<PlainTextDto> plainTextDto) { List<Message<PlainTextDto>> messageList = plainTextDto.parallelStream() .map(v -> MessageBuilder.withPayload(v) .setHeader("KEYS", "PlainTextDto_" + v.getId()) .build()) .collect(Collectors.toList()); rocketMQTemplate.asyncSend("plain-text-message", messageList, logSendCallbackService); } }
UnhandledSendMessageSorter
UnhandledSendMessageSorter,处理由于Sentinel流控导致未能及时消费Http请求的信息。获取UnhandledHttpRequestDto,封装后发送至UnhandledPlainTextMessageConsumer。
@Component @RequiredArgsConstructor public class UnhandledSendMessageSorter implements MessageListenerConcurrently { private final RocketMQTemplate rocketMQTemplate; private final LogSendCallbackService logSendCallbackService; @Override public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> messageExtList, ConsumeConcurrentlyContext context) { List<Message<UnhandledHttpRequestDto>> messageList = messageExtList.parallelStream() .map(v -> JSON.parseObject(new String(v.getBody()), UnhandledHttpRequest.class)) .map(this::toUnhandledHttpRequestDto) .filter(Objects::nonNull) .map(v -> MessageBuilder.withPayload(v) .setHeader("KEYS", "UnhandledHttpRequestDto_" + v.getId()) .build()) .collect(Collectors.toList()); rocketMQTemplate.asyncSend("unhandled-group-plain-text-message", messageList, logSendCallbackService); return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; } private UnhandledHttpRequestDto toUnhandledHttpRequestDto(UnhandledHttpRequest unhandledHttpRequest) { if (!unhandledHttpRequest.getMethod().equals("/send/msg") || unhandledHttpRequest.getGroupId() == null) return null; MessageChainBuilder append = new MessageChainBuilder() .append(new PlainText(unhandledHttpRequest.getMsg())) .append(new At(unhandledHttpRequest.getQQ())); return new UnhandledHttpRequestDto(unhandledHttpRequest.getId(), MiraiCode.serializeToMiraiCode(append), unhandledHttpRequest.getGroupId()); } }
ImageMessageDBConsumer
ImageMessageDBConsumer,批量接收来自ImageMessageSorter的信息,批量存入数据库
@Component @RequiredArgsConstructor public class ImageMessageDBConsumer implements MessageListenerConcurrently { private final TImageService tImageService; @Override public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> messageExtList, ConsumeConcurrentlyContext context) { List<TImage> imageList = messageExtList.parallelStream() .map(v -> JSON.parseObject(new String(v.getBody()), ImageDto.class)) .map(this::toTImage) .collect(Collectors.toList()); tImageService.saveBatch(imageList) .share() .collectList() .block(); return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; } private TImage toTImage(ImageDto imageDto) { TImage image = TImage.builder().build(); BeanUtil.copyProperties(imageDto, image); return image; } }
PlainTextMessageDBConsumer
PlainTextMessageDBConsumer,批量接收来自PlainTextMessageSorter的信息,批量存入数据库
@Component @RequiredArgsConstructor public class PlainTextMessageDBConsumer implements MessageListenerConcurrently { private final TPlainTextService tPlainTextService; @Override public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> messageExtList, ConsumeConcurrentlyContext context) { List<TPlainText> plainTextList = messageExtList.parallelStream() .map(v -> JSON.parseObject(new String(v.getBody()), PlainTextDto.class)) .map(this::toTPlainText) .collect(Collectors.toList()); tPlainTextService.saveBatch(plainTextList) .share() .collectList() .block(); return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; } private TPlainText toTPlainText(PlainTextDto plainTextDto) { TPlainText plainText = TPlainText.builder().build(); BeanUtil.copyProperties(plainTextDto, plainText); return plainText; } }
UnhandledPlainTextMessageConsumer
UnhandledPlainTextMessageConsumer,通过翻看上层源码,发现只有抛异常才会触发消费失败,然后触发重复投递。
@Component @RequiredArgsConstructor @RocketMQMessageListener(topic = "unhandled-group-plain-text-message", consumerGroup = "plain-text-message") public class UnhandledPlainTextMessageConsumer implements RocketMQListener<String> { @Resource private ReactiveRedisOperations<String, Long> reactiveRedisOperations; @Override @SneakyThrows public void onMessage(String message) { if (Bot.getInstances().isEmpty()) return; UnhandledHttpRequestDto unhandledHttpRequestDto = JSON.parseObject(message, UnhandledHttpRequestDto.class); Bot bot = Bot.getInstances() .stream() .filter(v -> v.getGroup(unhandledHttpRequestDto.getGroupId()) != null) .findFirst() .orElse(null); if (bot == null) throw new RuntimeException("无Bot在目标群"); String msgId = unhandledHttpRequestDto.getId(); Boolean nonExist = reactiveRedisOperations.opsForValue() .setIfAbsent(msgId, bot.getId()) .share() .block(); if (!nonExist) throw new RuntimeException("重复消费"); Duration ttl = Duration.ofSeconds(5); reactiveRedisOperations.expire(msgId, ttl); MessageChain messageChain = MiraiCode.deserializeMiraiCode(unhandledHttpRequestDto.getMiraiCode()); bot.getGroup(unhandledHttpRequestDto.getGroupId()) .sendMessage(messageChain); Thread.sleep(500); } }
RocketMQ相关的配置到此结束
DTO
这里主要是贴一下上文所用到的DTO
BaseMessageDto@Data public abstract class BaseMessageDto { private Long id; private Integer miraiId; private Long botAccount; private Long groupNumber; private Long friendNumber; }
ImageDto
@Data @AllArgsConstructor @NoArgsConstructor @EqualsAndHashCode(callSuper = true) public class ImageDto extends BaseMessageDto implements Serializable { static final long serialVersionUID = 1L; private String url; private String path; }
MessageChainDto
@Data @AllArgsConstructor @NoArgsConstructor @EqualsAndHashCode(callSuper = true) public class MessageChainDto extends BaseMessageDto implements Serializable { static final long serialVersionUID = 1L; private int[] ids; private String miraiCode; }
PlainTextDto
@Data @AllArgsConstructor @NoArgsConstructor @EqualsAndHashCode(callSuper = true) public class PlainTextDto extends BaseMessageDto implements Serializable { static final long serialVersionUID = 1L; private String text; }
UnhandledHttpRequestDto
@Data @AllArgsConstructor @NoArgsConstructor public class UnhandledHttpRequestDto implements Serializable { private static final long serialVersionUID = 1L; private String id; private String miraiCode; private Long groupId; }
其他
RobotService#handleStoredHttpRequest
private void handleStoredHttpRequest() { List<Message<UnhandledHttpRequest>> messageList = unhandledHttpRequestService.getAll() .filter(Objects::nonNull) .map(v -> MessageBuilder.withPayload(v).build()) .share() .collectList() .block(); if (messageList == null) return; rocketMQTemplate.asyncSend("unhandled-group-message", messageList, logSendCallbackService); }
参考配置
MybatisGeneratorTest
@Slf4j class MybatisGeneratorTest { @Test void mybatisPlusGenerator() { FastAutoGenerator.create(getDataSourceConfig()) // 全局配置 .globalConfig(getGlobalConfig()) // 包配置 .packageConfig(getPackageConfig()) // 策略配置 .strategyConfig(getStrategyConfig()) .execute(); } private DataSourceConfig.Builder getDataSourceConfig() { return new DataSourceConfig.Builder("jdbc:mysql://localhost:3306/test?useUnicode=true" + "&useSSL=false&autoReconnect=true&characterEncoding=utf-8&serverTimezone=GMT%2B8" + "&rewriteBatchedStatements=true", "root", "root") .typeConvert(new MySqlTypeConvert()) .keyWordsHandler(new MySqlKeyWordsHandler()); } private Consumer<StrategyConfig.Builder> getStrategyConfig() { return builder -> builder // 添加表名 .addInclude( "t_image", "t_plain_text") .entityBuilder() .disableSerialVersionUID() .enableChainModel() .enableLombok() .enableRemoveIsPrefix() .enableTableFieldAnnotation() .enableActiveRecord() // 乐观锁 .versionColumnName("version") .versionPropertyName("version") // 逻辑删除 .logicDeleteColumnName("enable") .logicDeletePropertyName("enable") .naming(NamingStrategy.underline_to_camel) .columnNaming(NamingStrategy.underline_to_camel) .addTableFills(new Column("create_time", FieldFill.INSERT)) .addTableFills(new Column("update_time", FieldFill.INSERT_UPDATE)) .idType(IdType.AUTO) .serviceBuilder() .formatServiceFileName("%sService") .formatServiceImplFileName("%sServiceImp") .mapperBuilder() .formatMapperFileName("%sDAO"); } private BiConsumer<Function<String, String>, PackageConfig.Builder> getPackageConfig() { return (scanner, builder) -> builder.parent("org.test") .moduleName("test") .entity("entity") .service("service") .serviceImpl("service.impl") .mapper("dao") .controller("controller") .other("other"); } private BiConsumer<Function<String, String>, GlobalConfig.Builder> getGlobalConfig() { return (scanner, builder) -> builder.author("yourName") .disableOpenDir() .dateType(DateType.ONLY_DATE) .outputDir(System.getProperty("user.dir") + "/src/main/java") // Swagger 2 API Doc相关注解,Knife4j适用 // .enableSwagger() .fileOverride(); } }
-
RE: BotSelfCensor 机器人自我审核插件
git push是有缓存的,建议本地清一下cache再提交修正后的代码。当然,直接删repository重新push也不失为一种快捷手段……
-
RE: BotSelfCensor 机器人自我审核插件
看了一下你的项目,合规的access_token直接写在代码中真的可以吗?个人建议立刻调用相关API将已公开的access_token失效掉。
-
RE: 是否可以通过IDS获取到发送的消息
可以使用漫游消息,但是目前只支持好友消息,详见。对于实际使用,建议还是手动存储消息id对应的消息内容。此外,如果存储的消息过多,试着上ElasticSearch吧。
-
RE: SuperCourseTimetableBot - 基于 mirai-console 的 超级课表上课提醒QQ机器人插件
@cssxsh 那估计得作者引入第三方ORM,然后用ORM编写各个DB的方言,达到适配SQL DB
-
RE: 新人开发求助->怎么将数据保存到pluginData
JAutoSavePluginData无法保存的话,可以试着存到数据库啊,如果连不上,转为JSON格式的文本存本地也行。读取的时候可以从本地读取到程序缓存(手撸个Map就能当缓存),然后从Map读。
-
RE: 萌新求助windowsX64打不开cmd
点开mcl.cmd 复制里面的代码手动粘贴到cmd中执行看看报错的提示。会不会是Java环境?cmd输入java -version 看看
-
RE: 如何获取某一权限下的所有被授权人id
请说出你是用的是什么模块(mirai -core 还是 mirai-console),另外,你是否使用的是第三方的插件,也请说明。如果对于你这个需求,mirai-core可以让机器人获取全部群成员,通过 并行流 + 筛选 获取全部符合结果的群
ContactList<NormalMember> members = event.getBot() .getGroup(1L) .getMembers(); List<Long> idList = members.parallelStream() // 更改为你自己的逻辑 .filter(v -> true) .map(NormalMember::getId) .collect(Collectors.toList()); if (idList.isEmpty()){ }
-
RE: 整合SpringBoot无法登录的问题
@xiaojiuc 你的mirai-core版本多少?尝试升级至至少2.6.8版本,另外建议清空一下mirai生成的cache文件夹,device.json不用清
-
RE: 整合SpringBoot无法登录的问题
看了一下你的错误,我的解决办法无非也就是 1:把Bot的活跃度拉满 2:去Mirai Android登录一下Bot,复制device.json放到Linux上