MiraiForum

    • 注册
    • 登录
    • 搜索
    • 热门
    • 最新
    • 未解决
    • 标签
    • 群组
    • 友情链接
    1. 主页
    2. wssy001
    3. 帖子
    • 资料
    • 关注 0
    • 粉丝 0
    • 主题 5
    • 帖子 86
    • 最佳 9
    • 有争议的 0
    • 群组 1

    wssy001 发布的帖子

    • RE: 整合SpringBoot无法登录的问题

      这个方法就是手动完成验证,基本上能解决90%的登录问题。登录完成后请备份device.json,下次直接套用这个device.json就行了

      发布在 开发交流
      wssy001
      wssy001
    • RE: 整合SpringBoot无法登录的问题

      @xiaojiuc 在 整合SpringBoot无法登录的问题 中说:

      https://docs.mirai.mamoe.net/mirai-login-solver-selenium

      先看一下这边的文档,添加参数“-Dmirai.slider.captcha.supported”

      有安卓手机不?有的话去这边下载一个手动滑动验证的APP https://github.com/mzdluo123/TxCaptchaHelper/releases
      使用步骤:
      打开那个APP,输入Mirai显示的Captcha link(验证码地址)到APP中间的输入框,点击下一步,完成滑动验证。之后APP会有一个URL,你复制后输入到Mirai中,按回车键就能完成登录验证。

      发布在 开发交流
      wssy001
      wssy001
    • RE: Java + Springboot 2.X+ mirai最新RELEASE の采坑记录

      @liuzheng2000 个人觉得对springboot熟悉的人看我写的示例应该知道怎么整合mirai core。如有疑问可以发到这儿或者论坛其他地方。

      发布在 精华主题
      wssy001
      wssy001
    • 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();
      
      发布在 使用交流
      wssy001
      wssy001
    • 非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);
      
      发布在 使用交流
      wssy001
      wssy001
    • 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,基本是直接登录的。至于什么不常用设备,老老实实挂个一礼拜再说。如果运行环境再国外,那就看运气了。

      发布在 开发交流
      wssy001
      wssy001
    • 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版稍后奉献。一些代码实际应用中会有坑,我正在看源码,未来会改正。 代码更新完毕,文档更新略慢。

      目录

      1. 主要组件版本控制
      2. 与第一版的文件变动
      3. Mysql
        3.1 引入Mybatis Plus
        3.2 扫描Dao
      4. MongoDB
        4.1 [引入Spring Data MongoDB Reactive](#引入Spring Data MongoDB Reactive)
        4.2 编写entity
      5. 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
      6. DTO
      7. 其他
      8. 参考配置
        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();
          }
      }
      
      发布在 使用交流
      wssy001
      wssy001
    • RE: BotSelfCensor 机器人自我审核插件

      git push是有缓存的,建议本地清一下cache再提交修正后的代码。当然,直接删repository重新push也不失为一种快捷手段……

      发布在 插件发布
      wssy001
      wssy001
    • RE: BotSelfCensor 机器人自我审核插件

      看了一下你的项目,合规的access_token直接写在代码中真的可以吗?个人建议立刻调用相关API将已公开的access_token失效掉。

      发布在 插件发布
      wssy001
      wssy001
    • RE: 整合SpringBoot无法登录的问题

      @xiaojiuc 论坛官方公告

      发布在 开发交流
      wssy001
      wssy001
    • RE: 是否可以通过IDS获取到发送的消息

      可以使用漫游消息,但是目前只支持好友消息,详见。对于实际使用,建议还是手动存储消息id对应的消息内容。此外,如果存储的消息过多,试着上ElasticSearch吧。

      发布在 技术交流板块
      wssy001
      wssy001
    • RE: SuperCourseTimetableBot - 基于 mirai-console 的 超级课表上课提醒QQ机器人插件

      @cssxsh 那估计得作者引入第三方ORM,然后用ORM编写各个DB的方言,达到适配SQL DB

      发布在 插件发布
      wssy001
      wssy001
    • RE: 新人开发求助->怎么将数据保存到pluginData

      JAutoSavePluginData无法保存的话,可以试着存到数据库啊,如果连不上,转为JSON格式的文本存本地也行。读取的时候可以从本地读取到程序缓存(手撸个Map就能当缓存),然后从Map读。

      发布在 使用交流
      wssy001
      wssy001
    • RE: 萌新求助windowsX64打不开cmd

      点开mcl.cmd 复制里面的代码手动粘贴到cmd中执行看看报错的提示。会不会是Java环境?cmd输入java -version 看看

      发布在 使用交流
      wssy001
      wssy001
    • 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()){
      
      }
      
      发布在 开发交流
      wssy001
      wssy001
    • RE: 整合SpringBoot无法登录的问题

      @xiaojiuc 你的mirai-core版本多少?尝试升级至至少2.6.8版本,另外建议清空一下mirai生成的cache文件夹,device.json不用清

      发布在 开发交流
      wssy001
      wssy001
    • RE: 整合SpringBoot无法登录的问题

      @xiaojiuc 另外建议详细阅读一下官方文档,了解一下Bot的相关配置。

      发布在 开发交流
      wssy001
      wssy001
    • RE: 整合SpringBoot无法登录的问题

      device.json默认是在.jar包同级目录中的 所以和jar文件放在一起

      发布在 开发交流
      wssy001
      wssy001
    • RE: 整合SpringBoot无法登录的问题

      看了一下你的错误,我的解决办法无非也就是 1:把Bot的活跃度拉满 2:去Mirai Android登录一下Bot,复制device.json放到Linux上

      发布在 开发交流
      wssy001
      wssy001
    • RE: Spring Cloud Alibaba + Mirai Core の使用蕉♂流

      @840670339 这个项目后续还在更新,完结了再分享吧

      发布在 使用交流
      wssy001
      wssy001
    • 1
    • 2
    • 3
    • 4
    • 5
    • 3 / 5