Crazy 9 months ago
parent
commit
f41f68ce32
100 changed files with 5190 additions and 26 deletions
  1. 1 1
      pom.xml
  2. 0 12
      yudao-module-md/pom.xml
  3. 23 13
      yudao-module-md/yudao-module-md-biz/pom.xml
  4. 25 0
      yudao-module-mp/pom.xml
  5. 26 0
      yudao-module-mp/yudao-module-mp-api/pom.xml
  6. 64 0
      yudao-module-mp/yudao-module-mp-api/src/main/java/cn/iocoder/yudao/module/mp/enums/ErrorCodeConstants.java
  7. 28 0
      yudao-module-mp/yudao-module-mp-api/src/main/java/cn/iocoder/yudao/module/mp/enums/message/MpAutoReplyMatchEnum.java
  8. 29 0
      yudao-module-mp/yudao-module-mp-api/src/main/java/cn/iocoder/yudao/module/mp/enums/message/MpAutoReplyTypeEnum.java
  9. 28 0
      yudao-module-mp/yudao-module-mp-api/src/main/java/cn/iocoder/yudao/module/mp/enums/message/MpMessageSendFromEnum.java
  10. 8 0
      yudao-module-mp/yudao-module-mp-api/src/main/java/cn/iocoder/yudao/module/mp/package-info.java
  11. 92 0
      yudao-module-mp/yudao-module-mp-biz/pom.xml
  12. 98 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/MpAccountController.java
  13. 43 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountBaseVO.java
  14. 14 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountCreateReqVO.java
  15. 24 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountPageReqVO.java
  16. 25 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountRespVO.java
  17. 16 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountSimpleRespVO.java
  18. 20 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountUpdateReqVO.java
  19. 5 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/MpMaterialController.http
  20. 74 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/MpMaterialController.java
  21. 27 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/vo/MpMaterialPageReqVO.java
  22. 47 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/vo/MpMaterialRespVO.java
  23. 23 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/vo/MpMaterialUploadNewsImageReqVO.java
  24. 53 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/vo/MpMaterialUploadPermanentReqVO.java
  25. 16 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/vo/MpMaterialUploadRespVO.java
  26. 28 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/vo/MpMaterialUploadTemporaryReqVO.java
  27. 50 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/menu/MpMenuController.http
  28. 57 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/menu/MpMenuController.java
  29. 115 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/menu/vo/MpMenuBaseVO.java
  30. 28 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/menu/vo/MpMenuRespVO.java
  31. 34 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/menu/vo/MpMenuSaveReqVO.java
  32. 5 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/MpAutoReplyController.http
  33. 74 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/MpAutoReplyController.java
  34. 33 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/MpMessageController.http
  35. 129 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/MpMessageController.java
  36. 109 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyBaseVO.java
  37. 20 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyCreateReqVO.java
  38. 21 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyPageReqVO.java
  39. 27 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyRespVO.java
  40. 20 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyUpdateReqVO.java
  41. 35 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/vo/message/MpMessagePageReqVO.java
  42. 101 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/vo/message/MpMessageRespVO.java
  43. 63 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/vo/message/MpMessageSendReqVO.java
  44. 62 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/vo/message/attendanceMessageSendReqVO.java
  45. 54 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/news/MpDraftController.http
  46. 136 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/news/MpDraftController.java
  47. 13 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/news/MpFreePublishController.http
  48. 119 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/news/MpFreePublishController.java
  49. 21 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/news/vo/MpDraftPageReqVO.java
  50. 21 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/news/vo/MpFreePublishPageReqVO.java
  51. 114 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/open/MpOpenController.java
  52. 29 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/open/vo/MpOpenCheckSignatureReqVO.java
  53. 37 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/open/vo/MpOpenHandleMessageReqVO.java
  54. 68 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/statistics/MpStatisticsController.java
  55. 25 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/statistics/vo/MpStatisticsGetReqVO.java
  56. 27 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/statistics/vo/MpStatisticsInterfaceSummaryRespVO.java
  57. 21 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/statistics/vo/MpStatisticsUpstreamMessageRespVO.java
  58. 18 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/statistics/vo/MpStatisticsUserCumulateRespVO.java
  59. 24 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/statistics/vo/MpStatisticsUserSummaryRespVO.java
  60. 39 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/tag/MpTagController.http
  61. 88 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/tag/MpTagController.java
  62. 21 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/tag/vo/MpTagBaseVO.java
  63. 20 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/tag/vo/MpTagCreateReqVO.java
  64. 24 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/tag/vo/MpTagPageReqVO.java
  65. 25 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/tag/vo/MpTagRespVO.java
  66. 19 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/tag/vo/MpTagSimpleRespVO.java
  67. 20 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/tag/vo/MpTagUpdateReqVO.java
  68. 18 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/user/MpUserController.http
  69. 65 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/user/MpUserController.java
  70. 30 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/user/vo/MpUserPageReqVO.java
  71. 55 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/user/vo/MpUserRespVO.java
  72. 26 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/user/vo/MpUserUpdateReqVO.java
  73. 6 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/package-info.java
  74. 31 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/account/MpAccountConvert.java
  75. 47 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/material/MpMaterialConvert.java
  76. 50 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/menu/MpMenuConvert.java
  77. 37 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/message/MpAutoReplyConvert.java
  78. 172 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/message/MpMessageConvert.java
  79. 52 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/statistics/MpStatisticsConvert.java
  80. 44 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/tag/MpTagConvert.java
  81. 56 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/user/MpUserConvert.java
  82. 62 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/account/MpAccountDO.java
  83. 99 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/material/MpMaterialDO.java
  84. 185 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/menu/MpMenuDO.java
  85. 165 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/message/MpAutoReplyDO.java
  86. 242 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/message/MpMessageDO.java
  87. 58 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/tag/MpTagDO.java
  88. 114 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/user/MpUserDO.java
  89. 31 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/account/MpAccountMapper.java
  90. 33 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/material/MpMaterialMapper.java
  91. 25 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/menu/MpMenuMapper.java
  92. 70 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/message/MpAutoReplyMapper.java
  93. 22 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/message/MpMessageMapper.java
  94. 31 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/tag/MpTagMapper.java
  95. 44 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/user/MpUserMapper.java
  96. 54 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/mp/config/MpConfiguration.java
  97. 177 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/mp/core/DefaultMpServiceFactory.java
  98. 66 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/mp/core/MpServiceFactory.java
  99. 53 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/mp/core/context/MpContextHolder.java
  100. 167 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/mp/core/util/MpUtils.java

+ 1 - 1
pom.xml

@@ -19,7 +19,7 @@
         <!--        <module>yudao-module-member</module>-->
 <!--        <module>yudao-module-bpm</module>-->
 <!--        <module>yudao-module-report</module>-->
-<!--        <module>yudao-module-mp</module>-->
+        <module>yudao-module-mp</module>
 <!--        <module>yudao-module-pay</module>-->
 <!--        <module>yudao-module-mall</module>-->
 <!--        <module>yudao-module-crm</module>-->

+ 0 - 12
yudao-module-md/pom.xml

@@ -33,19 +33,7 @@
         <version>3.0.9</version>
     </dependency>
 
-    <!-- 本地 JSON 依赖 -->
-    <dependency>
-        <groupId>com.sun.jna</groupId>
-        <artifactId>json</artifactId>
-        <version>1.0.0</version>
-    </dependency>
 
-    <!-- 本地 Examples 依赖 -->
-    <dependency>
-        <groupId>com.sun.jna</groupId>
-        <artifactId>examples</artifactId>
-        <version>1.0.0</version>
-    </dependency>
         <!-- JNA JSON 依赖 -->
 
         <!-- ZIP文件中文处理 -->

+ 23 - 13
yudao-module-md/yudao-module-md-biz/pom.xml

@@ -109,19 +109,29 @@
 <!--        </dependency>-->
 
 <!--        &lt;!&ndash; JNA JSON 依赖 &ndash;&gt;-->
-<!--        <dependency>-->
-<!--            <groupId>com.sun.jna</groupId>-->
-<!--            <artifactId>json</artifactId>-->
-<!--            <version>1.0</version>-->
-<!--        </dependency>-->
-<!--        <dependency>-->
-<!--            <groupId>com.sun.jna.examples</groupId>-->
-<!--            <artifactId>test</artifactId>-->
-<!--            <version>1.0</version>-->
-<!--            <scope>system</scope>-->
-<!--            <systemPath>${basedir}/src/main/resources/examples.jar</systemPath>-->
-<!--        </dependency>-->
-<!--        <dependency>-->
+        <dependency>
+            <groupId>com.sun.jna</groupId>
+            <artifactId>json</artifactId>
+            <version>1.0</version>
+            <scope>system</scope>
+            <systemPath>${basedir}/src/main/resources/jna.jar</systemPath>
+        </dependency>
+        <dependency>
+            <groupId>com.sun.jna.examples</groupId>
+            <artifactId>test</artifactId>
+            <version>1.0</version>
+            <scope>system</scope>
+            <systemPath>${basedir}/src/main/resources/examples.jar</systemPath>
+        </dependency>
+        <dependency>
+            <groupId>com.sun.jna.json</groupId>
+            <artifactId>test</artifactId>
+            <version>1.0</version>
+            <scope>system</scope>
+            <systemPath>${basedir}/src/main/resources/json.jar</systemPath>
+        </dependency>
+
+        <!--        <dependency>-->
 <!--            <groupId>com.sun.jna.json</groupId>-->
 <!--            <artifactId>test</artifactId>-->
 <!--            <version>1.0</version>-->

+ 25 - 0
yudao-module-mp/pom.xml

@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <groupId>cn.iocoder.boot</groupId>
+        <artifactId>yudao</artifactId>
+        <version>${revision}</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>yudao-module-mp</artifactId>
+    <packaging>pom</packaging>
+
+    <name>${project.artifactId}</name>
+    <description>
+        wechat 模块,主要实现微信平台的相关业务。
+        例如:微信公众号、企业微信 SCRM 等
+    </description>
+    <modules>
+        <module>yudao-module-mp-api</module>
+        <module>yudao-module-mp-biz</module>
+    </modules>
+
+</project>

+ 26 - 0
yudao-module-mp/yudao-module-mp-api/pom.xml

@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>yudao-module-mp</artifactId>
+        <groupId>cn.iocoder.boot</groupId>
+        <version>${revision}</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>yudao-module-mp-api</artifactId>
+    <packaging>jar</packaging>
+
+    <name>${project.artifactId}</name>
+    <description>
+        mp 模块 API,暴露给其它模块调用
+    </description>
+
+    <dependencies>
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-common</artifactId>
+        </dependency>
+    </dependencies>
+
+</project>

+ 64 - 0
yudao-module-mp/yudao-module-mp-api/src/main/java/cn/iocoder/yudao/module/mp/enums/ErrorCodeConstants.java

@@ -0,0 +1,64 @@
+package cn.iocoder.yudao.module.mp.enums;
+
+import cn.iocoder.yudao.framework.common.exception.ErrorCode;
+
+/**
+ * Mp 错误码枚举类
+ *
+ * mp 系统,使用 1-006-000-000 段
+ */
+public interface ErrorCodeConstants {
+
+    // ========== 公众号账号 1-006-000-000 ============
+    ErrorCode ACCOUNT_NOT_EXISTS = new ErrorCode(1_006_000_000, "公众号账号不存在");
+    ErrorCode ACCOUNT_GENERATE_QR_CODE_FAIL = new ErrorCode(1_006_000_001, "生成公众号二维码失败,原因:{}");
+    ErrorCode ACCOUNT_CLEAR_QUOTA_FAIL = new ErrorCode(1_006_000_002, "清空公众号的 API 配额失败,原因:{}");
+
+    // ========== 公众号统计 1-006-001-000 ============
+    ErrorCode STATISTICS_GET_USER_SUMMARY_FAIL = new ErrorCode(1_006_001_000, "获取粉丝增减数据失败,原因:{}");
+    ErrorCode STATISTICS_GET_USER_CUMULATE_FAIL = new ErrorCode(1_006_001_001, "获得粉丝累计数据失败,原因:{}");
+    ErrorCode STATISTICS_GET_UPSTREAM_MESSAGE_FAIL = new ErrorCode(1_006_001_002, "获得消息发送概况数据失败,原因:{}");
+    ErrorCode STATISTICS_GET_INTERFACE_SUMMARY_FAIL = new ErrorCode(1_006_001_003, "获得接口分析数据失败,原因:{}");
+
+    // ========== 公众号标签 1-006-002-000 ============
+    ErrorCode TAG_NOT_EXISTS = new ErrorCode(1_006_002_000, "标签不存在");
+    ErrorCode TAG_CREATE_FAIL = new ErrorCode(1_006_002_001, "创建标签失败,原因:{}");
+    ErrorCode TAG_UPDATE_FAIL = new ErrorCode(1_006_002_002, "更新标签失败,原因:{}");
+    ErrorCode TAG_DELETE_FAIL = new ErrorCode(1_006_002_003, "删除标签失败,原因:{}");
+    ErrorCode TAG_GET_FAIL = new ErrorCode(1_006_002_004, "获得标签失败,原因:{}");
+
+    // ========== 公众号粉丝 1-006-003-000 ============
+    ErrorCode USER_NOT_EXISTS = new ErrorCode(1_006_003_000, "粉丝不存在");
+    ErrorCode USER_UPDATE_TAG_FAIL = new ErrorCode(1_006_003_001, "更新粉丝标签失败,原因:{}");
+
+    // ========== 公众号素材 1-006-004-000 ============
+    ErrorCode MATERIAL_NOT_EXISTS = new ErrorCode(1_006_004_000, "素材不存在");
+    ErrorCode MATERIAL_UPLOAD_FAIL = new ErrorCode(1_006_004_001, "上传素材失败,原因:{}");
+    ErrorCode MATERIAL_IMAGE_UPLOAD_FAIL = new ErrorCode(1_006_004_002, "上传图片失败,原因:{}");
+    ErrorCode MATERIAL_DELETE_FAIL = new ErrorCode(1_006_004_003, "删除素材失败,原因:{}");
+
+    // ========== 公众号消息 1-006-005-000 ============
+    ErrorCode MESSAGE_SEND_FAIL = new ErrorCode(1_006_005_000, "发送消息失败,原因:{}");
+
+    // ========== 公众号发布能力 1-006-006-000 ============
+    ErrorCode FREE_PUBLISH_LIST_FAIL = new ErrorCode(1_006_006_000, "获得已成功发布列表失败,原因:{}");
+    ErrorCode FREE_PUBLISH_SUBMIT_FAIL = new ErrorCode(1_006_006_001, "提交发布失败,原因:{}");
+    ErrorCode FREE_PUBLISH_DELETE_FAIL = new ErrorCode(1_006_006_002, "删除发布失败,原因:{}");
+
+    // ========== 公众号草稿 1-006-007-000 ============
+    ErrorCode DRAFT_LIST_FAIL = new ErrorCode(1_006_007_000, "获得草稿列表失败,原因:{}");
+    ErrorCode DRAFT_CREATE_FAIL = new ErrorCode(1_006_007_001, "创建草稿失败,原因:{}");
+    ErrorCode DRAFT_UPDATE_FAIL = new ErrorCode(1_006_007_002, "更新草稿失败,原因:{}");
+    ErrorCode DRAFT_DELETE_FAIL = new ErrorCode(1_006_007_003, "删除草稿失败,原因:{}");
+
+    // ========== 公众号菜单 1-006-008-000 ============
+    ErrorCode MENU_SAVE_FAIL = new ErrorCode(1_006_008_000, "创建菜单失败,原因:{}");
+    ErrorCode MENU_DELETE_FAIL = new ErrorCode(1_006_008_001, "删除菜单失败,原因:{}");
+
+    // ========== 公众号自动回复 1-006-009-000 ============
+    ErrorCode AUTO_REPLY_NOT_EXISTS = new ErrorCode(1_006_009_000, "自动回复不存在");
+    ErrorCode AUTO_REPLY_ADD_SUBSCRIBE_FAIL_EXISTS = new ErrorCode(1_006_009_001, "操作失败,原因:已存在关注时的回复");
+    ErrorCode AUTO_REPLY_ADD_MESSAGE_FAIL_EXISTS = new ErrorCode(1_006_009_002, "操作失败,原因:已存在该消息类型的回复");
+    ErrorCode AUTO_REPLY_ADD_KEYWORD_FAIL_EXISTS = new ErrorCode(1_006_009_003, "操作失败,原因:已关在该关键字的回复");
+
+}

+ 28 - 0
yudao-module-mp/yudao-module-mp-api/src/main/java/cn/iocoder/yudao/module/mp/enums/message/MpAutoReplyMatchEnum.java

@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.module.mp.enums.message;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 公众号消息自动回复的匹配模式
+ *
+ * @author 芋道源码
+ */
+@Getter
+@AllArgsConstructor
+public enum MpAutoReplyMatchEnum {
+
+    ALL(1, "完全匹配"),
+    LIKE(2, "半匹配"),
+    ;
+
+    /**
+     * 匹配
+     */
+    private final Integer match;
+    /**
+     * 匹配的名字
+     */
+    private final String name;
+
+}

+ 29 - 0
yudao-module-mp/yudao-module-mp-api/src/main/java/cn/iocoder/yudao/module/mp/enums/message/MpAutoReplyTypeEnum.java

@@ -0,0 +1,29 @@
+package cn.iocoder.yudao.module.mp.enums.message;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 公众号消息自动回复的类型
+ *
+ * @author 芋道源码
+ */
+@Getter
+@AllArgsConstructor
+public enum MpAutoReplyTypeEnum {
+
+    SUBSCRIBE(1, "关注时回复"),
+    MESSAGE(2, "收到消息回复"),
+    KEYWORD(3, "关键词回复"),
+    ;
+
+    /**
+     * 来源
+     */
+    private final Integer type;
+    /**
+     * 类型的名字
+     */
+    private final String name;
+
+}

+ 28 - 0
yudao-module-mp/yudao-module-mp-api/src/main/java/cn/iocoder/yudao/module/mp/enums/message/MpMessageSendFromEnum.java

@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.module.mp.enums.message;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 微信公众号消息的发送来源
+ *
+ * @author 芋道源码
+ */
+@Getter
+@AllArgsConstructor
+public enum MpMessageSendFromEnum {
+
+    USER_TO_MP(1, "粉丝发送给公众号"),
+    MP_TO_USER(2, "公众号发给粉丝"),
+    ;
+
+    /**
+     * 来源
+     */
+    private final Integer from;
+    /**
+     * 来源的名字
+     */
+    private final String name;
+
+}

+ 8 - 0
yudao-module-mp/yudao-module-mp-api/src/main/java/cn/iocoder/yudao/module/mp/package-info.java

@@ -0,0 +1,8 @@
+/**
+ * mp 模块,我们放微信微信公众号。
+ * 例如说:提供微信公众号的账号、菜单、粉丝、标签、消息、自动回复、素材、模板通知、运营数据等功能
+ *
+ * 1. Controller URL:以 /mp/ 开头,避免和其它 Module 冲突
+ * 2. DataObject 表名:以 mp_ 开头,方便在数据库中区分
+ */
+package cn.iocoder.yudao.module.mp;

+ 92 - 0
yudao-module-mp/yudao-module-mp-biz/pom.xml

@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>yudao-module-mp</artifactId>
+        <groupId>cn.iocoder.boot</groupId>
+        <version>${revision}</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>yudao-module-mp-biz</artifactId>
+    <packaging>jar</packaging>
+
+    <name>${project.artifactId}</name>
+    <description>
+        mp 模块,我们放微信微信公众号。
+        例如说:提供微信公众号的账号、菜单、粉丝、标签、消息、自动回复、素材、模板通知、运营数据等功能
+    </description>
+
+    <dependencies>
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-module-mp-api</artifactId>
+            <version>${revision}</version>
+        </dependency>
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-module-system-api</artifactId>
+            <version>${revision}</version>
+        </dependency>
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-module-infra-api</artifactId>
+            <version>${revision}</version>
+        </dependency>
+
+        <!-- 业务组件 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-biz-tenant</artifactId>
+        </dependency>
+
+        <!-- Web 相关 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-security</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-validation</artifactId>
+        </dependency>
+
+        <!-- DB 相关 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-mybatis</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-redis</artifactId>
+        </dependency>
+
+        <!-- 消息队列相关 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-mq</artifactId>
+        </dependency>
+
+
+        <!-- 工具类相关 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-excel</artifactId>
+        </dependency>
+
+        <!-- 三方云服务相关 -->
+        <dependency>
+            <groupId>com.github.binarywang</groupId>
+            <artifactId>wx-java-mp-spring-boot-starter</artifactId> <!-- 微信登录(公众号) -->
+        </dependency>
+
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-module-system-biz</artifactId>
+            <version>2.2.0-jdk8-snapshot</version>
+            <scope>compile</scope>
+        </dependency>
+    </dependencies>
+
+</project>

+ 98 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/MpAccountController.java

@@ -0,0 +1,98 @@
+package cn.iocoder.yudao.module.mp.controller.admin.account;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.mp.controller.admin.account.vo.*;
+import cn.iocoder.yudao.module.mp.convert.account.MpAccountConvert;
+import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO;
+import cn.iocoder.yudao.module.mp.service.account.MpAccountService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.validation.Valid;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+@Tag(name = "管理后台 - 公众号账号")
+@RestController
+@RequestMapping("/mp/account")
+@Validated
+public class MpAccountController {
+
+    @Resource
+    private MpAccountService mpAccountService;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建公众号账号")
+    @PreAuthorize("@ss.hasPermission('mp:account:create')")
+    public CommonResult<Long> createAccount(@Valid @RequestBody MpAccountCreateReqVO createReqVO) {
+        return success(mpAccountService.createAccount(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新公众号账号")
+    @PreAuthorize("@ss.hasPermission('mp:account:update')")
+    public CommonResult<Boolean> updateAccount(@Valid @RequestBody MpAccountUpdateReqVO updateReqVO) {
+        mpAccountService.updateAccount(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除公众号账号")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('mp:account:delete')")
+    public CommonResult<Boolean> deleteAccount(@RequestParam("id") Long id) {
+        mpAccountService.deleteAccount(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得公众号账号")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('mp:account:query')")
+    public CommonResult<MpAccountRespVO> getAccount(@RequestParam("id") Long id) {
+        MpAccountDO wxAccount = mpAccountService.getAccount(id);
+        return success(MpAccountConvert.INSTANCE.convert(wxAccount));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得公众号账号分页")
+    @PreAuthorize("@ss.hasPermission('mp:account:query')")
+    public CommonResult<PageResult<MpAccountRespVO>> getAccountPage(@Valid MpAccountPageReqVO pageVO) {
+        PageResult<MpAccountDO> pageResult = mpAccountService.getAccountPage(pageVO);
+        return success(MpAccountConvert.INSTANCE.convertPage(pageResult));
+    }
+
+    @GetMapping("/list-all-simple")
+    @Operation(summary = "获取公众号账号精简信息列表")
+    @PreAuthorize("@ss.hasPermission('mp:account:query')")
+    public CommonResult<List<MpAccountSimpleRespVO>> getSimpleAccounts() {
+        List<MpAccountDO> list = mpAccountService.getAccountList();
+        return success(MpAccountConvert.INSTANCE.convertList02(list));
+    }
+
+    @PutMapping("/generate-qr-code")
+    @Operation(summary = "生成公众号二维码")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('mp:account:qr-code')")
+    public CommonResult<Boolean> generateAccountQrCode(@RequestParam("id") Long id) {
+        mpAccountService.generateAccountQrCode(id);
+        return success(true);
+    }
+
+    @PutMapping("/clear-quota")
+    @Operation(summary = "清空公众号 API 配额")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('mp:account:clear-quota')")
+    public CommonResult<Boolean> clearAccountQuota(@RequestParam("id") Long id) {
+        mpAccountService.clearAccountQuota(id);
+        return success(true);
+    }
+
+}

+ 43 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountBaseVO.java

@@ -0,0 +1,43 @@
+package cn.iocoder.yudao.module.mp.controller.admin.account.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotEmpty;
+
+/**
+ * 公众号账号 Base VO,提供给添加、修改、详细的子 VO 使用
+ * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
+ *
+ * @author fengdan
+ */
+@Data
+public class MpAccountBaseVO {
+
+    @Schema(description = "公众号名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道源码")
+    @NotEmpty(message = "公众号名称不能为空")
+    private String name;
+
+    @Schema(description = "公众号微信号", requiredMode = Schema.RequiredMode.REQUIRED, example = "yudaoyuanma")
+    @NotEmpty(message = "公众号微信号不能为空")
+    private String account;
+
+    @Schema(description = "公众号 appId", requiredMode = Schema.RequiredMode.REQUIRED, example = "wx5b23ba7a5589ecbb")
+    @NotEmpty(message = "公众号 appId 不能为空")
+    private String appId;
+
+    @Schema(description = "公众号密钥", requiredMode = Schema.RequiredMode.REQUIRED, example = "3a7b3b20c537e52e74afd395eb85f61f")
+    @NotEmpty(message = "公众号密钥不能为空")
+    private String appSecret;
+
+    @Schema(description = "公众号 token", requiredMode = Schema.RequiredMode.REQUIRED, example = "kangdayuzhen")
+    @NotEmpty(message = "公众号 token 不能为空")
+    private String token;
+
+    @Schema(description = "加密密钥", example = "gjN+Ksei")
+    private String aesKey;
+
+    @Schema(description = "备注", example = "请关注芋道源码,学习技术")
+    private String remark;
+
+}

+ 14 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountCreateReqVO.java

@@ -0,0 +1,14 @@
+package cn.iocoder.yudao.module.mp.controller.admin.account.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+@Schema(description = "管理后台 - 公众号账号创建 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class MpAccountCreateReqVO extends MpAccountBaseVO {
+
+}

+ 24 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountPageReqVO.java

@@ -0,0 +1,24 @@
+package cn.iocoder.yudao.module.mp.controller.admin.account.vo;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+@Schema(description = "管理后台 - 公众号账号分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class MpAccountPageReqVO extends PageParam {
+
+    @Schema(name = "公众号名称", description = "模糊匹配")
+    private String name;
+
+    @Schema(name = "公众号账号", description = "模糊匹配")
+    private String account;
+
+    @Schema(name = "公众号 appid", description = "模糊匹配")
+    private String appId;
+
+}

+ 25 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountRespVO.java

@@ -0,0 +1,25 @@
+package cn.iocoder.yudao.module.mp.controller.admin.account.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - 公众号账号 Response VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class MpAccountRespVO extends MpAccountBaseVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    private Long id;
+
+    @Schema(description = "二维码图片URL", example = "https://www.iocoder.cn/1024.png")
+    private String qrCodeUrl;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    private LocalDateTime createTime;
+
+}

+ 16 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountSimpleRespVO.java

@@ -0,0 +1,16 @@
+package cn.iocoder.yudao.module.mp.controller.admin.account.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Schema(description = "管理后台 - 公众号账号精简信息 Response VO")
+@Data
+public class MpAccountSimpleRespVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    private Long id;
+
+    @Schema(description = "公众号名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道源码")
+    private String name;
+
+}

+ 20 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountUpdateReqVO.java

@@ -0,0 +1,20 @@
+package cn.iocoder.yudao.module.mp.controller.admin.account.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import javax.validation.constraints.NotNull;
+
+@Schema(description = "管理后台 - 公众号账号更新 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class MpAccountUpdateReqVO extends MpAccountBaseVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    @NotNull(message = "编号不能为空")
+    private Long id;
+
+}

+ 5 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/MpMaterialController.http

@@ -0,0 +1,5 @@
+### 请求 /mp/material/page 接口 => 成功
+GET {{baseUrl}}/mp/material/page?permanent=true&pageNo=1&pageSize=10
+Content-Type: application/json
+Authorization: Bearer {{token}}
+tenant-id: {{adminTenentId}}

+ 74 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/MpMaterialController.java

@@ -0,0 +1,74 @@
+package cn.iocoder.yudao.module.mp.controller.admin.material;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.mp.controller.admin.material.vo.*;
+import cn.iocoder.yudao.module.mp.convert.material.MpMaterialConvert;
+import cn.iocoder.yudao.module.mp.dal.dataobject.material.MpMaterialDO;
+import cn.iocoder.yudao.module.mp.service.material.MpMaterialService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.validation.Valid;
+import java.io.IOException;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+@Tag(name = "管理后台 - 公众号素材")
+@RestController
+@RequestMapping("/mp/material")
+@Validated
+public class MpMaterialController {
+
+    @Resource
+    private MpMaterialService mpMaterialService;
+
+    @Operation(summary = "上传临时素材")
+    @PostMapping("/upload-temporary")
+    @PreAuthorize("@ss.hasPermission('mp:material:upload-temporary')")
+    public CommonResult<MpMaterialUploadRespVO> uploadTemporaryMaterial(
+            @Valid MpMaterialUploadTemporaryReqVO reqVO) throws IOException {
+        MpMaterialDO material = mpMaterialService.uploadTemporaryMaterial(reqVO);
+        return success(MpMaterialConvert.INSTANCE.convert(material));
+    }
+
+    @Operation(summary = "上传永久素材")
+    @PostMapping("/upload-permanent")
+    @PreAuthorize("@ss.hasPermission('mp:material:upload-permanent')")
+    public CommonResult<MpMaterialUploadRespVO> uploadPermanentMaterial(
+            @Valid MpMaterialUploadPermanentReqVO reqVO) throws IOException {
+        MpMaterialDO material = mpMaterialService.uploadPermanentMaterial(reqVO);
+        return success(MpMaterialConvert.INSTANCE.convert(material));
+    }
+
+    @Operation(summary = "删除素材")
+    @DeleteMapping("/delete-permanent")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('mp:material:delete')")
+    public CommonResult<Boolean> deleteMaterial(@RequestParam("id") Long id) {
+        mpMaterialService.deleteMaterial(id);
+        return success(true);
+    }
+
+    @Operation(summary = "上传图文内容中的图片")
+    @PostMapping("/upload-news-image")
+    @PreAuthorize("@ss.hasPermission('mp:material:upload-news-image')")
+    public CommonResult<String> uploadNewsImage(@Valid MpMaterialUploadNewsImageReqVO reqVO)
+            throws IOException {
+        return success(mpMaterialService.uploadNewsImage(reqVO));
+    }
+
+    @Operation(summary = "获得素材分页")
+    @GetMapping("/page")
+    @PreAuthorize("@ss.hasPermission('mp:material:query')")
+    public CommonResult<PageResult<MpMaterialRespVO>> getMaterialPage(@Valid MpMaterialPageReqVO pageReqVO) {
+        PageResult<MpMaterialDO> pageResult = mpMaterialService.getMaterialPage(pageReqVO);
+        return success(MpMaterialConvert.INSTANCE.convertPage(pageResult));
+    }
+
+}

+ 27 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/vo/MpMaterialPageReqVO.java

@@ -0,0 +1,27 @@
+package cn.iocoder.yudao.module.mp.controller.admin.material.vo;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import javax.validation.constraints.NotNull;
+
+@Schema(description = "管理后台 - 公众号素材的分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class MpMaterialPageReqVO extends PageParam {
+
+    @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
+    @NotNull(message = "公众号账号的编号不能为空")
+    private Long accountId;
+
+    @Schema(description = "是否永久", example = "true")
+    private Boolean permanent;
+
+    @Schema(description = "文件类型 参见 WxConsts.MediaFileType 枚举", example = "image")
+    private String type;
+
+}

+ 47 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/vo/MpMaterialRespVO.java

@@ -0,0 +1,47 @@
+package cn.iocoder.yudao.module.mp.controller.admin.material.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - 公众号素材 Response VO")
+@Data
+public class MpMaterialRespVO {
+
+    @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    private Long id;
+
+    @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Long accountId;
+    @Schema(description = "公众号账号的 appId", requiredMode = Schema.RequiredMode.REQUIRED, example = "wx1234567890")
+    private String appId;
+
+    @Schema(description = "素材的 media_id", requiredMode = Schema.RequiredMode.REQUIRED, example = "123")
+    private String mediaId;
+
+    @Schema(description = "文件类型 参见 WxConsts.MediaFileType 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "image")
+    private String type;
+
+    @Schema(description = "是否永久 true - 永久;false - 临时", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
+    private Boolean permanent;
+
+    @Schema(description = "素材的 URL", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png")
+    private String url;
+
+
+    @Schema(description = "名字", example = "yunai.png")
+    private String name;
+
+    @Schema(description = "公众号文件 URL 只有【永久素材】使用", example = "https://mmbiz.qpic.cn/xxx.mp3")
+    private String mpUrl;
+
+    @Schema(description = "视频素材的标题 只有【永久素材】使用", example = "我是标题")
+    private String title;
+    @Schema(description = "视频素材的描述 只有【永久素材】使用", example = "我是介绍")
+    private String introduction;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    private LocalDateTime createTime;
+
+}

+ 23 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/vo/MpMaterialUploadNewsImageReqVO.java

@@ -0,0 +1,23 @@
+package cn.iocoder.yudao.module.mp.controller.admin.material.vo;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.validation.constraints.NotNull;
+
+@Schema(description = "管理后台 - 公众号素材上传图文内容中的图片 Request VO")
+@Data
+public class MpMaterialUploadNewsImageReqVO {
+
+    @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
+    @NotNull(message = "公众号账号的编号不能为空")
+    private Long accountId;
+
+    @Schema(description = "文件附件", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "文件不能为空")
+    @JsonIgnore // 避免被操作日志,进行序列化,导致报错
+    private MultipartFile file;
+
+}

+ 53 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/vo/MpMaterialUploadPermanentReqVO.java

@@ -0,0 +1,53 @@
+package cn.iocoder.yudao.module.mp.controller.admin.material.vo;
+
+import cn.hutool.core.util.ObjectUtil;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import me.chanjar.weixin.common.api.WxConsts;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.validation.constraints.AssertTrue;
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+
+@Schema(description = "管理后台 - 公众号素材上传永久 Request VO")
+@Data
+public class MpMaterialUploadPermanentReqVO {
+
+    @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
+    @NotNull(message = "公众号账号的编号不能为空")
+    private Long accountId;
+
+    @Schema(description = "文件类型 参见 WxConsts.MediaFileType 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "image")
+    @NotEmpty(message = "文件类型不能为空")
+    private String type;
+
+    @Schema(description = "文件附件", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "文件不能为空")
+    @JsonIgnore // 避免被操作日志,进行序列化,导致报错
+    private MultipartFile file;
+
+    @Schema(description = "名字 如果 name 为空,则使用 file 文件名", example = "wechat.mp")
+    private String name;
+
+    @Schema(description = "视频素材的标题 文件类型为 video 时,必填", example = "视频素材的标题")
+    private String title;
+    @Schema(description = "视频素材的描述 文件类型为 video 时,必填", example = "视频素材的描述")
+    private String introduction;
+
+    @AssertTrue(message = "标题不能为空")
+    public boolean isTitleValid() {
+        // 生成场景为管理后台时,必须设置上级菜单,不然生成的菜单 SQL 是无父级菜单的
+        return ObjectUtil.notEqual(type, WxConsts.MediaFileType.VIDEO)
+                || title != null;
+    }
+
+    @AssertTrue(message = "描述不能为空")
+    public boolean isIntroductionValid() {
+        // 生成场景为管理后台时,必须设置上级菜单,不然生成的菜单 SQL 是无父级菜单的
+        return ObjectUtil.notEqual(type, WxConsts.MediaFileType.VIDEO)
+                || introduction != null;
+    }
+
+}

+ 16 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/vo/MpMaterialUploadRespVO.java

@@ -0,0 +1,16 @@
+package cn.iocoder.yudao.module.mp.controller.admin.material.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Schema(description = "管理后台 - 公众号素材上传结果 Response VO")
+@Data
+public class MpMaterialUploadRespVO {
+
+    @Schema(description = "素材的 media_id", requiredMode = Schema.RequiredMode.REQUIRED, example = "123")
+    private String mediaId;
+
+    @Schema(description = "素材的 URL", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png")
+    private String url;
+
+}

+ 28 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/vo/MpMaterialUploadTemporaryReqVO.java

@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.module.mp.controller.admin.material.vo;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+
+@Schema(description = "管理后台 - 公众号素材上传临时 Request VO")
+@Data
+public class MpMaterialUploadTemporaryReqVO {
+
+    @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
+    @NotNull(message = "公众号账号的编号不能为空")
+    private Long accountId;
+
+    @Schema(description = "文件类型 参见 WxConsts.MediaFileType 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "image")
+    @NotEmpty(message = "文件类型不能为空")
+    private String type;
+
+    @Schema(description = "文件附件", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "文件不能为空")
+    @JsonIgnore // 避免被操作日志,进行序列化,导致报错
+    private MultipartFile file;
+
+}

+ 50 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/menu/MpMenuController.http

@@ -0,0 +1,50 @@
+### 请求 /mp/menu/save 接口 => 成功
+POST {{baseUrl}}/mp/menu/save
+Content-Type: application/json
+Authorization: Bearer {{token}}
+tenant-id: {{adminTenentId}}
+
+{
+  "accountId": "1",
+  "menus": [
+    {
+      "type":"click",
+      "name":"今日歌曲",
+      "menuKey":"V1001_TODAY_MUSIC"
+    },
+    {
+      "name":"搜索",
+      "type":"view",
+      "url":"https://www.soso.com/"
+    },
+    {
+      "name": "父按钮",
+      "children": [
+        {
+          "type":"click",
+          "name":"归去来兮",
+          "menuKey":"MUSIC"
+        },
+        {
+          "name":"不说",
+          "type":"view",
+          "url":"https://www.soso.com/"
+        }]
+    }]
+}
+
+### 请求 /mp/menu/save 接口 => 成功(清空)
+POST {{baseUrl}}/mp/menu/save
+Content-Type: application/json
+Authorization: Bearer {{token}}
+tenant-id: {{adminTenentId}}
+
+{
+  "accountId": "1",
+  "menus": []
+}
+
+### 请求 /mp/menu/list 接口 => 成功
+GET {{baseUrl}}/mp/menu/list?accountId=1
+Authorization: Bearer {{token}}
+tenant-id: {{adminTenentId}}

+ 57 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/menu/MpMenuController.java

@@ -0,0 +1,57 @@
+package cn.iocoder.yudao.module.mp.controller.admin.menu;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.module.mp.controller.admin.menu.vo.MpMenuRespVO;
+import cn.iocoder.yudao.module.mp.controller.admin.menu.vo.MpMenuSaveReqVO;
+import cn.iocoder.yudao.module.mp.convert.menu.MpMenuConvert;
+import cn.iocoder.yudao.module.mp.dal.dataobject.menu.MpMenuDO;
+import cn.iocoder.yudao.module.mp.service.menu.MpMenuService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.validation.Valid;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+@Tag(name = "管理后台 - 公众号菜单")
+@RestController
+@RequestMapping("/mp/menu")
+@Validated
+public class MpMenuController {
+
+    @Resource
+    private MpMenuService mpMenuService;
+
+    @PostMapping("/save")
+    @Operation(summary = "保存公众号菜单")
+    @PreAuthorize("@ss.hasPermission('mp:menu:save')")
+    public CommonResult<Boolean> saveMenu(@Valid @RequestBody MpMenuSaveReqVO createReqVO) {
+        mpMenuService.saveMenu(createReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除公众号菜单")
+    @Parameter(name = "accountId", description = "公众号账号的编号", required = true, example = "10")
+    @PreAuthorize("@ss.hasPermission('mp:menu:delete')")
+    public CommonResult<Boolean> deleteMenu(@RequestParam("accountId") Long accountId) {
+        mpMenuService.deleteMenuByAccountId(accountId);
+        return success(true);
+    }
+
+    @GetMapping("/list")
+    @Operation(summary = "获得公众号菜单列表")
+    @Parameter(name = "accountId", description = "公众号账号的编号", required = true, example = "10")
+    @PreAuthorize("@ss.hasPermission('mp:menu:query')")
+    public CommonResult<List<MpMenuRespVO>> getMenuList(@RequestParam("accountId") Long accountId) {
+        List<MpMenuDO> list = mpMenuService.getMenuListByAccountId(accountId);
+        return success(MpMenuConvert.INSTANCE.convertList(list));
+    }
+
+}

+ 115 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/menu/vo/MpMenuBaseVO.java

@@ -0,0 +1,115 @@
+package cn.iocoder.yudao.module.mp.controller.admin.menu.vo;
+
+import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import me.chanjar.weixin.common.api.WxConsts;
+import org.hibernate.validator.constraints.URL;
+
+import javax.validation.Valid;
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+import java.util.List;
+
+import static cn.iocoder.yudao.module.mp.framework.mp.core.util.MpUtils.*;
+
+/**
+ * 公众号菜单 Base VO,提供给添加、修改、详细的子 VO 使用
+ * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
+ */
+@Data
+public class MpMenuBaseVO {
+
+    /**
+     * 菜单名称
+     */
+    private String name;
+    /**
+     * 菜单标识
+     *
+     * 支持多 DB 类型时,无法直接使用 key + @TableField("menuKey") 来实现转换,原因是 "menuKey" AS key 而存在报错
+     */
+    private String menuKey;
+    /**
+     * 父菜单编号
+     */
+    private Long parentId;
+
+    // ========== 按钮操作 ==========
+
+    /**
+     * 按钮类型
+     *
+     * 枚举 {@link WxConsts.MenuButtonType}
+     */
+    private String type;
+
+    @Schema(description = "网页链接", example = "https://www.iocoder.cn/")
+    @NotEmpty(message = "网页链接不能为空", groups = {ViewButtonGroup.class, MiniProgramButtonGroup.class})
+    @URL(message = "网页链接必须是 URL 格式")
+    private String url;
+
+    @Schema(description = "小程序的 appId", example = "wx1234567890")
+    @NotEmpty(message = "小程序的 appId 不能为空", groups = MiniProgramButtonGroup.class)
+    private String miniProgramAppId;
+
+    @Schema(description = "小程序的页面路径", example = "pages/index/index")
+    @NotEmpty(message = "小程序的页面路径不能为空", groups = MiniProgramButtonGroup.class)
+    private String miniProgramPagePath;
+
+    @Schema(description ="跳转图文的媒体编号", example = "jCQk93AIIgp8ixClWcW_NXXqBKInNWNmq2XnPeDZl7IMVqWiNeL4FfELtggRXd83")
+    @NotEmpty(message = "跳转图文的媒体编号不能为空", groups = ViewLimitedButtonGroup.class)
+    private String articleId;
+
+    // ========== 消息内容 ==========
+
+    @Schema(description = "回复的消息类型 枚举 TEXT、IMAGE、VOICE、VIDEO、NEWS、MUSIC", example = "text")
+    @NotEmpty(message = "回复的消息类型不能为空", groups = {ClickButtonGroup.class, ScanCodeWaitMsgButtonGroup.class})
+    private String replyMessageType;
+
+    @Schema(description = "回复的消息内容", example = "欢迎关注")
+    @NotEmpty(message = "回复的消息内容不能为空", groups = TextMessageGroup.class)
+    private String replyContent;
+
+    @Schema(description = "回复的媒体 id", example = "123456")
+    @NotEmpty(message = "回复的消息 mediaId 不能为空",
+            groups = {ImageMessageGroup.class, VoiceMessageGroup.class, VideoMessageGroup.class})
+    private String replyMediaId;
+    @Schema(description = "回复的媒体 URL", example = "https://www.iocoder.cn/xxx.jpg")
+    @NotEmpty(message = "回复的消息 mediaId 不能为空",
+            groups = {ImageMessageGroup.class, VoiceMessageGroup.class, VideoMessageGroup.class})
+    private String replyMediaUrl;
+
+    @Schema(description = "缩略图的媒体 id", example = "123456")
+    @NotEmpty(message = "回复的消息 thumbMediaId 不能为空", groups = {MusicMessageGroup.class})
+    private String replyThumbMediaId;
+    @Schema(description = "缩略图的媒体 URL",example = "https://www.iocoder.cn/xxx.jpg")
+    @NotEmpty(message = "回复的消息 thumbMedia 地址不能为空", groups = {MusicMessageGroup.class})
+    private String replyThumbMediaUrl;
+
+    @Schema(description = "回复的标题", example = "视频标题")
+    @NotEmpty(message = "回复的消息标题不能为空", groups = VideoMessageGroup.class)
+    private String replyTitle;
+    @Schema(description = "回复的描述", example = "视频描述")
+    @NotEmpty(message = "消息描述不能为空", groups = VideoMessageGroup.class)
+    private String replyDescription;
+
+    /**
+     * 回复的图文消息数组
+     *
+     * 消息类型为 {@link WxConsts.XmlMsgType} 的 NEWS
+     */
+    @NotNull(message = "回复的图文消息不能为空", groups = {NewsMessageGroup.class, ViewLimitedButtonGroup.class})
+    @Valid
+    private List<MpMessageDO.Article> replyArticles;
+
+    @Schema(description = "回复的音乐链接", example = "https://www.iocoder.cn/xxx.mp3")
+    @NotEmpty(message = "回复的音乐链接不能为空", groups = MusicMessageGroup.class)
+    @URL(message = "回复的高质量音乐链接格式不正确", groups = MusicMessageGroup.class)
+    private String replyMusicUrl;
+    @Schema(description = "高质量音乐链接", example = "https://www.iocoder.cn/xxx.mp3")
+    @NotEmpty(message = "回复的高质量音乐链接不能为空", groups = MusicMessageGroup.class)
+    @URL(message = "回复的高质量音乐链接格式不正确", groups = MusicMessageGroup.class)
+    private String replyHqMusicUrl;
+
+}

+ 28 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/menu/vo/MpMenuRespVO.java

@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.module.mp.controller.admin.menu.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - 公众号菜单 Response VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class MpMenuRespVO extends MpMenuBaseVO {
+
+    @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    private Long id;
+
+    @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
+    private Long accountId;
+
+    @Schema(description = "公众号 appId", requiredMode = Schema.RequiredMode.REQUIRED, example = "wx1234567890ox")
+    private String appId;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    private LocalDateTime createTime;
+
+}

+ 34 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/menu/vo/MpMenuSaveReqVO.java

@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.module.mp.controller.admin.menu.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.Valid;
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+import java.util.List;
+
+@Schema(description = "管理后台 - 公众号菜单保存 Request VO")
+@Data
+public class MpMenuSaveReqVO {
+
+    @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
+    @NotNull(message = "公众号账号的编号不能为空")
+    private Long accountId;
+
+    @NotEmpty(message = "菜单不能为空")
+    @Valid
+    private List<Menu> menus;
+
+    @Schema(description = "管理后台 - 公众号菜单保存时的每个菜单")
+    @Data
+    public static class Menu extends MpMenuBaseVO {
+
+        /**
+         * 子菜单数组
+         */
+        private List<Menu> children;
+
+    }
+
+}

+ 5 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/MpAutoReplyController.http

@@ -0,0 +1,5 @@
+### 请求 /mp/message/page 接口 => 成功
+GET {{baseUrl}}/mp/auto-reply/page?accountId=1&pageNo=1&pageSize=10
+Content-Type: application/json
+Authorization: Bearer {{token}}
+tenant-id: {{adminTenentId}}

+ 74 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/MpAutoReplyController.java

@@ -0,0 +1,74 @@
+package cn.iocoder.yudao.module.mp.controller.admin.message;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.mp.controller.admin.message.vo.autoreply.MpAutoReplyCreateReqVO;
+import cn.iocoder.yudao.module.mp.controller.admin.message.vo.autoreply.MpAutoReplyRespVO;
+import cn.iocoder.yudao.module.mp.controller.admin.message.vo.autoreply.MpAutoReplyUpdateReqVO;
+import cn.iocoder.yudao.module.mp.controller.admin.message.vo.message.MpMessagePageReqVO;
+import cn.iocoder.yudao.module.mp.convert.message.MpAutoReplyConvert;
+import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpAutoReplyDO;
+import cn.iocoder.yudao.module.mp.service.message.MpAutoReplyService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.validation.Valid;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+@Tag(name = "管理后台 - 公众号自动回复")
+@RestController
+@RequestMapping("/mp/auto-reply")
+@Validated
+public class MpAutoReplyController {
+
+    @Resource
+    private MpAutoReplyService mpAutoReplyService;
+
+    @GetMapping("/page")
+    @Operation(summary = "获得公众号自动回复分页")
+    @PreAuthorize("@ss.hasPermission('mp:auto-reply:query')")
+    public CommonResult<PageResult<MpAutoReplyRespVO>> getAutoReplyPage(@Valid MpMessagePageReqVO pageVO) {
+        PageResult<MpAutoReplyDO> pageResult = mpAutoReplyService.getAutoReplyPage(pageVO);
+        return success(MpAutoReplyConvert.INSTANCE.convertPage(pageResult));
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得公众号自动回复")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('mp:auto-reply:query')")
+    public CommonResult<MpAutoReplyRespVO> getAutoReply(@RequestParam("id") Long id) {
+        MpAutoReplyDO autoReply = mpAutoReplyService.getAutoReply(id);
+        return success(MpAutoReplyConvert.INSTANCE.convert(autoReply));
+    }
+
+    @PostMapping("/create")
+    @Operation(summary = "创建公众号自动回复")
+    @PreAuthorize("@ss.hasPermission('mp:auto-reply:create')")
+    public CommonResult<Long> createAutoReply(@Valid @RequestBody MpAutoReplyCreateReqVO createReqVO) {
+        return success(mpAutoReplyService.createAutoReply(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新公众号自动回复")
+    @PreAuthorize("@ss.hasPermission('mp:auto-reply:update')")
+    public CommonResult<Boolean> updateAutoReply(@Valid @RequestBody MpAutoReplyUpdateReqVO updateReqVO) {
+        mpAutoReplyService.updateAutoReply(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除公众号自动回复")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('mp:auto-reply:delete')")
+    public CommonResult<Boolean> deleteAutoReply(@RequestParam("id") Long id) {
+        mpAutoReplyService.deleteAutoReply(id);
+        return success(true);
+    }
+
+}

+ 33 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/MpMessageController.http

@@ -0,0 +1,33 @@
+### 请求 /mp/message/page 接口 => 成功
+GET {{baseUrl}}/mp/message/page?accountId=1&pageNo=1&pageSize=10
+Content-Type: application/json
+Authorization: Bearer {{token}}
+tenant-id: {{adminTenentId}}
+
+### 请求 /mp/message/send 接口 => 成功(文本)
+POST {{baseUrl}}/mp/message/send
+Content-Type: application/json
+Authorization: Bearer {{token}}
+tenant-id: {{adminTenentId}}
+
+{
+  "userId": 3,
+  "type": "text",
+  "content": "测试消息"
+}
+
+### 请求 /mp/message/send 接口 => 成功(音乐)
+POST {{baseUrl}}/mp/message/send
+Content-Type: application/json
+Authorization: Bearer {{token}}
+tenant-id: {{adminTenentId}}
+
+{
+  "userId": 3,
+  "type": "music",
+  "title": "测试音乐标题",
+  "description": "测试音乐内容",
+  "musicUrl": "https://www.iocoder.cn/xx.mp3",
+  "hqMusicUrl": "https://www.iocoder.cn/xx_high.mp3",
+  "thumbMediaId": "s98Iveeg9vDVFwa9q0u8-zSfdKe3xIzAm7wCrFE4WKGPIo4d9qAhtC-n6qvnyWyH"
+}

+ 129 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/MpMessageController.java

@@ -0,0 +1,129 @@
+package cn.iocoder.yudao.module.mp.controller.admin.message;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
+import cn.iocoder.yudao.module.mp.controller.admin.message.vo.message.MpMessagePageReqVO;
+import cn.iocoder.yudao.module.mp.controller.admin.message.vo.message.MpMessageRespVO;
+import cn.iocoder.yudao.module.mp.controller.admin.message.vo.message.MpMessageSendReqVO;
+import cn.iocoder.yudao.module.mp.controller.admin.message.vo.message.attendanceMessageSendReqVO;
+import cn.iocoder.yudao.module.mp.convert.message.MpMessageConvert;
+import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO;
+import cn.iocoder.yudao.module.mp.dal.dataobject.tag.MpTagDO;
+import cn.iocoder.yudao.module.mp.dal.dataobject.user.MpUserDO;
+import cn.iocoder.yudao.module.mp.service.message.MpMessageService;
+import cn.iocoder.yudao.module.mp.service.tag.MpTagService;
+import cn.iocoder.yudao.module.mp.service.user.MpUserService;
+import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO;
+import cn.iocoder.yudao.module.system.service.dept.DeptService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.validation.Valid;
+
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+@Tag(name = "管理后台 - 公众号消息")
+@RestController
+@RequestMapping("/mp/message")
+@Validated
+public class MpMessageController {
+
+    @Resource
+    private MpMessageService mpMessageService;
+
+    @Resource
+    private MpUserService mpUserService;
+
+    @Resource
+    private MpTagService mpTagService;
+    @Resource
+    private DeptService deptService;
+
+    //自动运行 发给所有导师标签所有今天的信息
+    @Scheduled(cron = "0 50 17 * * ?")
+    public void sendAttendanceScheduled() {
+        attendanceMessageSendReqVO reqVO =new attendanceMessageSendReqVO();
+        List<MpUserDO> userList = mpUserService.selectUsersByTagId("100");//导师标签id
+        if (userList!=null&&userList.size()>0) {
+            for (MpUserDO user : userList) {
+                reqVO.setUserId(user.getId());
+                reqVO.setType("text");
+                mpMessageService.sendKefuAttendanceMessage(reqVO);
+                System.out.println("运行了");
+            }
+        }
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得公众号消息分页")
+    @PreAuthorize("@ss.hasPermission('mp:message:query')")
+    public CommonResult<PageResult<MpMessageRespVO>> getMessagePage(@Valid MpMessagePageReqVO pageVO) {
+        PageResult<MpMessageDO> pageResult = mpMessageService.getMessagePage(pageVO);
+        return success(MpMessageConvert.INSTANCE.convertPage(pageResult));
+    }
+
+    @PostMapping("/send")
+    @Operation(summary = "给粉丝发送消息")
+    @PreAuthorize("@ss.hasPermission('mp:message:send')")
+    public CommonResult<MpMessageRespVO> sendMessage(@Valid @RequestBody MpMessageSendReqVO reqVO) {//装上了考勤信息
+        MpMessageDO message = mpMessageService.sendKefuMessage(reqVO);
+        return success(MpMessageConvert.INSTANCE.convert(message));
+    }
+
+
+    //可设置成自动运行
+    @PostMapping("/sendAttendance")
+    @Operation(summary = "向导师发送今天的考勤信息")
+    public void sendAttendance() {
+        attendanceMessageSendReqVO reqVO =new attendanceMessageSendReqVO();
+        List<MpUserDO> userList = mpUserService.selectUsersByTagId("100");//导师标签id
+        if (userList!=null&&userList.size()>0) {
+            for (MpUserDO user : userList) {
+                reqVO.setUserId(user.getId());
+                reqVO.setType("text");
+                mpMessageService.sendKefuAttendanceMessage(reqVO);
+                System.out.println("运行了");
+            }
+        }
+    }
+
+
+    //TODO可以将标签名称设定成每个工作间名称(里面学生的家长就用这个标签)
+    @PostMapping("/sendAttendanceToFamily")
+    @Operation(summary = "导师向工作间学生家长发送今天工作间所有学生的考勤信息")
+    public void sendAttendanceToFamily() {
+        Long loginUserDeptIdId = SecurityFrameworkUtils.getLoginUserDeptId();
+        DeptDO dept =  deptService.getDept(loginUserDeptIdId);
+        MpTagDO Tag = mpTagService.selectByName(dept.getName());
+        attendanceMessageSendReqVO reqVO =new attendanceMessageSendReqVO();
+        List<MpUserDO> userList = mpUserService.selectUsersByTagId(String.valueOf(Tag.getTagId()));//家长标签id
+        if (userList!=null&&userList.size()>0) {
+            for (MpUserDO user : userList) {
+                reqVO.setUserId(user.getId());
+                reqVO.setType("text");
+                mpMessageService.sendAttendanceToFamily(reqVO);
+            }
+        }
+    }
+
+
+//    @PostMapping("/test")
+//    @Operation(summary = "向对应家长发送对应考勤信息")
+//    public void sendAttendanceToFamilyOnly(Long userId) {
+//        attendanceMessageSendReqVO reqVO =new attendanceMessageSendReqVO();
+//
+//        reqVO.setUserId(userId);
+//        reqVO.setType("text");
+//        mpMessageService.sendKefuAttendanceMessage(reqVO);
+//
+//    }
+}

+ 109 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyBaseVO.java

@@ -0,0 +1,109 @@
+package cn.iocoder.yudao.module.mp.controller.admin.message.vo.autoreply;
+
+import cn.hutool.core.util.ObjectUtil;
+import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO;
+import cn.iocoder.yudao.module.mp.enums.message.MpAutoReplyTypeEnum;
+import cn.iocoder.yudao.module.mp.framework.mp.core.util.MpUtils.*;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import me.chanjar.weixin.common.api.WxConsts;
+import org.hibernate.validator.constraints.URL;
+
+import javax.validation.Valid;
+import javax.validation.constraints.AssertTrue;
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+import java.util.List;
+
+/**
+ * 公众号自动回复  Base VO,提供给添加、修改、详细的子 VO 使用
+ * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
+ */
+@Data
+public class MpAutoReplyBaseVO {
+
+    @Schema(description = "回复类型 参见 MpAutoReplyTypeEnum 枚举", example = "1")
+    @NotNull(message = "回复类型不能为空")
+    private Integer type;
+
+    // ==================== 请求消息 ====================
+
+    @Schema(description = "请求的关键字 当 type 为 MpAutoReplyTypeEnum#KEYWORD 时,必填", example = "关键字")
+    private String requestKeyword;
+    @Schema(description = "请求的匹配方式 当 type 为 MpAutoReplyTypeEnum#KEYWORD 时,必填", example = "1")
+    private Integer requestMatch;
+
+    @Schema(description = "请求的消息类型 当 type 为 MpAutoReplyTypeEnum#MESSAGE 时,必填", example = "text")
+    private String requestMessageType;
+
+    // ==================== 响应消息 ====================
+
+    @Schema(description = "回复的消息类型 枚举 TEXT、IMAGE、VOICE、VIDEO、NEWS、MUSIC", example = "text")
+    @NotEmpty(message = "回复的消息类型不能为空")
+    private String responseMessageType;
+
+    @Schema(description = "回复的消息内容", example = "欢迎关注")
+    @NotEmpty(message = "回复的消息内容不能为空", groups = TextMessageGroup.class)
+    private String responseContent;
+
+    @Schema(description = "回复的媒体 id", example = "123456")
+    @NotEmpty(message = "回复的消息 mediaId 不能为空",
+            groups = {ImageMessageGroup.class, VoiceMessageGroup.class, VideoMessageGroup.class})
+    private String responseMediaId;
+    @Schema(description = "回复的媒体 URL", example = "https://www.iocoder.cn/xxx.jpg")
+    @NotEmpty(message = "回复的消息 mediaId 不能为空",
+            groups = {ImageMessageGroup.class, VoiceMessageGroup.class, VideoMessageGroup.class})
+    private String responseMediaUrl;
+
+    @Schema(description = "缩略图的媒体 id", example = "123456")
+    @NotEmpty(message = "回复的消息 thumbMediaId 不能为空", groups = {MusicMessageGroup.class})
+    private String responseThumbMediaId;
+    @Schema(description = "缩略图的媒体 URL",example = "https://www.iocoder.cn/xxx.jpg")
+    @NotEmpty(message = "回复的消息 thumbMedia 地址不能为空", groups = {MusicMessageGroup.class})
+    private String responseThumbMediaUrl;
+
+    @Schema(description = "回复的标题", example = "视频标题")
+    @NotEmpty(message = "回复的消息标题不能为空", groups = VideoMessageGroup.class)
+    private String responseTitle;
+    @Schema(description = "回复的描述", example = "视频描述")
+    @NotEmpty(message = "消息描述不能为空", groups = VideoMessageGroup.class)
+    private String responseDescription;
+
+    /**
+     * 回复的图文消息
+     *
+     * 消息类型为 {@link WxConsts.XmlMsgType} 的 NEWS
+     */
+    @NotNull(message = "回复的图文消息不能为空", groups = {NewsMessageGroup.class, ViewLimitedButtonGroup.class})
+    @Valid
+    private List<MpMessageDO.Article> responseArticles;
+
+    @Schema(description = "回复的音乐链接", example = "https://www.iocoder.cn/xxx.mp3")
+    @NotEmpty(message = "回复的音乐链接不能为空", groups = MusicMessageGroup.class)
+    @URL(message = "回复的高质量音乐链接格式不正确", groups = MusicMessageGroup.class)
+    private String responseMusicUrl;
+    @Schema(description = "高质量音乐链接", example = "https://www.iocoder.cn/xxx.mp3")
+    @NotEmpty(message = "回复的高质量音乐链接不能为空", groups = MusicMessageGroup.class)
+    @URL(message = "回复的高质量音乐链接格式不正确", groups = MusicMessageGroup.class)
+    private String responseHqMusicUrl;
+
+    @AssertTrue(message = "请求的关键字不能为空")
+    public boolean isRequestKeywordValid() {
+        return ObjectUtil.notEqual(type, MpAutoReplyTypeEnum.KEYWORD)
+                || requestKeyword != null;
+    }
+
+    @AssertTrue(message = "请求的关键字的匹配不能为空")
+    public boolean isRequestMatchValid() {
+        return ObjectUtil.notEqual(type, MpAutoReplyTypeEnum.KEYWORD)
+                || requestMatch != null;
+    }
+
+    @AssertTrue(message = "请求的消息类型不能为空")
+    public boolean isRequestMessageTypeValid() {
+        return ObjectUtil.notEqual(type, MpAutoReplyTypeEnum.MESSAGE)
+                || requestMessageType != null;
+    }
+
+
+}

+ 20 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyCreateReqVO.java

@@ -0,0 +1,20 @@
+package cn.iocoder.yudao.module.mp.controller.admin.message.vo.autoreply;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import javax.validation.constraints.NotNull;
+
+@Schema(description = "管理后台 - 公众号自动回复的创建 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class MpAutoReplyCreateReqVO extends MpAutoReplyBaseVO {
+
+    @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    @NotNull(message = "公众号账号的编号不能为空")
+    private Long accountId;
+
+}

+ 21 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyPageReqVO.java

@@ -0,0 +1,21 @@
+package cn.iocoder.yudao.module.mp.controller.admin.message.vo.autoreply;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import javax.validation.constraints.NotNull;
+
+@Schema(description = "管理后台 - 公众号自动回复的分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class MpAutoReplyPageReqVO extends PageParam {
+
+    @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @NotNull(message = "公众号账号的编号不能为空")
+    private Long accountId;
+
+}

+ 27 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyRespVO.java

@@ -0,0 +1,27 @@
+package cn.iocoder.yudao.module.mp.controller.admin.message.vo.autoreply;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - 公众号自动回复 Response VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class MpAutoReplyRespVO extends MpAutoReplyBaseVO {
+
+    @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    private Long id;
+
+    @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    private Long accountId;
+    @Schema(description = "公众号 appId", requiredMode = Schema.RequiredMode.REQUIRED, example = "wx1234567890")
+    private String appId;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    private LocalDateTime createTime;
+
+}

+ 20 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyUpdateReqVO.java

@@ -0,0 +1,20 @@
+package cn.iocoder.yudao.module.mp.controller.admin.message.vo.autoreply;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import javax.validation.constraints.NotNull;
+
+@Schema(description = "管理后台 - 公众号自动回复的更新 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class MpAutoReplyUpdateReqVO extends MpAutoReplyBaseVO {
+
+    @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    @NotNull(message = "主键不能为空")
+    private Long id;
+
+}

+ 35 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/vo/message/MpMessagePageReqVO.java

@@ -0,0 +1,35 @@
+package cn.iocoder.yudao.module.mp.controller.admin.message.vo.message;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import javax.validation.constraints.NotNull;
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - 公众号消息分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class MpMessagePageReqVO extends PageParam {
+
+    @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    @NotNull(message = "公众号账号的编号不能为空")
+    private Long accountId;
+
+    @Schema(description = "消息类型 参见 WxConsts.XmlMsgType 枚举", example = "text")
+    private String type;
+
+    @Schema(description = "公众号粉丝标识", example = "o6_bmjrPTlm6_2sgVt7hMZOPfL2M")
+    private String openid;
+
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    @Schema(description = "创建时间")
+    private LocalDateTime[] createTime;
+
+}

+ 101 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/vo/message/MpMessageRespVO.java

@@ -0,0 +1,101 @@
+package cn.iocoder.yudao.module.mp.controller.admin.message.vo.message;
+
+import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import me.chanjar.weixin.common.api.WxConsts;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Schema(description = "管理后台 - 公众号消息 Response VO")
+@Data
+public class MpMessageRespVO {
+
+    @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    private Integer id;
+
+    @Schema(description = "微信公众号消息 id", requiredMode = Schema.RequiredMode.REQUIRED, example = "23953173569869169")
+    private Long msgId;
+
+    @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Long accountId;
+    @Schema(description = "公众号账号的 appid", requiredMode = Schema.RequiredMode.REQUIRED, example = "wx1234567890")
+    private String appId;
+
+    @Schema(description = "公众号粉丝编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
+    private Long userId;
+    @Schema(description = "公众号粉丝标志", requiredMode = Schema.RequiredMode.REQUIRED, example = "o6_bmjrPTlm6_2sgVt7hMZOPfL2M")
+    private String openid;
+
+    @Schema(description = "消息类型 参见 WxConsts.XmlMsgType 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "text")
+    private String type;
+    @Schema(description = "消息来源 参见 MpMessageSendFromEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Integer sendFrom;
+
+    // ========= 普通消息内容 https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Receiving_standard_messages.html
+
+    @Schema(description = "消息内容 消息类型为 text 时,才有值", example = "你好呀")
+    private String content;
+
+    @Schema(description = "媒体素材的编号 消息类型为 image、voice、video 时,才有值", example = "1234567890")
+    private String mediaId;
+    @Schema(description = "媒体文件的 URL 消息类型为 image、voice、video 时,才有值", example = "https://www.iocoder.cn/xxx.png")
+    private String mediaUrl;
+
+    @Schema(description = "语音识别后文本 消息类型为 voice 时,才有值", example = "语音识别后文本")
+    private String recognition;
+    @Schema(description = "语音格式 消息类型为 voice 时,才有值", example = "amr")
+    private String format;
+
+    @Schema(description = "标题 消息类型为 video、music、link 时,才有值", example = "我是标题")
+    private String title;
+
+    @Schema(description = "描述 消息类型为 video、music 时,才有值", example = "我是描述")
+    private String description;
+
+    @Schema(description = "缩略图的媒体 id 消息类型为 video、music 时,才有值", example = "1234567890")
+    private String thumbMediaId;
+    @Schema(description = "缩略图的媒体 URL 消息类型为 video、music 时,才有值", example = "https://www.iocoder.cn/xxx.png")
+    private String thumbMediaUrl;
+
+    @Schema(description = "点击图文消息跳转链接 消息类型为 link 时,才有值", example = "https://www.iocoder.cn")
+    private String url;
+
+    @Schema(description = "地理位置维度 消息类型为 location 时,才有值", example = "23.137466")
+    private Double locationX;
+
+    @Schema(description = "地理位置经度 消息类型为 location 时,才有值", example = "113.352425")
+    private Double locationY;
+
+    @Schema(description = "地图缩放大小 消息类型为 location 时,才有值", example = "13")
+    private Double scale;
+
+    @Schema(description = "详细地址 消息类型为 location 时,才有值", example = "杨浦区黄兴路 221-4 号临")
+    private String label;
+
+    /**
+     * 图文消息数组
+     *
+     * 消息类型为 {@link WxConsts.XmlMsgType} 的 NEWS
+     */
+    private List<MpMessageDO.Article> articles;
+
+    @Schema(description = "音乐链接 消息类型为 music 时,才有值", example = "https://www.iocoder.cn/xxx.mp3")
+    private String musicUrl;
+    @Schema(description = "高质量音乐链接 消息类型为 music 时,才有值", example = "https://www.iocoder.cn/xxx.mp3")
+    private String hqMusicUrl;
+
+    // ========= 事件推送 https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Receiving_event_pushes.html
+
+    @Schema(description = "事件类型 参见 WxConsts.EventType 枚举", example = "subscribe")
+    private String event;
+    @Schema(description = "事件 Key 参见 WxConsts.EventType 枚举", example = "qrscene_123456")
+    private String eventKey;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    private LocalDateTime createTime;
+
+}

+ 63 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/vo/message/MpMessageSendReqVO.java

@@ -0,0 +1,63 @@
+package cn.iocoder.yudao.module.mp.controller.admin.message.vo.message;
+
+import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO;
+import cn.iocoder.yudao.module.mp.framework.mp.core.util.MpUtils.*;
+import cn.iocoder.yudao.module.system.dal.dataobject.studentAttendance.StudentAttendanceDO;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.Valid;
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+import java.util.List;
+
+@Schema(description = "管理后台 - 公众号消息发送 Request VO")
+@Data
+public class MpMessageSendReqVO {
+
+    @Schema(description = "公众号粉丝的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    @NotNull(message = "公众号粉丝的编号不能为空")
+    private Long userId;
+
+    // ========== 消息内容 ==========
+
+    @Schema(description = "消息类型 TEXT/IMAGE/VOICE/VIDEO/NEWS", requiredMode = Schema.RequiredMode.REQUIRED, example = "text")
+    @NotEmpty(message = "消息类型不能为空")
+    public String type;
+
+    @Schema(description = "消息内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "你好呀")
+    @NotEmpty(message = "消息内容不能为空", groups = TextMessageGroup.class)
+    private String content;
+
+    @Schema(description = "媒体 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "qqc_2Fot30Jse-HDoZmo5RrUDijz2nGUkP")
+    @NotEmpty(message = "消息内容不能为空", groups = {ImageMessageGroup.class, VoiceMessageGroup.class, VideoMessageGroup.class})
+    private String mediaId;
+
+    @Schema(description = "标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "没有标题")
+    @NotEmpty(message = "消息内容不能为空", groups = VideoMessageGroup.class)
+    private String title;
+
+    @Schema(description = "描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "你猜")
+    @NotEmpty(message = "消息描述不能为空", groups = VideoMessageGroup.class)
+    private String description;
+
+    @Schema(description = "缩略图的媒体 id", requiredMode = Schema.RequiredMode.REQUIRED, example = "qqc_2Fot30Jse-HDoZmo5RrUDijz2nGUkP")
+    @NotEmpty(message = "缩略图的媒体 id 不能为空", groups = MusicMessageGroup.class)
+    private String thumbMediaId;
+
+    @Schema(description = "图文消息", requiredMode = Schema.RequiredMode.REQUIRED)
+    @Valid
+    @NotNull(message = "图文消息不能为空", groups = NewsMessageGroup.class)
+    private List<MpMessageDO.Article> articles;
+
+    @Schema(description = "音乐链接 消息类型为 MUSIC 时", example = "https://www.iocoder.cn/music.mp3")
+    private String musicUrl;
+
+    @Schema(description = "高质量音乐链接 消息类型为 MUSIC 时", example = "https://www.iocoder.cn/music.mp3")
+    private String hqMusicUrl;
+
+//    //新加的
+//    @Schema(description = "考勤记录列表")
+//    private List<StudentAttendanceDO> attendanceList;
+
+}

+ 62 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/vo/message/attendanceMessageSendReqVO.java

@@ -0,0 +1,62 @@
+package cn.iocoder.yudao.module.mp.controller.admin.message.vo.message;
+
+import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO;
+import cn.iocoder.yudao.module.mp.framework.mp.core.util.MpUtils.*;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.Valid;
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+import java.util.List;
+
+@Schema(description = "管理后台 - 公众号消息发送 Request VO")
+@Data
+public class attendanceMessageSendReqVO {
+
+    @Schema(description = "公众号粉丝的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    @NotNull(message = "公众号粉丝的编号不能为空")
+    private Long userId;
+
+    // ========== 消息内容 ==========
+
+    @Schema(description = "消息类型 TEXT/IMAGE/VOICE/VIDEO/NEWS", requiredMode = Schema.RequiredMode.REQUIRED, example = "text")
+    @NotEmpty(message = "消息类型不能为空")
+    public String type;
+
+    @Schema(description = "消息内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "你好呀")
+//    @NotEmpty(message = "消息内容不能为空", groups = TextMessageGroup.class)
+    private String content;
+
+    @Schema(description = "媒体 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "qqc_2Fot30Jse-HDoZmo5RrUDijz2nGUkP")
+    @NotEmpty(message = "消息内容不能为空", groups = {ImageMessageGroup.class, VoiceMessageGroup.class, VideoMessageGroup.class})
+    private String mediaId;
+
+    @Schema(description = "标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "没有标题")
+    @NotEmpty(message = "消息内容不能为空", groups = VideoMessageGroup.class)
+    private String title;
+
+    @Schema(description = "描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "你猜")
+    @NotEmpty(message = "消息描述不能为空", groups = VideoMessageGroup.class)
+    private String description;
+
+    @Schema(description = "缩略图的媒体 id", requiredMode = Schema.RequiredMode.REQUIRED, example = "qqc_2Fot30Jse-HDoZmo5RrUDijz2nGUkP")
+    @NotEmpty(message = "缩略图的媒体 id 不能为空", groups = MusicMessageGroup.class)
+    private String thumbMediaId;
+
+    @Schema(description = "图文消息", requiredMode = Schema.RequiredMode.REQUIRED)
+    @Valid
+    @NotNull(message = "图文消息不能为空", groups = NewsMessageGroup.class)
+    private List<MpMessageDO.Article> articles;
+
+    @Schema(description = "音乐链接 消息类型为 MUSIC 时", example = "https://www.iocoder.cn/music.mp3")
+    private String musicUrl;
+
+    @Schema(description = "高质量音乐链接 消息类型为 MUSIC 时", example = "https://www.iocoder.cn/music.mp3")
+    private String hqMusicUrl;
+
+//    //新加的
+//    @Schema(description = "考勤记录列表")
+//    private List<StudentAttendanceDO> attendanceList;
+
+}

+ 54 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/news/MpDraftController.http

@@ -0,0 +1,54 @@
+### 请求 /mp/draft/page 接口 => 成功
+GET {{baseUrl}}/mp/draft/page?accountId=1&pageNo=1&pageSize=10
+Content-Type: application/json
+Authorization: Bearer {{token}}
+tenant-id: {{adminTenentId}}
+
+### 请求 /mp/draft/create 接口 => 成功
+POST {{baseUrl}}/mp/draft/create?accountId=1
+Content-Type: application/json
+Authorization: Bearer {{token}}
+tenant-id: {{adminTenentId}}
+
+{
+  "articles": [
+    {
+      "title": "我是标题",
+      "author": "我是作者",
+      "digest": "我是摘要",
+      "content": "我是内容",
+      "contentSourceUrl": "https://www.iocoder.cn",
+      "thumbMediaId": "r6ryvl6LrxBU0miaST4Y-pIcmK-zAAId-9TGgy-DrSLhjVuWbuT3ZBjk9K1yQ0Dn"
+    },
+    {
+      "title": "我是标题 2",
+      "author": "我是作者 2",
+      "digest": "我是摘要 2",
+      "content": "我是内容 2",
+      "contentSourceUrl": "https://www.iocoder.cn",
+      "thumbMediaId": "r6ryvl6LrxBU0miaST4Y-pIcmK-zAAId-9TGgy-DrSLhjVuWbuT3ZBjk9K1yQ0Dn"
+    }
+  ]
+}
+
+### 请求 /mp/draft/create 接口 => 成功
+PUT {{baseUrl}}/mp/draft/update?accountId=1&mediaId=r6ryvl6LrxBU0miaST4Y-q-G9pdsmZw0OYG4FzHQkKfpLfEwIH51wy2bxisx8PvW
+Content-Type: application/json
+Authorization: Bearer {{token}}
+tenant-id: {{adminTenentId}}
+
+[{
+    "title": "我是标题(OOO)",
+    "author": "我是作者",
+    "digest": "我是摘要",
+    "content": "我是内容",
+    "contentSourceUrl": "https://www.iocoder.cn",
+    "thumbMediaId": "r6ryvl6LrxBU0miaST4Y-pIcmK-zAAId-9TGgy-DrSLhjVuWbuT3ZBjk9K1yQ0Dn"
+}, {
+  "title": "我是标题(XXX)",
+  "author": "我是作者",
+  "digest": "我是摘要",
+  "content": "我是内容",
+  "contentSourceUrl": "https://www.iocoder.cn",
+  "thumbMediaId": "r6ryvl6LrxBU0miaST4Y-pIcmK-zAAId-9TGgy-DrSLhjVuWbuT3ZBjk9K1yQ0Dn"
+}]

+ 136 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/news/MpDraftController.java

@@ -0,0 +1,136 @@
+package cn.iocoder.yudao.module.mp.controller.admin.news;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
+import cn.iocoder.yudao.framework.common.util.object.PageUtils;
+import cn.iocoder.yudao.module.mp.controller.admin.news.vo.MpDraftPageReqVO;
+import cn.iocoder.yudao.module.mp.dal.dataobject.material.MpMaterialDO;
+import cn.iocoder.yudao.module.mp.framework.mp.core.MpServiceFactory;
+import cn.iocoder.yudao.module.mp.service.material.MpMaterialService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.Parameters;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.mp.api.WxMpService;
+import me.chanjar.weixin.mp.bean.draft.*;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen;
+import static cn.iocoder.yudao.module.mp.enums.ErrorCodeConstants.*;
+
+@Tag(name = "管理后台 - 公众号草稿")
+@RestController
+@RequestMapping("/mp/draft")
+@Validated
+public class MpDraftController {
+
+    @Resource
+    private MpServiceFactory mpServiceFactory;
+
+    @Resource
+    private MpMaterialService mpMaterialService;
+
+    @GetMapping("/page")
+    @Operation(summary = "获得草稿分页")
+    @PreAuthorize("@ss.hasPermission('mp:draft:query')")
+    public CommonResult<PageResult<WxMpDraftItem>> getDraftPage(MpDraftPageReqVO reqVO) {
+        // 从公众号查询草稿箱
+        WxMpService mpService = mpServiceFactory.getRequiredMpService(reqVO.getAccountId());
+        WxMpDraftList draftList;
+        try {
+            draftList = mpService.getDraftService().listDraft(PageUtils.getStart(reqVO), reqVO.getPageSize());
+        } catch (WxErrorException e) {
+            throw exception(DRAFT_LIST_FAIL, e.getError().getErrorMsg());
+        }
+        // 查询对应的图片地址。目的:解决公众号的图片链接无法在我们后台展示
+        setDraftThumbUrl(draftList.getItems());
+
+        // 返回分页
+        return success(new PageResult<>(draftList.getItems(), draftList.getTotalCount().longValue()));
+    }
+
+    private void setDraftThumbUrl(List<WxMpDraftItem> items) {
+        // 1.1 获得 mediaId 数组
+        Set<String> mediaIds = new HashSet<>();
+        items.forEach(item -> item.getContent().getNewsItem().forEach(newsItem -> mediaIds.add(newsItem.getThumbMediaId())));
+        if (CollUtil.isEmpty(mediaIds)) {
+            return;
+        }
+        // 1.2 批量查询对应的 Media 素材
+        Map<String, MpMaterialDO> materials = CollectionUtils.convertMap(mpMaterialService.getMaterialListByMediaId(mediaIds),
+                MpMaterialDO::getMediaId);
+
+        // 2. 设置回 WxMpDraftItem 记录
+        items.forEach(item -> item.getContent().getNewsItem().forEach(newsItem ->
+                findAndThen(materials, newsItem.getThumbMediaId(), material -> newsItem.setThumbUrl(material.getUrl()))));
+    }
+
+    @PostMapping("/create")
+    @Operation(summary = "创建草稿")
+    @Parameter(name = "accountId", description = "公众号账号的编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('mp:draft:create')")
+    public CommonResult<String> deleteDraft(@RequestParam("accountId") Long accountId,
+                                            @RequestBody WxMpAddDraft draft) {
+        WxMpService mpService = mpServiceFactory.getRequiredMpService(accountId);
+        try {
+            String mediaId = mpService.getDraftService().addDraft(draft);
+            return success(mediaId);
+        } catch (WxErrorException e) {
+            throw exception(DRAFT_CREATE_FAIL, e.getError().getErrorMsg());
+        }
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新草稿")
+    @Parameters({
+            @Parameter(name = "accountId", description = "公众号账号的编号", required = true, example = "1024"),
+            @Parameter(name = "mediaId", description = "草稿素材的编号", required = true, example = "xxx")
+    })
+    @PreAuthorize("@ss.hasPermission('mp:draft:update')")
+    public CommonResult<Boolean> deleteDraft(@RequestParam("accountId") Long accountId,
+                                             @RequestParam("mediaId") String mediaId,
+                                             @RequestBody List<WxMpDraftArticles> articles) {
+        WxMpService mpService = mpServiceFactory.getRequiredMpService(accountId);
+        try {
+            for (int i = 0; i < articles.size(); i++) {
+                WxMpDraftArticles article = articles.get(i);
+                mpService.getDraftService().updateDraft(new WxMpUpdateDraft(mediaId, i, article));
+            }
+            return success(true);
+        } catch (WxErrorException e) {
+            throw exception(DRAFT_UPDATE_FAIL, e.getError().getErrorMsg());
+        }
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除草稿")
+    @Parameters({
+            @Parameter(name = "accountId", description = "公众号账号的编号", required = true, example = "1024"),
+            @Parameter(name = "mediaId", description = "草稿素材的编号", required = true, example = "xxx")
+    })
+    @PreAuthorize("@ss.hasPermission('mp:draft:delete')")
+    public CommonResult<Boolean> deleteDraft(@RequestParam("accountId") Long accountId,
+                                             @RequestParam("mediaId") String mediaId) {
+        WxMpService mpService = mpServiceFactory.getRequiredMpService(accountId);
+        try {
+            mpService.getDraftService().delDraft(mediaId);
+            return success(true);
+        } catch (WxErrorException e) {
+            throw exception(DRAFT_DELETE_FAIL, e.getError().getErrorMsg());
+        }
+    }
+
+}

+ 13 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/news/MpFreePublishController.http

@@ -0,0 +1,13 @@
+### 请求 /mp/free-publish/page 接口 => 成功
+GET {{baseUrl}}/mp/free-publish/page?accountId=1&pageNo=1&pageSize=10
+Content-Type: application/json
+Authorization: Bearer {{token}}
+tenant-id: {{adminTenentId}}
+
+### 请求 /mp/free-publish/submit 接口 => 成功
+POST {{baseUrl}}/mp/free-publish/submit?accountId=1&mediaId=r6ryvl6LrxBU0miaST4Y-vilmd7iS51D8IPddxflWrau0hIQ2ovY8YanO5jlgUcM
+Content-Type: application/json
+Authorization: Bearer {{token}}
+tenant-id: {{adminTenentId}}
+
+{}

+ 119 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/news/MpFreePublishController.java

@@ -0,0 +1,119 @@
+package cn.iocoder.yudao.module.mp.controller.admin.news;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
+import cn.iocoder.yudao.framework.common.util.object.PageUtils;
+import cn.iocoder.yudao.module.mp.controller.admin.news.vo.MpFreePublishPageReqVO;
+import cn.iocoder.yudao.module.mp.dal.dataobject.material.MpMaterialDO;
+import cn.iocoder.yudao.module.mp.framework.mp.core.MpServiceFactory;
+import cn.iocoder.yudao.module.mp.service.material.MpMaterialService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.Parameters;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.mp.api.WxMpService;
+import me.chanjar.weixin.mp.bean.freepublish.WxMpFreePublishItem;
+import me.chanjar.weixin.mp.bean.freepublish.WxMpFreePublishList;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen;
+import static cn.iocoder.yudao.module.mp.enums.ErrorCodeConstants.*;
+
+@Tag(name = "管理后台 - 公众号发布能力")
+@RestController
+@RequestMapping("/mp/free-publish")
+@Validated
+public class MpFreePublishController {
+
+    @Resource
+    private MpServiceFactory mpServiceFactory;
+
+    @Resource
+    private MpMaterialService mpMaterialService;
+
+    @GetMapping("/page")
+    @Operation(summary = "获得已发布的图文分页")
+    @PreAuthorize("@ss.hasPermission('mp:free-publish:query')")
+    public CommonResult<PageResult<WxMpFreePublishItem>> getFreePublishPage(MpFreePublishPageReqVO reqVO) {
+        // 从公众号查询已发布的图文列表
+        WxMpService mpService = mpServiceFactory.getRequiredMpService(reqVO.getAccountId());
+        WxMpFreePublishList publicationRecords;
+        try {
+            publicationRecords = mpService.getFreePublishService().getPublicationRecords(
+                    PageUtils.getStart(reqVO), reqVO.getPageSize());
+        } catch (WxErrorException e) {
+            throw exception(FREE_PUBLISH_LIST_FAIL, e.getError().getErrorMsg());
+        }
+        // 查询对应的图片地址。目的:解决公众号的图片链接无法在我们后台展示
+        setFreePublishThumbUrl(publicationRecords.getItems());
+
+        // 返回分页
+        return success(new PageResult<>(publicationRecords.getItems(), publicationRecords.getTotalCount().longValue()));
+    }
+
+    private void setFreePublishThumbUrl(List<WxMpFreePublishItem> items) {
+        // 1.1 获得 mediaId 数组
+        Set<String> mediaIds = new HashSet<>();
+        items.forEach(item -> item.getContent().getNewsItem().forEach(newsItem -> mediaIds.add(newsItem.getThumbMediaId())));
+        if (CollUtil.isEmpty(mediaIds)) {
+            return;
+        }
+        // 1.2 批量查询对应的 Media 素材
+        Map<String, MpMaterialDO> materials = CollectionUtils.convertMap(mpMaterialService.getMaterialListByMediaId(mediaIds),
+                MpMaterialDO::getMediaId);
+
+        // 2. 设置回 WxMpFreePublishItem 记录
+        items.forEach(item -> item.getContent().getNewsItem().forEach(newsItem ->
+                findAndThen(materials, newsItem.getThumbMediaId(), material -> newsItem.setThumbUrl(material.getUrl()))));
+    }
+
+    @PostMapping("/submit")
+    @Operation(summary = "发布草稿")
+    @Parameters({
+            @Parameter(name = "accountId", description = "公众号账号的编号", required = true, example = "1024"),
+            @Parameter(name = "mediaId", description = "要发布的草稿的 media_id", required = true, example = "2048")
+    })
+    @PreAuthorize("@ss.hasPermission('mp:free-publish:submit')")
+    public CommonResult<String> submitFreePublish(@RequestParam("accountId") Long accountId,
+                                                  @RequestParam("mediaId") String mediaId) {
+        WxMpService mpService = mpServiceFactory.getRequiredMpService(accountId);
+        try {
+            String publishId = mpService.getFreePublishService().submit(mediaId);
+            return success(publishId);
+        } catch (WxErrorException e) {
+            throw exception(FREE_PUBLISH_SUBMIT_FAIL, e.getError().getErrorMsg());
+        }
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除草稿")
+    @Parameters({
+            @Parameter(name = "accountId", description = "公众号账号的编号", required = true, example = "1024"),
+            @Parameter(name = "articleId", description = "发布记录的编号", required = true, example = "2048")
+    })
+    @PreAuthorize("@ss.hasPermission('mp:free-publish:delete')")
+    public CommonResult<Boolean> deleteFreePublish(@RequestParam("accountId") Long accountId,
+                                                   @RequestParam("articleId") String articleId) {
+        WxMpService mpService = mpServiceFactory.getRequiredMpService(accountId);
+        try {
+            mpService.getFreePublishService().deletePushAllArticle(articleId);
+            return success(true);
+        } catch (WxErrorException e) {
+            throw exception(FREE_PUBLISH_DELETE_FAIL, e.getError().getErrorMsg());
+        }
+    }
+
+}

+ 21 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/news/vo/MpDraftPageReqVO.java

@@ -0,0 +1,21 @@
+package cn.iocoder.yudao.module.mp.controller.admin.news.vo;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import javax.validation.constraints.NotNull;
+
+@Schema(description = "管理后台 - 公众号草稿的分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class MpDraftPageReqVO extends PageParam {
+
+    @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    @NotNull(message = "公众号账号的编号不能为空")
+    private Long accountId;
+
+}

+ 21 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/news/vo/MpFreePublishPageReqVO.java

@@ -0,0 +1,21 @@
+package cn.iocoder.yudao.module.mp.controller.admin.news.vo;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import javax.validation.constraints.NotNull;
+
+@Schema(description = "管理后台 - 公众号已发布列表的分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class MpFreePublishPageReqVO extends PageParam {
+
+    @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    @NotNull(message = "公众号账号的编号不能为空")
+    private Long accountId;
+
+}

+ 114 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/open/MpOpenController.java

@@ -0,0 +1,114 @@
+package cn.iocoder.yudao.module.mp.controller.admin.open;
+
+import cn.hutool.core.lang.Assert;
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
+import cn.iocoder.yudao.module.mp.controller.admin.open.vo.MpOpenCheckSignatureReqVO;
+import cn.iocoder.yudao.module.mp.controller.admin.open.vo.MpOpenHandleMessageReqVO;
+import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO;
+import cn.iocoder.yudao.module.mp.framework.mp.core.MpServiceFactory;
+import cn.iocoder.yudao.module.mp.framework.mp.core.context.MpContextHolder;
+import cn.iocoder.yudao.module.mp.service.account.MpAccountService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.extern.slf4j.Slf4j;
+import me.chanjar.weixin.mp.api.WxMpMessageRouter;
+import me.chanjar.weixin.mp.api.WxMpService;
+import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
+import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.Objects;
+
+@Tag(name = "管理后台 - 公众号回调")
+@RestController
+@RequestMapping("/mp/open")
+@Validated
+@Slf4j
+public class MpOpenController {
+
+    @Resource
+    private MpServiceFactory mpServiceFactory;
+
+    @Resource
+    private MpAccountService mpAccountService;
+
+    /**
+     * 接收微信公众号的校验签名
+     *
+     * 对应 <a href="https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Access_Overview.html">文档</a>
+     */
+    @Operation(summary = "校验签名") // 参见
+    @GetMapping(value = "/{appId}", produces = "text/plain;charset=utf-8")
+    public String checkSignature(@PathVariable("appId") String appId,
+                                 MpOpenCheckSignatureReqVO reqVO) {
+        log.info("[checkSignature][appId({}) 接收到来自微信服务器的认证消息({})]", appId, reqVO);
+        // 校验请求签名
+        WxMpService wxMpService = mpServiceFactory.getRequiredMpService(appId);
+        // 校验通过
+        if (wxMpService.checkSignature(reqVO.getTimestamp(), reqVO.getNonce(), reqVO.getSignature())) {
+            return reqVO.getEchostr();
+        }
+        // 校验不通过
+        return "非法请求";
+    }
+
+    /**LSQ
+     * 用户发给公众号的消息,公众号都会回调到 POST /admin-api/mp/open/{appID} 接口,进行消息的处理。
+     *
+     * <a href="https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Receiving_standard_messages.html">文档</a>
+     */
+    @Operation(summary = "处理消息")
+    @PostMapping(value = "/{appId}", produces = "application/xml; charset=UTF-8")
+    public String handleMessage(@PathVariable("appId") String appId,
+                                @RequestBody String content,
+                                MpOpenHandleMessageReqVO reqVO) {
+        log.info("[handleMessage][appId({}) 推送消息,参数({}) 内容({})]", appId, reqVO, content);
+
+        // 处理 appId + 多租户的上下文
+        MpAccountDO account = mpAccountService.getAccountFromCache(appId);
+        Assert.notNull(account, "公众号 appId({}) 不存在", appId);
+        try {
+            MpContextHolder.setAppId(appId);
+            return TenantUtils.execute(account.getTenantId(),
+                    () -> handleMessage0(appId, content, reqVO));
+        } finally {
+            MpContextHolder.clear();
+        }
+    }
+
+    private String handleMessage0(String appId, String content, MpOpenHandleMessageReqVO reqVO) {
+        // 校验请求签名
+        WxMpService mppService = mpServiceFactory.getRequiredMpService(appId);
+        Assert.isTrue(mppService.checkSignature(reqVO.getTimestamp(), reqVO.getNonce(), reqVO.getSignature()),
+                "非法请求");
+
+        // 第一步,解析消息
+        WxMpXmlMessage inMessage = null;
+        if (StrUtil.isBlank(reqVO.getEncrypt_type())) { // 明文模式
+            inMessage = WxMpXmlMessage.fromXml(content);
+        } else if (Objects.equals(reqVO.getEncrypt_type(), MpOpenHandleMessageReqVO.ENCRYPT_TYPE_AES)) { // AES 加密模式
+            inMessage = WxMpXmlMessage.fromEncryptedXml(content, mppService.getWxMpConfigStorage(),
+                    reqVO.getTimestamp(), reqVO.getNonce(), reqVO.getMsg_signature());
+        }
+        Assert.notNull(inMessage, "消息解析失败,原因:消息为空");
+
+        // 第二步,处理消息
+        WxMpMessageRouter mpMessageRouter = mpServiceFactory.getRequiredMpMessageRouter(appId);
+        WxMpXmlOutMessage outMessage = mpMessageRouter.route(inMessage);
+        if (outMessage == null) {
+            return "";
+        }
+
+        // 第三步,返回消息
+        if (StrUtil.isBlank(reqVO.getEncrypt_type())) { // 明文模式
+            return outMessage.toXml();
+        } else if (Objects.equals(reqVO.getEncrypt_type(), MpOpenHandleMessageReqVO.ENCRYPT_TYPE_AES)) { // AES 加密模式
+            return outMessage.toEncryptedXml(mppService.getWxMpConfigStorage());
+        }
+        return "";
+    }
+
+}

+ 29 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/open/vo/MpOpenCheckSignatureReqVO.java

@@ -0,0 +1,29 @@
+package cn.iocoder.yudao.module.mp.controller.admin.open.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotEmpty;
+
+@Schema(description = "管理后台 - 公众号校验签名 Request VO")
+@Data
+public class MpOpenCheckSignatureReqVO {
+
+    @Schema(description = "微信加密签名", requiredMode = Schema.RequiredMode.REQUIRED, example = "490eb57f448b87bd5f20ccef58aa4de46aa1908e")
+    @NotEmpty(message = "微信加密签名不能为空")
+    private String signature;
+
+    @Schema(description = "时间戳", requiredMode = Schema.RequiredMode.REQUIRED, example = "1672587863")
+    @NotEmpty(message = "时间戳不能为空")
+    private String timestamp;
+
+    @Schema(description = "随机数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1827365808")
+    @NotEmpty(message = "随机数不能为空")
+    private String nonce;
+
+    @Schema(description = "随机字符串", requiredMode = Schema.RequiredMode.REQUIRED, example = "2721154047828672511")
+    @NotEmpty(message = "随机字符串不能为空")
+    @SuppressWarnings("SpellCheckingInspection")
+    private String echostr;
+
+}

+ 37 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/open/vo/MpOpenHandleMessageReqVO.java

@@ -0,0 +1,37 @@
+package cn.iocoder.yudao.module.mp.controller.admin.open.vo;
+
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotEmpty;
+
+@Schema(description = "管理后台 - 公众号处理消息 Request VO")
+@Data
+public class MpOpenHandleMessageReqVO {
+
+    public static final String ENCRYPT_TYPE_AES = "aes";
+
+    @Schema(description = "微信加密签名", requiredMode = Schema.RequiredMode.REQUIRED, example = "490eb57f448b87bd5f20ccef58aa4de46aa1908e")
+    @NotEmpty(message = "微信加密签名不能为空")
+    private String signature;
+
+    @Schema(description = "时间戳", requiredMode = Schema.RequiredMode.REQUIRED, example = "1672587863")
+    @NotEmpty(message = "时间戳不能为空")
+    private String timestamp;
+
+    @Schema(description = "随机数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1827365808")
+    @NotEmpty(message = "随机数不能为空")
+    private String nonce;
+
+    @Schema(description = "粉丝 openid", requiredMode = Schema.RequiredMode.REQUIRED, example = "oz-Jdtyn-WGm4C4I5Z-nvBMO_ZfY")
+    @NotEmpty(message = "粉丝 openid 不能为空")
+    private String openid;
+
+    @Schema(description = "消息加密类型", example = "aes")
+    private String encrypt_type;
+
+    @Schema(description = "微信签名", example = "QW5kcm9pZCBUaGUgQmFzZTY0IGlzIGEgZ2VuZXJhdGVkIHN0cmluZw==")
+    private String msg_signature;
+
+}

+ 68 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/statistics/MpStatisticsController.java

@@ -0,0 +1,68 @@
+package cn.iocoder.yudao.module.mp.controller.admin.statistics;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.module.mp.controller.admin.statistics.vo.*;
+import cn.iocoder.yudao.module.mp.convert.statistics.MpStatisticsConvert;
+import cn.iocoder.yudao.module.mp.service.statistics.MpStatisticsService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import me.chanjar.weixin.mp.bean.datacube.WxDataCubeInterfaceResult;
+import me.chanjar.weixin.mp.bean.datacube.WxDataCubeMsgResult;
+import me.chanjar.weixin.mp.bean.datacube.WxDataCubeUserCumulate;
+import me.chanjar.weixin.mp.bean.datacube.WxDataCubeUserSummary;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+@Tag(name = "管理后台 - 公众号统计")
+@RestController
+@RequestMapping("/mp/statistics")
+@Validated
+public class MpStatisticsController {
+
+    @Resource
+    private MpStatisticsService mpStatisticsService;
+
+    @GetMapping("/user-summary")
+    @Operation(summary = "获得粉丝增减数据")
+    @PreAuthorize("@ss.hasPermission('mp:statistics:query')")
+    public CommonResult<List<MpStatisticsUserSummaryRespVO>> getUserSummary(MpStatisticsGetReqVO getReqVO) {
+        List<WxDataCubeUserSummary> list = mpStatisticsService.getUserSummary(
+                getReqVO.getAccountId(), getReqVO.getDate());
+        return success(MpStatisticsConvert.INSTANCE.convertList01(list));
+    }
+
+    @GetMapping("/user-cumulate")
+    @Operation(summary = "获得粉丝累计数据")
+    @PreAuthorize("@ss.hasPermission('mp:statistics:query')")
+    public CommonResult<List<MpStatisticsUserCumulateRespVO>> getUserCumulate(MpStatisticsGetReqVO getReqVO) {
+        List<WxDataCubeUserCumulate> list = mpStatisticsService.getUserCumulate(
+                getReqVO.getAccountId(), getReqVO.getDate());
+        return success(MpStatisticsConvert.INSTANCE.convertList02(list));
+    }
+
+    @GetMapping("/upstream-message")
+    @Operation(summary = "获取消息发送概况数据")
+    @PreAuthorize("@ss.hasPermission('mp:statistics:query')")
+    public CommonResult<List<MpStatisticsUpstreamMessageRespVO>> getUpstreamMessage(MpStatisticsGetReqVO getReqVO) {
+        List<WxDataCubeMsgResult> list = mpStatisticsService.getUpstreamMessage(
+                getReqVO.getAccountId(), getReqVO.getDate());
+        return success(MpStatisticsConvert.INSTANCE.convertList03(list));
+    }
+
+    @GetMapping("/interface-summary")
+    @Operation(summary = "获取消息发送概况数据")
+    @PreAuthorize("@ss.hasPermission('mp:statistics:query')")
+    public CommonResult<List<MpStatisticsInterfaceSummaryRespVO>> getInterfaceSummary(MpStatisticsGetReqVO getReqVO) {
+        List<WxDataCubeInterfaceResult> list = mpStatisticsService.getInterfaceSummary(
+                getReqVO.getAccountId(), getReqVO.getDate());
+        return success(MpStatisticsConvert.INSTANCE.convertList04(list));
+    }
+}

+ 25 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/statistics/vo/MpStatisticsGetReqVO.java

@@ -0,0 +1,25 @@
+package cn.iocoder.yudao.module.mp.controller.admin.statistics.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import javax.validation.constraints.NotNull;
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - 获得统计数据 Request VO")
+@Data
+public class MpStatisticsGetReqVO {
+
+    @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    @NotNull(message = "公众号账号的编号不能为空")
+    private Long accountId;
+
+    @Schema(description = "查询时间范围", example = "[2022-07-01 00:00:00, 2022-07-01 23:59:59]")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    @NotNull(message = "查询时间范围不能为空")
+    private LocalDateTime[] date;
+
+}

+ 27 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/statistics/vo/MpStatisticsInterfaceSummaryRespVO.java

@@ -0,0 +1,27 @@
+package cn.iocoder.yudao.module.mp.controller.admin.statistics.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - 某一天的接口分析数据 Response VO")
+@Data
+public class MpStatisticsInterfaceSummaryRespVO {
+
+    @Schema(description = "日期", requiredMode = Schema.RequiredMode.REQUIRED)
+    private LocalDateTime refDate;
+
+    @Schema(description = "通过服务器配置地址获得消息后,被动回复粉丝消息的次数", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
+    private Integer callbackCount;
+
+    @Schema(description = "上述动作的失败次数", requiredMode = Schema.RequiredMode.REQUIRED, example = "20")
+    private Integer failCount;
+
+    @Schema(description = "总耗时,除以 callback_count 即为平均耗时", requiredMode = Schema.RequiredMode.REQUIRED, example = "30")
+    private Integer totalTimeCost;
+
+    @Schema(description = "最大耗时", requiredMode = Schema.RequiredMode.REQUIRED, example = "40")
+    private Integer maxTimeCost;
+
+}

+ 21 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/statistics/vo/MpStatisticsUpstreamMessageRespVO.java

@@ -0,0 +1,21 @@
+package cn.iocoder.yudao.module.mp.controller.admin.statistics.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - 某一天的粉丝增减数据 Response VO")
+@Data
+public class MpStatisticsUpstreamMessageRespVO {
+
+    @Schema(description = "日期", requiredMode = Schema.RequiredMode.REQUIRED)
+    private LocalDateTime refDate;
+
+    @Schema(description = "上行发送了(向公众号发送了)消息的粉丝数", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
+    private Integer messageUser;
+
+    @Schema(description = "上行发送了消息的消息总数", requiredMode = Schema.RequiredMode.REQUIRED, example = "20")
+    private Integer messageCount;
+
+}

+ 18 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/statistics/vo/MpStatisticsUserCumulateRespVO.java

@@ -0,0 +1,18 @@
+package cn.iocoder.yudao.module.mp.controller.admin.statistics.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - 某一天的消息发送概况数据 Response VO")
+@Data
+public class MpStatisticsUserCumulateRespVO {
+
+    @Schema(description = "日期", requiredMode = Schema.RequiredMode.REQUIRED)
+    private LocalDateTime refDate;
+
+    @Schema(description = "累计粉丝量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
+    private Integer cumulateUser;
+
+}

+ 24 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/statistics/vo/MpStatisticsUserSummaryRespVO.java

@@ -0,0 +1,24 @@
+package cn.iocoder.yudao.module.mp.controller.admin.statistics.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - 某一天的粉丝增减数据 Response VO")
+@Data
+public class MpStatisticsUserSummaryRespVO {
+
+    @Schema(description = "日期", requiredMode = Schema.RequiredMode.REQUIRED)
+    private LocalDateTime refDate;
+
+    @Schema(description = "粉丝来源", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
+    private Integer userSource;
+
+    @Schema(description = "新关注的粉丝数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
+    private Integer newUser;
+
+    @Schema(description = "取消关注的粉丝数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "20")
+    private Integer cancelUser;
+
+}

+ 39 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/tag/MpTagController.http

@@ -0,0 +1,39 @@
+### 请求 /mp/tag/create 接口 => 成功
+POST {{baseUrl}}/mp/tag/create
+Content-Type: application/json
+Authorization: Bearer {{token}}
+tenant-id: {{adminTenentId}}
+
+{
+  "accountId": "1",
+  "name": "测试"
+}
+
+### 请求 /mp/tag/update 接口 => 成功
+PUT {{baseUrl}}/mp/tag/update
+Content-Type: application/json
+Authorization: Bearer {{token}}
+tenant-id: {{adminTenentId}}
+
+{
+  "id": "3",
+  "name": "测试标签啦"
+}
+
+### 请求 /mp/tag/delete 接口 => 成功
+DELETE {{baseUrl}}/mp/tag/delete?id=3
+Content-Type: application/json
+Authorization: Bearer {{token}}
+tenant-id: {{adminTenentId}}
+
+### 请求 /mp/tag/page 接口 => 成功
+GET {{baseUrl}}/mp/tag/page?accountId=1&pageNo=1&pageSize=10
+Content-Type: application/json
+Authorization: Bearer {{token}}
+tenant-id: {{adminTenentId}}
+
+### 请求 /mp/tag/sync 接口 => 成功
+POST {{baseUrl}}/mp/tag/sync?accountId=1
+Content-Type: application/json
+Authorization: Bearer {{token}}
+tenant-id: {{adminTenentId}}

+ 88 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/tag/MpTagController.java

@@ -0,0 +1,88 @@
+package cn.iocoder.yudao.module.mp.controller.admin.tag;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.mp.controller.admin.tag.vo.*;
+import cn.iocoder.yudao.module.mp.convert.tag.MpTagConvert;
+import cn.iocoder.yudao.module.mp.dal.dataobject.tag.MpTagDO;
+import cn.iocoder.yudao.module.mp.service.tag.MpTagService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.validation.Valid;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+@Tag(name = "管理后台 - 公众号标签")
+@RestController
+@RequestMapping("/mp/tag")
+@Validated
+public class MpTagController {
+
+    @Resource
+    private MpTagService mpTagService;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建公众号标签")
+    @PreAuthorize("@ss.hasPermission('mp:tag:create')")
+    public CommonResult<Long> createTag(@Valid @RequestBody MpTagCreateReqVO createReqVO) {
+        return success(mpTagService.createTag(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新公众号标签")
+    @PreAuthorize("@ss.hasPermission('mp:tag:update')")
+    public CommonResult<Boolean> updateTag(@Valid @RequestBody MpTagUpdateReqVO updateReqVO) {
+        mpTagService.updateTag(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除公众号标签")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('mp:tag:delete')")
+    public CommonResult<Boolean> deleteTag(@RequestParam("id") Long id) {
+        mpTagService.deleteTag(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获取公众号标签详情")
+    @PreAuthorize("@ss.hasPermission('mp:tag:query')")
+    public CommonResult<MpTagRespVO> get(@RequestParam("id") Long id) {
+        MpTagDO mpTagDO = mpTagService.get(id);
+        return success(MpTagConvert.INSTANCE.convert(mpTagDO));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获取公众号标签分页")
+    @PreAuthorize("@ss.hasPermission('mp:tag:query')")
+    public CommonResult<PageResult<MpTagRespVO>> getTagPage(MpTagPageReqVO pageReqVO) {
+        PageResult<MpTagDO> pageResult = mpTagService.getTagPage(pageReqVO);
+        return success(MpTagConvert.INSTANCE.convertPage(pageResult));
+    }
+
+    @GetMapping("/list-all-simple")
+    @Operation(summary = "获取公众号账号精简信息列表")
+    @PreAuthorize("@ss.hasPermission('mp:account:query')")
+    public CommonResult<List<MpTagSimpleRespVO>> getSimpleTags() {
+        List<MpTagDO> list = mpTagService.getTagList();
+        return success(MpTagConvert.INSTANCE.convertList02(list));
+    }
+
+    @PostMapping("/sync")
+    @Operation(summary = "同步公众号标签")
+    @Parameter(name = "accountId", description = "公众号账号的编号", required = true)
+    @PreAuthorize("@ss.hasPermission('mp:tag:sync')")
+    public CommonResult<Boolean> syncTag(@RequestParam("accountId") Long accountId) {
+        mpTagService.syncTag(accountId);
+        return success(true);
+    }
+
+}

+ 21 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/tag/vo/MpTagBaseVO.java

@@ -0,0 +1,21 @@
+package cn.iocoder.yudao.module.mp.controller.admin.tag.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotEmpty;
+
+/**
+ * 公众号标签 Base VO,提供给添加、修改、详细的子 VO 使用
+ * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
+ *
+ * @author fengdan
+ */
+@Data
+public class MpTagBaseVO {
+
+    @Schema(description = "标签名", requiredMode = Schema.RequiredMode.REQUIRED, example = "土豆")
+    @NotEmpty(message = "标签名不能为空")
+    private String name;
+
+}

+ 20 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/tag/vo/MpTagCreateReqVO.java

@@ -0,0 +1,20 @@
+package cn.iocoder.yudao.module.mp.controller.admin.tag.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import javax.validation.constraints.NotNull;
+
+@Schema(description = "管理后台 - 公众号标签创建 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class MpTagCreateReqVO extends MpTagBaseVO {
+
+    @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
+    @NotNull(message = "公众号账号的编号不能为空")
+    private Long accountId;
+
+}

+ 24 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/tag/vo/MpTagPageReqVO.java

@@ -0,0 +1,24 @@
+package cn.iocoder.yudao.module.mp.controller.admin.tag.vo;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import javax.validation.constraints.NotEmpty;
+
+@Schema(description = "管理后台 - 公众号标签分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class MpTagPageReqVO extends PageParam {
+
+    @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
+    @NotEmpty(message = "公众号账号的编号不能为空")
+    private Long accountId;
+
+    @Schema(description = "标签名,模糊匹配", example = "哈哈")
+    private String name;
+
+}

+ 25 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/tag/vo/MpTagRespVO.java

@@ -0,0 +1,25 @@
+package cn.iocoder.yudao.module.mp.controller.admin.tag.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - 公众号标签 Response VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class MpTagRespVO extends MpTagBaseVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    private Long id;
+
+    @Schema(description = "此标签下粉丝数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
+    private Integer count;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    private LocalDateTime createTime;
+
+}

+ 19 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/tag/vo/MpTagSimpleRespVO.java

@@ -0,0 +1,19 @@
+package cn.iocoder.yudao.module.mp.controller.admin.tag.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Schema(description = "管理后台 - 公众号标签精简信息 Response VO")
+@Data
+public class MpTagSimpleRespVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    private Long id;
+
+    @Schema(description = "公众号的标签编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
+    private Long tagId;
+
+    @Schema(description = "标签名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "快乐")
+    private String name;
+
+}

+ 20 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/tag/vo/MpTagUpdateReqVO.java

@@ -0,0 +1,20 @@
+package cn.iocoder.yudao.module.mp.controller.admin.tag.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import javax.validation.constraints.NotNull;
+
+@Schema(description = "管理后台 - 公众号标签更新 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class MpTagUpdateReqVO extends MpTagBaseVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "编号不能为空")
+    private Long id;
+
+}

+ 18 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/user/MpUserController.http

@@ -0,0 +1,18 @@
+### 请求 /mp/user/sync 接口 => 成功
+POST {{baseUrl}}/mp/user/sync?accountId=1
+Content-Type: application/json
+Authorization: Bearer {{token}}
+tenant-id: {{adminTenentId}}
+
+### 请求 /mp/user/update 接口 => 成功
+PUT {{baseUrl}}/mp/user/update
+Content-Type: application/json
+Authorization: Bearer {{token}}
+tenant-id: {{adminTenentId}}
+
+{
+  "id": "3",
+  "nickname": "test",
+  "remark": "测试备注",
+  "tagIds": [103, 104]
+}

+ 65 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/user/MpUserController.java

@@ -0,0 +1,65 @@
+package cn.iocoder.yudao.module.mp.controller.admin.user;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.mp.controller.admin.user.vo.MpUserPageReqVO;
+import cn.iocoder.yudao.module.mp.controller.admin.user.vo.MpUserRespVO;
+import cn.iocoder.yudao.module.mp.controller.admin.user.vo.MpUserUpdateReqVO;
+import cn.iocoder.yudao.module.mp.convert.user.MpUserConvert;
+import cn.iocoder.yudao.module.mp.dal.dataobject.user.MpUserDO;
+import cn.iocoder.yudao.module.mp.service.user.MpUserService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.validation.Valid;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+@Tag(name = "管理后台 - 公众号粉丝")
+@RestController
+@RequestMapping("/mp/user")
+@Validated
+public class MpUserController {
+
+    @Resource
+    private MpUserService mpUserService;
+
+    @GetMapping("/page")
+    @Operation(summary = "获得公众号粉丝分页")
+    @PreAuthorize("@ss.hasPermission('mp:user:query')")
+    public CommonResult<PageResult<MpUserRespVO>> getUserPage(@Valid MpUserPageReqVO pageVO) {
+        PageResult<MpUserDO> pageResult = mpUserService.getUserPage(pageVO);
+        return success(MpUserConvert.INSTANCE.convertPage(pageResult));
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得公众号粉丝")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('mp:user:query')")
+    public CommonResult<MpUserRespVO> getUser(@RequestParam("id") Long id) {
+        return success(MpUserConvert.INSTANCE.convert(mpUserService.getUser(id)));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新公众号粉丝")
+    @PreAuthorize("@ss.hasPermission('mp:user:update')")
+    public CommonResult<Boolean> updateUser(@Valid @RequestBody MpUserUpdateReqVO updateReqVO) {
+        mpUserService.updateUser(updateReqVO);
+        return success(true);
+    }
+
+    @PostMapping("/sync")
+    @Operation(summary = "同步公众号粉丝")
+    @Parameter(name = "accountId", description = "公众号账号的编号", required = true)
+    @PreAuthorize("@ss.hasPermission('mp:user:sync')")
+    public CommonResult<Boolean> syncUser(@RequestParam("accountId") Long accountId) {
+        mpUserService.syncUser(accountId);
+        return success(true);
+    }
+
+}

+ 30 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/user/vo/MpUserPageReqVO.java

@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.module.mp.controller.admin.user.vo;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import javax.validation.constraints.NotNull;
+
+@Schema(description = "管理后台 - 公众号粉丝分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class MpUserPageReqVO extends PageParam {
+
+    @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
+    @NotNull(message = "公众号账号的编号不能为空")
+    private Long accountId;
+
+    @Schema(description = "公众号粉丝标识,模糊匹配", example = "o6_bmjrPTlm6_2sgVt7hMZOPfL2M")
+    private String openid;
+
+    @Schema(description = "微信生态唯一标识,模糊匹配", example = "o6_bmjrPTlm6_2sgVt7hMZOPfL2M")
+    private String unionId;
+
+    @Schema(description = "公众号粉丝昵称,模糊匹配", example = "芋艿")
+    private String nickname;
+
+}

+ 55 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/user/vo/MpUserRespVO.java

@@ -0,0 +1,55 @@
+package cn.iocoder.yudao.module.mp.controller.admin.user.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Schema(description = "管理后台 - 公众号粉丝 Response VO")
+@Data
+public class MpUserRespVO  {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    private Long id;
+
+    @Schema(description = "公众号粉丝标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "o6_bmjrPTlm6_2sgVt7hMZOPfL2M")
+    private String openid;
+
+    @Schema(description = "微信生态唯一标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "o6_bmjrPTlm6_2sgVt7hMZOPfL2M")
+    private String unionId;
+
+    @Schema(description = "关注状态 参见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Integer subscribeStatus;
+    @Schema(description = "关注时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    private LocalDateTime subscribeTime;
+    @Schema(description = "取消关注时间")
+    private LocalDateTime unsubscribeTime;
+
+    @Schema(description = "昵称", example = "芋道")
+    private String nickname;
+    @Schema(description = "头像地址", example = "https://www.iocoder.cn/1.png")
+    private String headImageUrl;
+    @Schema(description = "语言", example = "zh_CN")
+    private String language;
+    @Schema(description = "国家", example = "中国")
+    private String country;
+    @Schema(description = "省份", example = "广东省")
+    private String province;
+    @Schema(description = "城市", example = "广州市")
+    private String city;
+    @Schema(description = "备注", example = "你是一个芋头嘛")
+    private String remark;
+
+    @Schema(description = "标签编号数组", example = "1,2,3")
+    private List<Long> tagIds;
+
+    @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Long accountId;
+    @Schema(description = "公众号账号的 appId", requiredMode = Schema.RequiredMode.REQUIRED, example = "wx1234567890")
+    private String appId;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    private LocalDateTime createTime;
+
+}

+ 26 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/user/vo/MpUserUpdateReqVO.java

@@ -0,0 +1,26 @@
+package cn.iocoder.yudao.module.mp.controller.admin.user.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+import java.util.List;
+
+@Schema(description = "管理后台 - 公众号粉丝更新 Request VO")
+@Data
+public class MpUserUpdateReqVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    @NotNull(message = "编号不能为空")
+    private Long id;
+
+    @Schema(description = "昵称", example = "芋道")
+    private String nickname;
+
+    @Schema(description = "备注", example = "你是一个芋头嘛")
+    private String remark;
+
+    @Schema(description = "标签编号数组", example = "1,2,3")
+    private List<Long> tagIds;
+
+}

+ 6 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/package-info.java

@@ -0,0 +1,6 @@
+/**
+ * 提供 RESTful API 给前端:
+ * 1. admin 包:提供给管理后台 yudao-ui-admin 前端项目
+ * 2. app 包:提供给用户 APP yudao-ui-app 前端项目,它的 Controller 和 VO 都要添加 App 前缀,用于和管理后台进行区分
+ */
+package cn.iocoder.yudao.module.mp.controller;

+ 31 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/account/MpAccountConvert.java

@@ -0,0 +1,31 @@
+package cn.iocoder.yudao.module.mp.convert.account;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.mp.controller.admin.account.vo.MpAccountCreateReqVO;
+import cn.iocoder.yudao.module.mp.controller.admin.account.vo.MpAccountRespVO;
+import cn.iocoder.yudao.module.mp.controller.admin.account.vo.MpAccountSimpleRespVO;
+import cn.iocoder.yudao.module.mp.controller.admin.account.vo.MpAccountUpdateReqVO;
+import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO;
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+
+import java.util.List;
+
+@Mapper
+public interface MpAccountConvert {
+
+    MpAccountConvert INSTANCE = Mappers.getMapper(MpAccountConvert.class);
+
+    MpAccountDO convert(MpAccountCreateReqVO bean);
+
+    MpAccountDO convert(MpAccountUpdateReqVO bean);
+
+    MpAccountRespVO convert(MpAccountDO bean);
+
+    List<MpAccountRespVO> convertList(List<MpAccountDO> list);
+
+    PageResult<MpAccountRespVO> convertPage(PageResult<MpAccountDO> page);
+
+    List<MpAccountSimpleRespVO> convertList02(List<MpAccountDO> list);
+
+}

+ 47 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/material/MpMaterialConvert.java

@@ -0,0 +1,47 @@
+package cn.iocoder.yudao.module.mp.convert.material;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.mp.controller.admin.material.vo.MpMaterialRespVO;
+import cn.iocoder.yudao.module.mp.controller.admin.material.vo.MpMaterialUploadRespVO;
+import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO;
+import cn.iocoder.yudao.module.mp.dal.dataobject.material.MpMaterialDO;
+import me.chanjar.weixin.mp.bean.material.WxMpMaterial;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.Mappings;
+import org.mapstruct.factory.Mappers;
+
+import java.io.File;
+
+@Mapper
+public interface MpMaterialConvert {
+
+    MpMaterialConvert INSTANCE = Mappers.getMapper(MpMaterialConvert.class);
+
+    @Mappings({
+            @Mapping(target = "id", ignore = true),
+            @Mapping(source = "account.id", target = "accountId"),
+            @Mapping(source = "account.appId", target = "appId"),
+            @Mapping(source = "name", target = "name")
+    })
+    MpMaterialDO convert(String mediaId, String type, String url, MpAccountDO account,
+                         String name);
+
+    @Mappings({
+            @Mapping(target = "id", ignore = true),
+            @Mapping(source = "account.id", target = "accountId"),
+            @Mapping(source = "account.appId", target = "appId"),
+            @Mapping(source = "name", target = "name")
+    })
+    MpMaterialDO convert(String mediaId, String type, String url, MpAccountDO account,
+                         String name, String title, String introduction, String mpUrl);
+
+    MpMaterialUploadRespVO convert(MpMaterialDO bean);
+
+    default WxMpMaterial convert(String name, File file, String title, String introduction) {
+        return new WxMpMaterial(name, file, title, introduction);
+    }
+
+    PageResult<MpMaterialRespVO> convertPage(PageResult<MpMaterialDO> page);
+
+}

+ 50 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/menu/MpMenuConvert.java

@@ -0,0 +1,50 @@
+package cn.iocoder.yudao.module.mp.convert.menu;
+
+import cn.iocoder.yudao.module.mp.controller.admin.menu.vo.MpMenuRespVO;
+import cn.iocoder.yudao.module.mp.controller.admin.menu.vo.MpMenuSaveReqVO;
+import cn.iocoder.yudao.module.mp.dal.dataobject.menu.MpMenuDO;
+import cn.iocoder.yudao.module.mp.service.message.bo.MpMessageSendOutReqBO;
+import me.chanjar.weixin.common.bean.menu.WxMenuButton;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.Mappings;
+import org.mapstruct.factory.Mappers;
+
+import java.util.List;
+
+@Mapper
+public interface MpMenuConvert {
+
+    MpMenuConvert INSTANCE = Mappers.getMapper(MpMenuConvert.class);
+
+    MpMenuRespVO convert(MpMenuDO bean);
+
+    List<MpMenuRespVO> convertList(List<MpMenuDO> list);
+
+    @Mappings({
+            @Mapping(source = "menu.appId", target = "appId"),
+            @Mapping(source = "menu.replyMessageType", target = "type"),
+            @Mapping(source = "menu.replyContent", target = "content"),
+            @Mapping(source = "menu.replyMediaId", target = "mediaId"),
+            @Mapping(source = "menu.replyThumbMediaId", target = "thumbMediaId"),
+            @Mapping(source = "menu.replyTitle", target = "title"),
+            @Mapping(source = "menu.replyDescription", target = "description"),
+            @Mapping(source = "menu.replyArticles", target = "articles"),
+            @Mapping(source = "menu.replyMusicUrl", target = "musicUrl"),
+            @Mapping(source = "menu.replyHqMusicUrl", target = "hqMusicUrl"),
+    })
+    MpMessageSendOutReqBO convert(String openid, MpMenuDO menu);
+
+    List<WxMenuButton> convert(List<MpMenuSaveReqVO.Menu> list);
+
+    @Mappings({
+            @Mapping(source = "menuKey", target = "key"),
+            @Mapping(source = "children", target = "subButtons"),
+            @Mapping(source = "miniProgramAppId", target = "appId"),
+            @Mapping(source = "miniProgramPagePath", target = "pagePath"),
+    })
+    WxMenuButton convert(MpMenuSaveReqVO.Menu bean);
+
+    MpMenuDO convert02(MpMenuSaveReqVO.Menu menu);
+
+}

+ 37 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/message/MpAutoReplyConvert.java

@@ -0,0 +1,37 @@
+package cn.iocoder.yudao.module.mp.convert.message;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.mp.controller.admin.message.vo.autoreply.MpAutoReplyCreateReqVO;
+import cn.iocoder.yudao.module.mp.controller.admin.message.vo.autoreply.MpAutoReplyRespVO;
+import cn.iocoder.yudao.module.mp.controller.admin.message.vo.autoreply.MpAutoReplyUpdateReqVO;
+import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpAutoReplyDO;
+import cn.iocoder.yudao.module.mp.service.message.bo.MpMessageSendOutReqBO;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.Mappings;
+import org.mapstruct.factory.Mappers;
+
+@Mapper
+public interface MpAutoReplyConvert {
+
+    MpAutoReplyConvert INSTANCE = Mappers.getMapper(MpAutoReplyConvert.class);
+
+    @Mappings({
+            @Mapping(source = "reply.appId", target = "appId"),
+            @Mapping(source = "reply.responseMessageType", target = "type"),
+            @Mapping(source = "reply.responseContent", target = "content"),
+            @Mapping(source = "reply.responseMediaId", target = "mediaId"),
+            @Mapping(source = "reply.responseTitle", target = "title"),
+            @Mapping(source = "reply.responseDescription", target = "description"),
+            @Mapping(source = "reply.responseArticles", target = "articles"),
+    })
+    MpMessageSendOutReqBO convert(String openid, MpAutoReplyDO reply);
+
+    PageResult<MpAutoReplyRespVO> convertPage(PageResult<MpAutoReplyDO> page);
+
+    MpAutoReplyRespVO convert(MpAutoReplyDO bean);
+
+    MpAutoReplyDO convert(MpAutoReplyCreateReqVO bean);
+
+    MpAutoReplyDO convert(MpAutoReplyUpdateReqVO bean);
+}

+ 172 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/message/MpMessageConvert.java

@@ -0,0 +1,172 @@
+package cn.iocoder.yudao.module.mp.convert.message;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.mp.controller.admin.message.vo.message.MpMessageRespVO;
+import cn.iocoder.yudao.module.mp.controller.admin.message.vo.message.MpMessageSendReqVO;
+import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO;
+import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO;
+import cn.iocoder.yudao.module.mp.dal.dataobject.user.MpUserDO;
+import cn.iocoder.yudao.module.mp.service.message.bo.MpMessageSendOutReqBO;
+import me.chanjar.weixin.common.api.WxConsts;
+import me.chanjar.weixin.mp.bean.kefu.WxMpKefuMessage;
+import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
+import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
+import me.chanjar.weixin.mp.bean.message.WxMpXmlOutNewsMessage;
+import me.chanjar.weixin.mp.builder.outxml.BaseBuilder;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.Mappings;
+import org.mapstruct.factory.Mappers;
+
+import java.util.List;
+
+@Mapper
+public interface MpMessageConvert {
+
+    MpMessageConvert INSTANCE = Mappers.getMapper(MpMessageConvert.class);
+
+    MpMessageRespVO convert(MpMessageDO bean);
+
+    List<MpMessageRespVO> convertList(List<MpMessageDO> list);
+
+    PageResult<MpMessageRespVO> convertPage(PageResult<MpMessageDO> page);
+
+    default MpMessageDO convert(WxMpXmlMessage wxMessage, MpAccountDO account, MpUserDO user) {
+        MpMessageDO message = convert(wxMessage);
+        if (account != null) {
+            message.setAccountId(account.getId()).setAppId(account.getAppId());
+        }
+        if (user != null) {
+            message.setUserId(user.getId()).setOpenid(user.getOpenid());
+        }
+        return message;
+    }
+    @Mappings(value = {
+            @Mapping(source = "msgType", target = "type"),
+            @Mapping(target = "createTime", ignore = true),
+    })
+    MpMessageDO convert(WxMpXmlMessage bean);
+
+    default MpMessageDO convert(MpMessageSendOutReqBO sendReqBO, MpAccountDO account, MpUserDO user) {
+        // 构建消息
+        MpMessageDO message = new MpMessageDO();
+        message.setType(sendReqBO.getType());
+        switch (sendReqBO.getType()) {
+            case WxConsts.XmlMsgType.TEXT: // 1. 文本
+                message.setContent(sendReqBO.getContent());
+                break;
+            case WxConsts.XmlMsgType.IMAGE: // 2. 图片
+            case WxConsts.XmlMsgType.VOICE: // 3. 语音
+                message.setMediaId(sendReqBO.getMediaId());
+                break;
+            case WxConsts.XmlMsgType.VIDEO: // 4. 视频
+                message.setMediaId(sendReqBO.getMediaId())
+                        .setTitle(sendReqBO.getTitle()).setDescription(sendReqBO.getDescription());
+                break;
+            case WxConsts.XmlMsgType.NEWS: // 5. 图文
+                message.setArticles(sendReqBO.getArticles());
+            case WxConsts.XmlMsgType.MUSIC: // 6. 音乐
+                message.setTitle(sendReqBO.getTitle()).setDescription(sendReqBO.getDescription())
+                        .setMusicUrl(sendReqBO.getMusicUrl()).setHqMusicUrl(sendReqBO.getHqMusicUrl())
+                        .setThumbMediaId(sendReqBO.getThumbMediaId());
+                break;
+            default:
+                throw new IllegalArgumentException("不支持的消息类型:" + message.getType());
+        }
+
+        // 其它字段
+        if (account != null) {
+            message.setAccountId(account.getId()).setAppId(account.getAppId());
+        }
+        if (user != null) {
+            message.setUserId(user.getId()).setOpenid(user.getOpenid());
+        }
+        return message;
+    }
+
+    default WxMpXmlOutMessage convert02(MpMessageDO message, MpAccountDO account) {
+        BaseBuilder<?, ? extends WxMpXmlOutMessage> builder;
+        // 个性化字段
+        switch (message.getType()) {
+            case WxConsts.XmlMsgType.TEXT:
+                builder = WxMpXmlOutMessage.TEXT().content(message.getContent());
+                break;
+            case WxConsts.XmlMsgType.IMAGE:
+                builder = WxMpXmlOutMessage.IMAGE().mediaId(message.getMediaId());
+                break;
+            case WxConsts.XmlMsgType.VOICE:
+                builder = WxMpXmlOutMessage.VOICE().mediaId(message.getMediaId());
+                break;
+            case WxConsts.XmlMsgType.VIDEO:
+                builder = WxMpXmlOutMessage.VIDEO().mediaId(message.getMediaId())
+                        .title(message.getTitle()).description(message.getDescription());
+                break;
+            case WxConsts.XmlMsgType.NEWS:
+                builder = WxMpXmlOutMessage.NEWS().articles(convertList02(message.getArticles()));
+                break;
+            case WxConsts.XmlMsgType.MUSIC:
+                builder = WxMpXmlOutMessage.MUSIC().title(message.getTitle()).description(message.getDescription())
+                        .musicUrl(message.getMusicUrl()).hqMusicUrl(message.getHqMusicUrl())
+                        .thumbMediaId(message.getThumbMediaId());
+                break;
+            default:
+                throw new IllegalArgumentException("不支持的消息类型:" + message.getType());
+        }
+        // 通用字段
+        builder.fromUser(account.getAccount());
+        builder.toUser(message.getOpenid());
+        return builder.build();
+    }
+    List<WxMpXmlOutNewsMessage.Item> convertList02(List<MpMessageDO.Article> list);
+
+    default WxMpKefuMessage convert(MpMessageSendReqVO sendReqVO, MpUserDO user) {
+        me.chanjar.weixin.mp.builder.kefu.BaseBuilder<?> builder;
+        // 个性化字段
+        switch (sendReqVO.getType()) {
+            case WxConsts.KefuMsgType.TEXT:
+                builder = WxMpKefuMessage.TEXT().content(sendReqVO.getContent());
+                break;
+            case WxConsts.KefuMsgType.IMAGE:
+                builder = WxMpKefuMessage.IMAGE().mediaId(sendReqVO.getMediaId());
+                break;
+            case WxConsts.KefuMsgType.VOICE:
+                builder = WxMpKefuMessage.VOICE().mediaId(sendReqVO.getMediaId());
+                break;
+            case WxConsts.KefuMsgType.VIDEO:
+                builder = WxMpKefuMessage.VIDEO().mediaId(sendReqVO.getMediaId())
+                        .title(sendReqVO.getTitle()).description(sendReqVO.getDescription());
+                break;
+            case WxConsts.KefuMsgType.NEWS:
+                builder = WxMpKefuMessage.NEWS().articles(convertList03(sendReqVO.getArticles()));
+                break;
+            case WxConsts.KefuMsgType.MUSIC:
+                builder = WxMpKefuMessage.MUSIC().title(sendReqVO.getTitle()).description(sendReqVO.getDescription())
+                        .thumbMediaId(sendReqVO.getThumbMediaId())
+                        .musicUrl(sendReqVO.getMusicUrl()).hqMusicUrl(sendReqVO.getHqMusicUrl());
+                break;
+            default:
+                throw new IllegalArgumentException("不支持的消息类型:" + sendReqVO.getType());
+        }
+        // 通用字段
+        builder.toUser(user.getOpenid());
+        return builder.build();
+    }
+    List<WxMpKefuMessage.WxArticle> convertList03(List<MpMessageDO.Article> list);
+
+    default MpMessageDO convert(WxMpKefuMessage wxMessage, MpAccountDO account, MpUserDO user) {
+        MpMessageDO message = convert(wxMessage);
+        if (account != null) {
+            message.setAccountId(account.getId()).setAppId(account.getAppId());
+        }
+        if (user != null) {
+            message.setUserId(user.getId()).setOpenid(user.getOpenid());
+        }
+        return message;
+    }
+    @Mappings(value = {
+            @Mapping(source = "msgType", target = "type"),
+            @Mapping(target = "createTime", ignore = true),
+    })
+    MpMessageDO convert(WxMpKefuMessage bean);
+
+}

+ 52 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/statistics/MpStatisticsConvert.java

@@ -0,0 +1,52 @@
+package cn.iocoder.yudao.module.mp.convert.statistics;
+
+import cn.iocoder.yudao.module.mp.controller.admin.statistics.vo.MpStatisticsInterfaceSummaryRespVO;
+import cn.iocoder.yudao.module.mp.controller.admin.statistics.vo.MpStatisticsUpstreamMessageRespVO;
+import cn.iocoder.yudao.module.mp.controller.admin.statistics.vo.MpStatisticsUserCumulateRespVO;
+import cn.iocoder.yudao.module.mp.controller.admin.statistics.vo.MpStatisticsUserSummaryRespVO;
+import me.chanjar.weixin.mp.bean.datacube.WxDataCubeInterfaceResult;
+import me.chanjar.weixin.mp.bean.datacube.WxDataCubeMsgResult;
+import me.chanjar.weixin.mp.bean.datacube.WxDataCubeUserCumulate;
+import me.chanjar.weixin.mp.bean.datacube.WxDataCubeUserSummary;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.Mappings;
+import org.mapstruct.Named;
+import org.mapstruct.factory.Mappers;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY;
+
+@Mapper
+public interface MpStatisticsConvert {
+
+    MpStatisticsConvert INSTANCE = Mappers.getMapper(MpStatisticsConvert.class);
+
+    List<MpStatisticsUserSummaryRespVO> convertList01(List<WxDataCubeUserSummary> list);
+
+    List<MpStatisticsUserCumulateRespVO> convertList02(List<WxDataCubeUserCumulate> list);
+
+    List<MpStatisticsUpstreamMessageRespVO> convertList03(List<WxDataCubeMsgResult> list);
+
+    @Mappings({
+            @Mapping(target = "refDate", expression = "java(dateFormat0(bean.getRefDate()))"),
+            @Mapping(source = "msgUser", target = "messageUser"),
+            @Mapping(source = "msgCount", target = "messageCount"),
+    })
+    MpStatisticsUpstreamMessageRespVO convert(WxDataCubeMsgResult bean);
+
+    List<MpStatisticsInterfaceSummaryRespVO> convertList04(List<WxDataCubeInterfaceResult> list);
+
+    @Mapping(target = "refDate", expression = "java(dateFormat0(bean.getRefDate()))")
+    MpStatisticsInterfaceSummaryRespVO convert(WxDataCubeInterfaceResult bean);
+
+    @Named("dateFormat0")
+    default LocalDateTime dateFormat0(String date) {
+        return LocalDate.parse(date, DateTimeFormatter.ofPattern(FORMAT_YEAR_MONTH_DAY)).atStartOfDay();
+    }
+
+}

+ 44 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/tag/MpTagConvert.java

@@ -0,0 +1,44 @@
+package cn.iocoder.yudao.module.mp.convert.tag;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.mp.controller.admin.tag.vo.MpTagRespVO;
+import cn.iocoder.yudao.module.mp.controller.admin.tag.vo.MpTagSimpleRespVO;
+import cn.iocoder.yudao.module.mp.controller.admin.tag.vo.MpTagUpdateReqVO;
+import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO;
+import cn.iocoder.yudao.module.mp.dal.dataobject.tag.MpTagDO;
+import me.chanjar.weixin.mp.bean.tag.WxUserTag;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.Mappings;
+import org.mapstruct.factory.Mappers;
+
+import java.util.List;
+
+@Mapper
+public interface MpTagConvert {
+
+    MpTagConvert INSTANCE = Mappers.getMapper(MpTagConvert.class);
+
+    WxUserTag convert(MpTagUpdateReqVO bean);
+
+    MpTagRespVO convert(WxUserTag bean);
+
+    List<MpTagRespVO> convertList(List<WxUserTag> list);
+
+    PageResult<MpTagRespVO> convertPage(PageResult<MpTagDO> page);
+
+    @Mappings({
+            @Mapping(target = "id", ignore = true),
+            @Mapping(source = "tag.id", target = "tagId"),
+            @Mapping(source = "tag.name", target = "name"),
+            @Mapping(source = "tag.count", target = "count"),
+            @Mapping(source = "account.id", target = "accountId"),
+            @Mapping(source = "account.appId", target = "appId"),
+    })
+    MpTagDO convert(WxUserTag tag, MpAccountDO account);
+
+    MpTagRespVO convert(MpTagDO mpTagDO);
+
+    List<MpTagSimpleRespVO> convertList02(List<MpTagDO> list);
+
+}

+ 56 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/user/MpUserConvert.java

@@ -0,0 +1,56 @@
+package cn.iocoder.yudao.module.mp.convert.user;
+
+import cn.hutool.core.date.LocalDateTimeUtil;
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
+import cn.iocoder.yudao.module.mp.controller.admin.user.vo.MpUserRespVO;
+import cn.iocoder.yudao.module.mp.controller.admin.user.vo.MpUserUpdateReqVO;
+import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO;
+import cn.iocoder.yudao.module.mp.dal.dataobject.user.MpUserDO;
+import me.chanjar.weixin.mp.bean.result.WxMpUser;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.Mappings;
+import org.mapstruct.factory.Mappers;
+
+import java.util.List;
+
+@Mapper
+public interface MpUserConvert {
+
+    MpUserConvert INSTANCE = Mappers.getMapper(MpUserConvert.class);
+
+    MpUserRespVO convert(MpUserDO bean);
+
+    List<MpUserRespVO> convertList(List<MpUserDO> list);
+
+    PageResult<MpUserRespVO> convertPage(PageResult<MpUserDO> page);
+
+    @Mappings(value = {
+            @Mapping(source = "openId", target = "openid"),
+            @Mapping(source = "unionId", target = "unionId"),
+            @Mapping(source = "headImgUrl", target = "headImageUrl"),
+            @Mapping(target = "subscribeTime", ignore = true), // 单独转换
+    })
+    MpUserDO convert(WxMpUser wxMpUser);
+
+    default MpUserDO convert(MpAccountDO account, WxMpUser wxMpUser) {
+        MpUserDO user = convert(wxMpUser);
+        user.setSubscribeStatus(wxMpUser.getSubscribe() ? CommonStatusEnum.ENABLE.getStatus()
+                : CommonStatusEnum.DISABLE.getStatus());
+        user.setSubscribeTime(LocalDateTimeUtil.of(wxMpUser.getSubscribeTime() * 1000L));
+        if (account != null) {
+            user.setAccountId(account.getId());
+            user.setAppId(account.getAppId());
+        }
+        return user;
+    }
+
+    default List<MpUserDO> convertList(MpAccountDO account, List<WxMpUser> wxUsers) {
+        return CollectionUtils.convertList(wxUsers, wxUser -> convert(account, wxUser));
+    }
+
+    MpUserDO convert(MpUserUpdateReqVO bean);
+
+}

+ 62 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/account/MpAccountDO.java

@@ -0,0 +1,62 @@
+package cn.iocoder.yudao.module.mp.dal.dataobject.account;
+
+import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+/**
+ * 公众号账号 DO
+ *
+ * @author 芋道源码
+ */
+@TableName("mp_account")
+@KeySequence("mp_account_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class MpAccountDO extends TenantBaseDO {
+
+    /**
+     * 编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 公众号名称
+     */
+    private String name;
+    /**
+     * 公众号账号
+     */
+    private String account;
+    /**
+     * 公众号 appid
+     */
+    private String appId;
+    /**
+     * 公众号密钥
+     */
+    private String appSecret;
+    /**
+     * 公众号token
+     */
+    private String token;
+    /**
+     * 消息加解密密钥
+     */
+    private String aesKey;
+    /**
+     * 二维码图片 URL
+     */
+    private String qrCodeUrl;
+    /**
+     * 备注
+     */
+    private String remark;
+
+}

+ 99 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/material/MpMaterialDO.java

@@ -0,0 +1,99 @@
+package cn.iocoder.yudao.module.mp.dal.dataobject.material;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+import me.chanjar.weixin.common.api.WxConsts;
+
+/**
+ * 公众号素材 DO
+ *
+ * 1. <a href="https://developers.weixin.qq.com/doc/offiaccount/Asset_Management/New_temporary_materials.html">临时素材</a>
+ * 2. <a href="https://developers.weixin.qq.com/doc/offiaccount/Asset_Management/Adding_Permanent_Assets.html">永久素材</a>
+ *
+ * @author 芋道源码
+ */
+@TableName("mp_material")
+@KeySequence("mp_material_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class MpMaterialDO extends BaseDO {
+
+    /**
+     * 主键
+     */
+    @TableId
+    private Long id;
+    /**
+     * 公众号账号的编号
+     *
+     * 关联 {@link MpAccountDO#getId()}
+     */
+    private Long accountId;
+    /**
+     * 公众号 appId
+     *
+     * 冗余 {@link MpAccountDO#getAppId()}
+     */
+    private String appId;
+
+    /**
+     * 公众号素材 id
+     */
+    private String mediaId;
+    /**
+     * 文件类型
+     *
+     * 枚举 {@link WxConsts.MediaFileType}
+     */
+    private String type;
+    /**
+     * 是否永久
+     *
+     * true - 永久素材
+     * false - 临时素材
+     */
+    private Boolean permanent;
+    /**
+     * 文件服务器的 URL
+     */
+    private String url;
+
+    /**
+     * 名字
+     *
+     * 永久素材:非空
+     * 临时素材:可能为空。
+     *      1. 为空的情况:粉丝主动发送的图片、语音等
+     *      2. 非空的情况:主动发送给粉丝的图片、语音等
+     */
+    private String name;
+
+    /**
+     * 公众号文件 URL
+     *
+     * 只有【永久素材】使用
+     */
+    private String mpUrl;
+
+    /**
+     * 视频素材的标题
+     *
+     * 只有【永久素材】使用
+     */
+    private String title;
+    /**
+     * 视频素材的描述
+     *
+     * 只有【永久素材】使用
+     */
+    private String introduction;
+
+}

+ 185 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/menu/MpMenuDO.java

@@ -0,0 +1,185 @@
+package cn.iocoder.yudao.module.mp.dal.dataobject.menu;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO;
+import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import me.chanjar.weixin.common.api.WxConsts;
+import me.chanjar.weixin.common.api.WxConsts.MenuButtonType;
+
+import java.util.List;
+
+/**
+ * 公众号菜单 DO
+ *
+ * @author 芋道源码
+ */
+@TableName(value = "mp_menu", autoResultMap = true)
+@KeySequence("mp_menu_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class MpMenuDO extends BaseDO {
+
+    /**
+     * 编号 - 顶级菜单
+     */
+    public static final Long ID_ROOT = 0L;
+
+    /**
+     * 编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 公众号账号的编号
+     *
+     * 关联 {@link MpAccountDO#getId()}
+     */
+    private Long accountId;
+    /**
+     * 公众号 appId
+     *
+     * 冗余 {@link MpAccountDO#getAppId()}
+     */
+    private String appId;
+
+    /**
+     * 菜单名称
+     */
+    private String name;
+    /**
+     * 菜单标识
+     *
+     * 支持多 DB 类型时,无法直接使用 key + @TableField("menuKey") 来实现转换,原因是 "menuKey" AS key 而存在报错
+     */
+    private String menuKey;
+    /**
+     * 父菜单编号
+     */
+    private Long parentId;
+
+    // ========== 按钮操作 ==========
+
+    /**
+     * 按钮类型
+     *
+     * 枚举 {@link MenuButtonType}
+     */
+    private String type;
+
+    /**
+     * 网页链接
+     *
+     * 粉丝点击菜单可打开链接,不超过 1024 字节
+     *
+     * 类型为 {@link WxConsts.XmlMsgType} 的 VIEW、MINIPROGRAM
+     */
+    private String url;
+
+    /**
+     * 小程序的 appId
+     *
+     * 类型为 {@link MenuButtonType} 的 MINIPROGRAM
+     */
+    private String miniProgramAppId;
+    /**
+     * 小程序的页面路径
+     *
+     * 类型为 {@link MenuButtonType} 的 MINIPROGRAM
+     */
+    private String miniProgramPagePath;
+
+    /**
+     * 跳转图文的媒体编号
+     */
+    private String articleId;
+
+    // ========== 消息内容 ==========
+
+    /**
+     * 消息类型
+     *
+     * 当 {@link #type} 为 CLICK、SCANCODE_WAITMSG
+     *
+     * 枚举 {@link WxConsts.XmlMsgType} 中的 TEXT、IMAGE、VOICE、VIDEO、NEWS、MUSIC
+     */
+    private String replyMessageType;
+
+    /**
+     * 回复的消息内容
+     *
+     * 消息类型为 {@link WxConsts.XmlMsgType} 的 TEXT
+     */
+    private String replyContent;
+
+    /**
+     * 回复的媒体 id
+     *
+     * 消息类型为 {@link WxConsts.XmlMsgType} 的 IMAGE、VOICE、VIDEO
+     */
+    private String replyMediaId;
+    /**
+     * 回复的媒体 URL
+     *
+     * 消息类型为 {@link WxConsts.XmlMsgType} 的 IMAGE、VOICE、VIDEO
+     */
+    private String replyMediaUrl;
+
+    /**
+     * 回复的标题
+     *
+     * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO
+     */
+    private String replyTitle;
+    /**
+     * 回复的描述
+     *
+     * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO
+     */
+    private String replyDescription;
+
+    /**
+     * 回复的缩略图的媒体 id,通过素材管理中的接口上传多媒体文件,得到的 id
+     *
+     * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC、VIDEO
+     */
+    private String replyThumbMediaId;
+    /**
+     * 回复的缩略图的媒体 URL
+     *
+     * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC、VIDEO
+     */
+    private String replyThumbMediaUrl;
+
+    /**
+     * 回复的图文消息数组
+     *
+     * 消息类型为 {@link WxConsts.XmlMsgType} 的 NEWS
+     */
+    @TableField(typeHandler = JacksonTypeHandler.class)
+    private List<MpMessageDO.Article> replyArticles;
+
+    /**
+     * 回复的音乐链接
+     *
+     * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC
+     */
+    private String replyMusicUrl;
+    /**
+     * 回复的高质量音乐链接
+     *
+     * WIFI 环境优先使用该链接播放音乐
+     *
+     * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC
+     */
+    private String replyHqMusicUrl;
+
+}

+ 165 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/message/MpAutoReplyDO.java

@@ -0,0 +1,165 @@
+package cn.iocoder.yudao.module.mp.dal.dataobject.message;
+
+import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO;
+import cn.iocoder.yudao.module.mp.enums.message.MpAutoReplyMatchEnum;
+import cn.iocoder.yudao.module.mp.enums.message.MpAutoReplyTypeEnum;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import me.chanjar.weixin.common.api.WxConsts;
+import me.chanjar.weixin.common.api.WxConsts.XmlMsgType;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ * 公众号消息自动回复 DO
+ *
+ * @author 芋道源码
+ */
+@TableName(value = "mp_auto_reply", autoResultMap = true)
+@KeySequence("mp_auto_reply_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class MpAutoReplyDO extends BaseDO {
+
+    public static Set<String> REQUEST_MESSAGE_TYPE = SetUtils.asSet(WxConsts.XmlMsgType.TEXT, WxConsts.XmlMsgType.IMAGE,
+            WxConsts.XmlMsgType.VOICE, WxConsts.XmlMsgType.VIDEO, WxConsts.XmlMsgType.SHORTVIDEO,
+            WxConsts.XmlMsgType.LOCATION, WxConsts.XmlMsgType.LINK);
+
+    /**
+     * 主键
+     */
+    @TableId
+    private Long id;
+    /**
+     * 公众号账号的编号
+     *
+     * 关联 {@link MpAccountDO#getId()}
+     */
+    private Long accountId;
+    /**
+     * 公众号 appId
+     *
+     * 冗余 {@link MpAccountDO#getAppId()}
+     */
+    private String appId;
+
+    /**
+     * 回复类型
+     *
+     * 枚举 {@link MpAutoReplyTypeEnum}
+     */
+    private Integer type;
+
+    // ==================== 请求消息 ====================
+
+    /**
+     * 请求的关键字
+     *
+     * 当 {@link #type} 为 {@link MpAutoReplyTypeEnum#KEYWORD}
+     */
+    private String requestKeyword;
+    /**
+     * 请求的关键字的匹配
+     *
+     * 当 {@link #type} 为 {@link MpAutoReplyTypeEnum#KEYWORD}
+     *
+     * 枚举 {@link MpAutoReplyMatchEnum}
+     */
+    private Integer requestMatch;
+
+    /**
+     * 请求的消息类型
+     *
+     * 当 {@link #type} 为 {@link MpAutoReplyTypeEnum#MESSAGE}
+     *
+     * 枚举 {@link XmlMsgType} 中的 {@link #REQUEST_MESSAGE_TYPE}
+     */
+    private String requestMessageType;
+
+    // ==================== 响应消息 ====================
+
+    /**
+     * 回复的消息类型
+     *
+     * 枚举 {@link XmlMsgType} 中的 TEXT、IMAGE、VOICE、VIDEO、NEWS
+     */
+    private String responseMessageType;
+
+    /**
+     * 回复的消息内容
+     *
+     * 消息类型为 {@link WxConsts.XmlMsgType} 的 TEXT
+     */
+    private String responseContent;
+
+    /**
+     * 回复的媒体 id
+     *
+     * 消息类型为 {@link WxConsts.XmlMsgType} 的 IMAGE、VOICE、VIDEO
+     */
+    private String responseMediaId;
+    /**
+     * 回复的媒体 URL
+     */
+    private String responseMediaUrl;
+
+    /**
+     * 回复的标题
+     *
+     * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO
+     */
+    private String responseTitle;
+    /**
+     * 回复的描述
+     *
+     * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO
+     */
+    private String responseDescription;
+
+    /**
+     * 回复的缩略图的媒体 id,通过素材管理中的接口上传多媒体文件,得到的 id
+     *
+     * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC、VIDEO
+     */
+    private String responseThumbMediaId;
+    /**
+     * 回复的缩略图的媒体 URL
+     *
+     * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC、VIDEO
+     */
+    private String responseThumbMediaUrl;
+
+    /**
+     * 回复的图文消息
+     *
+     * 消息类型为 {@link WxConsts.XmlMsgType} 的 NEWS
+     */
+    @TableField(typeHandler = JacksonTypeHandler.class)
+    private List<MpMessageDO.Article> responseArticles;
+
+    /**
+     * 回复的音乐链接
+     *
+     * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC
+     */
+    private String responseMusicUrl;
+    /**
+     * 回复的高质量音乐链接
+     *
+     * WIFI 环境优先使用该链接播放音乐
+     *
+     * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC
+     */
+    private String responseHqMusicUrl;
+
+}

+ 242 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/message/MpMessageDO.java

@@ -0,0 +1,242 @@
+package cn.iocoder.yudao.module.mp.dal.dataobject.message;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO;
+import cn.iocoder.yudao.module.mp.dal.dataobject.user.MpUserDO;
+import cn.iocoder.yudao.module.mp.enums.message.MpMessageSendFromEnum;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import me.chanjar.weixin.common.api.WxConsts;
+import me.chanjar.weixin.mp.builder.kefu.NewsBuilder;
+
+import javax.validation.constraints.NotEmpty;
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 公众号消息 DO
+ *
+ * @author 芋道源码
+ */
+@TableName(value = "mp_message", autoResultMap = true)
+@KeySequence("mp_message_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class MpMessageDO extends BaseDO {
+
+    /**
+     * 主键
+     */
+    @TableId
+    private Long id;
+    /**
+     * 微信公众号消息 id
+     */
+    private Long msgId;
+    /**
+     * 公众号账号的 ID
+     *
+     * 关联 {@link MpAccountDO#getId()}
+     */
+    private Long accountId;
+    /**
+     * 公众号 appid
+     *
+     * 冗余 {@link MpAccountDO#getAppId()}
+     */
+    private String appId;
+    /**
+     * 公众号粉丝的编号
+     *
+     * 关联 {@link MpUserDO#getId()}
+     */
+    private Long userId;
+    /**
+     * 公众号粉丝标志
+     *
+     * 冗余 {@link MpUserDO#getOpenid()}
+     */
+    private String openid;
+
+    /**
+     * 消息类型
+     *
+     * 枚举 {@link WxConsts.XmlMsgType}
+     */
+    private String type;
+    /**
+     * 消息来源
+     *
+     * 枚举 {@link MpMessageSendFromEnum}
+     */
+    private Integer sendFrom;
+
+    // ========= 普通消息内容 https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Receiving_standard_messages.html
+
+    /**
+     * 消息内容
+     *
+     * 消息类型为 {@link WxConsts.XmlMsgType} 的 TEXT
+     */
+    private String content;
+
+    /**
+     * 媒体文件的编号
+     *
+     * 消息类型为 {@link WxConsts.XmlMsgType} 的 IMAGE、VOICE、VIDEO
+     */
+    private String mediaId;
+    /**
+     * 媒体文件的 URL
+     */
+    private String mediaUrl;
+    /**
+     * 语音识别后文本
+     *
+     * 消息类型为 {@link WxConsts.XmlMsgType} 的 VOICE
+     */
+    private String recognition;
+    /**
+     * 语音格式,如 amr,speex 等
+     *
+     * 消息类型为 {@link WxConsts.XmlMsgType} 的 VOICE
+     */
+    private String format;
+    /**
+     * 标题
+     *
+     * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO、MUSIC、LINK
+     */
+    private String title;
+    /**
+     * 描述
+     *
+     * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO、MUSIC
+     */
+    private String description;
+
+    /**
+     * 缩略图的媒体 id,通过素材管理中的接口上传多媒体文件,得到的 id
+     *
+     * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC、VIDEO
+     */
+    private String thumbMediaId;
+    /**
+     * 缩略图的媒体 URL
+     *
+     * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC、VIDEO
+     */
+    private String thumbMediaUrl;
+
+    /**
+     * 点击图文消息跳转链接
+     *
+     * 消息类型为 {@link WxConsts.XmlMsgType} 的 LINK
+     */
+    private String url;
+
+    /**
+     * 地理位置维度
+     *
+     * 消息类型为 {@link WxConsts.XmlMsgType} 的 LOCATION
+     */
+    private Double locationX;
+    /**
+     * 地理位置经度
+     *
+     * 消息类型为 {@link WxConsts.XmlMsgType} 的 LOCATION
+     */
+    private Double locationY;
+    /**
+     * 地图缩放大小
+     *
+     * 消息类型为 {@link WxConsts.XmlMsgType} 的 LOCATION
+     */
+    private Double scale;
+    /**
+     * 详细地址
+     *
+     * 消息类型为 {@link WxConsts.XmlMsgType} 的 LOCATION
+     *
+     * 例如说杨浦区黄兴路 221-4 号临
+     */
+    private String label;
+
+    /**
+     * 图文消息数组
+     *
+     * 消息类型为 {@link WxConsts.XmlMsgType} 的 NEWS
+     */
+    @TableField(typeHandler = JacksonTypeHandler.class)
+    private List<Article> articles;
+
+    /**
+     * 音乐链接
+     *
+     * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC
+     */
+    private String musicUrl;
+    /**
+     * 高质量音乐链接
+     *
+     * WIFI 环境优先使用该链接播放音乐
+     *
+     * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC
+     */
+    private String hqMusicUrl;
+
+    // ========= 事件推送 https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Receiving_event_pushes.html
+
+    /**
+     * 事件类型
+     *
+     * 枚举 {@link WxConsts.EventType}
+     */
+    private String event;
+    /**
+     * 事件 Key
+     *
+     * 1. {@link WxConsts.EventType} 的 SCAN:qrscene_ 为前缀,后面为二维码的参数值
+     * 2. {@link WxConsts.EventType} 的 CLICK:与自定义菜单接口中 KEY 值对应
+     */
+    private String eventKey;
+
+    /**
+     * 文章
+     */
+    @Data
+    public static class Article implements Serializable {
+
+        /**
+         * 图文消息标题
+         */
+        @NotEmpty(message = "图文消息标题不能为空", groups = NewsBuilder.class)
+        private String title;
+        /**
+         * 图文消息描述
+         */
+        @NotEmpty(message = "图文消息描述不能为空", groups = NewsBuilder.class)
+        private String description;
+        /**
+         * 图片链接
+         *
+         * 支持 JPG、PNG 格式,较好的效果为大图 360*200,小图 200*200
+         */
+        @NotEmpty(message = "图片链接不能为空", groups = NewsBuilder.class)
+        private String picUrl;
+        /**
+         * 点击图文消息跳转链接
+         */
+        @NotEmpty(message = "点击图文消息跳转链接不能为空", groups = NewsBuilder.class)
+        private String url;
+
+    }
+
+}

+ 58 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/tag/MpTagDO.java

@@ -0,0 +1,58 @@
+package cn.iocoder.yudao.module.mp.dal.dataobject.tag;
+
+import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO;
+import lombok.*;
+
+import com.baomidou.mybatisplus.annotation.*;
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import me.chanjar.weixin.mp.bean.tag.WxUserTag;
+
+/**
+ * 公众号标签 DO
+ *
+ * @author 芋道源码
+ */
+@TableName("mp_tag")
+@KeySequence("mp_tag_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class MpTagDO extends BaseDO {
+
+    /**
+     * 主键
+     */
+    @TableId(type = IdType.INPUT)
+    private Long id;
+    /**
+     * 公众号标签 id
+     */
+    private Long tagId;
+    /**
+     * 标签名
+     */
+    private String name;
+    /**
+     * 此标签下粉丝数
+     *
+     * 冗余:{@link WxUserTag#getCount()} 字段,需要管理员点击【同步】后,更新该字段
+     */
+    private Integer count;
+
+    /**
+     * 公众号账号的编号
+     *
+     * 关联 {@link MpAccountDO#getId()}
+     */
+    private Long accountId;
+    /**
+     * 公众号 appId
+     *
+     * 冗余 {@link MpAccountDO#getAppId()}
+     */
+    private String appId;
+
+}

+ 114 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/user/MpUserDO.java

@@ -0,0 +1,114 @@
+package cn.iocoder.yudao.module.mp.dal.dataobject.user;
+
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler;
+import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO;
+import cn.iocoder.yudao.module.mp.dal.dataobject.tag.MpTagDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 微信公众号粉丝 DO
+ *
+ * @author 芋道源码
+ */
+@TableName(value = "mp_user", autoResultMap = true)
+@KeySequence("mp_user_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class MpUserDO extends BaseDO {
+
+    /**
+     * 编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 粉丝标识
+     */
+    private String openid;
+    /**
+     * 微信生态唯一标识
+     */
+    private String unionId;
+    /**
+     * 关注状态
+     *
+     * 枚举 {@link CommonStatusEnum}
+     * 1. 开启 - 已关注
+     * 2. 禁用 - 取消关注
+     */
+    private Integer subscribeStatus;
+    /**
+     * 关注时间
+     */
+    private LocalDateTime subscribeTime;
+    /**
+     * 取消关注时间
+     */
+    private LocalDateTime unsubscribeTime;
+    /**
+     * 昵称
+     *
+     * 注意,2021-12-27 公众号接口不再返回头像和昵称,只能通过微信公众号的网页登录获取
+     */
+    private String nickname;
+    /**
+     * 头像地址
+     *
+     * 注意,2021-12-27 公众号接口不再返回头像和昵称,只能通过微信公众号的网页登录获取
+     */
+    private String headImageUrl;
+    /**
+     * 语言
+     */
+    private String language;
+    /**
+     * 国家
+     */
+    private String country;
+    /**
+     * 省份
+     */
+    private String province;
+    /**
+     * 城市
+     */
+    private String city;
+    /**
+     * 备注
+     */
+    private String remark;
+    /**
+     * 标签编号数组
+     *
+     * 注意,对应的是 {@link MpTagDO#getTagId()} 字段
+     */
+    @TableField(typeHandler = LongListTypeHandler.class)
+    private List<Long> tagIds;
+
+    /**
+     * 公众号账号的编号
+     *
+     * 关联 {@link MpAccountDO#getId()}
+     */
+    private Long accountId;
+    /**
+     * 公众号 appId
+     *
+     * 冗余 {@link MpAccountDO#getAppId()}
+     */
+    private String appId;
+
+}

+ 31 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/account/MpAccountMapper.java

@@ -0,0 +1,31 @@
+package cn.iocoder.yudao.module.mp.dal.mysql.account;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.module.mp.controller.admin.account.vo.MpAccountPageReqVO;
+import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Select;
+
+import java.time.LocalDateTime;
+
+@Mapper
+public interface MpAccountMapper extends BaseMapperX<MpAccountDO> {
+
+    default PageResult<MpAccountDO> selectPage(MpAccountPageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<MpAccountDO>()
+                .likeIfPresent(MpAccountDO::getName, reqVO.getName())
+                .likeIfPresent(MpAccountDO::getAccount, reqVO.getAccount())
+                .likeIfPresent(MpAccountDO::getAppId, reqVO.getAppId())
+                .orderByDesc(MpAccountDO::getId));
+    }
+
+    default MpAccountDO selectByAppId(String appId) {
+        return selectOne(MpAccountDO::getAppId, appId);
+    }
+
+    @Select("SELECT COUNT(*) FROM mp_account WHERE update_time > #{maxUpdateTime}")
+    Long selectCountByUpdateTimeGt(LocalDateTime maxUpdateTime);
+
+}

+ 33 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/material/MpMaterialMapper.java

@@ -0,0 +1,33 @@
+package cn.iocoder.yudao.module.mp.dal.mysql.material;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.module.mp.controller.admin.material.vo.MpMaterialPageReqVO;
+import cn.iocoder.yudao.module.mp.dal.dataobject.material.MpMaterialDO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.Collection;
+import java.util.List;
+
+@Mapper
+public interface MpMaterialMapper extends BaseMapperX<MpMaterialDO> {
+
+    default MpMaterialDO selectByAccountIdAndMediaId(Long accountId, String mediaId) {
+        return selectOne(MpMaterialDO::getAccountId, accountId,
+                MpMaterialDO::getMediaId, mediaId);
+    }
+
+    default PageResult<MpMaterialDO> selectPage(MpMaterialPageReqVO pageReqVO) {
+        return selectPage(pageReqVO, new LambdaQueryWrapperX<MpMaterialDO>()
+                .eq(MpMaterialDO::getAccountId, pageReqVO.getAccountId())
+                .eqIfPresent(MpMaterialDO::getPermanent, pageReqVO.getPermanent())
+                .eqIfPresent(MpMaterialDO::getType, pageReqVO.getType())
+                .orderByDesc(MpMaterialDO::getId));
+    }
+
+    default List<MpMaterialDO> selectListByMediaId(Collection<String> mediaIds) {
+        return selectList(MpMaterialDO::getMediaId, mediaIds);
+    }
+
+}

+ 25 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/menu/MpMenuMapper.java

@@ -0,0 +1,25 @@
+package cn.iocoder.yudao.module.mp.dal.mysql.menu;
+
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.module.mp.dal.dataobject.menu.MpMenuDO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+@Mapper
+public interface MpMenuMapper extends BaseMapperX<MpMenuDO> {
+
+    default MpMenuDO selectByAppIdAndMenuKey(String appId, String menuKey) {
+        return selectOne(MpMenuDO::getAppId, appId,
+                MpMenuDO::getMenuKey, menuKey);
+    }
+
+    default List<MpMenuDO> selectListByAccountId(Long accountId) {
+        return selectList(MpMenuDO::getAccountId, accountId);
+    }
+
+    default void deleteByAccountId(Long accountId) {
+        delete(new LambdaQueryWrapperX<MpMenuDO>().eq(MpMenuDO::getAccountId, accountId));
+    }
+}

+ 70 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/message/MpAutoReplyMapper.java

@@ -0,0 +1,70 @@
+package cn.iocoder.yudao.module.mp.dal.mysql.message;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.module.mp.controller.admin.message.vo.message.MpMessagePageReqVO;
+import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpAutoReplyDO;
+import cn.iocoder.yudao.module.mp.enums.message.MpAutoReplyMatchEnum;
+import cn.iocoder.yudao.module.mp.enums.message.MpAutoReplyTypeEnum;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+@Mapper
+public interface MpAutoReplyMapper extends BaseMapperX<MpAutoReplyDO> {
+
+    default PageResult<MpAutoReplyDO> selectPage(MpMessagePageReqVO pageVO) {
+        return selectPage(pageVO, new LambdaQueryWrapperX<MpAutoReplyDO>()
+                .eq(MpAutoReplyDO::getAccountId, pageVO.getAccountId())
+                .eqIfPresent(MpAutoReplyDO::getType, pageVO.getType()));
+    }
+
+    default List<MpAutoReplyDO> selectListByAppIdAndKeywordAll(String appId, String requestKeyword) {
+        return selectList(new LambdaQueryWrapperX<MpAutoReplyDO>()
+                .eq(MpAutoReplyDO::getAppId, appId)
+                .eq(MpAutoReplyDO::getType, MpAutoReplyTypeEnum.KEYWORD.getType())
+                .eq(MpAutoReplyDO::getRequestMatch, MpAutoReplyMatchEnum.ALL.getMatch())
+                .eq(MpAutoReplyDO::getRequestKeyword, requestKeyword));
+    }
+
+    default List<MpAutoReplyDO> selectListByAppIdAndKeywordLike(String appId, String requestKeyword) {
+        return selectList(new LambdaQueryWrapperX<MpAutoReplyDO>()
+                .eq(MpAutoReplyDO::getAppId, appId)
+                .eq(MpAutoReplyDO::getType, MpAutoReplyTypeEnum.KEYWORD.getType())
+                .eq(MpAutoReplyDO::getRequestMatch, MpAutoReplyMatchEnum.LIKE.getMatch())
+                .like(MpAutoReplyDO::getRequestKeyword, requestKeyword));
+    }
+
+    default List<MpAutoReplyDO> selectListByAppIdAndMessage(String appId, String requestMessageType) {
+        return selectList(new LambdaQueryWrapperX<MpAutoReplyDO>()
+                .eq(MpAutoReplyDO::getAppId, appId)
+                .eq(MpAutoReplyDO::getType, MpAutoReplyTypeEnum.MESSAGE.getType())
+                .eq(MpAutoReplyDO::getRequestMessageType, requestMessageType));
+    }
+
+    default List<MpAutoReplyDO> selectListByAppIdAndSubscribe(String appId) {
+        return selectList(new LambdaQueryWrapperX<MpAutoReplyDO>()
+                .eq(MpAutoReplyDO::getAppId, appId)
+                .eq(MpAutoReplyDO::getType, MpAutoReplyTypeEnum.SUBSCRIBE.getType()));
+    }
+
+    default MpAutoReplyDO selectByAccountIdAndSubscribe(Long accountId) {
+        return selectOne(MpAutoReplyDO::getAccountId, accountId,
+                MpAutoReplyDO::getType, MpAutoReplyTypeEnum.SUBSCRIBE.getType());
+    }
+
+    default MpAutoReplyDO selectByAccountIdAndMessage(Long accountId, String requestMessageType) {
+        return selectOne(new LambdaQueryWrapperX<MpAutoReplyDO>()
+                .eq(MpAutoReplyDO::getAccountId, accountId)
+                .eq(MpAutoReplyDO::getType, MpAutoReplyTypeEnum.MESSAGE.getType())
+                .eq(MpAutoReplyDO::getRequestMessageType, requestMessageType));
+    }
+
+    default MpAutoReplyDO selectByAccountIdAndKeyword(Long accountId, String requestKeyword) {
+        return selectOne(new LambdaQueryWrapperX<MpAutoReplyDO>()
+                .eq(MpAutoReplyDO::getAccountId, accountId)
+                .eq(MpAutoReplyDO::getType, MpAutoReplyTypeEnum.KEYWORD.getType())
+                .eq(MpAutoReplyDO::getRequestKeyword, requestKeyword));
+    }
+}

+ 22 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/message/MpMessageMapper.java

@@ -0,0 +1,22 @@
+package cn.iocoder.yudao.module.mp.dal.mysql.message;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.module.mp.controller.admin.message.vo.message.MpMessagePageReqVO;
+import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface MpMessageMapper extends BaseMapperX<MpMessageDO> {
+
+    default PageResult<MpMessageDO> selectPage(MpMessagePageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<MpMessageDO>()
+                .eqIfPresent(MpMessageDO::getAccountId, reqVO.getAccountId())
+                .eqIfPresent(MpMessageDO::getType, reqVO.getType())
+                .eqIfPresent(MpMessageDO::getOpenid, reqVO.getOpenid())
+                .betweenIfPresent(MpMessageDO::getCreateTime, reqVO.getCreateTime())
+                .orderByDesc(MpMessageDO::getId));
+    }
+
+}

+ 31 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/tag/MpTagMapper.java

@@ -0,0 +1,31 @@
+package cn.iocoder.yudao.module.mp.dal.mysql.tag;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.module.mp.controller.admin.tag.vo.MpTagPageReqVO;
+import cn.iocoder.yudao.module.mp.dal.dataobject.tag.MpTagDO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+@Mapper
+public interface MpTagMapper extends BaseMapperX<MpTagDO> {
+
+    default PageResult<MpTagDO> selectPage(MpTagPageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<MpTagDO>()
+                .eqIfPresent(MpTagDO::getAccountId, reqVO.getAccountId())
+                .likeIfPresent(MpTagDO::getName, reqVO.getName())
+                .orderByDesc(MpTagDO::getId));
+    }
+
+    default List<MpTagDO> selectListByAccountId(Long accountId) {
+        return selectList(MpTagDO::getAccountId, accountId);
+    }
+
+    default MpTagDO selectByName(String name) {
+        return selectOne( new LambdaQueryWrapperX<MpTagDO>()
+                .eqIfPresent(MpTagDO::getName,name));
+
+    }
+}

+ 44 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/user/MpUserMapper.java

@@ -0,0 +1,44 @@
+package cn.iocoder.yudao.module.mp.dal.mysql.user;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.module.mp.controller.admin.user.vo.MpUserPageReqVO;
+import cn.iocoder.yudao.module.mp.dal.dataobject.user.MpUserDO;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+
+@Mapper
+public interface MpUserMapper extends BaseMapperX<MpUserDO> {
+
+    default PageResult<MpUserDO> selectPage(MpUserPageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<MpUserDO>()
+                .likeIfPresent(MpUserDO::getOpenid, reqVO.getOpenid())
+                .likeIfPresent(MpUserDO::getNickname, reqVO.getNickname())
+                .eqIfPresent(MpUserDO::getAccountId, reqVO.getAccountId())
+                .orderByDesc(MpUserDO::getId));
+    }
+
+    default MpUserDO selectByAppIdAndOpenid(String appId, String openid) {
+        return selectOne(MpUserDO::getAppId, appId,
+                MpUserDO::getOpenid, openid);
+    }
+
+    default List<MpUserDO> selectListByAppIdAndOpenid(String appId, List<String> openids) {
+        return selectList(new LambdaQueryWrapperX<MpUserDO>()
+                .eq(MpUserDO::getAppId, appId)
+                .in(MpUserDO::getOpenid, openids));
+
+    }
+
+    //通过标签号来找粉丝
+    @Select("SELECT * FROM mp_user WHERE tag_ids LIKE CONCAT('%', #{tagId}, '%')")
+    List<MpUserDO> selectUsersByTagId(@Param("tagId") String tagId);
+
+
+
+
+}

+ 54 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/mp/config/MpConfiguration.java

@@ -0,0 +1,54 @@
+package cn.iocoder.yudao.module.mp.framework.mp.config;
+
+import cn.iocoder.yudao.module.mp.framework.mp.core.DefaultMpServiceFactory;
+import cn.iocoder.yudao.module.mp.framework.mp.core.MpServiceFactory;
+import cn.iocoder.yudao.module.mp.service.handler.menu.MenuHandler;
+import cn.iocoder.yudao.module.mp.service.handler.message.MessageReceiveHandler;
+import cn.iocoder.yudao.module.mp.service.handler.message.MessageAutoReplyHandler;
+import cn.iocoder.yudao.module.mp.service.handler.other.KfSessionHandler;
+import cn.iocoder.yudao.module.mp.service.handler.other.NullHandler;
+import cn.iocoder.yudao.module.mp.service.handler.other.ScanHandler;
+import cn.iocoder.yudao.module.mp.service.handler.other.StoreCheckNotifyHandler;
+import cn.iocoder.yudao.module.mp.service.handler.user.LocationHandler;
+import cn.iocoder.yudao.module.mp.service.handler.user.SubscribeHandler;
+import cn.iocoder.yudao.module.mp.service.handler.user.UnsubscribeHandler;
+import com.binarywang.spring.starter.wxjava.mp.properties.WxMpProperties;
+import me.chanjar.weixin.common.redis.RedisTemplateWxRedisOps;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.core.StringRedisTemplate;
+
+/**
+ * 微信公众号的配置类
+ *
+ * @author 芋道源码
+ */
+@Configuration
+public class MpConfiguration {
+
+    @Bean
+    @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
+    public RedisTemplateWxRedisOps redisTemplateWxRedisOps(StringRedisTemplate stringRedisTemplate) {
+        return new RedisTemplateWxRedisOps(stringRedisTemplate);
+    }
+
+    @Bean
+    @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
+    public MpServiceFactory mpServiceFactory(RedisTemplateWxRedisOps redisTemplateWxRedisOps,
+                                             WxMpProperties wxMpProperties,
+                                             MessageReceiveHandler messageReceiveHandler,
+                                             KfSessionHandler kfSessionHandler,
+                                             StoreCheckNotifyHandler storeCheckNotifyHandler,
+                                             MenuHandler menuHandler,
+                                             NullHandler nullHandler,
+                                             SubscribeHandler subscribeHandler,
+                                             UnsubscribeHandler unsubscribeHandler,
+                                             LocationHandler locationHandler,
+                                             ScanHandler scanHandler,
+                                             MessageAutoReplyHandler messageAutoReplyHandler) {
+        return new DefaultMpServiceFactory(redisTemplateWxRedisOps, wxMpProperties,
+                messageReceiveHandler, kfSessionHandler, storeCheckNotifyHandler, menuHandler,
+                nullHandler, subscribeHandler, unsubscribeHandler, locationHandler, scanHandler, messageAutoReplyHandler);
+    }
+
+}

+ 177 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/mp/core/DefaultMpServiceFactory.java

@@ -0,0 +1,177 @@
+package cn.iocoder.yudao.module.mp.framework.mp.core;
+
+import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO;
+import cn.iocoder.yudao.module.mp.service.handler.menu.MenuHandler;
+import cn.iocoder.yudao.module.mp.service.handler.message.MessageReceiveHandler;
+import cn.iocoder.yudao.module.mp.service.handler.message.MessageAutoReplyHandler;
+import cn.iocoder.yudao.module.mp.service.handler.other.KfSessionHandler;
+import cn.iocoder.yudao.module.mp.service.handler.other.NullHandler;
+import cn.iocoder.yudao.module.mp.service.handler.other.ScanHandler;
+import cn.iocoder.yudao.module.mp.service.handler.other.StoreCheckNotifyHandler;
+import cn.iocoder.yudao.module.mp.service.handler.user.LocationHandler;
+import cn.iocoder.yudao.module.mp.service.handler.user.SubscribeHandler;
+import cn.iocoder.yudao.module.mp.service.handler.user.UnsubscribeHandler;
+import com.binarywang.spring.starter.wxjava.mp.properties.WxMpProperties;
+import com.google.common.collect.Maps;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import me.chanjar.weixin.common.api.WxConsts;
+import me.chanjar.weixin.common.redis.RedisTemplateWxRedisOps;
+import me.chanjar.weixin.mp.api.WxMpMessageRouter;
+import me.chanjar.weixin.mp.api.WxMpService;
+import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl;
+import me.chanjar.weixin.mp.config.impl.WxMpRedisConfigImpl;
+import me.chanjar.weixin.mp.constant.WxMpEventConstants;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 默认的 {@link MpServiceFactory} 实现类
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+@RequiredArgsConstructor
+public class DefaultMpServiceFactory implements MpServiceFactory {
+
+    /**
+     * 微信 appId 与 WxMpService 的映射
+     */
+    private volatile Map<String, WxMpService> appId2MpServices;
+    /**
+     * 公众号账号 id 与 WxMpService 的映射
+     */
+    private volatile Map<Long, WxMpService> id2MpServices;
+    /**
+     * 微信 appId 与 WxMpMessageRouter 的映射
+     */
+    private volatile Map<String, WxMpMessageRouter> mpMessageRouters;
+
+    private final RedisTemplateWxRedisOps redisTemplateWxRedisOps;
+    private final WxMpProperties mpProperties;
+
+    // ========== 各种 Handler ==========
+
+    private final MessageReceiveHandler messageReceiveHandler;
+    private final KfSessionHandler kfSessionHandler;
+    private final StoreCheckNotifyHandler storeCheckNotifyHandler;
+    private final MenuHandler menuHandler;
+    private final NullHandler nullHandler;
+    private final SubscribeHandler subscribeHandler;
+    private final UnsubscribeHandler unsubscribeHandler;
+    private final LocationHandler locationHandler;
+    private final ScanHandler scanHandler;
+    private final MessageAutoReplyHandler messageAutoReplyHandler;
+
+    @Override
+    public void init(List<MpAccountDO> list) {
+        Map<String, WxMpService> appId2MpServices = Maps.newHashMap();
+        Map<Long, WxMpService> id2MpServices = Maps.newHashMap();
+        Map<String, WxMpMessageRouter> mpMessageRouters = Maps.newHashMap();
+        // 处理 list
+        list.forEach(account -> {
+            // 构建 WxMpService 对象
+            WxMpService mpService = buildMpService(account);
+            appId2MpServices.put(account.getAppId(), mpService);
+            id2MpServices.put(account.getId(), mpService);
+            // 构建 WxMpMessageRouter 对象
+            WxMpMessageRouter mpMessageRouter = buildMpMessageRouter(mpService);
+            mpMessageRouters.put(account.getAppId(), mpMessageRouter);
+        });
+
+        // 设置到缓存
+        this.appId2MpServices = appId2MpServices;
+        this.id2MpServices = id2MpServices;
+        this.mpMessageRouters = mpMessageRouters;
+    }
+
+    @Override
+    public WxMpService getMpService(Long id) {
+        return id2MpServices.get(id);
+    }
+
+    @Override
+    public WxMpService getMpService(String appId) {
+        return appId2MpServices.get(appId);
+    }
+
+    @Override
+    public WxMpMessageRouter getMpMessageRouter(String appId) {
+        return mpMessageRouters.get(appId);
+    }
+
+    private WxMpService buildMpService(MpAccountDO account) {
+        // 第一步,创建 WxMpRedisConfigImpl 对象
+        WxMpRedisConfigImpl configStorage = new WxMpRedisConfigImpl(
+                redisTemplateWxRedisOps, mpProperties.getConfigStorage().getKeyPrefix());
+        configStorage.setAppId(account.getAppId());
+        configStorage.setSecret(account.getAppSecret());
+        configStorage.setToken(account.getToken());
+        configStorage.setAesKey(account.getAesKey());
+
+        // 第二步,创建 WxMpService 对象
+        WxMpService service = new WxMpServiceImpl();
+        service.setWxMpConfigStorage(configStorage);
+        return service;
+    }
+
+    private WxMpMessageRouter buildMpMessageRouter(WxMpService mpService) {
+        WxMpMessageRouter router = new WxMpMessageRouter(mpService);
+        // 记录所有事件的日志(异步执行)
+        router.rule().handler(messageReceiveHandler).next();
+
+        // 接收客服会话管理事件
+        router.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT)
+                .event(WxMpEventConstants.CustomerService.KF_CREATE_SESSION)
+                .handler(kfSessionHandler).end();
+        router.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT)
+                .event(WxMpEventConstants.CustomerService.KF_CLOSE_SESSION)
+                .handler(kfSessionHandler)
+                .end();
+        router.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT)
+                .event(WxMpEventConstants.CustomerService.KF_SWITCH_SESSION)
+                .handler(kfSessionHandler).end();
+
+        // 门店审核事件
+        router.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT)
+                .event(WxMpEventConstants.POI_CHECK_NOTIFY)
+                .handler(storeCheckNotifyHandler).end();
+
+        // 自定义菜单事件
+        router.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT)
+                .event(WxConsts.MenuButtonType.CLICK).handler(menuHandler).end();
+
+        // 点击菜单连接事件
+        router.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT)
+                .event(WxConsts.MenuButtonType.VIEW).handler(nullHandler).end();
+
+        // 关注事件
+        router.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT)
+                .event(WxConsts.EventType.SUBSCRIBE).handler(subscribeHandler)
+                .end();
+
+        // 取消关注事件
+        router.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT)
+                .event(WxConsts.EventType.UNSUBSCRIBE)
+                .handler(unsubscribeHandler).end();
+
+        // 上报地理位置事件
+        router.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT)
+                .event(WxConsts.EventType.LOCATION).handler(locationHandler)
+                .end();
+
+        // 接收地理位置消息
+        router.rule().async(false).msgType(WxConsts.XmlMsgType.LOCATION)
+                .handler(locationHandler).end();
+
+        // 扫码事件
+        router.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT)
+                .event(WxConsts.EventType.SCAN).handler(scanHandler).end();
+
+        // 默认
+        router.rule().async(false).handler(messageAutoReplyHandler).end();
+        return router;
+    }
+
+}

+ 66 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/mp/core/MpServiceFactory.java

@@ -0,0 +1,66 @@
+package cn.iocoder.yudao.module.mp.framework.mp.core;
+
+import cn.hutool.core.lang.Assert;
+import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO;
+import me.chanjar.weixin.mp.api.WxMpMessageRouter;
+import me.chanjar.weixin.mp.api.WxMpService;
+
+import java.util.List;
+
+/**
+ * {@link WxMpService} 工厂接口
+ *
+ * @author 芋道源码
+ */
+public interface MpServiceFactory {
+
+    /**
+     * 基于微信公众号的账号,初始化对应的 WxMpService 与 WxMpMessageRouter 实例
+     *
+     * @param list 公众号的账号列表
+     */
+    void init(List<MpAccountDO> list);
+
+    /**
+     * 获得 id 对应的 WxMpService 实例
+     *
+     * @param id 微信公众号的编号
+     * @return WxMpService 实例
+     */
+    WxMpService getMpService(Long id);
+
+    default WxMpService getRequiredMpService(Long id) {
+        WxMpService wxMpService = getMpService(id);
+        Assert.notNull(wxMpService, "找到对应 id({}) 的 WxMpService,请核实!", id);
+        return wxMpService;
+    }
+
+    /**
+     * 获得 appId 对应的 WxMpService 实例
+     *
+     * @param appId 微信公众号 appId
+     * @return WxMpService 实例
+     */
+    WxMpService getMpService(String appId);
+
+    default WxMpService getRequiredMpService(String appId) {
+        WxMpService wxMpService = getMpService(appId);
+        Assert.notNull(wxMpService, "找到对应 appId({}) 的 WxMpService,请核实!", appId);
+        return wxMpService;
+    }
+
+    /**
+     * 获得 appId 对应的 WxMpMessageRouter 实例
+     *
+     * @param appId 微信公众号 appId
+     * @return WxMpMessageRouter 实例
+     */
+    WxMpMessageRouter getMpMessageRouter(String appId);
+
+    default WxMpMessageRouter getRequiredMpMessageRouter(String appId) {
+        WxMpMessageRouter wxMpMessageRouter = getMpMessageRouter(appId);
+        Assert.notNull(wxMpMessageRouter, "找到对应 appId({}) 的 WxMpMessageRouter,请核实!", appId);
+        return wxMpMessageRouter;
+    }
+
+}

+ 53 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/mp/core/context/MpContextHolder.java

@@ -0,0 +1,53 @@
+/*
+ *    Copyright (c) 2018-2025, lengleng All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * Neither the name of the pig4cloud.com developer nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ * Author: lengleng (wangiegie@gmail.com)
+ */
+
+package cn.iocoder.yudao.module.mp.framework.mp.core.context;
+
+import cn.iocoder.yudao.module.mp.controller.admin.open.vo.MpOpenHandleMessageReqVO;
+import com.alibaba.ttl.TransmittableThreadLocal;
+import lombok.experimental.UtilityClass;
+import me.chanjar.weixin.mp.api.WxMpMessageHandler;
+
+/**
+ * 微信上下文 Context
+ *
+ * 目的:解决微信多公众号的问题,在 {@link WxMpMessageHandler} 实现类中,可以通过 {@link #getAppId()} 获取到当前的 appId
+ *
+ * @see cn.iocoder.yudao.module.mp.controller.admin.open.MpOpenController#handleMessage(String, String, MpOpenHandleMessageReqVO)
+ *
+ * @author 芋道源码
+ */
+public class MpContextHolder {
+
+    /**
+     * 微信公众号的 appId 上下文
+     */
+	private static final ThreadLocal<String> APPID = new TransmittableThreadLocal<>();
+
+	public static void setAppId(String appId) {
+		APPID.set(appId);
+	}
+
+	public static String getAppId() {
+		return APPID.get();
+	}
+
+	public static void clear() {
+		APPID.remove();
+	}
+
+}

+ 167 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/mp/core/util/MpUtils.java

@@ -0,0 +1,167 @@
+package cn.iocoder.yudao.module.mp.framework.mp.core.util;
+
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
+import lombok.extern.slf4j.Slf4j;
+import me.chanjar.weixin.common.api.WxConsts;
+
+import javax.validation.Validator;
+
+/**
+ * 公众号工具类
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+public class MpUtils {
+
+    /**
+     * 校验消息的格式是否符合要求
+     *
+     * @param type 类型
+     * @param message 消息
+     */
+    public static void validateMessage(Validator validator, String type, Object message) {
+        // 获得对应的校验 group
+        Class<?> group;
+        switch (type) {
+            case WxConsts.XmlMsgType.TEXT:
+                group = TextMessageGroup.class;
+                break;
+            case WxConsts.XmlMsgType.IMAGE:
+                group = ImageMessageGroup.class;
+                break;
+            case WxConsts.XmlMsgType.VOICE:
+                group = VoiceMessageGroup.class;
+                break;
+            case WxConsts.XmlMsgType.VIDEO:
+                group = VideoMessageGroup.class;
+                break;
+            case WxConsts.XmlMsgType.NEWS:
+                group = NewsMessageGroup.class;
+                break;
+            case WxConsts.XmlMsgType.MUSIC:
+                group = MusicMessageGroup.class;
+                break;
+            default:
+                log.error("[validateMessage][未知的消息类型({})]", message);
+                throw new IllegalArgumentException("不支持的消息类型:" + type);
+        }
+        // 执行校验
+        ValidationUtils.validate(validator, message, group);
+    }
+
+    public static void validateButton(Validator validator, String type, String messageType, Object button) {
+        if (StrUtil.isBlank(type)) {
+            return;
+        }
+        // 获得对应的校验 group
+        Class<?> group;
+        switch (type) {
+            case WxConsts.MenuButtonType.CLICK:
+                group = ClickButtonGroup.class;
+                validateMessage(validator, messageType, button); // 需要额外校验回复的消息格式
+                break;
+            case WxConsts.MenuButtonType.VIEW:
+                group = ViewButtonGroup.class;
+                break;
+            case WxConsts.MenuButtonType.MINIPROGRAM:
+                group = MiniProgramButtonGroup.class;
+                break;
+            case WxConsts.MenuButtonType.SCANCODE_WAITMSG:
+                group = ScanCodeWaitMsgButtonGroup.class;
+                validateMessage(validator, messageType, button); // 需要额外校验回复的消息格式
+                break;
+            case "article_" + WxConsts.MenuButtonType.VIEW_LIMITED:
+                group = ViewLimitedButtonGroup.class;
+                break;
+            case WxConsts.MenuButtonType.SCANCODE_PUSH: // 不用校验,直接 return 即可
+            case WxConsts.MenuButtonType.PIC_SYSPHOTO:
+            case WxConsts.MenuButtonType.PIC_PHOTO_OR_ALBUM:
+            case WxConsts.MenuButtonType.PIC_WEIXIN:
+            case WxConsts.MenuButtonType.LOCATION_SELECT:
+                return;
+            default:
+                log.error("[validateButton][未知的按钮({})]", button);
+                throw new IllegalArgumentException("不支持的按钮类型:" + type);
+        }
+        // 执行校验
+        ValidationUtils.validate(validator, button, group);
+    }
+
+    /**
+     * 根据消息类型,获得对应的媒体文件类型
+     *
+     * 注意,不会返回 WxConsts.MediaFileType.THUMB,因为该类型会有明确标注
+     *
+     * @param messageType 消息类型 {@link  WxConsts.XmlMsgType}
+     * @return 媒体文件类型 {@link WxConsts.MediaFileType}
+     */
+    public static String getMediaFileType(String messageType) {
+        switch (messageType) {
+            case WxConsts.XmlMsgType.IMAGE:
+                return WxConsts.MediaFileType.IMAGE;
+            case WxConsts.XmlMsgType.VOICE:
+                return WxConsts.MediaFileType.VOICE;
+            case WxConsts.XmlMsgType.VIDEO:
+                return WxConsts.MediaFileType.VIDEO;
+            default:
+                return WxConsts.MediaFileType.FILE;
+        }
+    }
+
+    /**
+     * Text 类型的消息,参数校验 Group
+     */
+    public interface TextMessageGroup {}
+
+    /**
+     * Image 类型的消息,参数校验 Group
+     */
+    public interface ImageMessageGroup {}
+
+    /**
+     * Voice 类型的消息,参数校验 Group
+     */
+    public interface VoiceMessageGroup {}
+
+    /**
+     * Video 类型的消息,参数校验 Group
+     */
+    public interface VideoMessageGroup {}
+
+    /**
+     * News 类型的消息,参数校验 Group
+     */
+    public interface NewsMessageGroup {}
+
+    /**
+     * Music 类型的消息,参数校验 Group
+     */
+    public interface MusicMessageGroup {}
+
+    /**
+     * Click 类型的按钮,参数校验 Group
+     */
+    public interface ClickButtonGroup {}
+
+    /**
+     * View 类型的按钮,参数校验 Group
+     */
+    public interface ViewButtonGroup {}
+
+    /**
+     * MiniProgram 类型的按钮,参数校验 Group
+     */
+    public interface MiniProgramButtonGroup {}
+
+    /**
+     * SCANCODE_WAITMSG 类型的按钮,参数校验 Group
+     */
+    public interface ScanCodeWaitMsgButtonGroup {}
+
+    /**
+     * VIEW_LIMITED 类型的按钮,参数校验 Group
+     */
+    public interface ViewLimitedButtonGroup {}
+}

Some files were not shown because too many files changed in this diff