From 9a18483482630207b6c61d90234bd96b126ee414 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Thu, 27 Jul 2023 19:55:34 +0800 Subject: [PATCH] =?UTF-8?q?2.=20=E5=A2=9E=E5=8A=A0=20mp=20=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 3 +- .../excel/core/convert/MoneyConvert.java | 39 +++ .../src/main/resources/application.yaml | 23 +- .../task/BpmProcessInstanceServiceImpl.java | 308 +----------------- yudao-module-mp/pom.xml | 24 ++ yudao-module-mp/yudao-module-mp-api/pom.xml | 26 ++ .../module/mp/enums/ErrorCodeConstants.java | 64 ++++ .../enums/message/MpAutoReplyMatchEnum.java | 28 ++ .../mp/enums/message/MpAutoReplyTypeEnum.java | 29 ++ .../enums/message/MpMessageSendFromEnum.java | 28 ++ .../iocoder/yudao/module/mp/package-info.java | 8 + yudao-module-mp/yudao-module-mp-biz/pom.xml | 119 +++++++ .../admin/account/MpAccountController.java | 98 ++++++ .../admin/account/vo/MpAccountBaseVO.java | 43 +++ .../account/vo/MpAccountCreateReqVO.java | 14 + .../admin/account/vo/MpAccountPageReqVO.java | 24 ++ .../admin/account/vo/MpAccountRespVO.java | 25 ++ .../account/vo/MpAccountSimpleRespVO.java | 16 + .../account/vo/MpAccountUpdateReqVO.java | 20 ++ .../admin/material/MpMaterialController.http | 5 + .../admin/material/MpMaterialController.java | 74 +++++ .../material/vo/MpMaterialPageReqVO.java | 27 ++ .../admin/material/vo/MpMaterialRespVO.java | 47 +++ .../vo/MpMaterialUploadNewsImageReqVO.java | 23 ++ .../vo/MpMaterialUploadPermanentReqVO.java | 53 +++ .../material/vo/MpMaterialUploadRespVO.java | 16 + .../vo/MpMaterialUploadTemporaryReqVO.java | 28 ++ .../admin/menu/MpMenuController.http | 50 +++ .../admin/menu/MpMenuController.java | 57 ++++ .../admin/menu/vo/MpMenuBaseVO.java | 115 +++++++ .../admin/menu/vo/MpMenuRespVO.java | 28 ++ .../admin/menu/vo/MpMenuSaveReqVO.java | 34 ++ .../admin/message/MpAutoReplyController.http | 5 + .../admin/message/MpAutoReplyController.java | 74 +++++ .../admin/message/MpMessageController.http | 33 ++ .../admin/message/MpMessageController.java | 47 +++ .../vo/autoreply/MpAutoReplyBaseVO.java | 109 +++++++ .../vo/autoreply/MpAutoReplyCreateReqVO.java | 20 ++ .../vo/autoreply/MpAutoReplyPageReqVO.java | 21 ++ .../vo/autoreply/MpAutoReplyRespVO.java | 27 ++ .../vo/autoreply/MpAutoReplyUpdateReqVO.java | 20 ++ .../vo/message/MpMessagePageReqVO.java | 35 ++ .../message/vo/message/MpMessageRespVO.java | 101 ++++++ .../vo/message/MpMessageSendReqVO.java | 58 ++++ .../admin/news/MpDraftController.http | 54 +++ .../admin/news/MpDraftController.java | 136 ++++++++ .../admin/news/MpFreePublishController.http | 13 + .../admin/news/MpFreePublishController.java | 119 +++++++ .../admin/news/vo/MpDraftPageReqVO.java | 21 ++ .../admin/news/vo/MpFreePublishPageReqVO.java | 21 ++ .../admin/open/MpOpenController.java | 116 +++++++ .../open/vo/MpOpenCheckSignatureReqVO.java | 29 ++ .../open/vo/MpOpenHandleMessageReqVO.java | 37 +++ .../statistics/MpStatisticsController.java | 68 ++++ .../statistics/vo/MpStatisticsGetReqVO.java | 25 ++ .../MpStatisticsInterfaceSummaryRespVO.java | 27 ++ .../vo/MpStatisticsUpstreamMessageRespVO.java | 21 ++ .../vo/MpStatisticsUserCumulateRespVO.java | 18 + .../vo/MpStatisticsUserSummaryRespVO.java | 24 ++ .../controller/admin/tag/MpTagController.http | 39 +++ .../controller/admin/tag/MpTagController.java | 88 +++++ .../controller/admin/tag/vo/MpTagBaseVO.java | 21 ++ .../admin/tag/vo/MpTagCreateReqVO.java | 20 ++ .../admin/tag/vo/MpTagPageReqVO.java | 24 ++ .../controller/admin/tag/vo/MpTagRespVO.java | 25 ++ .../admin/tag/vo/MpTagSimpleRespVO.java | 19 ++ .../admin/tag/vo/MpTagUpdateReqVO.java | 20 ++ .../admin/user/MpUserController.http | 18 + .../admin/user/MpUserController.java | 65 ++++ .../admin/user/vo/MpUserPageReqVO.java | 27 ++ .../admin/user/vo/MpUserRespVO.java | 53 +++ .../admin/user/vo/MpUserUpdateReqVO.java | 26 ++ .../module/mp/controller/package-info.java | 6 + .../mp/convert/account/MpAccountConvert.java | 31 ++ .../convert/material/MpMaterialConvert.java | 47 +++ .../module/mp/convert/menu/MpMenuConvert.java | 48 +++ .../convert/message/MpAutoReplyConvert.java | 37 +++ .../mp/convert/message/MpMessageConvert.java | 172 ++++++++++ .../statistics/MpStatisticsConvert.java | 40 +++ .../module/mp/convert/tag/MpTagConvert.java | 44 +++ .../module/mp/convert/user/MpUserConvert.java | 55 ++++ .../dal/dataobject/account/MpAccountDO.java | 62 ++++ .../dal/dataobject/material/MpMaterialDO.java | 99 ++++++ .../mp/dal/dataobject/menu/MpMenuDO.java | 184 +++++++++++ .../dal/dataobject/message/MpAutoReplyDO.java | 164 ++++++++++ .../dal/dataobject/message/MpMessageDO.java | 255 +++++++++++++++ .../module/mp/dal/dataobject/tag/MpTagDO.java | 58 ++++ .../mp/dal/dataobject/user/MpUserDO.java | 110 +++++++ .../mp/dal/mysql/account/MpAccountMapper.java | 31 ++ .../dal/mysql/material/MpMaterialMapper.java | 33 ++ .../mp/dal/mysql/menu/MpMenuMapper.java | 25 ++ .../dal/mysql/message/MpAutoReplyMapper.java | 70 ++++ .../mp/dal/mysql/message/MpMessageMapper.java | 22 ++ .../module/mp/dal/mysql/tag/MpTagMapper.java | 26 ++ .../mp/dal/mysql/user/MpUserMapper.java | 35 ++ .../framework/mp/config/MpConfiguration.java | 54 +++ .../mp/core/DefaultMpServiceFactory.java | 177 ++++++++++ .../framework/mp/core/MpServiceFactory.java | 66 ++++ .../mp/core/context/MpContextHolder.java | 53 +++ .../mp/framework/mp/core/util/MpUtils.java | 167 ++++++++++ .../module/mp/framework/package-info.java | 6 + .../web/config/MpWebConfiguration.java | 24 ++ .../module/mp/framework/web/package-info.java | 4 + .../iocoder/yudao/module/mp/package-info.java | 8 + .../mp/service/account/MpAccountService.java | 110 +++++++ .../service/account/MpAccountServiceImpl.java | 220 +++++++++++++ .../mp/service/handler/menu/MenuHandler.java | 37 +++ .../message/MessageAutoReplyHandler.java | 41 +++ .../message/MessageReceiveHandler.java | 36 ++ .../handler/other/KfSessionHandler.java | 26 ++ .../mp/service/handler/other/NullHandler.java | 24 ++ .../mp/service/handler/other/ScanHandler.java | 25 ++ .../other/StoreCheckNotifyHandler.java | 24 ++ .../service/handler/other/package-info.java | 4 + .../service/handler/user/LocationHandler.java | 49 +++ .../handler/user/SubscribeHandler.java | 52 +++ .../handler/user/UnsubscribeHandler.java | 39 +++ .../service/material/MpMaterialService.java | 84 +++++ .../material/MpMaterialServiceImpl.java | 224 +++++++++++++ .../module/mp/service/menu/MpMenuService.java | 49 +++ .../mp/service/menu/MpMenuServiceImpl.java | 171 ++++++++++ .../service/message/MpAutoReplyService.java | 75 +++++ .../message/MpAutoReplyServiceImpl.java | 202 ++++++++++++ .../mp/service/message/MpMessageService.java | 58 ++++ .../service/message/MpMessageServiceImpl.java | 148 +++++++++ .../message/bo/MpMessageSendOutReqBO.java | 110 +++++++ .../statistics/MpStatisticsService.java | 54 +++ .../statistics/MpStatisticsServiceImpl.java | 77 +++++ .../module/mp/service/tag/MpTagService.java | 65 ++++ .../mp/service/tag/MpTagServiceImpl.java | 164 ++++++++++ .../module/mp/service/user/MpUserService.java | 102 ++++++ .../mp/service/user/MpUserServiceImpl.java | 215 ++++++++++++ 132 files changed, 7607 insertions(+), 311 deletions(-) create mode 100644 yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/MoneyConvert.java create mode 100644 yudao-module-mp/pom.xml create mode 100644 yudao-module-mp/yudao-module-mp-api/pom.xml create mode 100644 yudao-module-mp/yudao-module-mp-api/src/main/java/cn/iocoder/yudao/module/mp/enums/ErrorCodeConstants.java create mode 100644 yudao-module-mp/yudao-module-mp-api/src/main/java/cn/iocoder/yudao/module/mp/enums/message/MpAutoReplyMatchEnum.java create mode 100644 yudao-module-mp/yudao-module-mp-api/src/main/java/cn/iocoder/yudao/module/mp/enums/message/MpAutoReplyTypeEnum.java create mode 100644 yudao-module-mp/yudao-module-mp-api/src/main/java/cn/iocoder/yudao/module/mp/enums/message/MpMessageSendFromEnum.java create mode 100644 yudao-module-mp/yudao-module-mp-api/src/main/java/cn/iocoder/yudao/module/mp/package-info.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/pom.xml create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/MpAccountController.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountBaseVO.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountCreateReqVO.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountPageReqVO.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountRespVO.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountSimpleRespVO.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountUpdateReqVO.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/MpMaterialController.http create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/MpMaterialController.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/vo/MpMaterialPageReqVO.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/vo/MpMaterialRespVO.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/vo/MpMaterialUploadNewsImageReqVO.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/vo/MpMaterialUploadPermanentReqVO.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/vo/MpMaterialUploadRespVO.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/vo/MpMaterialUploadTemporaryReqVO.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/menu/MpMenuController.http create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/menu/MpMenuController.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/menu/vo/MpMenuBaseVO.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/menu/vo/MpMenuRespVO.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/menu/vo/MpMenuSaveReqVO.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/MpAutoReplyController.http create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/MpAutoReplyController.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/MpMessageController.http create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/MpMessageController.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyBaseVO.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyCreateReqVO.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyPageReqVO.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyRespVO.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyUpdateReqVO.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/vo/message/MpMessagePageReqVO.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/vo/message/MpMessageRespVO.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/vo/message/MpMessageSendReqVO.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/news/MpDraftController.http create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/news/MpDraftController.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/news/MpFreePublishController.http create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/news/MpFreePublishController.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/news/vo/MpDraftPageReqVO.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/news/vo/MpFreePublishPageReqVO.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/open/MpOpenController.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/open/vo/MpOpenCheckSignatureReqVO.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/open/vo/MpOpenHandleMessageReqVO.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/statistics/MpStatisticsController.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/statistics/vo/MpStatisticsGetReqVO.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/statistics/vo/MpStatisticsInterfaceSummaryRespVO.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/statistics/vo/MpStatisticsUpstreamMessageRespVO.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/statistics/vo/MpStatisticsUserCumulateRespVO.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/statistics/vo/MpStatisticsUserSummaryRespVO.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/tag/MpTagController.http create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/tag/MpTagController.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/tag/vo/MpTagBaseVO.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/tag/vo/MpTagCreateReqVO.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/tag/vo/MpTagPageReqVO.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/tag/vo/MpTagRespVO.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/tag/vo/MpTagSimpleRespVO.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/tag/vo/MpTagUpdateReqVO.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/user/MpUserController.http create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/user/MpUserController.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/user/vo/MpUserPageReqVO.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/user/vo/MpUserRespVO.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/user/vo/MpUserUpdateReqVO.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/package-info.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/account/MpAccountConvert.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/material/MpMaterialConvert.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/menu/MpMenuConvert.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/message/MpAutoReplyConvert.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/message/MpMessageConvert.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/statistics/MpStatisticsConvert.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/tag/MpTagConvert.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/user/MpUserConvert.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/account/MpAccountDO.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/material/MpMaterialDO.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/menu/MpMenuDO.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/message/MpAutoReplyDO.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/message/MpMessageDO.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/tag/MpTagDO.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/user/MpUserDO.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/account/MpAccountMapper.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/material/MpMaterialMapper.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/menu/MpMenuMapper.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/message/MpAutoReplyMapper.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/message/MpMessageMapper.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/tag/MpTagMapper.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/user/MpUserMapper.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/mp/config/MpConfiguration.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/mp/core/DefaultMpServiceFactory.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/mp/core/MpServiceFactory.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/mp/core/context/MpContextHolder.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/mp/core/util/MpUtils.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/package-info.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/web/config/MpWebConfiguration.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/web/package-info.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/package-info.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/account/MpAccountService.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/account/MpAccountServiceImpl.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/menu/MenuHandler.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/message/MessageAutoReplyHandler.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/message/MessageReceiveHandler.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/other/KfSessionHandler.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/other/NullHandler.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/other/ScanHandler.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/other/StoreCheckNotifyHandler.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/other/package-info.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/user/LocationHandler.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/user/SubscribeHandler.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/user/UnsubscribeHandler.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/material/MpMaterialService.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/material/MpMaterialServiceImpl.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/menu/MpMenuService.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/menu/MpMenuServiceImpl.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/message/MpAutoReplyService.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/message/MpAutoReplyServiceImpl.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/message/MpMessageService.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/message/MpMessageServiceImpl.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/message/bo/MpMessageSendOutReqBO.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/statistics/MpStatisticsService.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/statistics/MpStatisticsServiceImpl.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/tag/MpTagService.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/tag/MpTagServiceImpl.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/user/MpUserService.java create mode 100644 yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/user/MpUserServiceImpl.java diff --git a/pom.xml b/pom.xml index c88e46ed6..b602f6a0e 100644 --- a/pom.xml +++ b/pom.xml @@ -16,8 +16,9 @@ yudao-module-bpm yudao-module-system yudao-module-infra - + yudao-module-pay yudao-module-report + yudao-module-mp ${project.artifactId} diff --git a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/MoneyConvert.java b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/MoneyConvert.java new file mode 100644 index 000000000..ee66fe7de --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/MoneyConvert.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.framework.excel.core.convert; + +import com.alibaba.excel.converters.Converter; +import com.alibaba.excel.enums.CellDataTypeEnum; +import com.alibaba.excel.metadata.GlobalConfiguration; +import com.alibaba.excel.metadata.data.WriteCellData; +import com.alibaba.excel.metadata.property.ExcelContentProperty; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +/** + * 金额转换器 + * + * 金额单位:分 + * + * @author 芋道源码 + */ +public class MoneyConvert implements Converter { + + @Override + public Class supportJavaTypeKey() { + throw new UnsupportedOperationException("暂不支持,也不需要"); + } + + @Override + public CellDataTypeEnum supportExcelTypeKey() { + throw new UnsupportedOperationException("暂不支持,也不需要"); + } + + @Override + public WriteCellData convertToExcelData(Integer value, ExcelContentProperty contentProperty, + GlobalConfiguration globalConfiguration) { + BigDecimal result = BigDecimal.valueOf(value) + .divide(new BigDecimal(100), 2, RoundingMode.HALF_UP); + return new WriteCellData<>(result.toString()); + } + +} diff --git a/yudao-gateway/src/main/resources/application.yaml b/yudao-gateway/src/main/resources/application.yaml index 7e9f626e5..6ea12a77d 100644 --- a/yudao-gateway/src/main/resources/application.yaml +++ b/yudao-gateway/src/main/resources/application.yaml @@ -7,6 +7,7 @@ spring: gateway: # 路由配置项,对应 RouteDefinition 数组 routes: + ## system-server 服务 - id: system-admin-api # 路由的编号 uri: grayLb://system-server predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组 @@ -19,6 +20,7 @@ spring: - Path=/app-api/system/** filters: - RewritePath=/app-api/system/v3/api-docs, /v3/api-docs + ## infra-server 服务 - id: infra-admin-api # 路由的编号 uri: grayLb://infra-server predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组 @@ -35,22 +37,34 @@ spring: uri: grayLb://infra-server predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组 - Path=/admin/** + ## bpm-server 服务 - id: bpm-admin-api # 路由的编号 uri: grayLb://bpm-server predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组 - Path=/admin-api/bpm/** filters: - RewritePath=/admin-api/bpm/v3/api-docs, /v3/api-docs + ## report-server 服务 - id: report-admin-api # 路由的编号 uri: grayLb://report-server predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组 - Path=/admin-api/report/** filters: - RewritePath=/admin-api/report/v3/api-docs, /v3/api-docs - - id: report-jmreport # 路由的编号(积木报表) - uri: grayLb://report-server + ## pay-server 服务 + - id: pay-admin-api # 路由的编号 + uri: grayLb://pay-server predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组 - - Path=/jmreport/** + - Path=/admin-api/pay/** + filters: + - RewritePath=/admin-api/pay/v3/api-docs, /v3/api-docs # 配置,保证转发到 /v3/api-docs + - id: pay-app-api # 路由的编号 + uri: grayLb://pay-server + predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组 + - Path=/app-api/pay/** + filters: + - RewritePath=/app-api/pay/v3/api-docs, /v3/api-docs + x-forwarded: prefix-enabled: false # 避免 Swagger 重复带上额外的 /admin-api/system 前缀 @@ -68,3 +82,6 @@ knife4j: - name: bpm-server service-name: bpm-server url: /admin-api/bpm/v3/api-docs + - name: pay-server + service-name: pay-server + url: /admin-api/pay/v3/api-docs diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java index b8cdf7209..e36dd9628 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java @@ -1,307 +1 @@ -package cn.iocoder.yudao.module.bpm.service.task; - -import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.lang.Assert; -import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.framework.common.util.number.NumberUtils; -import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO; -import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.*; -import cn.iocoder.yudao.module.bpm.convert.task.BpmProcessInstanceConvert; -import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionExtDO; -import cn.iocoder.yudao.module.bpm.dal.dataobject.task.BpmProcessInstanceExtDO; -import cn.iocoder.yudao.module.bpm.dal.mysql.task.BpmProcessInstanceExtMapper; -import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceDeleteReasonEnum; -import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceResultEnum; -import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum; -import cn.iocoder.yudao.module.bpm.framework.bpm.core.event.BpmProcessInstanceResultEventPublisher; -import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService; -import cn.iocoder.yudao.module.bpm.service.message.BpmMessageService; -import cn.iocoder.yudao.module.system.api.dept.DeptApi; -import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; -import cn.iocoder.yudao.module.system.api.user.AdminUserApi; -import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; -import lombok.extern.slf4j.Slf4j; -import org.flowable.engine.HistoryService; -import org.flowable.engine.RuntimeService; -import org.flowable.engine.delegate.event.FlowableCancelledEvent; -import org.flowable.engine.history.HistoricProcessInstance; -import org.flowable.engine.repository.ProcessDefinition; -import org.flowable.engine.runtime.ProcessInstance; -import org.flowable.task.api.Task; -import org.springframework.context.annotation.Lazy; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.validation.annotation.Validated; - -import javax.annotation.Resource; -import javax.validation.Valid; -import java.time.LocalDateTime; -import java.util.*; - -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; -import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*; - -/** - * 流程实例 Service 实现类 - * - * ProcessDefinition & ProcessInstance & Execution & Task 的关系: - * 1. - * - * HistoricProcessInstance & ProcessInstance 的关系: - * 1. - * - * 简单来说,前者 = 历史 + 运行中的流程实例,后者仅是运行中的流程实例 - * - * @author 芋道源码 - */ -@Service -@Validated -@Slf4j -public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService { - - @Resource - private RuntimeService runtimeService; - @Resource - private BpmProcessInstanceExtMapper processInstanceExtMapper; - @Resource - @Lazy // 解决循环依赖 - private BpmTaskService taskService; - @Resource - private BpmProcessDefinitionService processDefinitionService; - @Resource - private HistoryService historyService; - @Resource - private AdminUserApi adminUserApi; - @Resource - private DeptApi deptApi; - @Resource - private BpmProcessInstanceResultEventPublisher processInstanceResultEventPublisher; - @Resource - private BpmMessageService messageService; - @Override - public ProcessInstance getProcessInstance(String id) { - return runtimeService.createProcessInstanceQuery().processInstanceId(id).singleResult(); - } - - @Override - public List getProcessInstances(Set ids) { - return runtimeService.createProcessInstanceQuery().processInstanceIds(ids).list(); - } - - @Override - public PageResult getMyProcessInstancePage(Long userId, - BpmProcessInstanceMyPageReqVO pageReqVO) { - // 通过 BpmProcessInstanceExtDO 表,先查询到对应的分页 - PageResult pageResult = processInstanceExtMapper.selectPage(userId, pageReqVO); - if (CollUtil.isEmpty(pageResult.getList())) { - return new PageResult<>(pageResult.getTotal()); - } - - // 获得流程 Task Map - List processInstanceIds = convertList(pageResult.getList(), BpmProcessInstanceExtDO::getProcessInstanceId); - Map> taskMap = taskService.getTaskMapByProcessInstanceIds(processInstanceIds); - // 转换返回 - return BpmProcessInstanceConvert.INSTANCE.convertPage(pageResult, taskMap); - } - - @Override - @Transactional(rollbackFor = Exception.class) - public String createProcessInstance(Long userId, @Valid BpmProcessInstanceCreateReqVO createReqVO) { - // 获得流程定义 - ProcessDefinition definition = processDefinitionService.getProcessDefinition(createReqVO.getProcessDefinitionId()); - // 发起流程 - return createProcessInstance0(userId, definition, createReqVO.getVariables(), null); - } - - @Override - public String createProcessInstance(Long userId, @Valid BpmProcessInstanceCreateReqDTO createReqDTO) { - // 获得流程定义 - ProcessDefinition definition = processDefinitionService.getActiveProcessDefinition(createReqDTO.getProcessDefinitionKey()); - // 发起流程 - return createProcessInstance0(userId, definition, createReqDTO.getVariables(), createReqDTO.getBusinessKey()); - } - - @Override - public BpmProcessInstanceRespVO getProcessInstanceVO(String id) { - // 获得流程实例 - HistoricProcessInstance processInstance = getHistoricProcessInstance(id); - if (processInstance == null) { - return null; - } - BpmProcessInstanceExtDO processInstanceExt = processInstanceExtMapper.selectByProcessInstanceId(id); - Assert.notNull(processInstanceExt, "流程实例拓展({}) 不存在", id); - - // 获得流程定义 - ProcessDefinition processDefinition = processDefinitionService - .getProcessDefinition(processInstance.getProcessDefinitionId()); - Assert.notNull(processDefinition, "流程定义({}) 不存在", processInstance.getProcessDefinitionId()); - BpmProcessDefinitionExtDO processDefinitionExt = processDefinitionService.getProcessDefinitionExt( - processInstance.getProcessDefinitionId()); - Assert.notNull(processDefinitionExt, "流程定义拓展({}) 不存在", id); - String bpmnXml = processDefinitionService.getProcessDefinitionBpmnXML(processInstance.getProcessDefinitionId()); - - // 获得 User - AdminUserRespDTO startUser = adminUserApi.getUser(NumberUtils.parseLong(processInstance.getStartUserId())).getCheckedData(); - DeptRespDTO dept = null; - if (startUser != null) { - dept = deptApi.getDept(startUser.getDeptId()).getCheckedData(); - } - - // 拼接结果 - return BpmProcessInstanceConvert.INSTANCE.convert2(processInstance, processInstanceExt, - processDefinition, processDefinitionExt, bpmnXml, startUser, dept); - } - - @Override - public void cancelProcessInstance(Long userId, @Valid BpmProcessInstanceCancelReqVO cancelReqVO) { - // 校验流程实例存在 - ProcessInstance instance = getProcessInstance(cancelReqVO.getId()); - if (instance == null) { - throw exception(PROCESS_INSTANCE_CANCEL_FAIL_NOT_EXISTS); - } - // 只能取消自己的 - if (!Objects.equals(instance.getStartUserId(), String.valueOf(userId))) { - throw exception(PROCESS_INSTANCE_CANCEL_FAIL_NOT_SELF); - } - - // 通过删除流程实例,实现流程实例的取消, - // 删除流程实例,正则执行任务 ACT_RU_TASK. 任务会被删除。通过历史表查询 - deleteProcessInstance(cancelReqVO.getId(), - BpmProcessInstanceDeleteReasonEnum.CANCEL_TASK.format(cancelReqVO.getReason())); - } - - /** - * 获得历史的流程实例 - * - * @param id 流程实例的编号 - * @return 历史的流程实例 - */ - @Override - public HistoricProcessInstance getHistoricProcessInstance(String id) { - return historyService.createHistoricProcessInstanceQuery().processInstanceId(id).singleResult(); - } - - @Override - public List getHistoricProcessInstances(Set ids) { - return historyService.createHistoricProcessInstanceQuery().processInstanceIds(ids).list(); - } - - @Override - public void createProcessInstanceExt(ProcessInstance instance) { - // 获得流程定义 - ProcessDefinition definition = processDefinitionService.getProcessDefinition2(instance.getProcessDefinitionId()); - // 插入 BpmProcessInstanceExtDO 对象 - BpmProcessInstanceExtDO instanceExtDO = new BpmProcessInstanceExtDO() - .setProcessInstanceId(instance.getId()) - .setProcessDefinitionId(definition.getId()) - .setName(instance.getProcessDefinitionName()) - .setStartUserId(Long.valueOf(instance.getStartUserId())) - .setCategory(definition.getCategory()) - .setStatus(BpmProcessInstanceStatusEnum.RUNNING.getStatus()) - .setResult(BpmProcessInstanceResultEnum.PROCESS.getResult()); - - processInstanceExtMapper.insert(instanceExtDO); - } - - @Override - public void updateProcessInstanceExtCancel(FlowableCancelledEvent event) { - // 判断是否为 Reject 不通过。如果是,则不进行更新. - // 因为,updateProcessInstanceExtReject 方法,已经进行更新了 - if (BpmProcessInstanceDeleteReasonEnum.isRejectReason((String)event.getCause())) { - return; - } - - // 需要主动查询,因为 instance 只有 id 属性 - // 另外,此时如果去查询 ProcessInstance 的话,字段是不全的,所以去查询了 HistoricProcessInstance - HistoricProcessInstance processInstance = getHistoricProcessInstance(event.getProcessInstanceId()); - // 更新拓展表 - BpmProcessInstanceExtDO instanceExtDO = new BpmProcessInstanceExtDO() - .setProcessInstanceId(event.getProcessInstanceId()) - .setEndTime(LocalDateTime.now()) // 由于 ProcessInstance 里没有办法拿到 endTime,所以这里设置 - .setStatus(BpmProcessInstanceStatusEnum.FINISH.getStatus()) - .setResult(BpmProcessInstanceResultEnum.CANCEL.getResult()); - processInstanceExtMapper.updateByProcessInstanceId(instanceExtDO); - - // 发送流程实例的状态事件 - processInstanceResultEventPublisher.sendProcessInstanceResultEvent( - BpmProcessInstanceConvert.INSTANCE.convert(this, processInstance, instanceExtDO.getResult())); - } - - @Override - public void updateProcessInstanceExtComplete(ProcessInstance instance) { - // 需要主动查询,因为 instance 只有 id 属性 - // 另外,此时如果去查询 ProcessInstance 的话,字段是不全的,所以去查询了 HistoricProcessInstance - HistoricProcessInstance processInstance = getHistoricProcessInstance(instance.getId()); - // 更新拓展表 - BpmProcessInstanceExtDO instanceExtDO = new BpmProcessInstanceExtDO() - .setProcessInstanceId(instance.getProcessInstanceId()) - .setEndTime(LocalDateTime.now()) // 由于 ProcessInstance 里没有办法拿到 endTime,所以这里设置 - .setStatus(BpmProcessInstanceStatusEnum.FINISH.getStatus()) - .setResult(BpmProcessInstanceResultEnum.APPROVE.getResult()); // 如果正常完全,说明审批通过 - processInstanceExtMapper.updateByProcessInstanceId(instanceExtDO); - - // 发送流程被通过的消息 - messageService.sendMessageWhenProcessInstanceApprove(BpmProcessInstanceConvert.INSTANCE.convert2ApprovedReq(instance)); - - // 发送流程实例的状态事件 - processInstanceResultEventPublisher.sendProcessInstanceResultEvent( - BpmProcessInstanceConvert.INSTANCE.convert(this, processInstance, instanceExtDO.getResult())); - } - - @Override - @Transactional(rollbackFor = Exception.class) - public void updateProcessInstanceExtReject(String id, String reason) { - // 需要主动查询,因为 instance 只有 id 属性 - ProcessInstance processInstance = getProcessInstance(id); - // 删除流程实例,以实现驳回任务时,取消整个审批流程 - deleteProcessInstance(id, StrUtil.format(BpmProcessInstanceDeleteReasonEnum.REJECT_TASK.format(reason))); - - // 更新 status + result - // 注意,不能和上面的逻辑更换位置。因为 deleteProcessInstance 会触发流程的取消,进而调用 updateProcessInstanceExtCancel 方法, - // 设置 result 为 BpmProcessInstanceStatusEnum.CANCEL,显然和 result 不一定是一致的 - BpmProcessInstanceExtDO instanceExtDO = new BpmProcessInstanceExtDO().setProcessInstanceId(id) - .setStatus(BpmProcessInstanceStatusEnum.FINISH.getStatus()) - .setResult(BpmProcessInstanceResultEnum.REJECT.getResult()); - processInstanceExtMapper.updateByProcessInstanceId(instanceExtDO); - - // 发送流程被不通过的消息 - messageService.sendMessageWhenProcessInstanceReject(BpmProcessInstanceConvert.INSTANCE.convert2RejectReq(processInstance, reason)); - - // 发送流程实例的状态事件 - processInstanceResultEventPublisher.sendProcessInstanceResultEvent( - BpmProcessInstanceConvert.INSTANCE.convert(this, processInstance, instanceExtDO.getResult())); - } - - private void deleteProcessInstance(String id, String reason) { - runtimeService.deleteProcessInstance(id, reason); - } - - private String createProcessInstance0(Long userId, ProcessDefinition definition, - Map variables, String businessKey) { - // 校验流程定义 - if (definition == null) { - throw exception(PROCESS_DEFINITION_NOT_EXISTS); - } - if (definition.isSuspended()) { - throw exception(PROCESS_DEFINITION_IS_SUSPENDED); - } - - // 创建流程实例 - ProcessInstance instance = runtimeService.createProcessInstanceBuilder() - .processDefinitionId(definition.getId()) - .businessKey(businessKey) - .name(definition.getName().trim()) - .variables(variables) - .start(); - // 设置流程名字 - runtimeService.setProcessInstanceName(instance.getId(), definition.getName()); - - // 补全流程实例的拓展表 - processInstanceExtMapper.updateByProcessInstanceId(new BpmProcessInstanceExtDO().setProcessInstanceId(instance.getId()) - .setFormVariables(variables)); - return instance.getId(); - } - -} +package cn.iocoder.yudao.module.bpm.service.task; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.number.NumberUtils; import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO; import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.*; import cn.iocoder.yudao.module.bpm.convert.task.BpmProcessInstanceConvert; import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionExtDO; import cn.iocoder.yudao.module.bpm.dal.dataobject.task.BpmProcessInstanceExtDO; import cn.iocoder.yudao.module.bpm.dal.mysql.task.BpmProcessInstanceExtMapper; import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceDeleteReasonEnum; import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceResultEnum; import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum; import cn.iocoder.yudao.module.bpm.framework.bpm.core.event.BpmProcessInstanceResultEventPublisher; import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService; import cn.iocoder.yudao.module.bpm.service.message.BpmMessageService; import cn.iocoder.yudao.module.system.api.dept.DeptApi; import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; import cn.iocoder.yudao.module.system.api.user.AdminUserApi; import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; import lombok.extern.slf4j.Slf4j; import org.flowable.engine.HistoryService; import org.flowable.engine.RuntimeService; import org.flowable.engine.delegate.event.FlowableCancelledEvent; import org.flowable.engine.history.HistoricProcessInstance; import org.flowable.engine.repository.ProcessDefinition; import org.flowable.engine.runtime.ProcessInstance; import org.flowable.task.api.Task; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import javax.annotation.Resource; import javax.validation.Valid; import java.time.LocalDateTime; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*; /** * 流程实例 Service 实现类 * * ProcessDefinition & ProcessInstance & Execution & Task 的关系: * 1. * * HistoricProcessInstance & ProcessInstance 的关系: * 1. * * 简单来说,前者 = 历史 + 运行中的流程实例,后者仅是运行中的流程实例 * * @author 芋道源码 */ @Service @Validated @Slf4j public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService { @Resource private RuntimeService runtimeService; @Resource private BpmProcessInstanceExtMapper processInstanceExtMapper; @Resource @Lazy // 解决循环依赖 private BpmTaskService taskService; @Resource private BpmProcessDefinitionService processDefinitionService; @Resource private HistoryService historyService; @Resource private AdminUserApi adminUserApi; @Resource private DeptApi deptApi; @Resource private BpmProcessInstanceResultEventPublisher processInstanceResultEventPublisher; @Resource private BpmMessageService messageService; @Override public ProcessInstance getProcessInstance(String id) { return runtimeService.createProcessInstanceQuery().processInstanceId(id).singleResult(); } @Override public List getProcessInstances(Set ids) { return runtimeService.createProcessInstanceQuery().processInstanceIds(ids).list(); } @Override public PageResult getMyProcessInstancePage(Long userId, BpmProcessInstanceMyPageReqVO pageReqVO) { // 通过 BpmProcessInstanceExtDO 表,先查询到对应的分页 PageResult pageResult = processInstanceExtMapper.selectPage(userId, pageReqVO); if (CollUtil.isEmpty(pageResult.getList())) { return new PageResult<>(pageResult.getTotal()); } // 获得流程 Task Map List processInstanceIds = convertList(pageResult.getList(), BpmProcessInstanceExtDO::getProcessInstanceId); Map> taskMap = taskService.getTaskMapByProcessInstanceIds(processInstanceIds); // 转换返回 return BpmProcessInstanceConvert.INSTANCE.convertPage(pageResult, taskMap); } @Override @Transactional(rollbackFor = Exception.class) public String createProcessInstance(Long userId, @Valid BpmProcessInstanceCreateReqVO createReqVO) { // 获得流程定义 ProcessDefinition definition = processDefinitionService.getProcessDefinition(createReqVO.getProcessDefinitionId()); // 发起流程 return createProcessInstance0(userId, definition, createReqVO.getVariables(), null); } @Override public String createProcessInstance(Long userId, @Valid BpmProcessInstanceCreateReqDTO createReqDTO) { // 获得流程定义 ProcessDefinition definition = processDefinitionService.getActiveProcessDefinition(createReqDTO.getProcessDefinitionKey()); // 发起流程 return createProcessInstance0(userId, definition, createReqDTO.getVariables(), createReqDTO.getBusinessKey()); } @Override public BpmProcessInstanceRespVO getProcessInstanceVO(String id) { // 获得流程实例 HistoricProcessInstance processInstance = getHistoricProcessInstance(id); if (processInstance == null) { return null; } BpmProcessInstanceExtDO processInstanceExt = processInstanceExtMapper.selectByProcessInstanceId(id); Assert.notNull(processInstanceExt, "流程实例拓展({}) 不存在", id); // 获得流程定义 ProcessDefinition processDefinition = processDefinitionService .getProcessDefinition(processInstance.getProcessDefinitionId()); Assert.notNull(processDefinition, "流程定义({}) 不存在", processInstance.getProcessDefinitionId()); BpmProcessDefinitionExtDO processDefinitionExt = processDefinitionService.getProcessDefinitionExt( processInstance.getProcessDefinitionId()); Assert.notNull(processDefinitionExt, "流程定义拓展({}) 不存在", id); String bpmnXml = processDefinitionService.getProcessDefinitionBpmnXML(processInstance.getProcessDefinitionId()); // 获得 User AdminUserRespDTO startUser = adminUserApi.getUser(NumberUtils.parseLong(processInstance.getStartUserId())).getCheckedData(); DeptRespDTO dept = null; if (startUser != null) { dept = deptApi.getDept(startUser.getDeptId()).getCheckedData(); } // 拼接结果 return BpmProcessInstanceConvert.INSTANCE.convert2(processInstance, processInstanceExt, processDefinition, processDefinitionExt, bpmnXml, startUser, dept); } @Override public void cancelProcessInstance(Long userId, @Valid BpmProcessInstanceCancelReqVO cancelReqVO) { // 校验流程实例存在 ProcessInstance instance = getProcessInstance(cancelReqVO.getId()); if (instance == null) { throw exception(PROCESS_INSTANCE_CANCEL_FAIL_NOT_EXISTS); } // 只能取消自己的 if (!Objects.equals(instance.getStartUserId(), String.valueOf(userId))) { throw exception(PROCESS_INSTANCE_CANCEL_FAIL_NOT_SELF); } // 通过删除流程实例,实现流程实例的取消, // 删除流程实例,正则执行任务 ACT_RU_TASK. 任务会被删除。通过历史表查询 deleteProcessInstance(cancelReqVO.getId(), BpmProcessInstanceDeleteReasonEnum.CANCEL_TASK.format(cancelReqVO.getReason())); } /** * 获得历史的流程实例 * * @param id 流程实例的编号 * @return 历史的流程实例 */ @Override public HistoricProcessInstance getHistoricProcessInstance(String id) { return historyService.createHistoricProcessInstanceQuery().processInstanceId(id).singleResult(); } @Override public List getHistoricProcessInstances(Set ids) { return historyService.createHistoricProcessInstanceQuery().processInstanceIds(ids).list(); } @Override public void createProcessInstanceExt(ProcessInstance instance) { // 获得流程定义 ProcessDefinition definition = processDefinitionService.getProcessDefinition2(instance.getProcessDefinitionId()); // 插入 BpmProcessInstanceExtDO 对象 BpmProcessInstanceExtDO instanceExtDO = new BpmProcessInstanceExtDO() .setProcessInstanceId(instance.getId()) .setProcessDefinitionId(definition.getId()) .setName(instance.getProcessDefinitionName()) .setStartUserId(Long.valueOf(instance.getStartUserId())) .setCategory(definition.getCategory()) .setStatus(BpmProcessInstanceStatusEnum.RUNNING.getStatus()) .setResult(BpmProcessInstanceResultEnum.PROCESS.getResult()); processInstanceExtMapper.insert(instanceExtDO); } @Override public void updateProcessInstanceExtCancel(FlowableCancelledEvent event) { // 判断是否为 Reject 不通过。如果是,则不进行更新. // 因为,updateProcessInstanceExtReject 方法,已经进行更新了 if (BpmProcessInstanceDeleteReasonEnum.isRejectReason((String)event.getCause())) { return; } // 需要主动查询,因为 instance 只有 id 属性 // 另外,此时如果去查询 ProcessInstance 的话,字段是不全的,所以去查询了 HistoricProcessInstance HistoricProcessInstance processInstance = getHistoricProcessInstance(event.getProcessInstanceId()); // 更新拓展表 BpmProcessInstanceExtDO instanceExtDO = new BpmProcessInstanceExtDO() .setProcessInstanceId(event.getProcessInstanceId()) .setEndTime(LocalDateTime.now()) // 由于 ProcessInstance 里没有办法拿到 endTime,所以这里设置 .setStatus(BpmProcessInstanceStatusEnum.FINISH.getStatus()) .setResult(BpmProcessInstanceResultEnum.CANCEL.getResult()); processInstanceExtMapper.updateByProcessInstanceId(instanceExtDO); // 发送流程实例的状态事件 processInstanceResultEventPublisher.sendProcessInstanceResultEvent( BpmProcessInstanceConvert.INSTANCE.convert(this, processInstance, instanceExtDO.getResult())); } @Override public void updateProcessInstanceExtComplete(ProcessInstance instance) { // 需要主动查询,因为 instance 只有 id 属性 // 另外,此时如果去查询 ProcessInstance 的话,字段是不全的,所以去查询了 HistoricProcessInstance HistoricProcessInstance processInstance = getHistoricProcessInstance(instance.getId()); // 更新拓展表 BpmProcessInstanceExtDO instanceExtDO = new BpmProcessInstanceExtDO() .setProcessInstanceId(instance.getProcessInstanceId()) .setEndTime(LocalDateTime.now()) // 由于 ProcessInstance 里没有办法拿到 endTime,所以这里设置 .setStatus(BpmProcessInstanceStatusEnum.FINISH.getStatus()) .setResult(BpmProcessInstanceResultEnum.APPROVE.getResult()); // 如果正常完全,说明审批通过 processInstanceExtMapper.updateByProcessInstanceId(instanceExtDO); // 发送流程被通过的消息 messageService.sendMessageWhenProcessInstanceApprove(BpmProcessInstanceConvert.INSTANCE.convert2ApprovedReq(instance)); // 发送流程实例的状态事件 processInstanceResultEventPublisher.sendProcessInstanceResultEvent( BpmProcessInstanceConvert.INSTANCE.convert(this, processInstance, instanceExtDO.getResult())); } @Override @Transactional(rollbackFor = Exception.class) public void updateProcessInstanceExtReject(String id, String reason) { // 需要主动查询,因为 instance 只有 id 属性 ProcessInstance processInstance = getProcessInstance(id); // 删除流程实例,以实现驳回任务时,取消整个审批流程 deleteProcessInstance(id, StrUtil.format(BpmProcessInstanceDeleteReasonEnum.REJECT_TASK.format(reason))); // 更新 status + result // 注意,不能和上面的逻辑更换位置。因为 deleteProcessInstance 会触发流程的取消,进而调用 updateProcessInstanceExtCancel 方法, // 设置 result 为 BpmProcessInstanceStatusEnum.CANCEL,显然和 result 不一定是一致的 BpmProcessInstanceExtDO instanceExtDO = new BpmProcessInstanceExtDO().setProcessInstanceId(id) .setStatus(BpmProcessInstanceStatusEnum.FINISH.getStatus()) .setResult(BpmProcessInstanceResultEnum.REJECT.getResult()); processInstanceExtMapper.updateByProcessInstanceId(instanceExtDO); // 发送流程被不通过的消息 messageService.sendMessageWhenProcessInstanceReject(BpmProcessInstanceConvert.INSTANCE.convert2RejectReq(processInstance, reason)); // 发送流程实例的状态事件 processInstanceResultEventPublisher.sendProcessInstanceResultEvent( BpmProcessInstanceConvert.INSTANCE.convert(this, processInstance, instanceExtDO.getResult())); } private void deleteProcessInstance(String id, String reason) { runtimeService.deleteProcessInstance(id, reason); } private String createProcessInstance0(Long userId, ProcessDefinition definition, Map variables, String businessKey) { // 校验流程定义 if (definition == null) { throw exception(PROCESS_DEFINITION_NOT_EXISTS); } if (definition.isSuspended()) { throw exception(PROCESS_DEFINITION_IS_SUSPENDED); } // 创建流程实例 ProcessInstance instance = runtimeService.createProcessInstanceBuilder() .processDefinitionId(definition.getId()) .businessKey(businessKey) .name(definition.getName().trim()) .variables(variables) .start(); // 设置流程名字 runtimeService.setProcessInstanceName(instance.getId(), definition.getName()); // 补全流程实例的拓展表 processInstanceExtMapper.updateByProcessInstanceId(new BpmProcessInstanceExtDO().setProcessInstanceId(instance.getId()) .setFormVariables(variables)); return instance.getId(); } } \ No newline at end of file diff --git a/yudao-module-mp/pom.xml b/yudao-module-mp/pom.xml new file mode 100644 index 000000000..88735881d --- /dev/null +++ b/yudao-module-mp/pom.xml @@ -0,0 +1,24 @@ + + + + yudao + cn.iocoder.cloud + ${revision} + + 4.0.0 + + yudao-module-mp + pom + + + wechat 模块,主要实现微信平台的相关业务。 + 例如:微信公众号、企业微信 SCRM 等 + + + yudao-module-mp-api + yudao-module-mp-biz + + + diff --git a/yudao-module-mp/yudao-module-mp-api/pom.xml b/yudao-module-mp/yudao-module-mp-api/pom.xml new file mode 100644 index 000000000..9cad0d93f --- /dev/null +++ b/yudao-module-mp/yudao-module-mp-api/pom.xml @@ -0,0 +1,26 @@ + + + + yudao-module-mp + cn.iocoder.cloud + ${revision} + + 4.0.0 + yudao-module-mp-api + jar + + ${project.artifactId} + + mp 模块 API,暴露给其它模块调用 + + + + + cn.iocoder.cloud + yudao-common + + + + diff --git a/yudao-module-mp/yudao-module-mp-api/src/main/java/cn/iocoder/yudao/module/mp/enums/ErrorCodeConstants.java b/yudao-module-mp/yudao-module-mp-api/src/main/java/cn/iocoder/yudao/module/mp/enums/ErrorCodeConstants.java new file mode 100644 index 000000000..262306e34 --- /dev/null +++ b/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 { + + // ========== 公众号账号 1006000000============ + ErrorCode ACCOUNT_NOT_EXISTS = new ErrorCode(1006000000, "公众号账号不存在"); + ErrorCode ACCOUNT_GENERATE_QR_CODE_FAIL = new ErrorCode(1006000001, "生成公众号二维码失败,原因:{}"); + ErrorCode ACCOUNT_CLEAR_QUOTA_FAIL = new ErrorCode(1006000002, "清空公众号的 API 配额失败,原因:{}"); + + // ========== 公众号统计 1006001000============ + ErrorCode STATISTICS_GET_USER_SUMMARY_FAIL = new ErrorCode(1006001000, "获取粉丝增减数据失败,原因:{}"); + ErrorCode STATISTICS_GET_USER_CUMULATE_FAIL = new ErrorCode(1006001001, "获得粉丝累计数据失败,原因:{}"); + ErrorCode STATISTICS_GET_UPSTREAM_MESSAGE_FAIL = new ErrorCode(1006001002, "获得消息发送概况数据失败,原因:{}"); + ErrorCode STATISTICS_GET_INTERFACE_SUMMARY_FAIL = new ErrorCode(1006001003, "获得接口分析数据失败,原因:{}"); + + // ========== 公众号标签 1006002000============ + ErrorCode TAG_NOT_EXISTS = new ErrorCode(1006002000, "标签不存在"); + ErrorCode TAG_CREATE_FAIL = new ErrorCode(1006002001, "创建标签失败,原因:{}"); + ErrorCode TAG_UPDATE_FAIL = new ErrorCode(1006002002, "更新标签失败,原因:{}"); + ErrorCode TAG_DELETE_FAIL = new ErrorCode(1006002003, "删除标签失败,原因:{}"); + ErrorCode TAG_GET_FAIL = new ErrorCode(1006002004, "获得标签失败,原因:{}"); + + // ========== 公众号粉丝 1006003000============ + ErrorCode USER_NOT_EXISTS = new ErrorCode(1006003000, "粉丝不存在"); + ErrorCode USER_UPDATE_TAG_FAIL = new ErrorCode(1006003001, "更新粉丝标签失败,原因:{}"); + + // ========== 公众号素材 1006004000============ + ErrorCode MATERIAL_NOT_EXISTS = new ErrorCode(1006004000, "素材不存在"); + ErrorCode MATERIAL_UPLOAD_FAIL = new ErrorCode(1006004001, "上传素材失败,原因:{}"); + ErrorCode MATERIAL_IMAGE_UPLOAD_FAIL = new ErrorCode(1006004002, "上传图片失败,原因:{}"); + ErrorCode MATERIAL_DELETE_FAIL = new ErrorCode(1006004003, "删除素材失败,原因:{}"); + + // ========== 公众号消息 1006005000============ + ErrorCode MESSAGE_SEND_FAIL = new ErrorCode(1006005000, "发送消息失败,原因:{}"); + + // ========== 公众号发布能力 1006006000============ + ErrorCode FREE_PUBLISH_LIST_FAIL = new ErrorCode(1006006000, "获得已成功发布列表失败,原因:{}"); + ErrorCode FREE_PUBLISH_SUBMIT_FAIL = new ErrorCode(1006006001, "提交发布失败,原因:{}"); + ErrorCode FREE_PUBLISH_DELETE_FAIL = new ErrorCode(1006006002, "删除发布失败,原因:{}"); + + // ========== 公众号草稿 1006007000============ + ErrorCode DRAFT_LIST_FAIL = new ErrorCode(1006007000, "获得草稿列表失败,原因:{}"); + ErrorCode DRAFT_CREATE_FAIL = new ErrorCode(1006007001, "创建草稿失败,原因:{}"); + ErrorCode DRAFT_UPDATE_FAIL = new ErrorCode(1006007002, "更新草稿失败,原因:{}"); + ErrorCode DRAFT_DELETE_FAIL = new ErrorCode(1006007003, "删除草稿失败,原因:{}"); + + // ========== 公众号菜单 1006008000============ + ErrorCode MENU_SAVE_FAIL = new ErrorCode(1006008000, "创建菜单失败,原因:{}"); + ErrorCode MENU_DELETE_FAIL = new ErrorCode(1006008001, "删除菜单失败,原因:{}"); + + // ========== 公众号自动回复 1006009000============ + ErrorCode AUTO_REPLY_NOT_EXISTS = new ErrorCode(1006009000, "自动回复不存在"); + ErrorCode AUTO_REPLY_ADD_SUBSCRIBE_FAIL_EXISTS = new ErrorCode(1006009001, "操作失败,原因:已存在关注时的回复"); + ErrorCode AUTO_REPLY_ADD_MESSAGE_FAIL_EXISTS = new ErrorCode(1006009002, "操作失败,原因:已存在该消息类型的回复"); + ErrorCode AUTO_REPLY_ADD_KEYWORD_FAIL_EXISTS = new ErrorCode(1006009003, "操作失败,原因:已关在该关键字的回复"); + +} diff --git a/yudao-module-mp/yudao-module-mp-api/src/main/java/cn/iocoder/yudao/module/mp/enums/message/MpAutoReplyMatchEnum.java b/yudao-module-mp/yudao-module-mp-api/src/main/java/cn/iocoder/yudao/module/mp/enums/message/MpAutoReplyMatchEnum.java new file mode 100644 index 000000000..a5298dbf3 --- /dev/null +++ b/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; + +} diff --git a/yudao-module-mp/yudao-module-mp-api/src/main/java/cn/iocoder/yudao/module/mp/enums/message/MpAutoReplyTypeEnum.java b/yudao-module-mp/yudao-module-mp-api/src/main/java/cn/iocoder/yudao/module/mp/enums/message/MpAutoReplyTypeEnum.java new file mode 100644 index 000000000..300f1f2ba --- /dev/null +++ b/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; + +} diff --git a/yudao-module-mp/yudao-module-mp-api/src/main/java/cn/iocoder/yudao/module/mp/enums/message/MpMessageSendFromEnum.java b/yudao-module-mp/yudao-module-mp-api/src/main/java/cn/iocoder/yudao/module/mp/enums/message/MpMessageSendFromEnum.java new file mode 100644 index 000000000..a2af484a5 --- /dev/null +++ b/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; + +} diff --git a/yudao-module-mp/yudao-module-mp-api/src/main/java/cn/iocoder/yudao/module/mp/package-info.java b/yudao-module-mp/yudao-module-mp-api/src/main/java/cn/iocoder/yudao/module/mp/package-info.java new file mode 100644 index 000000000..598718088 --- /dev/null +++ b/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; diff --git a/yudao-module-mp/yudao-module-mp-biz/pom.xml b/yudao-module-mp/yudao-module-mp-biz/pom.xml new file mode 100644 index 000000000..ff1d72262 --- /dev/null +++ b/yudao-module-mp/yudao-module-mp-biz/pom.xml @@ -0,0 +1,119 @@ + + + + yudao-module-mp + cn.iocoder.cloud + ${revision} + + 4.0.0 + yudao-module-mp-biz + jar + + ${project.artifactId} + + mp 模块,我们放微信微信公众号。 + 例如说:提供微信公众号的账号、菜单、粉丝、标签、消息、自动回复、素材、模板通知、运营数据等功能 + + + + + + org.springframework.cloud + spring-cloud-starter-bootstrap + + + + + cn.iocoder.cloud + yudao-module-mp-api + ${revision} + + + cn.iocoder.cloud + yudao-module-system-api + ${revision} + + + cn.iocoder.cloud + yudao-module-infra-api + ${revision} + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-biz-operatelog + + + cn.iocoder.cloud + yudao-spring-boot-starter-biz-weixin + + + cn.iocoder.cloud + yudao-spring-boot-starter-biz-tenant + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-validation + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-mybatis + + + + cn.iocoder.cloud + yudao-spring-boot-starter-redis + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-rpc + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-test + test + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-monitor + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-excel + + + + + diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/MpAccountController.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/MpAccountController.java new file mode 100644 index 000000000..62be175ac --- /dev/null +++ b/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 createAccount(@Valid @RequestBody MpAccountCreateReqVO createReqVO) { + return success(mpAccountService.createAccount(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新公众号账号") + @PreAuthorize("@ss.hasPermission('mp:account:update')") + public CommonResult 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 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 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> getAccountPage(@Valid MpAccountPageReqVO pageVO) { + PageResult pageResult = mpAccountService.getAccountPage(pageVO); + return success(MpAccountConvert.INSTANCE.convertPage(pageResult)); + } + + @GetMapping("/list-all-simple") + @Operation(summary = "获取公众号账号精简信息列表") + @PreAuthorize("@ss.hasPermission('mp:account:query')") + public CommonResult> getSimpleAccounts() { + List 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 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 clearAccountQuota(@RequestParam("id") Long id) { + mpAccountService.clearAccountQuota(id); + return success(true); + } + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountBaseVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountBaseVO.java new file mode 100644 index 000000000..8f688309d --- /dev/null +++ b/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; + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountCreateReqVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountCreateReqVO.java new file mode 100644 index 000000000..b864f16c5 --- /dev/null +++ b/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 { + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountPageReqVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountPageReqVO.java new file mode 100644 index 000000000..93093dc19 --- /dev/null +++ b/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; + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountRespVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountRespVO.java new file mode 100644 index 000000000..cb37bb2de --- /dev/null +++ b/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.util.Date; + +@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 Date createTime; + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountSimpleRespVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountSimpleRespVO.java new file mode 100644 index 000000000..db4594f6d --- /dev/null +++ b/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; + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountUpdateReqVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountUpdateReqVO.java new file mode 100644 index 000000000..e93123e6c --- /dev/null +++ b/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; + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/MpMaterialController.http b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/MpMaterialController.http new file mode 100644 index 000000000..74b8f40b6 --- /dev/null +++ b/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}} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/MpMaterialController.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/MpMaterialController.java new file mode 100644 index 000000000..ef175d2af --- /dev/null +++ b/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 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 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 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 uploadNewsImage(@Valid MpMaterialUploadNewsImageReqVO reqVO) + throws IOException { + return success(mpMaterialService.uploadNewsImage(reqVO)); + } + + @Operation(summary = "获得素材分页") + @GetMapping("/page") + @PreAuthorize("@ss.hasPermission('mp:material:query')") + public CommonResult> getMaterialPage(@Valid MpMaterialPageReqVO pageReqVO) { + PageResult pageResult = mpMaterialService.getMaterialPage(pageReqVO); + return success(MpMaterialConvert.INSTANCE.convertPage(pageResult)); + } + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/vo/MpMaterialPageReqVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/vo/MpMaterialPageReqVO.java new file mode 100644 index 000000000..8a2b60ff6 --- /dev/null +++ b/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; + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/vo/MpMaterialRespVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/vo/MpMaterialRespVO.java new file mode 100644 index 000000000..222a4225b --- /dev/null +++ b/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.util.Date; + +@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 Date createTime; + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/vo/MpMaterialUploadNewsImageReqVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/vo/MpMaterialUploadNewsImageReqVO.java new file mode 100644 index 000000000..6ede4c5eb --- /dev/null +++ b/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; + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/vo/MpMaterialUploadPermanentReqVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/vo/MpMaterialUploadPermanentReqVO.java new file mode 100644 index 000000000..9ca8a4ad8 --- /dev/null +++ b/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; + } + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/vo/MpMaterialUploadRespVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/vo/MpMaterialUploadRespVO.java new file mode 100644 index 000000000..0174cbcc2 --- /dev/null +++ b/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; + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/vo/MpMaterialUploadTemporaryReqVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/vo/MpMaterialUploadTemporaryReqVO.java new file mode 100644 index 000000000..abd6d06d1 --- /dev/null +++ b/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; + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/menu/MpMenuController.http b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/menu/MpMenuController.http new file mode 100644 index 000000000..2276b3b4f --- /dev/null +++ b/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}} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/menu/MpMenuController.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/menu/MpMenuController.java new file mode 100644 index 000000000..b7c883519 --- /dev/null +++ b/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 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 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> getMenuList(@RequestParam("accountId") Long accountId) { + List list = mpMenuService.getMenuListByAccountId(accountId); + return success(MpMenuConvert.INSTANCE.convertList(list)); + } + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/menu/vo/MpMenuBaseVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/menu/vo/MpMenuBaseVO.java new file mode 100644 index 000000000..cd0e5b2e4 --- /dev/null +++ b/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 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; + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/menu/vo/MpMenuRespVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/menu/vo/MpMenuRespVO.java new file mode 100644 index 000000000..253e2d117 --- /dev/null +++ b/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.util.Date; + +@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 Date createTime; + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/menu/vo/MpMenuSaveReqVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/menu/vo/MpMenuSaveReqVO.java new file mode 100644 index 000000000..580e62845 --- /dev/null +++ b/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 menus; + + @Schema(description = "管理后台 - 公众号菜单保存时的每个菜单") + @Data + public static class Menu extends MpMenuBaseVO { + + /** + * 子菜单数组 + */ + private List children; + + } + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/MpAutoReplyController.http b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/MpAutoReplyController.http new file mode 100644 index 000000000..dbb3a7b22 --- /dev/null +++ b/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}} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/MpAutoReplyController.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/MpAutoReplyController.java new file mode 100644 index 000000000..f6a8b9b38 --- /dev/null +++ b/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> getAutoReplyPage(@Valid MpMessagePageReqVO pageVO) { + PageResult 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 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 createAutoReply(@Valid @RequestBody MpAutoReplyCreateReqVO createReqVO) { + return success(mpAutoReplyService.createAutoReply(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新公众号自动回复") + @PreAuthorize("@ss.hasPermission('mp:auto-reply:update')") + public CommonResult 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 deleteAutoReply(@RequestParam("id") Long id) { + mpAutoReplyService.deleteAutoReply(id); + return success(true); + } + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/MpMessageController.http b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/MpMessageController.http new file mode 100644 index 000000000..b9f9721c9 --- /dev/null +++ b/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" +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/MpMessageController.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/MpMessageController.java new file mode 100644 index 000000000..66befc65e --- /dev/null +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/MpMessageController.java @@ -0,0 +1,47 @@ +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.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.convert.message.MpMessageConvert; +import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO; +import cn.iocoder.yudao.module.mp.service.message.MpMessageService; +import io.swagger.v3.oas.annotations.Operation; +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/message") +@Validated +public class MpMessageController { + + @Resource + private MpMessageService mpMessageService; + + @GetMapping("/page") + @Operation(summary = "获得公众号消息分页") + @PreAuthorize("@ss.hasPermission('mp:message:query')") + public CommonResult> getMessagePage(@Valid MpMessagePageReqVO pageVO) { + PageResult pageResult = mpMessageService.getMessagePage(pageVO); + return success(MpMessageConvert.INSTANCE.convertPage(pageResult)); + } + + @PostMapping("/send") + @Operation(summary = "给粉丝发送消息") + @PreAuthorize("@ss.hasPermission('mp:message:send')") + public CommonResult sendMessage(@Valid @RequestBody MpMessageSendReqVO reqVO) { + MpMessageDO message = mpMessageService.sendKefuMessage(reqVO); + return success(MpMessageConvert.INSTANCE.convert(message)); + } + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyBaseVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyBaseVO.java new file mode 100644 index 000000000..4b84254fe --- /dev/null +++ b/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 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; + } + + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyCreateReqVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyCreateReqVO.java new file mode 100644 index 000000000..5eafb3693 --- /dev/null +++ b/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; + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyPageReqVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyPageReqVO.java new file mode 100644 index 000000000..a68029e92 --- /dev/null +++ b/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; + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyRespVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyRespVO.java new file mode 100644 index 000000000..65e0c4522 --- /dev/null +++ b/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.util.Date; + +@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 Date createTime; + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyUpdateReqVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyUpdateReqVO.java new file mode 100644 index 000000000..92095d773 --- /dev/null +++ b/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; + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/vo/message/MpMessagePageReqVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/vo/message/MpMessagePageReqVO.java new file mode 100644 index 000000000..3ca643ca6 --- /dev/null +++ b/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; + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/vo/message/MpMessageRespVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/vo/message/MpMessageRespVO.java new file mode 100644 index 000000000..ac2c49c9f --- /dev/null +++ b/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 io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import me.chanjar.weixin.common.api.WxConsts; + +import java.util.Date; +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 + */ + @TableField(typeHandler = MpMessageDO.ArticleTypeHandler.class) + private List 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 Date createTime; + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/vo/message/MpMessageSendReqVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/vo/message/MpMessageSendReqVO.java new file mode 100644 index 000000000..8e833d443 --- /dev/null +++ b/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,58 @@ +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 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 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; + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/news/MpDraftController.http b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/news/MpDraftController.http new file mode 100644 index 000000000..87f9d432a --- /dev/null +++ b/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" +}] diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/news/MpDraftController.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/news/MpDraftController.java new file mode 100644 index 000000000..c43d6c5d7 --- /dev/null +++ b/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> 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 items) { + // 1.1 获得 mediaId 数组 + Set mediaIds = new HashSet<>(); + items.forEach(item -> item.getContent().getNewsItem().forEach(newsItem -> mediaIds.add(newsItem.getThumbMediaId()))); + if (CollUtil.isEmpty(mediaIds)) { + return; + } + // 1.2 批量查询对应的 Media 素材 + Map 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 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 deleteDraft(@RequestParam("accountId") Long accountId, + @RequestParam("mediaId") String mediaId, + @RequestBody List 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 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()); + } + } + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/news/MpFreePublishController.http b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/news/MpFreePublishController.http new file mode 100644 index 000000000..122413200 --- /dev/null +++ b/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}} + +{} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/news/MpFreePublishController.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/news/MpFreePublishController.java new file mode 100644 index 000000000..e36d7343c --- /dev/null +++ b/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> 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 items) { + // 1.1 获得 mediaId 数组 + Set mediaIds = new HashSet<>(); + items.forEach(item -> item.getContent().getNewsItem().forEach(newsItem -> mediaIds.add(newsItem.getThumbMediaId()))); + if (CollUtil.isEmpty(mediaIds)) { + return; + } + // 1.2 批量查询对应的 Media 素材 + Map 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 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 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()); + } + } + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/news/vo/MpDraftPageReqVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/news/vo/MpDraftPageReqVO.java new file mode 100644 index 000000000..62bc6f8e5 --- /dev/null +++ b/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; + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/news/vo/MpFreePublishPageReqVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/news/vo/MpFreePublishPageReqVO.java new file mode 100644 index 000000000..da36248f0 --- /dev/null +++ b/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; + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/open/MpOpenController.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/open/MpOpenController.java new file mode 100644 index 000000000..067102e11 --- /dev/null +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/open/MpOpenController.java @@ -0,0 +1,116 @@ +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.operatelog.core.annotations.OperateLog; +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; + + /** + * 接收微信公众号的校验签名 + * + * 对应 文档 + */ + @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 "非法请求"; + } + + /** + * 接收微信公众号的消息推送 + * + * 文档 + */ + @Operation(summary = "处理消息") + @PostMapping(value = "/{appId}", produces = "application/xml; charset=UTF-8") + @OperateLog(enable = false) // 回调地址,无需记录操作日志 + 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 ""; + } + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/open/vo/MpOpenCheckSignatureReqVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/open/vo/MpOpenCheckSignatureReqVO.java new file mode 100644 index 000000000..c0c73a8ed --- /dev/null +++ b/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; + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/open/vo/MpOpenHandleMessageReqVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/open/vo/MpOpenHandleMessageReqVO.java new file mode 100644 index 000000000..cb8f9abf2 --- /dev/null +++ b/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; + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/statistics/MpStatisticsController.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/statistics/MpStatisticsController.java new file mode 100644 index 000000000..8a9df53c3 --- /dev/null +++ b/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> getUserSummary(MpStatisticsGetReqVO getReqVO) { + List 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> getUserCumulate(MpStatisticsGetReqVO getReqVO) { + List 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> getUpstreamMessage(MpStatisticsGetReqVO getReqVO) { + List 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> getInterfaceSummary(MpStatisticsGetReqVO getReqVO) { + List list = mpStatisticsService.getInterfaceSummary( + getReqVO.getAccountId(), getReqVO.getDate()); + return success(MpStatisticsConvert.INSTANCE.convertList04(list)); + } +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/statistics/vo/MpStatisticsGetReqVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/statistics/vo/MpStatisticsGetReqVO.java new file mode 100644 index 000000000..904c5d13e --- /dev/null +++ b/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 = "查询时间范围") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @NotNull(message = "查询时间范围不能为空") + private LocalDateTime[] date; + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/statistics/vo/MpStatisticsInterfaceSummaryRespVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/statistics/vo/MpStatisticsInterfaceSummaryRespVO.java new file mode 100644 index 000000000..c0db71a83 --- /dev/null +++ b/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.util.Date; + +@Schema(description = "管理后台 - 某一天的接口分析数据 Response VO") +@Data +public class MpStatisticsInterfaceSummaryRespVO { + + @Schema(description = "日期", requiredMode = Schema.RequiredMode.REQUIRED) + private Date 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; + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/statistics/vo/MpStatisticsUpstreamMessageRespVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/statistics/vo/MpStatisticsUpstreamMessageRespVO.java new file mode 100644 index 000000000..4c05a82e7 --- /dev/null +++ b/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.util.Date; + +@Schema(description = "管理后台 - 某一天的粉丝增减数据 Response VO") +@Data +public class MpStatisticsUpstreamMessageRespVO { + + @Schema(description = "日期", requiredMode = Schema.RequiredMode.REQUIRED) + private Date refDate; + + @Schema(description = "上行发送了(向公众号发送了)消息的粉丝数", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Integer messageUser; + + @Schema(description = "上行发送了消息的消息总数", requiredMode = Schema.RequiredMode.REQUIRED, example = "20") + private Integer messageCount; + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/statistics/vo/MpStatisticsUserCumulateRespVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/statistics/vo/MpStatisticsUserCumulateRespVO.java new file mode 100644 index 000000000..300b29ce4 --- /dev/null +++ b/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.util.Date; + +@Schema(description = "管理后台 - 某一天的消息发送概况数据 Response VO") +@Data +public class MpStatisticsUserCumulateRespVO { + + @Schema(description = "日期", requiredMode = Schema.RequiredMode.REQUIRED) + private Date refDate; + + @Schema(description = "累计粉丝量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Integer cumulateUser; + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/statistics/vo/MpStatisticsUserSummaryRespVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/statistics/vo/MpStatisticsUserSummaryRespVO.java new file mode 100644 index 000000000..4e957a601 --- /dev/null +++ b/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.util.Date; + +@Schema(description = "管理后台 - 某一天的粉丝增减数据 Response VO") +@Data +public class MpStatisticsUserSummaryRespVO { + + @Schema(description = "日期", requiredMode = Schema.RequiredMode.REQUIRED) + private Date 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; + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/tag/MpTagController.http b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/tag/MpTagController.http new file mode 100644 index 000000000..fe79105ba --- /dev/null +++ b/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}} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/tag/MpTagController.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/tag/MpTagController.java new file mode 100644 index 000000000..96af036dc --- /dev/null +++ b/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 createTag(@Valid @RequestBody MpTagCreateReqVO createReqVO) { + return success(mpTagService.createTag(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新公众号标签") + @PreAuthorize("@ss.hasPermission('mp:tag:update')") + public CommonResult 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 deleteTag(@RequestParam("id") Long id) { + mpTagService.deleteTag(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获取公众号标签详情") + @PreAuthorize("@ss.hasPermission('mp:tag:query')") + public CommonResult 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> getTagPage(MpTagPageReqVO pageReqVO) { + PageResult pageResult = mpTagService.getTagPage(pageReqVO); + return success(MpTagConvert.INSTANCE.convertPage(pageResult)); + } + + @GetMapping("/list-all-simple") + @Operation(summary = "获取公众号账号精简信息列表") + @PreAuthorize("@ss.hasPermission('mp:account:query')") + public CommonResult> getSimpleTags() { + List 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 syncTag(@RequestParam("accountId") Long accountId) { + mpTagService.syncTag(accountId); + return success(true); + } + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/tag/vo/MpTagBaseVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/tag/vo/MpTagBaseVO.java new file mode 100644 index 000000000..dc034e3e8 --- /dev/null +++ b/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; + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/tag/vo/MpTagCreateReqVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/tag/vo/MpTagCreateReqVO.java new file mode 100644 index 000000000..ba56a0271 --- /dev/null +++ b/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; + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/tag/vo/MpTagPageReqVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/tag/vo/MpTagPageReqVO.java new file mode 100644 index 000000000..0666b340a --- /dev/null +++ b/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; + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/tag/vo/MpTagRespVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/tag/vo/MpTagRespVO.java new file mode 100644 index 000000000..19bf82583 --- /dev/null +++ b/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.util.Date; + +@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 Date createTime; + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/tag/vo/MpTagSimpleRespVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/tag/vo/MpTagSimpleRespVO.java new file mode 100644 index 000000000..ca23be1c8 --- /dev/null +++ b/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; + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/tag/vo/MpTagUpdateReqVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/tag/vo/MpTagUpdateReqVO.java new file mode 100644 index 000000000..cb61f1afb --- /dev/null +++ b/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; + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/user/MpUserController.http b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/user/MpUserController.http new file mode 100644 index 000000000..7c615810f --- /dev/null +++ b/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] +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/user/MpUserController.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/user/MpUserController.java new file mode 100644 index 000000000..7098bc57e --- /dev/null +++ b/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> getUserPage(@Valid MpUserPageReqVO pageVO) { + PageResult 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 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 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 syncUser(@RequestParam("accountId") Long accountId) { + mpUserService.syncUser(accountId); + return success(true); + } + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/user/vo/MpUserPageReqVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/user/vo/MpUserPageReqVO.java new file mode 100644 index 000000000..84f7187dd --- /dev/null +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/user/vo/MpUserPageReqVO.java @@ -0,0 +1,27 @@ +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 = "芋艿") + private String nickname; + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/user/vo/MpUserRespVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/user/vo/MpUserRespVO.java new file mode 100644 index 000000000..a0adcd0a5 --- /dev/null +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/user/vo/MpUserRespVO.java @@ -0,0 +1,53 @@ +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.Date; +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 = "关注状态 参见 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 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 Date createTime; + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/user/vo/MpUserUpdateReqVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/user/vo/MpUserUpdateReqVO.java new file mode 100644 index 000000000..3089ed3d3 --- /dev/null +++ b/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 tagIds; + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/package-info.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/package-info.java new file mode 100644 index 000000000..31a057767 --- /dev/null +++ b/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; diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/account/MpAccountConvert.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/account/MpAccountConvert.java new file mode 100644 index 000000000..5f8fde36a --- /dev/null +++ b/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 convertList(List list); + + PageResult convertPage(PageResult page); + + List convertList02(List list); + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/material/MpMaterialConvert.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/material/MpMaterialConvert.java new file mode 100644 index 000000000..ca4f69c3d --- /dev/null +++ b/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 convertPage(PageResult page); + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/menu/MpMenuConvert.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/menu/MpMenuConvert.java new file mode 100644 index 000000000..b53e52af1 --- /dev/null +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/menu/MpMenuConvert.java @@ -0,0 +1,48 @@ +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 convertList(List 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 convert(List list); + + @Mappings({ + @Mapping(source = "menuKey", target = "key"), + @Mapping(source = "children", target = "subButtons"), + }) + WxMenuButton convert(MpMenuSaveReqVO.Menu bean); + + MpMenuDO convert02(MpMenuSaveReqVO.Menu menu); + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/message/MpAutoReplyConvert.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/message/MpAutoReplyConvert.java new file mode 100644 index 000000000..c7cf8902e --- /dev/null +++ b/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 convertPage(PageResult page); + + MpAutoReplyRespVO convert(MpAutoReplyDO bean); + + MpAutoReplyDO convert(MpAutoReplyCreateReqVO bean); + + MpAutoReplyDO convert(MpAutoReplyUpdateReqVO bean); +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/message/MpMessageConvert.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/message/MpMessageConvert.java new file mode 100644 index 000000000..8f31a683a --- /dev/null +++ b/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 convertList(List list); + + PageResult convertPage(PageResult 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 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 convertList02(List 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 convertList03(List 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); + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/statistics/MpStatisticsConvert.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/statistics/MpStatisticsConvert.java new file mode 100644 index 000000000..174b0fdc3 --- /dev/null +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/statistics/MpStatisticsConvert.java @@ -0,0 +1,40 @@ +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.factory.Mappers; + +import java.util.List; + +@Mapper +public interface MpStatisticsConvert { + + MpStatisticsConvert INSTANCE = Mappers.getMapper(MpStatisticsConvert.class); + + List convertList01(List list); + + List convertList02(List list); + + List convertList03(List list); + + @Mappings({ + @Mapping(source = "refDate", target = "refDate", dateFormat = "yyyy-MM-dd"), + @Mapping(source = "msgUser", target = "messageUser"), + @Mapping(source = "msgCount", target = "messageCount"), + }) + MpStatisticsUpstreamMessageRespVO convert(WxDataCubeMsgResult bean); + + List convertList04(List list); + + @Mapping(source = "refDate", target = "refDate", dateFormat = "yyyy-MM-dd") + MpStatisticsInterfaceSummaryRespVO convert(WxDataCubeInterfaceResult bean); +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/tag/MpTagConvert.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/tag/MpTagConvert.java new file mode 100644 index 000000000..727ccd314 --- /dev/null +++ b/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 convertList(List list); + + PageResult convertPage(PageResult 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 convertList02(List list); + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/user/MpUserConvert.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/user/MpUserConvert.java new file mode 100644 index 000000000..9ad7edf74 --- /dev/null +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/user/MpUserConvert.java @@ -0,0 +1,55 @@ +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 convertList(List list); + + PageResult convertPage(PageResult page); + + @Mappings(value = { + @Mapping(source = "openId", target = "openid"), + @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 convertList(MpAccountDO account, List wxUsers) { + return CollectionUtils.convertList(wxUsers, wxUser -> convert(account, wxUser)); + } + + MpUserDO convert(MpUserUpdateReqVO bean); + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/account/MpAccountDO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/account/MpAccountDO.java new file mode 100644 index 000000000..eddf87ba0 --- /dev/null +++ b/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; + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/material/MpMaterialDO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/material/MpMaterialDO.java new file mode 100644 index 000000000..735235e31 --- /dev/null +++ b/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. 临时素材 + * 2. 永久素材 + * + * @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; + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/menu/MpMenuDO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/menu/MpMenuDO.java new file mode 100644 index 000000000..2e6aa73ce --- /dev/null +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/menu/MpMenuDO.java @@ -0,0 +1,184 @@ +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 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 = MpMessageDO.ArticleTypeHandler.class) + private List replyArticles; + + /** + * 回复的音乐链接 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC + */ + private String replyMusicUrl; + /** + * 回复的高质量音乐链接 + * + * WIFI 环境优先使用该链接播放音乐 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC + */ + private String replyHqMusicUrl; + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/message/MpAutoReplyDO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/message/MpAutoReplyDO.java new file mode 100644 index 000000000..52f4539a4 --- /dev/null +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/message/MpAutoReplyDO.java @@ -0,0 +1,164 @@ +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 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 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 = MpMessageDO.ArticleTypeHandler.class) + private List responseArticles; + + /** + * 回复的音乐链接 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC + */ + private String responseMusicUrl; + /** + * 回复的高质量音乐链接 + * + * WIFI 环境优先使用该链接播放音乐 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC + */ + private String responseHqMusicUrl; + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/message/MpMessageDO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/message/MpMessageDO.java new file mode 100644 index 000000000..cb1401091 --- /dev/null +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/message/MpMessageDO.java @@ -0,0 +1,255 @@ +package cn.iocoder.yudao.module.mp.dal.dataobject.message; + +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +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.AbstractJsonTypeHandler; +import lombok.*; +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 = ArticleTypeHandler.class) + private List
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; + + } + + // TODO @芋艿:可以找一些新的思路 + public static class ArticleTypeHandler extends AbstractJsonTypeHandler> { + + @Override + protected List
parse(String json) { + return JsonUtils.parseArray(json, Article.class); + } + + @Override + protected String toJson(List
obj) { + return JsonUtils.toJsonString(obj); + } + + } +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/tag/MpTagDO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/tag/MpTagDO.java new file mode 100644 index 000000000..8a5b9f6eb --- /dev/null +++ b/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; + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/user/MpUserDO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/user/MpUserDO.java new file mode 100644 index 000000000..0b937759e --- /dev/null +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/user/MpUserDO.java @@ -0,0 +1,110 @@ +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; + /** + * 关注状态 + * + * 枚举 {@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 tagIds; + + /** + * 公众号账号的编号 + * + * 关联 {@link MpAccountDO#getId()} + */ + private Long accountId; + /** + * 公众号 appId + * + * 冗余 {@link MpAccountDO#getAppId()} + */ + private String appId; + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/account/MpAccountMapper.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/account/MpAccountMapper.java new file mode 100644 index 000000000..19daa374a --- /dev/null +++ b/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 { + + default PageResult selectPage(MpAccountPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .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 pay_account WHERE update_time > #{maxUpdateTime}") + Long selectCountByUpdateTimeGt(LocalDateTime maxUpdateTime); + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/material/MpMaterialMapper.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/material/MpMaterialMapper.java new file mode 100644 index 000000000..ca3c6b3a9 --- /dev/null +++ b/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 { + + default MpMaterialDO selectByAccountIdAndMediaId(Long accountId, String mediaId) { + return selectOne(MpMaterialDO::getAccountId, accountId, + MpMaterialDO::getMediaId, mediaId); + } + + default PageResult selectPage(MpMaterialPageReqVO pageReqVO) { + return selectPage(pageReqVO, new LambdaQueryWrapperX() + .eq(MpMaterialDO::getAccountId, pageReqVO.getAccountId()) + .eqIfPresent(MpMaterialDO::getPermanent, pageReqVO.getPermanent()) + .eqIfPresent(MpMaterialDO::getType, pageReqVO.getType()) + .orderByDesc(MpMaterialDO::getId)); + } + + default List selectListByMediaId(Collection mediaIds) { + return selectList(MpMaterialDO::getMediaId, mediaIds); + } + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/menu/MpMenuMapper.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/menu/MpMenuMapper.java new file mode 100644 index 000000000..fccd7f2d9 --- /dev/null +++ b/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 { + + default MpMenuDO selectByAppIdAndMenuKey(String appId, String menuKey) { + return selectOne(MpMenuDO::getAppId, appId, + MpMenuDO::getMenuKey, menuKey); + } + + default List selectListByAccountId(Long accountId) { + return selectList(MpMenuDO::getAccountId, accountId); + } + + default void deleteByAccountId(Long accountId) { + delete(new LambdaQueryWrapperX().eq(MpMenuDO::getAccountId, accountId)); + } +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/message/MpAutoReplyMapper.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/message/MpAutoReplyMapper.java new file mode 100644 index 000000000..c8260e8c7 --- /dev/null +++ b/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 { + + default PageResult selectPage(MpMessagePageReqVO pageVO) { + return selectPage(pageVO, new LambdaQueryWrapperX() + .eq(MpAutoReplyDO::getAccountId, pageVO.getAccountId()) + .eqIfPresent(MpAutoReplyDO::getType, pageVO.getType())); + } + + default List selectListByAppIdAndKeywordAll(String appId, String requestKeyword) { + return selectList(new LambdaQueryWrapperX() + .eq(MpAutoReplyDO::getAppId, appId) + .eq(MpAutoReplyDO::getType, MpAutoReplyTypeEnum.KEYWORD.getType()) + .eq(MpAutoReplyDO::getRequestMatch, MpAutoReplyMatchEnum.ALL.getMatch()) + .eq(MpAutoReplyDO::getRequestKeyword, requestKeyword)); + } + + default List selectListByAppIdAndKeywordLike(String appId, String requestKeyword) { + return selectList(new LambdaQueryWrapperX() + .eq(MpAutoReplyDO::getAppId, appId) + .eq(MpAutoReplyDO::getType, MpAutoReplyTypeEnum.KEYWORD.getType()) + .eq(MpAutoReplyDO::getRequestMatch, MpAutoReplyMatchEnum.LIKE.getMatch()) + .like(MpAutoReplyDO::getRequestKeyword, requestKeyword)); + } + + default List selectListByAppIdAndMessage(String appId, String requestMessageType) { + return selectList(new LambdaQueryWrapperX() + .eq(MpAutoReplyDO::getAppId, appId) + .eq(MpAutoReplyDO::getType, MpAutoReplyTypeEnum.MESSAGE.getType()) + .eq(MpAutoReplyDO::getRequestMessageType, requestMessageType)); + } + + default List selectListByAppIdAndSubscribe(String appId) { + return selectList(new LambdaQueryWrapperX() + .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() + .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() + .eq(MpAutoReplyDO::getAccountId, accountId) + .eq(MpAutoReplyDO::getType, MpAutoReplyTypeEnum.KEYWORD.getType()) + .eq(MpAutoReplyDO::getRequestKeyword, requestKeyword)); + } +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/message/MpMessageMapper.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/message/MpMessageMapper.java new file mode 100644 index 000000000..72ba56627 --- /dev/null +++ b/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 { + + default PageResult selectPage(MpMessagePageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(MpMessageDO::getAccountId, reqVO.getAccountId()) + .eqIfPresent(MpMessageDO::getType, reqVO.getType()) + .eqIfPresent(MpMessageDO::getOpenid, reqVO.getOpenid()) + .betweenIfPresent(MpMessageDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(MpMessageDO::getId)); + } + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/tag/MpTagMapper.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/tag/MpTagMapper.java new file mode 100644 index 000000000..760f53aa1 --- /dev/null +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/tag/MpTagMapper.java @@ -0,0 +1,26 @@ +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 { + + default PageResult selectPage(MpTagPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(MpTagDO::getAccountId, reqVO.getAccountId()) + .likeIfPresent(MpTagDO::getName, reqVO.getName()) + .orderByDesc(MpTagDO::getId)); + } + + default List selectListByAccountId(Long accountId) { + return selectList(MpTagDO::getAccountId, accountId); + } + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/user/MpUserMapper.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/user/MpUserMapper.java new file mode 100644 index 000000000..bd13f925a --- /dev/null +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/user/MpUserMapper.java @@ -0,0 +1,35 @@ +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 java.util.List; + +@Mapper +public interface MpUserMapper extends BaseMapperX { + + default PageResult selectPage(MpUserPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .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 selectListByAppIdAndOpenid(String appId, List openids) { + return selectList(new LambdaQueryWrapperX() + .eq(MpUserDO::getAppId, appId) + .in(MpUserDO::getOpenid, openids)); + + } + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/mp/config/MpConfiguration.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/mp/config/MpConfiguration.java new file mode 100644 index 000000000..c90866066 --- /dev/null +++ b/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); + } + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/mp/core/DefaultMpServiceFactory.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/mp/core/DefaultMpServiceFactory.java new file mode 100644 index 000000000..414f73aa8 --- /dev/null +++ b/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 appId2MpServices; + /** + * 公众号账号 id 与 WxMpService 的映射 + */ + private volatile Map id2MpServices; + /** + * 微信 appId 与 WxMpMessageRouter 的映射 + */ + private volatile Map 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 list) { + Map appId2MpServices = Maps.newHashMap(); + Map id2MpServices = Maps.newHashMap(); + Map 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; + } + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/mp/core/MpServiceFactory.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/mp/core/MpServiceFactory.java new file mode 100644 index 000000000..ce017ff64 --- /dev/null +++ b/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 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; + } + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/mp/core/context/MpContextHolder.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/mp/core/context/MpContextHolder.java new file mode 100644 index 000000000..e5c45c531 --- /dev/null +++ b/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 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(); + } + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/mp/core/util/MpUtils.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/mp/core/util/MpUtils.java new file mode 100644 index 000000000..880d8c8f5 --- /dev/null +++ b/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 {} +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/package-info.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/package-info.java new file mode 100644 index 000000000..112ecdd26 --- /dev/null +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/package-info.java @@ -0,0 +1,6 @@ +/** + * 属于 mp 模块的 framework 封装 + * + * @author 芋道源码 + */ +package cn.iocoder.yudao.module.mp.framework; diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/web/config/MpWebConfiguration.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/web/config/MpWebConfiguration.java new file mode 100644 index 000000000..3e6e1acfc --- /dev/null +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/web/config/MpWebConfiguration.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.mp.framework.web.config; + +import cn.iocoder.yudao.framework.swagger.config.YudaoSwaggerAutoConfiguration; +import org.springdoc.core.GroupedOpenApi; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * mp 模块的 web 组件的 Configuration + * + * @author 芋道源码 + */ +@Configuration(proxyBeanMethods = false) +public class MpWebConfiguration { + + /** + * mp 模块的 API 分组 + */ + @Bean + public GroupedOpenApi mpGroupedOpenApi() { + return YudaoSwaggerAutoConfiguration.buildGroupedOpenApi("mp"); + } + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/web/package-info.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/web/package-info.java new file mode 100644 index 000000000..a103f1c38 --- /dev/null +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/web/package-info.java @@ -0,0 +1,4 @@ +/** + * mp 模块的 web 配置 + */ +package cn.iocoder.yudao.module.mp.framework.web; diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/package-info.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/package-info.java new file mode 100644 index 000000000..598718088 --- /dev/null +++ b/yudao-module-mp/yudao-module-mp-biz/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; diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/account/MpAccountService.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/account/MpAccountService.java new file mode 100644 index 000000000..e122c6a98 --- /dev/null +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/account/MpAccountService.java @@ -0,0 +1,110 @@ +package cn.iocoder.yudao.module.mp.service.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.MpAccountPageReqVO; +import cn.iocoder.yudao.module.mp.controller.admin.account.vo.MpAccountUpdateReqVO; +import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO; + +import javax.validation.Valid; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.mp.enums.ErrorCodeConstants.ACCOUNT_NOT_EXISTS; + +/** + * 公众号账号 Service 接口 + * + * @author 芋道源码 + */ +public interface MpAccountService { + + /** + * 初始化缓存 + */ + void initLocalCache(); + + /** + * 创建公众号账号 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createAccount(@Valid MpAccountCreateReqVO createReqVO); + + /** + * 更新公众号账号 + * + * @param updateReqVO 更新信息 + */ + void updateAccount(@Valid MpAccountUpdateReqVO updateReqVO); + + /** + * 删除公众号账号 + * + * @param id 编号 + */ + void deleteAccount(Long id); + + /** + * 获得公众号账号 + * + * @param id 编号 + * @return 公众号账号 + */ + MpAccountDO getAccount(Long id); + + /** + * 获得公众号账号。若不存在,则抛出业务异常 + * + * @param id 编号 + * @return 公众号账号 + */ + default MpAccountDO getRequiredAccount(Long id) { + MpAccountDO account = getAccount(id); + if (account == null) { + throw exception(ACCOUNT_NOT_EXISTS); + } + return account; + } + + /** + * 从缓存中,获得公众号账号 + * + * @param appId 微信公众号 appId + * @return 公众号账号 + */ + MpAccountDO getAccountFromCache(String appId); + + /** + * 获得公众号账号分页 + * + * @param pageReqVO 分页查询 + * @return 公众号账号分页 + */ + PageResult getAccountPage(MpAccountPageReqVO pageReqVO); + + /** + * 获得公众号账号列表 + * + * @return 公众号账号列表 + */ + List getAccountList(); + + /** + * 生成公众号账号的二维码 + * + * @param id 编号 + */ + void generateAccountQrCode(Long id); + + /** + * 清空公众号账号的 API 配额 + * + * 参考文档:接口调用频次限制说明 + * + * @param id 编号 + */ + void clearAccountQuota(Long id); + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/account/MpAccountServiceImpl.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/account/MpAccountServiceImpl.java new file mode 100644 index 000000000..af359b02a --- /dev/null +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/account/MpAccountServiceImpl.java @@ -0,0 +1,220 @@ +package cn.iocoder.yudao.module.mp.service.account; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjUtil; +import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; +import cn.iocoder.yudao.module.mp.controller.admin.account.vo.MpAccountCreateReqVO; +import cn.iocoder.yudao.module.mp.controller.admin.account.vo.MpAccountPageReqVO; +import cn.iocoder.yudao.module.mp.controller.admin.account.vo.MpAccountUpdateReqVO; +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.dal.mysql.account.MpAccountMapper; +import cn.iocoder.yudao.module.mp.enums.ErrorCodeConstants; +import cn.iocoder.yudao.module.mp.framework.mp.core.MpServiceFactory; +import com.google.common.annotations.VisibleForTesting; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.mp.api.WxMpService; +import me.chanjar.weixin.mp.bean.result.WxMpQrCodeTicket; +import org.springframework.context.annotation.Lazy; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.getMaxValue; +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.USER_USERNAME_EXISTS; + +/** + * 公众号账号 Service 实现类 + * + * @author fengdan + */ +@Slf4j +@Service +@Validated +public class MpAccountServiceImpl implements MpAccountService { + + /** + * 账号缓存 + * key:账号编号 {@link MpAccountDO#getAppId()} + * + * 这里声明 volatile 修饰的原因是,每次刷新时,直接修改指向 + */ + @Getter + private volatile Map accountCache; + + @Resource + private MpAccountMapper mpAccountMapper; + + @Resource + @Lazy // 延迟加载,解决循环依赖的问题 + private MpServiceFactory mpServiceFactory; + + @Override + @PostConstruct + public void initLocalCache() { + // 注意:忽略自动多租户,因为要全局初始化缓存 + TenantUtils.executeIgnore(() -> { + // 第一步:查询数据 + List accounts = mpAccountMapper.selectList(); + log.info("[initLocalCacheIfUpdate][缓存公众号账号,数量为:{}]", accounts.size()); + + // 第二步:构建缓存。创建或更新支付 Client + mpServiceFactory.init(accounts); + accountCache = convertMap(accounts, MpAccountDO::getAppId); + }); + } + + /** + * 通过定时任务轮询,刷新缓存 + * + * 目的:多节点部署时,通过轮询”通知“所有节点,进行刷新 + */ + @Scheduled(initialDelay = 60, fixedRate = 60, timeUnit = TimeUnit.SECONDS) + public void refreshLocalCache() { + // 注意:忽略自动多租户,因为要全局初始化缓存 + TenantUtils.executeIgnore(() -> { + // 情况一:如果缓存里没有数据,则直接刷新缓存 + if (CollUtil.isEmpty(accountCache)) { + initLocalCache(); + return; + } + + // 情况二,如果缓存里数据,则通过 updateTime 判断是否有数据变更,有变更则刷新缓存 + LocalDateTime maxTime = getMaxValue(accountCache.values(), MpAccountDO::getUpdateTime); + if (mpAccountMapper.selectCountByUpdateTimeGt(maxTime) > 0) { + initLocalCache(); + } + }); + } + + @Override + public Long createAccount(MpAccountCreateReqVO createReqVO) { + // 校验 appId 唯一 + validateAppIdUnique(null, createReqVO.getAppId()); + + // 插入 + MpAccountDO account = MpAccountConvert.INSTANCE.convert(createReqVO); + mpAccountMapper.insert(account); + + // 刷新缓存 + initLocalCache(); + return account.getId(); + } + + @Override + public void updateAccount(MpAccountUpdateReqVO updateReqVO) { + // 校验存在 + validateAccountExists(updateReqVO.getId()); + // 校验 appId 唯一 + validateAppIdUnique(updateReqVO.getId(), updateReqVO.getAppId()); + + // 更新 + MpAccountDO updateObj = MpAccountConvert.INSTANCE.convert(updateReqVO); + mpAccountMapper.updateById(updateObj); + + // 刷新缓存 + initLocalCache(); + } + + @Override + public void deleteAccount(Long id) { + // 校验存在 + validateAccountExists(id); + // 删除 + mpAccountMapper.deleteById(id); + + // 刷新缓存 + initLocalCache(); + } + + private MpAccountDO validateAccountExists(Long id) { + MpAccountDO account = mpAccountMapper.selectById(id); + if (account == null) { + throw ServiceExceptionUtil.exception(ErrorCodeConstants.ACCOUNT_NOT_EXISTS); + } + return account; + } + + @VisibleForTesting + public void validateAppIdUnique(Long id, String appId) { + // 多个租户,appId 是不能重复,否则公众号回调会无法识别 + TenantUtils.executeIgnore(() -> { + MpAccountDO account = mpAccountMapper.selectByAppId(appId); + if (account == null) { + return; + } + // 存在 account 记录的情况下 + if (id == null // 新增时,说明重复 + || ObjUtil.notEqual(id, account.getId())) { // 更新时,如果 id 不一致,说明重复 + throw exception(USER_USERNAME_EXISTS); + } + }); + } + + @Override + public MpAccountDO getAccount(Long id) { + return mpAccountMapper.selectById(id); + } + + @Override + public MpAccountDO getAccountFromCache(String appId) { + return accountCache.get(appId); + } + + @Override + public PageResult getAccountPage(MpAccountPageReqVO pageReqVO) { + return mpAccountMapper.selectPage(pageReqVO); + } + + @Override + public List getAccountList() { + return mpAccountMapper.selectList(); + } + + @Override + public void generateAccountQrCode(Long id) { + // 校验存在 + MpAccountDO account = validateAccountExists(id); + + // 生成二维码 + WxMpService mpService = mpServiceFactory.getRequiredMpService(account.getAppId()); + String qrCodeUrl; + try { + WxMpQrCodeTicket qrCodeTicket = mpService.getQrcodeService().qrCodeCreateLastTicket("default"); + qrCodeUrl = mpService.getQrcodeService().qrCodePictureUrl(qrCodeTicket.getTicket()); + } catch (WxErrorException e) { + throw exception(ErrorCodeConstants.ACCOUNT_GENERATE_QR_CODE_FAIL, e.getError().getErrorMsg()); + } + + // 保存二维码 + mpAccountMapper.updateById(new MpAccountDO().setId(id).setQrCodeUrl(qrCodeUrl)); + } + + @Override + public void clearAccountQuota(Long id) { + // 校验存在 + MpAccountDO account = validateAccountExists(id); + + // 生成二维码 + WxMpService mpService = mpServiceFactory.getRequiredMpService(account.getAppId()); + try { + mpService.clearQuota(account.getAppId()); + } catch (WxErrorException e) { + throw exception(ErrorCodeConstants.ACCOUNT_CLEAR_QUOTA_FAIL, e.getError().getErrorMsg()); + } + } + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/menu/MenuHandler.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/menu/MenuHandler.java new file mode 100644 index 000000000..039712514 --- /dev/null +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/menu/MenuHandler.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.mp.service.handler.menu; + +import cn.iocoder.yudao.module.mp.framework.mp.core.context.MpContextHolder; +import cn.iocoder.yudao.module.mp.service.menu.MpMenuService; +import me.chanjar.weixin.common.session.WxSessionManager; +import me.chanjar.weixin.mp.api.WxMpMenuService; +import me.chanjar.weixin.mp.api.WxMpMessageHandler; +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.stereotype.Component; + +import javax.annotation.Resource; +import java.util.Map; + +import static me.chanjar.weixin.common.api.WxConsts.MenuButtonType; + +/** + * 自定义菜单的事件处理器 + * + * 逻辑:粉丝点击菜单时,触发对应的回复 + * + * @author 芋道源码 + */ +@Component +public class MenuHandler implements WxMpMessageHandler { + + @Resource + private MpMenuService mpMenuService; + + @Override + public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map context, + WxMpService weixinService, WxSessionManager sessionManager) { + return mpMenuService.reply(MpContextHolder.getAppId(), wxMessage.getEventKey(), wxMessage.getFromUser()); + } + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/message/MessageAutoReplyHandler.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/message/MessageAutoReplyHandler.java new file mode 100644 index 000000000..30d94a97c --- /dev/null +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/message/MessageAutoReplyHandler.java @@ -0,0 +1,41 @@ +package cn.iocoder.yudao.module.mp.service.handler.message; + +import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpAutoReplyDO; +import cn.iocoder.yudao.module.mp.framework.mp.core.context.MpContextHolder; +import cn.iocoder.yudao.module.mp.service.message.MpAutoReplyService; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.session.WxSessionManager; +import me.chanjar.weixin.mp.api.WxMpMessageHandler; +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.stereotype.Component; + +import javax.annotation.Resource; +import java.util.Map; + +/** + * 自动回复消息的事件处理器 + * + * @author 芋道源码 + */ +@Component +@Slf4j +public class MessageAutoReplyHandler implements WxMpMessageHandler { + + @Resource + private MpAutoReplyService mpAutoReplyService; + + @Override + public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map context, + WxMpService weixinService, WxSessionManager sessionManager) { + // 只处理指定类型的消息 + if (!MpAutoReplyDO.REQUEST_MESSAGE_TYPE.contains(wxMessage.getMsgType())) { + return null; + } + + // 自动回复 + return mpAutoReplyService.replyForMessage(MpContextHolder.getAppId(), wxMessage); + } + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/message/MessageReceiveHandler.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/message/MessageReceiveHandler.java new file mode 100644 index 000000000..4e4a4ebf4 --- /dev/null +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/message/MessageReceiveHandler.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.mp.service.handler.message; + +import cn.iocoder.yudao.module.mp.framework.mp.core.context.MpContextHolder; +import cn.iocoder.yudao.module.mp.service.message.MpMessageService; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.session.WxSessionManager; +import me.chanjar.weixin.mp.api.WxMpMessageHandler; +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.stereotype.Component; + +import javax.annotation.Resource; +import java.util.Map; + +/** + * 保存微信消息的事件处理器 + * + * @author 芋道源码 + */ +@Component +@Slf4j +public class MessageReceiveHandler implements WxMpMessageHandler { + + @Resource + private MpMessageService mpMessageService; + + @Override + public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map context, + WxMpService wxMpService, WxSessionManager sessionManager) { + log.info("[handle][接收到请求消息,内容:{}]", wxMessage); + mpMessageService.receiveMessage(MpContextHolder.getAppId(), wxMessage); + return null; + } + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/other/KfSessionHandler.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/other/KfSessionHandler.java new file mode 100644 index 000000000..fca7fab9a --- /dev/null +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/other/KfSessionHandler.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.module.mp.service.handler.other; + +import me.chanjar.weixin.common.session.WxSessionManager; +import me.chanjar.weixin.mp.api.WxMpMessageHandler; +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.stereotype.Component; + +import java.util.Map; + +/** + * 接收客服会话管理的事件处理器 + * + * @author 芋道源码 + */ +@Component +public class KfSessionHandler implements WxMpMessageHandler { + + @Override + public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map context, + WxMpService wxMpService, WxSessionManager sessionManager) { + throw new UnsupportedOperationException("未实现该处理,请自行重写"); + } + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/other/NullHandler.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/other/NullHandler.java new file mode 100644 index 000000000..09b3efbf2 --- /dev/null +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/other/NullHandler.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.mp.service.handler.other; + +import me.chanjar.weixin.common.session.WxSessionManager; +import me.chanjar.weixin.mp.api.WxMpMessageHandler; +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.stereotype.Component; + +import java.util.Map; + +/** + * 点击菜单连接的事件处理器 + */ +@Component +public class NullHandler implements WxMpMessageHandler { + + @Override + public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map context, + WxMpService wxMpService, WxSessionManager sessionManager) { + throw new UnsupportedOperationException("未实现该处理,请自行重写"); + } + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/other/ScanHandler.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/other/ScanHandler.java new file mode 100644 index 000000000..54a6bbc32 --- /dev/null +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/other/ScanHandler.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.mp.service.handler.other; + +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.session.WxSessionManager; +import me.chanjar.weixin.mp.api.WxMpMessageHandler; +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.stereotype.Component; + +import java.util.Map; + +/** + * 扫码的事件处理器 + */ +@Component +public class ScanHandler implements WxMpMessageHandler { + + @Override + public WxMpXmlOutMessage handle(WxMpXmlMessage wxMpXmlMessage, Map context, + WxMpService wxMpService, WxSessionManager wxSessionManager) throws WxErrorException { + throw new UnsupportedOperationException("未实现该处理,请自行重写"); + } + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/other/StoreCheckNotifyHandler.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/other/StoreCheckNotifyHandler.java new file mode 100644 index 000000000..475b8ee97 --- /dev/null +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/other/StoreCheckNotifyHandler.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.mp.service.handler.other; + +import me.chanjar.weixin.common.session.WxSessionManager; +import me.chanjar.weixin.mp.api.WxMpMessageHandler; +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.stereotype.Component; + +import java.util.Map; + +/** + * 门店审核事件的事件处理器 + */ +@Component +public class StoreCheckNotifyHandler implements WxMpMessageHandler { + + @Override + public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map context, + WxMpService wxMpService, WxSessionManager sessionManager) { + throw new UnsupportedOperationException("未实现该处理,请自行重写"); + } + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/other/package-info.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/other/package-info.java new file mode 100644 index 000000000..7fd662260 --- /dev/null +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/other/package-info.java @@ -0,0 +1,4 @@ +/** + * 本包内的 handler 都是一些不重要的,所以放在 other 其它里 + */ +package cn.iocoder.yudao.module.mp.service.handler.other; diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/user/LocationHandler.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/user/LocationHandler.java new file mode 100644 index 000000000..598ebb91a --- /dev/null +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/user/LocationHandler.java @@ -0,0 +1,49 @@ +package cn.iocoder.yudao.module.mp.service.handler.user; + +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; +import cn.iocoder.yudao.module.mp.framework.mp.core.context.MpContextHolder; +import cn.iocoder.yudao.module.mp.service.message.MpAutoReplyService; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.api.WxConsts; +import me.chanjar.weixin.common.session.WxSessionManager; +import me.chanjar.weixin.mp.api.WxMpMessageHandler; +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.stereotype.Component; + +import javax.annotation.Resource; +import java.util.Map; + +/** + * 上报地理位置的事件处理器 + * + * 触发操作:打开微信公众号 -> 点击 + 号 -> 选择「语音」 + * + * 逻辑:粉丝上传地理位置时,也可以触发自动回复 + * + * @author 芋道源码 + */ +@Component +@Slf4j +public class LocationHandler implements WxMpMessageHandler { + + @Resource + private MpAutoReplyService mpAutoReplyService; + + @Override + public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map context, + WxMpService wxMpService, WxSessionManager sessionManager) { + // 防御性编程:必须是 LOCATION 消息 + if (ObjectUtil.notEqual(wxMessage.getMsgType(), WxConsts.XmlMsgType.LOCATION)) { + return null; + } + log.info("[handle][上报地理位置,纬度({})、经度({})、精度({})", wxMessage.getLatitude(), + wxMessage.getLongitude(), wxMessage.getPrecision()); + + // 自动回复 + return mpAutoReplyService.replyForMessage(MpContextHolder.getAppId(), wxMessage); + } + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/user/SubscribeHandler.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/user/SubscribeHandler.java new file mode 100644 index 000000000..5f3c3eacd --- /dev/null +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/user/SubscribeHandler.java @@ -0,0 +1,52 @@ +package cn.iocoder.yudao.module.mp.service.handler.user; + +import cn.iocoder.yudao.module.mp.framework.mp.core.context.MpContextHolder; +import cn.iocoder.yudao.module.mp.service.message.MpAutoReplyService; +import cn.iocoder.yudao.module.mp.service.user.MpUserService; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.session.WxSessionManager; +import me.chanjar.weixin.mp.api.WxMpMessageHandler; +import me.chanjar.weixin.mp.api.WxMpService; +import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; +import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; +import me.chanjar.weixin.mp.bean.result.WxMpUser; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.Map; + +/** + * 关注的事件处理器 + * + * @author 芋道源码 + */ +@Component +@Slf4j +public class SubscribeHandler implements WxMpMessageHandler { + + @Resource + private MpUserService mpUserService; + @Resource + private MpAutoReplyService mpAutoReplyService; + + @Override + public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map context, + WxMpService weixinService, WxSessionManager sessionManager) throws WxErrorException { + // 第一步,从公众号平台,获取粉丝信息 + log.info("[handle][粉丝({}) 关注]", wxMessage.getFromUser()); + WxMpUser wxMpUser = null; + try { + wxMpUser = weixinService.getUserService().userInfo(wxMessage.getFromUser()); + } catch (WxErrorException e) { + log.error("[handle][粉丝({})] 获取粉丝信息失败!", wxMessage.getFromUser(), e); + } + + // 第二步,保存粉丝信息 + mpUserService.saveUser(MpContextHolder.getAppId(), wxMpUser); + + // 第三步,回复关注的欢迎语 + return mpAutoReplyService.replyForSubscribe(MpContextHolder.getAppId(), wxMessage); + } + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/user/UnsubscribeHandler.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/user/UnsubscribeHandler.java new file mode 100644 index 000000000..3bf363506 --- /dev/null +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/user/UnsubscribeHandler.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.mp.service.handler.user; + +import cn.iocoder.yudao.module.mp.framework.mp.core.context.MpContextHolder; +import cn.iocoder.yudao.module.mp.service.user.MpUserService; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.session.WxSessionManager; +import me.chanjar.weixin.mp.api.WxMpMessageHandler; +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.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.Map; + +/** + * 取消关注的事件处理器 + * + * @author 芋道源码 + */ +@Component +@Slf4j +public class UnsubscribeHandler implements WxMpMessageHandler { + + @Resource + @Lazy // 延迟加载,解决循环依赖的问题 + private MpUserService mpUserService; + + @Override + public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, + Map context, WxMpService wxMpService, + WxSessionManager sessionManager) { + log.info("[handle][粉丝({}) 取消关注]", wxMessage.getFromUser()); + mpUserService.updateUserUnsubscribe(MpContextHolder.getAppId(), wxMessage.getFromUser()); + return null; + } + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/material/MpMaterialService.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/material/MpMaterialService.java new file mode 100644 index 000000000..60e4c07b4 --- /dev/null +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/material/MpMaterialService.java @@ -0,0 +1,84 @@ +package cn.iocoder.yudao.module.mp.service.material; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.mp.controller.admin.material.vo.MpMaterialPageReqVO; +import cn.iocoder.yudao.module.mp.controller.admin.material.vo.MpMaterialUploadNewsImageReqVO; +import cn.iocoder.yudao.module.mp.controller.admin.material.vo.MpMaterialUploadPermanentReqVO; +import cn.iocoder.yudao.module.mp.controller.admin.material.vo.MpMaterialUploadTemporaryReqVO; +import cn.iocoder.yudao.module.mp.dal.dataobject.material.MpMaterialDO; +import me.chanjar.weixin.common.api.WxConsts; + +import javax.validation.Valid; +import java.io.IOException; +import java.util.Collection; +import java.util.List; + +/** + * 公众号素材 Service 接口 + * + * @author 芋道源码 + */ +public interface MpMaterialService { + + /** + * 获得素材的 URL + * + * 该 URL 来自我们自己的文件服务器存储的 URL,不是公众号存储的 URL + * + * @param accountId 公众号账号编号 + * @param mediaId 公众号素材 id + * @param type 文件类型 {@link WxConsts.MediaFileType} + * @return 素材的 URL + */ + String downloadMaterialUrl(Long accountId, String mediaId, String type); + + /** + * 上传临时素材 + * + * @param reqVO 请求 + * @return 素材 + * @throws IOException 文件操作发生异常 + */ + MpMaterialDO uploadTemporaryMaterial(@Valid MpMaterialUploadTemporaryReqVO reqVO) throws IOException; + + /** + * 上传永久素材 + * + * @param reqVO 请求 + * @return 素材 + * @throws IOException 文件操作发生异常 + */ + MpMaterialDO uploadPermanentMaterial(@Valid MpMaterialUploadPermanentReqVO reqVO) throws IOException; + + /** + * 上传图文内容中的图片 + * + * @param reqVO 上传请求 + * @return 图片地址 + */ + String uploadNewsImage(MpMaterialUploadNewsImageReqVO reqVO) throws IOException; + + /** + * 获得素材分页 + * + * @param pageReqVO 分页请求 + * @return 素材分页 + */ + PageResult getMaterialPage(MpMaterialPageReqVO pageReqVO); + + /** + * 获得素材列表 + * + * @param mediaIds 素材 mediaId 列表 + * @return 素材列表 + */ + List getMaterialListByMediaId(Collection mediaIds); + + /** + * 删除素材 + * + * @param id 编号 + */ + void deleteMaterial(Long id); + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/material/MpMaterialServiceImpl.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/material/MpMaterialServiceImpl.java new file mode 100644 index 000000000..9e48a8f97 --- /dev/null +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/material/MpMaterialServiceImpl.java @@ -0,0 +1,224 @@ +package cn.iocoder.yudao.module.mp.service.material; + +import cn.hutool.core.io.FileTypeUtil; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.infra.api.file.FileApi; +import cn.iocoder.yudao.module.mp.controller.admin.material.vo.MpMaterialPageReqVO; +import cn.iocoder.yudao.module.mp.controller.admin.material.vo.MpMaterialUploadNewsImageReqVO; +import cn.iocoder.yudao.module.mp.controller.admin.material.vo.MpMaterialUploadPermanentReqVO; +import cn.iocoder.yudao.module.mp.controller.admin.material.vo.MpMaterialUploadTemporaryReqVO; +import cn.iocoder.yudao.module.mp.convert.material.MpMaterialConvert; +import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO; +import cn.iocoder.yudao.module.mp.dal.dataobject.material.MpMaterialDO; +import cn.iocoder.yudao.module.mp.dal.mysql.material.MpMaterialMapper; +import cn.iocoder.yudao.module.mp.framework.mp.core.MpServiceFactory; +import cn.iocoder.yudao.module.mp.service.account.MpAccountService; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.bean.result.WxMediaUploadResult; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.mp.api.WxMpService; +import me.chanjar.weixin.mp.bean.material.WxMpMaterialUploadResult; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.io.File; +import java.io.IOException; +import java.util.Collection; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.mp.enums.ErrorCodeConstants.*; + +/** + * 公众号素材 Service 接口 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class MpMaterialServiceImpl implements MpMaterialService { + + @Resource + private MpMaterialMapper mpMaterialMapper; + + @Resource + private FileApi fileApi; + + @Resource + @Lazy // 延迟加载,解决循环依赖的问题 + private MpAccountService mpAccountService; + + @Resource + @Lazy // 延迟加载,解决循环依赖的问题 + private MpServiceFactory mpServiceFactory; + + @Override + public String downloadMaterialUrl(Long accountId, String mediaId, String type) { + // 第一步,直接从数据库查询。如果已经下载,直接返回 + MpMaterialDO material = mpMaterialMapper.selectByAccountIdAndMediaId(accountId, mediaId); + if (material != null) { + return material.getUrl(); + } + + // 第二步,尝试从临时素材中下载 + String url = downloadMedia(accountId, mediaId); + if (url == null) { + return null; + } + MpAccountDO account = mpAccountService.getRequiredAccount(accountId); + material = MpMaterialConvert.INSTANCE.convert(mediaId, type, url, account, null) + .setPermanent(false); + mpMaterialMapper.insert(material); + + // 不考虑下载永久素材,因为上传的时候已经保存 + return url; + } + + @Override + public MpMaterialDO uploadTemporaryMaterial(MpMaterialUploadTemporaryReqVO reqVO) throws IOException { + WxMpService mpService = mpServiceFactory.getRequiredMpService(reqVO.getAccountId()); + // 第一步,上传到公众号 + File file = null; + WxMediaUploadResult result; + String mediaId; + String url; + try { + // 写入到临时文件 + file = FileUtil.newFile(FileUtil.getTmpDirPath() + reqVO.getFile().getOriginalFilename()); + reqVO.getFile().transferTo(file); + // 上传到公众号 + result = mpService.getMaterialService().mediaUpload(reqVO.getType(), file); + // 上传到文件服务 + mediaId = ObjUtil.defaultIfNull(result.getMediaId(), result.getThumbMediaId()); + url = uploadFile(mediaId, file); + } catch (WxErrorException e) { + throw exception(MATERIAL_UPLOAD_FAIL, e.getError().getErrorMsg()); + } finally { + FileUtil.del(file); + } + + // 第二步,存储到数据库 + MpAccountDO account = mpAccountService.getRequiredAccount(reqVO.getAccountId()); + MpMaterialDO material = MpMaterialConvert.INSTANCE.convert(mediaId, reqVO.getType(), url, account, + reqVO.getFile().getName()).setPermanent(false); + mpMaterialMapper.insert(material); + return material; + } + + @Override + public MpMaterialDO uploadPermanentMaterial(MpMaterialUploadPermanentReqVO reqVO) throws IOException { + WxMpService mpService = mpServiceFactory.getRequiredMpService(reqVO.getAccountId()); + // 第一步,上传到公众号 + String name = StrUtil.blankToDefault(reqVO.getName(), reqVO.getFile().getName()); + File file = null; + WxMpMaterialUploadResult result; + String mediaId; + String url; + try { + // 写入到临时文件 + file = FileUtil.newFile(FileUtil.getTmpDirPath() + reqVO.getFile().getOriginalFilename()); + reqVO.getFile().transferTo(file); + // 上传到公众号 + result = mpService.getMaterialService().materialFileUpload(reqVO.getType(), + MpMaterialConvert.INSTANCE.convert(name, file, reqVO.getTitle(), reqVO.getIntroduction())); + // 上传到文件服务 + mediaId = ObjUtil.defaultIfNull(result.getMediaId(), result.getMediaId()); + url = uploadFile(mediaId, file); + } catch (WxErrorException e) { + throw exception(MATERIAL_UPLOAD_FAIL, e.getError().getErrorMsg()); + } finally { + FileUtil.del(file); + } + + // 第二步,存储到数据库 + MpAccountDO account = mpAccountService.getRequiredAccount(reqVO.getAccountId()); + MpMaterialDO material = MpMaterialConvert.INSTANCE.convert(mediaId, reqVO.getType(), url, account, + name, reqVO.getTitle(), reqVO.getIntroduction(), result.getUrl()).setPermanent(true); + mpMaterialMapper.insert(material); + return material; + } + + @Override + public String uploadNewsImage(MpMaterialUploadNewsImageReqVO reqVO) throws IOException { + WxMpService mpService = mpServiceFactory.getRequiredMpService(reqVO.getAccountId()); + File file = null; + try { + // 写入到临时文件 + file = FileUtil.newFile(FileUtil.getTmpDirPath() + reqVO.getFile().getOriginalFilename()); + reqVO.getFile().transferTo(file); + // 上传到公众号 + return mpService.getMaterialService().mediaImgUpload(file).getUrl(); + } catch (WxErrorException e) { + throw exception(MATERIAL_IMAGE_UPLOAD_FAIL, e.getError().getErrorMsg()); + } finally { + FileUtil.del(file); + } + } + + @Override + public PageResult getMaterialPage(MpMaterialPageReqVO pageReqVO) { + return mpMaterialMapper.selectPage(pageReqVO); + } + + @Override + public List getMaterialListByMediaId(Collection mediaIds) { + return mpMaterialMapper.selectListByMediaId(mediaIds); + } + + @Override + public void deleteMaterial(Long id) { + MpMaterialDO material = mpMaterialMapper.selectById(id); + if (material == null) { + throw exception(MATERIAL_NOT_EXISTS); + } + + // 第一步,从公众号删除 + if (material.getPermanent()) { + WxMpService mpService = mpServiceFactory.getRequiredMpService(material.getAppId()); + try { + mpService.getMaterialService().materialDelete(material.getMediaId()); + } catch (WxErrorException e) { + throw exception(MATERIAL_DELETE_FAIL, e.getError().getErrorMsg()); + } + } + + // 第二步,从数据库中删除 + mpMaterialMapper.deleteById(id); + } + + /** + * 下载微信媒体文件的内容,并上传到文件服务 + * + * 为什么要下载?媒体文件在微信后台保存时间为 3 天,即 3 天后 media_id 失效。 + * + * @param accountId 公众号账号的编号 + * @param mediaId 媒体文件编号 + * @return 上传后的 URL + */ + public String downloadMedia(Long accountId, String mediaId) { + WxMpService mpService = mpServiceFactory.getMpService(accountId); + for (int i = 0; i < 3; i++) { + try { + // 第一步,从公众号下载媒体文件 + File file = mpService.getMaterialService().mediaDownload(mediaId); + // 第二步,上传到文件服务 + return uploadFile(mediaId, file); + } catch (WxErrorException e) { + log.error("[mediaDownload][media({}) 第 ({}) 次下载失败]", mediaId, i); + } + } + return null; + } + + private String uploadFile(String mediaId, File file) { + String path = mediaId + "." + FileTypeUtil.getType(file); + return fileApi.createFile(path, FileUtil.readBytes(file)); + } + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/menu/MpMenuService.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/menu/MpMenuService.java new file mode 100644 index 000000000..19f3b1458 --- /dev/null +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/menu/MpMenuService.java @@ -0,0 +1,49 @@ +package cn.iocoder.yudao.module.mp.service.menu; + +import cn.iocoder.yudao.module.mp.controller.admin.menu.vo.MpMenuSaveReqVO; +import cn.iocoder.yudao.module.mp.dal.dataobject.menu.MpMenuDO; +import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; + +import javax.validation.Valid; +import java.util.List; + +/** + * 公众号菜单 Service 接口 + * + * @author 芋道源码 + */ +public interface MpMenuService { + + /** + * 保存公众号菜单 + * + * @param createReqVO 创建信息 + */ + void saveMenu(@Valid MpMenuSaveReqVO createReqVO); + + /** + * 删除公众号菜单 + * + * @param accountId 公众号账号的编号 + */ + void deleteMenuByAccountId(Long accountId); + + /** + * 粉丝点击菜单按钮时,回复对应的消息 + * + * @param appId 公众号 AppId + * @param key 菜单按钮的标识 + * @param openid 粉丝的 openid + * @return 消息 + */ + WxMpXmlOutMessage reply(String appId, String key, String openid); + + /** + * 获得公众号菜单列表 + * + * @param accountId 公众号账号的编号 + * @return 公众号菜单列表 + */ + List getMenuListByAccountId(Long accountId); + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/menu/MpMenuServiceImpl.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/menu/MpMenuServiceImpl.java new file mode 100644 index 000000000..eb9ceba89 --- /dev/null +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/menu/MpMenuServiceImpl.java @@ -0,0 +1,171 @@ +package cn.iocoder.yudao.module.mp.service.menu; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +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.account.MpAccountDO; +import cn.iocoder.yudao.module.mp.dal.dataobject.menu.MpMenuDO; +import cn.iocoder.yudao.module.mp.dal.mysql.menu.MpMenuMapper; +import cn.iocoder.yudao.module.mp.framework.mp.core.MpServiceFactory; +import cn.iocoder.yudao.module.mp.framework.mp.core.util.MpUtils; +import cn.iocoder.yudao.module.mp.service.account.MpAccountService; +import cn.iocoder.yudao.module.mp.service.message.MpMessageService; +import cn.iocoder.yudao.module.mp.service.message.bo.MpMessageSendOutReqBO; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.bean.menu.WxMenu; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.mp.api.WxMpService; +import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import javax.validation.Validator; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.mp.enums.ErrorCodeConstants.MENU_DELETE_FAIL; +import static cn.iocoder.yudao.module.mp.enums.ErrorCodeConstants.MENU_SAVE_FAIL; + +/** + * 公众号菜单 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class MpMenuServiceImpl implements MpMenuService { + + @Resource + private MpMessageService mpMessageService; + @Resource + @Lazy // 延迟加载,避免循环引用报错 + private MpAccountService mpAccountService; + + @Resource + @Lazy // 延迟加载,避免循环引用报错 + private MpServiceFactory mpServiceFactory; + + @Resource + private Validator validator; + + @Resource + private MpMenuMapper mpMenuMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public void saveMenu(MpMenuSaveReqVO createReqVO) { + MpAccountDO account = mpAccountService.getRequiredAccount(createReqVO.getAccountId()); + WxMpService mpService = mpServiceFactory.getRequiredMpService(createReqVO.getAccountId()); + + // 参数校验 + createReqVO.getMenus().forEach(this::validateMenu); + + // 第一步,同步公众号 + WxMenu wxMenu = new WxMenu(); + wxMenu.setButtons(MpMenuConvert.INSTANCE.convert(createReqVO.getMenus())); + try { + mpService.getMenuService().menuCreate(wxMenu); + } catch (WxErrorException e) { + throw exception(MENU_SAVE_FAIL, e.getError().getErrorMsg()); + } + + // 第二步,存储到数据库 + mpMenuMapper.deleteByAccountId(createReqVO.getAccountId()); + createReqVO.getMenus().forEach(menu -> { + // 先保存顶级菜单 + MpMenuDO menuDO = createMenu(menu, null, account); + // 再保存子菜单 + if (CollUtil.isEmpty(menu.getChildren())) { + return; + } + menu.getChildren().forEach(childMenu -> createMenu(childMenu, menuDO, account)); + }); + } + + /** + * 校验菜单的格式是否正确 + * + * @param menu 菜单 + */ + private void validateMenu(MpMenuSaveReqVO.Menu menu) { + MpUtils.validateButton(validator, menu.getType(), menu.getReplyMessageType(), menu); + // 子菜单 + if (CollUtil.isEmpty(menu.getChildren())) { + return; + } + menu.getChildren().forEach(this::validateMenu); + } + + /** + * 创建菜单,并存储到数据库 + * + * @param wxMenu 菜单信息 + * @param parentMenu 父菜单 + * @param account 公众号账号 + * @return 创建后的菜单 + */ + private MpMenuDO createMenu(MpMenuSaveReqVO.Menu wxMenu, MpMenuDO parentMenu, MpAccountDO account) { + // 创建菜单 + MpMenuDO menu = CollUtil.isNotEmpty(wxMenu.getChildren()) + ? new MpMenuDO().setName(wxMenu.getName()) + : MpMenuConvert.INSTANCE.convert02(wxMenu); + // 设置菜单的公众号账号信息 + if (account != null) { + menu.setAccountId(account.getId()).setAppId(account.getAppId()); + } + // 设置父编号 + if (parentMenu != null) { + menu.setParentId(parentMenu.getId()); + } else { + menu.setParentId(MpMenuDO.ID_ROOT); + } + + // 插入到数据库 + mpMenuMapper.insert(menu); + return menu; + } + + @Override + public void deleteMenuByAccountId(Long accountId) { + WxMpService mpService = mpServiceFactory.getRequiredMpService(accountId); + // 第一步,同步公众号 + try { + mpService.getMenuService().menuDelete(); + } catch (WxErrorException e) { + throw exception(MENU_DELETE_FAIL, e.getError().getErrorMsg()); + } + + // 第二步,存储到数据库 + mpMenuMapper.deleteByAccountId(accountId); + } + + @Override + public WxMpXmlOutMessage reply(String appId, String key, String openid) { + // 第一步,获得菜单 + MpMenuDO menu = mpMenuMapper.selectByAppIdAndMenuKey(appId, key); + if (menu == null) { + log.error("[reply][appId({}) key({}) 找不到对应的菜单]", appId, key); + return null; + } + // 按钮必须要有消息类型,不然后续无法回复消息 + if (StrUtil.isEmpty(menu.getReplyMessageType())) { + log.error("[reply][menu({}) 不存在对应的消息类型]", menu); + return null; + } + + // 第二步,回复消息 + MpMessageSendOutReqBO sendReqBO = MpMenuConvert.INSTANCE.convert(openid, menu); + return mpMessageService.sendOutMessage(sendReqBO); + } + + @Override + public List getMenuListByAccountId(Long accountId) { + return mpMenuMapper.selectListByAccountId(accountId); + } + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/message/MpAutoReplyService.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/message/MpAutoReplyService.java new file mode 100644 index 000000000..fc125d06d --- /dev/null +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/message/MpAutoReplyService.java @@ -0,0 +1,75 @@ +package cn.iocoder.yudao.module.mp.service.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.MpAutoReplyUpdateReqVO; +import cn.iocoder.yudao.module.mp.controller.admin.message.vo.message.MpMessagePageReqVO; +import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpAutoReplyDO; +import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; +import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; + +/** + * 公众号的自动回复 Service 接口 + * + * @author 芋道源码 + */ +public interface MpAutoReplyService { + + /** + * 获得公众号自动回复分页 + * + * @param pageVO 分页请求 + * @return 自动回复分页结果 + */ + PageResult getAutoReplyPage(MpMessagePageReqVO pageVO); + + /** + * 获得公众号自动回复 + * + * @param id 编号 + * @return 自动回复 + */ + MpAutoReplyDO getAutoReply(Long id); + + + /** + * 创建公众号自动回复 + * + * @param createReqVO 创建请求 + * @return 自动回复的编号 + */ + Long createAutoReply(MpAutoReplyCreateReqVO createReqVO); + + /** + * 更新公众号自动回复 + * + * @param updateReqVO 更新请求 + */ + void updateAutoReply(MpAutoReplyUpdateReqVO updateReqVO); + + /** + * 删除公众号自动回复 + * + * @param id 自动回复的编号 + */ + void deleteAutoReply(Long id); + + /** + * 当收到消息时,自动回复 + * + * @param appId 微信公众号 appId + * @param wxMessage 消息 + * @return 回复的消息 + */ + WxMpXmlOutMessage replyForMessage(String appId, WxMpXmlMessage wxMessage); + + /** + * 当粉丝关注时,自动回复 + * + * @param appId 微信公众号 appId + * @param wxMessage 消息 + * @return 回复的消息 + */ + WxMpXmlOutMessage replyForSubscribe(String appId, WxMpXmlMessage wxMessage); + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/message/MpAutoReplyServiceImpl.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/message/MpAutoReplyServiceImpl.java new file mode 100644 index 000000000..b90b82e96 --- /dev/null +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/message/MpAutoReplyServiceImpl.java @@ -0,0 +1,202 @@ +package cn.iocoder.yudao.module.mp.service.message; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ObjUtil; +import cn.iocoder.yudao.framework.common.exception.ErrorCode; +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.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.account.MpAccountDO; +import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpAutoReplyDO; +import cn.iocoder.yudao.module.mp.dal.mysql.message.MpAutoReplyMapper; +import cn.iocoder.yudao.module.mp.enums.message.MpAutoReplyTypeEnum; +import cn.iocoder.yudao.module.mp.framework.mp.core.util.MpUtils; +import cn.iocoder.yudao.module.mp.service.account.MpAccountService; +import cn.iocoder.yudao.module.mp.service.message.bo.MpMessageSendOutReqBO; +import me.chanjar.weixin.common.api.WxConsts; +import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; +import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import javax.validation.Validator; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.mp.enums.ErrorCodeConstants.*; + +/** + * 公众号的自动回复 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class MpAutoReplyServiceImpl implements MpAutoReplyService { + + @Resource + private MpMessageService mpMessageService; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private MpAccountService mpAccountService; + + @Resource + private Validator validator; + + @Resource + private MpAutoReplyMapper mpAutoReplyMapper; + + @Override + public PageResult getAutoReplyPage(MpMessagePageReqVO pageVO) { + return mpAutoReplyMapper.selectPage(pageVO); + } + + @Override + public MpAutoReplyDO getAutoReply(Long id) { + return mpAutoReplyMapper.selectById(id); + } + + @Override + public Long createAutoReply(MpAutoReplyCreateReqVO createReqVO) { + // 第一步,校验数据 + if (createReqVO.getResponseMessageType() != null) { + MpUtils.validateMessage(validator, createReqVO.getResponseMessageType(), createReqVO); + } + validateAutoReplyConflict(null, createReqVO.getAccountId(), createReqVO.getType(), + createReqVO.getRequestKeyword(), createReqVO.getRequestMessageType()); + + // 第二步,插入数据 + MpAccountDO account = mpAccountService.getRequiredAccount(createReqVO.getAccountId()); + MpAutoReplyDO autoReply = MpAutoReplyConvert.INSTANCE.convert(createReqVO) + .setAppId(account.getAppId()); + mpAutoReplyMapper.insert(autoReply); + return autoReply.getId(); + } + + @Override + public void updateAutoReply(MpAutoReplyUpdateReqVO updateReqVO) { + // 第一步,校验数据 + if (updateReqVO.getResponseMessageType() != null) { + MpUtils.validateMessage(validator, updateReqVO.getResponseMessageType(), updateReqVO); + } + MpAutoReplyDO autoReply = validateAutoReplyExists(updateReqVO.getId()); + validateAutoReplyConflict(updateReqVO.getId(), autoReply.getAccountId(), updateReqVO.getType(), + updateReqVO.getRequestKeyword(), updateReqVO.getRequestMessageType()); + + // 第二步,更新数据 + MpAutoReplyDO updateObj = MpAutoReplyConvert.INSTANCE.convert(updateReqVO) + .setAccountId(null).setAppId(null); // 避免前端传递,更新着两个字段 + mpAutoReplyMapper.updateById(updateObj); + } + + /** + * 校验自动回复是否冲突 + * + * 不同的 type,会有不同的逻辑: + * 1. type = SUBSCRIBE 时,不允许有其他的自动回复 + * 2. type = MESSAGE 时,校验 requestMessageType 已经存在自动回复 + * 3. type = KEYWORD 时,校验 keyword 已经存在自动回复 + * + * @param id 自动回复编号 + * @param accountId 公众号账号的编号 + * @param type 类型 + * @param requestKeyword 请求关键词 + * @param requestMessageType 请求消息类型 + */ + private void validateAutoReplyConflict(Long id, Long accountId, Integer type, + String requestKeyword, String requestMessageType) { + // 获得已经存在的自动回复 + MpAutoReplyDO autoReply = null; + ErrorCode errorCode = null; + if (MpAutoReplyTypeEnum.SUBSCRIBE.getType().equals(type)) { + autoReply = mpAutoReplyMapper.selectByAccountIdAndSubscribe(accountId); + errorCode = AUTO_REPLY_ADD_SUBSCRIBE_FAIL_EXISTS; + } else if (MpAutoReplyTypeEnum.MESSAGE.getType().equals(type)) { + autoReply = mpAutoReplyMapper.selectByAccountIdAndMessage(accountId, requestMessageType); + errorCode = AUTO_REPLY_ADD_MESSAGE_FAIL_EXISTS; + } else if (MpAutoReplyTypeEnum.KEYWORD.getType().equals(type)) { + autoReply = mpAutoReplyMapper.selectByAccountIdAndKeyword(accountId, requestKeyword); + errorCode = AUTO_REPLY_ADD_KEYWORD_FAIL_EXISTS; + } + if (autoReply == null) { + return; + } + + // 存在冲突,抛出业务异常 + if (id == null // 情况一,新增(id == null),存在记录,说明冲突 + || ObjUtil.notEqual(id, autoReply.getId())) { // 情况二,修改(id != null),id 不匹配,说明冲突 + throw exception(errorCode); + } + } + + @Override + public void deleteAutoReply(Long id) { + // 校验粉丝存在 + validateAutoReplyExists(id); + + // 删除自动回复 + mpAutoReplyMapper.deleteById(id); + } + + private MpAutoReplyDO validateAutoReplyExists(Long id) { + MpAutoReplyDO autoReply = mpAutoReplyMapper.selectById(id); + if (autoReply == null) { + throw exception(AUTO_REPLY_NOT_EXISTS); + } + return autoReply; + } + + @Override + public WxMpXmlOutMessage replyForMessage(String appId, WxMpXmlMessage wxMessage) { + // 第一步,匹配自动回复 + List replies = null; + // 1.1 关键字 + if (wxMessage.getMsgType().equals(WxConsts.XmlMsgType.TEXT)) { + // 完全匹配 + replies = mpAutoReplyMapper.selectListByAppIdAndKeywordAll(appId, wxMessage.getContent()); + if (CollUtil.isEmpty(replies)) { + // 模糊匹配 + replies = mpAutoReplyMapper.selectListByAppIdAndKeywordLike(appId, wxMessage.getContent()); + } + } + // 1.2 消息类型 + if (CollUtil.isEmpty(replies)) { + replies = mpAutoReplyMapper.selectListByAppIdAndMessage(appId, wxMessage.getMsgType()); + } + if (CollUtil.isEmpty(replies)) { + return null; + } + MpAutoReplyDO reply = CollUtil.getFirst(replies); + + // 第二步,基于自动回复,创建消息 + MpMessageSendOutReqBO sendReqBO = MpAutoReplyConvert.INSTANCE.convert(wxMessage.getFromUser(), reply); + return mpMessageService.sendOutMessage(sendReqBO); + } + + @Override + public WxMpXmlOutMessage replyForSubscribe(String appId, WxMpXmlMessage wxMessage) { + // 第一步,匹配自动回复 + List replies = mpAutoReplyMapper.selectListByAppIdAndSubscribe(appId); + MpAutoReplyDO reply = CollUtil.isNotEmpty(replies) ? CollUtil.getFirst(replies) + : buildDefaultSubscribeAutoReply(appId); // 如果不存在,提供一个默认末班 + + // 第二步,基于自动回复,创建消息 + MpMessageSendOutReqBO sendReqBO = MpAutoReplyConvert.INSTANCE.convert(wxMessage.getFromUser(), reply); + return mpMessageService.sendOutMessage(sendReqBO); + } + + private MpAutoReplyDO buildDefaultSubscribeAutoReply(String appId) { + MpAccountDO account = mpAccountService.getAccountFromCache(appId); + Assert.notNull(account, "公众号账号({}) 不存在", appId); + // 构建默认的【关注】自动回复 + return new MpAutoReplyDO().setAppId(appId).setAccountId(account.getId()) + .setType(MpAutoReplyTypeEnum.SUBSCRIBE.getType()) + .setResponseMessageType(WxConsts.XmlMsgType.TEXT).setResponseContent("感谢关注"); + } + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/message/MpMessageService.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/message/MpMessageService.java new file mode 100644 index 000000000..01c691a34 --- /dev/null +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/message/MpMessageService.java @@ -0,0 +1,58 @@ +package cn.iocoder.yudao.module.mp.service.message; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.mp.controller.admin.message.vo.message.MpMessagePageReqVO; +import cn.iocoder.yudao.module.mp.controller.admin.message.vo.message.MpMessageSendReqVO; +import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO; +import cn.iocoder.yudao.module.mp.service.message.bo.MpMessageSendOutReqBO; +import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; +import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; + +import javax.validation.Valid; + +/** + * 公众号消息 Service 接口 + * + * @author 芋道源码 + */ +public interface MpMessageService { + + /** + * 获得公众号消息分页 + * + * @param pageReqVO 分页查询 + * @return 公众号消息分页 + */ + PageResult getMessagePage(MpMessagePageReqVO pageReqVO); + + /** + * 从公众号,接收到粉丝消息 + * + * @param appId 微信公众号 appId + * @param wxMessage 消息 + */ + void receiveMessage(String appId, WxMpXmlMessage wxMessage); + + /** + * 使用公众号,给粉丝回复消息 + * + * 例如说:自动回复、客服消息、菜单回复消息等场景 + * + * 注意,该方法只是返回 WxMpXmlOutMessage 对象,不会真的发送消息 + * + * @param sendReqBO 消息内容 + * @return 微信回复消息 XML + */ + WxMpXmlOutMessage sendOutMessage(@Valid MpMessageSendOutReqBO sendReqBO); + + /** + * 使用公众号,给粉丝发送【客服】消息 + * + * 注意,该方法会真实发送消息 + * + * @param sendReqVO 消息内容 + * @return 消息 + */ + MpMessageDO sendKefuMessage(MpMessageSendReqVO sendReqVO); + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/message/MpMessageServiceImpl.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/message/MpMessageServiceImpl.java new file mode 100644 index 000000000..3cc269e76 --- /dev/null +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/message/MpMessageServiceImpl.java @@ -0,0 +1,148 @@ +package cn.iocoder.yudao.module.mp.service.message; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.mp.controller.admin.message.vo.message.MpMessagePageReqVO; +import cn.iocoder.yudao.module.mp.controller.admin.message.vo.message.MpMessageSendReqVO; +import cn.iocoder.yudao.module.mp.convert.message.MpMessageConvert; +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.dal.mysql.message.MpMessageMapper; +import cn.iocoder.yudao.module.mp.enums.message.MpMessageSendFromEnum; +import cn.iocoder.yudao.module.mp.framework.mp.core.MpServiceFactory; +import cn.iocoder.yudao.module.mp.framework.mp.core.util.MpUtils; +import cn.iocoder.yudao.module.mp.service.account.MpAccountService; +import cn.iocoder.yudao.module.mp.service.material.MpMaterialService; +import cn.iocoder.yudao.module.mp.service.message.bo.MpMessageSendOutReqBO; +import cn.iocoder.yudao.module.mp.service.user.MpUserService; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.api.WxConsts; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.mp.api.WxMpService; +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 org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import javax.validation.Validator; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.mp.enums.ErrorCodeConstants.MESSAGE_SEND_FAIL; + +/** + * 粉丝消息 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class MpMessageServiceImpl implements MpMessageService { + + @Resource + @Lazy // 延迟加载,避免循环依赖 + private MpAccountService mpAccountService; + @Resource + private MpUserService mpUserService; + @Resource + private MpMaterialService mpMaterialService; + + @Resource + private MpMessageMapper mpMessageMapper; + + @Resource + @Lazy // 延迟加载,解决循环依赖的问题 + private MpServiceFactory mpServiceFactory; + + @Resource + private Validator validator; + + @Override + public PageResult getMessagePage(MpMessagePageReqVO pageReqVO) { + return mpMessageMapper.selectPage(pageReqVO); + } + + @Override + public void receiveMessage(String appId, WxMpXmlMessage wxMessage) { + // 获得关联信息 + MpAccountDO account = mpAccountService.getAccountFromCache(appId); + Assert.notNull(account, "公众号账号({}) 不存在", appId); + MpUserDO user = mpUserService.getUser(appId, wxMessage.getFromUser()); + Assert.notNull(user, "公众号粉丝({}/{}) 不存在", appId, wxMessage.getFromUser()); + + // 记录消息 + MpMessageDO message = MpMessageConvert.INSTANCE.convert(wxMessage, account, user) + .setSendFrom(MpMessageSendFromEnum.USER_TO_MP.getFrom()); + downloadMessageMedia(message); + mpMessageMapper.insert(message); + } + + @Override + public WxMpXmlOutMessage sendOutMessage(MpMessageSendOutReqBO sendReqBO) { + // 校验消息格式 + MpUtils.validateMessage(validator, sendReqBO.getType(), sendReqBO); + + // 获得关联信息 + MpAccountDO account = mpAccountService.getAccountFromCache(sendReqBO.getAppId()); + Assert.notNull(account, "公众号账号({}) 不存在", sendReqBO.getAppId()); + MpUserDO user = mpUserService.getUser(sendReqBO.getAppId(), sendReqBO.getOpenid()); + Assert.notNull(user, "公众号粉丝({}/{}) 不存在", sendReqBO.getAppId(), sendReqBO.getOpenid()); + + // 记录消息 + MpMessageDO message = MpMessageConvert.INSTANCE.convert(sendReqBO, account, user). + setSendFrom(MpMessageSendFromEnum.MP_TO_USER.getFrom()); + downloadMessageMedia(message); + mpMessageMapper.insert(message); + + // 转换返回 WxMpXmlOutMessage 对象 + return MpMessageConvert.INSTANCE.convert02(message, account); + } + + @Override + public MpMessageDO sendKefuMessage(MpMessageSendReqVO sendReqVO) { + // 校验消息格式 + MpUtils.validateMessage(validator, sendReqVO.getType(), sendReqVO); + + // 获得关联信息 + MpUserDO user = mpUserService.getRequiredUser(sendReqVO.getUserId()); + MpAccountDO account = mpAccountService.getRequiredAccount(user.getAccountId()); + + // 发送客服消息 + WxMpKefuMessage wxMessage = MpMessageConvert.INSTANCE.convert(sendReqVO, user); + WxMpService mpService = mpServiceFactory.getRequiredMpService(user.getAppId()); + try { + mpService.getKefuService().sendKefuMessageWithResponse(wxMessage); + } catch (WxErrorException e) { + throw exception(MESSAGE_SEND_FAIL, e.getError().getErrorMsg()); + } + + // 记录消息 + MpMessageDO message = MpMessageConvert.INSTANCE.convert(wxMessage, account, user) + .setSendFrom(MpMessageSendFromEnum.MP_TO_USER.getFrom()); + downloadMessageMedia(message); + mpMessageMapper.insert(message); + return message; + } + + /** + * 下载消息使用到的媒体文件,并上传到文件服务 + * + * @param message 消息 + */ + private void downloadMessageMedia(MpMessageDO message) { + if (StrUtil.isNotEmpty(message.getMediaId())) { + message.setMediaUrl(mpMaterialService.downloadMaterialUrl(message.getAccountId(), + message.getMediaId(), MpUtils.getMediaFileType(message.getType()))); + } + if (StrUtil.isNotEmpty(message.getThumbMediaId())) { + message.setThumbMediaUrl(mpMaterialService.downloadMaterialUrl(message.getAccountId(), + message.getThumbMediaId(), WxConsts.MediaFileType.THUMB)); + } + } + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/message/bo/MpMessageSendOutReqBO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/message/bo/MpMessageSendOutReqBO.java new file mode 100644 index 000000000..9da709050 --- /dev/null +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/message/bo/MpMessageSendOutReqBO.java @@ -0,0 +1,110 @@ +package cn.iocoder.yudao.module.mp.service.message.bo; + +import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO; +import cn.iocoder.yudao.module.mp.framework.mp.core.util.MpUtils.*; +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; + +/** + * 公众号消息发送 Request BO + * + * 为什么要有该 BO 呢?在自动回复、客服消息、菜单回复消息等场景,都涉及到 MP 给粉丝发送消息,所以使用该 BO 统一承接 + * + * @author 芋道源码 + */ +@Data +public class MpMessageSendOutReqBO { + + /** + * 公众号 appId + */ + @NotEmpty(message = "公众号 appId 不能为空") + private String appId; + /** + * 公众号粉丝 openid + */ + @NotEmpty(message = "公众号粉丝 openid 不能为空") + private String openid; + + // ========== 消息内容 ========== + /** + * 消息类型 + * + * 枚举 {@link WxConsts.XmlMsgType} 中的 TEXT、IMAGE、VOICE、VIDEO、NEWS、MUSIC + */ + @NotEmpty(message = "消息类型不能为空") + public String type; + + /** + * 消息内容 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 TEXT + */ + @NotEmpty(message = "消息内容不能为空", groups = TextMessageGroup.class) + private String content; + + /** + * 媒体 id + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 IMAGE、VOICE、VIDEO + */ + @NotEmpty(message = "消息 mediaId 不能为空", groups = {ImageMessageGroup.class, VoiceMessageGroup.class, VideoMessageGroup.class}) + private String mediaId; + + /** + * 缩略图的媒体 id + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO、MUSIC + */ + @NotEmpty(message = "消息 thumbMediaId 不能为空", groups = {MusicMessageGroup.class}) + private String thumbMediaId; + + /** + * 标题 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO + */ + @NotEmpty(message = "消息标题不能为空", groups = VideoMessageGroup.class) + private String title; + /** + * 描述 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO + */ + @NotEmpty(message = "消息描述不能为空", groups = VideoMessageGroup.class) + private String description; + + /** + * 图文消息 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 NEWS + */ + @Valid + @NotNull(message = "图文消息不能为空", groups = NewsMessageGroup.class) + private List articles; + + /** + * 音乐链接 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC + */ + @NotEmpty(message = "音乐链接不能为空", groups = MusicMessageGroup.class) + @URL(message = "高质量音乐链接格式不正确", groups = MusicMessageGroup.class) + private String musicUrl; + + /** + * 高质量音乐链接 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC + */ + @NotEmpty(message = "高质量音乐链接不能为空", groups = MusicMessageGroup.class) + @URL(message = "高质量音乐链接格式不正确", groups = MusicMessageGroup.class) + private String hqMusicUrl; + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/statistics/MpStatisticsService.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/statistics/MpStatisticsService.java new file mode 100644 index 000000000..08e5c35bb --- /dev/null +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/statistics/MpStatisticsService.java @@ -0,0 +1,54 @@ +package cn.iocoder.yudao.module.mp.service.statistics; + +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 java.time.LocalDateTime; +import java.util.List; + +/** + * 公众号统计 Service 接口 + * + * @author 芋道源码 + */ +public interface MpStatisticsService { + + /** + * 获取粉丝增减数据 + * + * @param accountId 公众号账号编号 + * @param date 时间区间 + * @return 粉丝增减数据 + */ + List getUserSummary(Long accountId, LocalDateTime[] date); + + /** + * 获取粉丝累计数据 + * + * @param accountId 公众号账号编号 + * @param date 时间区间 + * @return 粉丝累计数据 + */ + List getUserCumulate(Long accountId, LocalDateTime[] date); + + /** + * 获取消息发送概况数据 + * + * @param accountId 公众号账号编号 + * @param date 时间区间 + * @return 消息发送概况数据 + */ + List getUpstreamMessage(Long accountId, LocalDateTime[] date); + + /** + * 获取接口分析数据 + * + * @param accountId 公众号账号编号 + * @param date 时间区间 + * @return 接口分析数据 + */ + List getInterfaceSummary(Long accountId, LocalDateTime[] date); + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/statistics/MpStatisticsServiceImpl.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/statistics/MpStatisticsServiceImpl.java new file mode 100644 index 000000000..4c3b37413 --- /dev/null +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/statistics/MpStatisticsServiceImpl.java @@ -0,0 +1,77 @@ +package cn.iocoder.yudao.module.mp.service.statistics; + +import cn.hutool.core.date.DateUtil; +import cn.iocoder.yudao.module.mp.framework.mp.core.MpServiceFactory; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.mp.api.WxMpService; +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.context.annotation.Lazy; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.mp.enums.ErrorCodeConstants.*; + +/** + * 公众号统计 Service 实现类 + * + * @author 芋道源码 + */ +@Service +public class MpStatisticsServiceImpl implements MpStatisticsService { + + @Resource + @Lazy // 延迟加载,解决循环依赖的问题 + private MpServiceFactory mpServiceFactory; + + @Override + public List getUserSummary(Long accountId, LocalDateTime[] date) { + WxMpService mpService = mpServiceFactory.getRequiredMpService(accountId); + try { + return mpService.getDataCubeService().getUserSummary( + DateUtil.date(date[0]), DateUtil.date(date[1])); + } catch (WxErrorException e) { + throw exception(STATISTICS_GET_USER_SUMMARY_FAIL, e.getError().getErrorMsg()); + } + } + + @Override + public List getUserCumulate(Long accountId, LocalDateTime[] date) { + WxMpService mpService = mpServiceFactory.getRequiredMpService(accountId); + try { + return mpService.getDataCubeService().getUserCumulate( + DateUtil.date(date[0]), DateUtil.date(date[1])); + } catch (WxErrorException e) { + throw exception(STATISTICS_GET_USER_CUMULATE_FAIL, e.getError().getErrorMsg()); + } + } + + @Override + public List getUpstreamMessage(Long accountId, LocalDateTime[] date) { + WxMpService mpService = mpServiceFactory.getRequiredMpService(accountId); + try { + return mpService.getDataCubeService().getUpstreamMsg( + DateUtil.date(date[0]), DateUtil.date(date[1])); + } catch (WxErrorException e) { + throw exception(STATISTICS_GET_UPSTREAM_MESSAGE_FAIL, e.getError().getErrorMsg()); + } + } + + @Override + public List getInterfaceSummary(Long accountId, LocalDateTime[] date) { + WxMpService mpService = mpServiceFactory.getRequiredMpService(accountId); + try { + return mpService.getDataCubeService().getInterfaceSummary( + DateUtil.date(date[0]), DateUtil.date(date[1])); + } catch (WxErrorException e) { + throw exception(STATISTICS_GET_INTERFACE_SUMMARY_FAIL, e.getError().getErrorMsg()); + } + } + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/tag/MpTagService.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/tag/MpTagService.java new file mode 100644 index 000000000..77dbf338f --- /dev/null +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/tag/MpTagService.java @@ -0,0 +1,65 @@ +package cn.iocoder.yudao.module.mp.service.tag; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.mp.controller.admin.tag.vo.MpTagCreateReqVO; +import cn.iocoder.yudao.module.mp.controller.admin.tag.vo.MpTagPageReqVO; +import cn.iocoder.yudao.module.mp.controller.admin.tag.vo.MpTagUpdateReqVO; +import cn.iocoder.yudao.module.mp.dal.dataobject.tag.MpTagDO; + +import javax.validation.Valid; +import java.util.List; + +/** + * 公众号标签 Service 接口 + * + * @author fengdan + */ +public interface MpTagService { + + /** + * 创建公众号标签 + * + * @param createReqVO 创建标签信息 + * @return 标签编号 + */ + Long createTag(@Valid MpTagCreateReqVO createReqVO); + + /** + * 更新公众号标签 + * + * @param updateReqVO 更新标签信息 + */ + void updateTag(@Valid MpTagUpdateReqVO updateReqVO); + + /** + * 删除公众号标签 + * + * @param id 编号 + */ + void deleteTag(Long id); + + /** + * 获得公众号标签分页 + * + * @param pageReqVO 分页查询 + * @return 公众号标签分页 + */ + PageResult getTagPage(MpTagPageReqVO pageReqVO); + + /** + * 获得公众号标签详情 + * @param id id查询 + * @return 公众号标签详情 + */ + MpTagDO get(Long id); + + List getTagList(); + + /** + * 同步公众号标签 + * + * @param accountId 公众号账号的编号 + */ + void syncTag(Long accountId); + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/tag/MpTagServiceImpl.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/tag/MpTagServiceImpl.java new file mode 100644 index 000000000..a4fa50923 --- /dev/null +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/tag/MpTagServiceImpl.java @@ -0,0 +1,164 @@ +package cn.iocoder.yudao.module.mp.service.tag; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.mp.controller.admin.tag.vo.MpTagCreateReqVO; +import cn.iocoder.yudao.module.mp.controller.admin.tag.vo.MpTagPageReqVO; +import cn.iocoder.yudao.module.mp.controller.admin.tag.vo.MpTagUpdateReqVO; +import cn.iocoder.yudao.module.mp.convert.tag.MpTagConvert; +import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO; +import cn.iocoder.yudao.module.mp.dal.dataobject.tag.MpTagDO; +import cn.iocoder.yudao.module.mp.dal.mysql.tag.MpTagMapper; +import cn.iocoder.yudao.module.mp.framework.mp.core.MpServiceFactory; +import cn.iocoder.yudao.module.mp.service.account.MpAccountService; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.mp.api.WxMpService; +import me.chanjar.weixin.mp.bean.tag.WxUserTag; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; +import static cn.iocoder.yudao.module.mp.enums.ErrorCodeConstants.*; + +/** + * 公众号标签 Service 实现类 + * + * @author fengdan + */ +@Slf4j +@Service +@Validated +public class MpTagServiceImpl implements MpTagService { + + @Resource + private MpTagMapper mpTagMapper; + + @Resource + private MpAccountService mpAccountService; + + @Resource + @Lazy // 延迟加载,为了解决延迟加载 + private MpServiceFactory mpServiceFactory; + + @Override + public Long createTag(MpTagCreateReqVO createReqVO) { + // 获得公众号账号 + MpAccountDO account = mpAccountService.getRequiredAccount(createReqVO.getAccountId()); + + // 第一步,新增标签到公众号平台。标签名的唯一,交给公众号平台 + WxMpService mpService = mpServiceFactory.getRequiredMpService(createReqVO.getAccountId()); + WxUserTag wxTag; + try { + wxTag = mpService.getUserTagService().tagCreate(createReqVO.getName()); + } catch (WxErrorException e) { + throw exception(TAG_CREATE_FAIL, e.getError().getErrorMsg()); + } + + // 第二步,新增标签到数据库 + MpTagDO tag = MpTagConvert.INSTANCE.convert(wxTag, account); + mpTagMapper.insert(tag); + return tag.getId(); + } + + @Override + public void updateTag(MpTagUpdateReqVO updateReqVO) { + // 校验标签存在 + MpTagDO tag = validateTagExists(updateReqVO.getId()); + + // 第一步,更新标签到公众号平台。标签名的唯一,交给公众号平台 + WxMpService mpService = mpServiceFactory.getRequiredMpService(tag.getAccountId()); + try { + mpService.getUserTagService().tagUpdate(tag.getTagId(), updateReqVO.getName()); + } catch (WxErrorException e) { + throw exception(TAG_UPDATE_FAIL, e.getError().getErrorMsg()); + } + + // 第二步,更新标签到数据库 + mpTagMapper.updateById(new MpTagDO().setId(tag.getId()).setName(updateReqVO.getName())); + } + + @Override + public void deleteTag(Long id) { + // 校验标签存在 + MpTagDO tag = validateTagExists(id); + + // 第一步,删除标签到公众号平台。 + WxMpService mpService = mpServiceFactory.getRequiredMpService(tag.getAccountId()); + try { + mpService.getUserTagService().tagDelete(tag.getTagId()); + } catch (WxErrorException e) { + throw exception(TAG_DELETE_FAIL, e.getError().getErrorMsg()); + } + + // 第二步,删除标签到数据库 + mpTagMapper.deleteById(tag.getId()); + } + + private MpTagDO validateTagExists(Long id) { + MpTagDO tag = mpTagMapper.selectById(id); + if (tag == null) { + throw exception(TAG_NOT_EXISTS); + } + return tag; + } + + @Override + public PageResult getTagPage(MpTagPageReqVO pageReqVO) { + return mpTagMapper.selectPage(pageReqVO); + } + + @Override + public MpTagDO get(Long id) { + return mpTagMapper.selectById(id); + } + + @Override + public List getTagList() { + return mpTagMapper.selectList(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void syncTag(Long accountId) { + MpAccountDO account = mpAccountService.getRequiredAccount(accountId); + + // 第一步,从公众号平台获取最新的标签列表 + WxMpService mpService = mpServiceFactory.getRequiredMpService(accountId); + List wxTags; + try { + wxTags = mpService.getUserTagService().tagGet(); + } catch (WxErrorException e) { + throw exception(TAG_GET_FAIL, e.getError().getErrorMsg()); + } + + // 第二步,合并更新回自己的数据库;由于标签只有 100 个,所以直接 for 循环操作 + Map tagMap = convertMap(mpTagMapper.selectListByAccountId(accountId), + MpTagDO::getTagId); + wxTags.forEach(wxTag -> { + MpTagDO tag = tagMap.remove(wxTag.getId()); + // 情况一,不存在,新增 + if (tag == null) { + tag = MpTagConvert.INSTANCE.convert(wxTag, account); + mpTagMapper.insert(tag); + return; + } + // 情况二,存在,则更新 + mpTagMapper.updateById(new MpTagDO().setId(tag.getId()) + .setName(wxTag.getName()).setCount(wxTag.getCount())); + }); + // 情况三,部分标签已经不存在了,删除 + if (CollUtil.isNotEmpty(tagMap)) { + mpTagMapper.deleteBatchIds(convertList(tagMap.values(), MpTagDO::getId)); + } + } + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/user/MpUserService.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/user/MpUserService.java new file mode 100644 index 000000000..bdefc0e60 --- /dev/null +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/user/MpUserService.java @@ -0,0 +1,102 @@ +package cn.iocoder.yudao.module.mp.service.user; + +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.MpUserUpdateReqVO; +import cn.iocoder.yudao.module.mp.dal.dataobject.user.MpUserDO; +import me.chanjar.weixin.mp.bean.result.WxMpUser; + +import java.util.Collection; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.mp.enums.ErrorCodeConstants.USER_NOT_EXISTS; + +/** + * 公众号粉丝 Service 接口 + * + * @author 芋道源码 + */ +public interface MpUserService { + + /** + * 获得公众号粉丝 + * + * @param id 编号 + * @return 公众号粉丝 + */ + MpUserDO getUser(Long id); + + /** + * 使用 appId + openId,获得公众号粉丝 + * + * @param appId 公众号 appId + * @param openId 公众号 openId + * @return 公众号粉丝 + */ + MpUserDO getUser(String appId, String openId); + + /** + * 获得公众号粉丝 + * + * @param id 编号 + * @return 公众号粉丝 + */ + default MpUserDO getRequiredUser(Long id) { + MpUserDO user = getUser(id); + if (user == null) { + throw exception(USER_NOT_EXISTS); + } + return user; + } + + /** + * 获得公众号粉丝列表 + * + * @param ids 编号 + * @return 公众号粉丝列表 + */ + List getUserList(Collection ids); + + /** + * 获得公众号粉丝分页 + * + * @param pageReqVO 分页查询 + * @return 公众号粉丝分页 + */ + PageResult getUserPage(MpUserPageReqVO pageReqVO); + + /** + * 保存公众号粉丝 + * + * 新增或更新,根据是否存在数据库中 + * + * @param appId 公众号 appId + * @param wxMpUser 公众号粉丝的信息 + * @return 公众号粉丝 + */ + MpUserDO saveUser(String appId, WxMpUser wxMpUser); + + /** + * 同步一个公众号粉丝 + * + * @param accountId 公众号账号的编号 + */ + void syncUser(Long accountId); + + /** + * 更新公众号粉丝,取消关注 + * + * @param appId 公众号 appId + * @param openId 公众号粉丝的 openid + */ + void updateUserUnsubscribe(String appId, String openId); + + /** + * 更新公众号粉丝 + * + * @param updateReqVO 更新信息 + */ + void updateUser(MpUserUpdateReqVO updateReqVO); + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/user/MpUserServiceImpl.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/user/MpUserServiceImpl.java new file mode 100644 index 000000000..0cef919d9 --- /dev/null +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/user/MpUserServiceImpl.java @@ -0,0 +1,215 @@ +package cn.iocoder.yudao.module.mp.service.user; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +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.MpUserPageReqVO; +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.account.MpAccountDO; +import cn.iocoder.yudao.module.mp.dal.dataobject.user.MpUserDO; +import cn.iocoder.yudao.module.mp.dal.mysql.user.MpUserMapper; +import cn.iocoder.yudao.module.mp.framework.mp.core.MpServiceFactory; +import cn.iocoder.yudao.module.mp.service.account.MpAccountService; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.mp.api.WxMpService; +import me.chanjar.weixin.mp.bean.result.WxMpUser; +import me.chanjar.weixin.mp.bean.result.WxMpUserList; +import org.springframework.context.annotation.Lazy; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.mp.enums.ErrorCodeConstants.USER_NOT_EXISTS; +import static cn.iocoder.yudao.module.mp.enums.ErrorCodeConstants.USER_UPDATE_TAG_FAIL; + +/** + * 微信公众号粉丝 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class MpUserServiceImpl implements MpUserService { + + @Resource + @Lazy // 延迟加载,解决循环依赖的问题 + private MpAccountService mpAccountService; + + @Resource + @Lazy // 延迟加载,解决循环依赖的问题 + private MpServiceFactory mpServiceFactory; + + @Resource + private MpUserMapper mpUserMapper; + + @Override + public MpUserDO getUser(Long id) { + return mpUserMapper.selectById(id); + } + + @Override + public MpUserDO getUser(String appId, String openId) { + return mpUserMapper.selectByAppIdAndOpenid(appId, openId); + } + + @Override + public List getUserList(Collection ids) { + return mpUserMapper.selectBatchIds(ids); + } + + @Override + public PageResult getUserPage(MpUserPageReqVO pageReqVO) { + return mpUserMapper.selectPage(pageReqVO); + } + + @Override + public MpUserDO saveUser(String appId, WxMpUser wxMpUser) { + // 构建保存的 MpUserDO 对象 + MpAccountDO account = mpAccountService.getAccountFromCache(appId); + MpUserDO user = MpUserConvert.INSTANCE.convert(account, wxMpUser); + + // 根据情况,插入或更新 + MpUserDO dbUser = mpUserMapper.selectByAppIdAndOpenid(appId, wxMpUser.getOpenId()); + if (dbUser == null) { + mpUserMapper.insert(user); + } else { + user.setId(dbUser.getId()); + mpUserMapper.updateById(user); + } + return user; + } + + @Override + @Async + public void syncUser(Long accountId) { + MpAccountDO account = mpAccountService.getRequiredAccount(accountId); + // for 循环,避免递归出意外问题,导致死循环 + String nextOpenid = null; + for (int i = 0; i < Short.MAX_VALUE; i++) { + log.info("[syncUser][第({}) 次加载公众号粉丝列表,nextOpenid({})]", i, nextOpenid); + try { + nextOpenid = syncUser0(account, nextOpenid); + } catch (WxErrorException e) { + log.error("[syncUser][第({}) 次同步粉丝异常]", i, e); + break; + } + // 如果 nextOpenid 为空,表示已经同步完毕 + if (StrUtil.isEmpty(nextOpenid)) { + break; + } + } + } + + private String syncUser0(MpAccountDO account, String nextOpenid) throws WxErrorException { + // 第一步,从公众号流式加载粉丝 + WxMpService mpService = mpServiceFactory.getRequiredMpService(account.getId()); + WxMpUserList wxUserList = mpService.getUserService().userList(nextOpenid); + if (CollUtil.isEmpty(wxUserList.getOpenids())) { + return null; + } + + // 第二步,分批加载粉丝信息 + List> openidsList = CollUtil.split(wxUserList.getOpenids(), 100); + for (List openids : openidsList) { + log.info("[syncUser][批量加载粉丝信息,openids({})]", openids); + List wxUsers = mpService.getUserService().userInfoList(openids); + batchSaveUser(account, wxUsers); + } + + // 返回下一次的 nextOpenId + return wxUserList.getNextOpenid(); + } + + private void batchSaveUser(MpAccountDO account, List wxUsers) { + if (CollUtil.isEmpty(wxUsers)) { + return; + } + // 1. 获得数据库已保存的粉丝列表 + List dbUsers = mpUserMapper.selectListByAppIdAndOpenid(account.getAppId(), + CollectionUtils.convertList(wxUsers, WxMpUser::getOpenId)); + Map openId2Users = CollectionUtils.convertMap(dbUsers, MpUserDO::getOpenid); + + // 2.1 根据情况,插入或更新 + List users = MpUserConvert.INSTANCE.convertList(account, wxUsers); + List newUsers = new ArrayList<>(); + for (MpUserDO user : users) { + MpUserDO dbUser = openId2Users.get(user.getOpenid()); + if (dbUser == null) { // 新增:稍后批量插入 + newUsers.add(user); + } else { // 更新:直接执行更新 + user.setId(dbUser.getId()); + mpUserMapper.updateById(user); + } + } + // 2.2 批量插入 + if (CollUtil.isNotEmpty(newUsers)) { + mpUserMapper.insertBatch(newUsers); + } + } + + @Override + public void updateUserUnsubscribe(String appId, String openid) { + MpUserDO dbUser = mpUserMapper.selectByAppIdAndOpenid(appId, openid); + if (dbUser == null) { + log.error("[updateUserUnsubscribe][微信公众号粉丝 appId({}) openid({}) 不存在]", appId, openid); + return; + } + mpUserMapper.updateById(new MpUserDO().setId(dbUser.getId()).setSubscribeStatus(CommonStatusEnum.DISABLE.getStatus()) + .setUnsubscribeTime(LocalDateTime.now())); + } + + @Override + public void updateUser(MpUserUpdateReqVO updateReqVO) { + // 校验存在 + MpUserDO user = validateUserExists(updateReqVO.getId()); + + // 第一步,更新标签到公众号 + updateUserTag(user.getAppId(), user.getOpenid(), updateReqVO.getTagIds()); + + // 第二步,更新基本信息到数据库 + MpUserDO updateObj = MpUserConvert.INSTANCE.convert(updateReqVO).setId(user.getId()); + mpUserMapper.updateById(updateObj); + } + + private MpUserDO validateUserExists(Long id) { + MpUserDO user = mpUserMapper.selectById(id); + if (user == null) { + throw exception(USER_NOT_EXISTS); + } + return user; + } + + private void updateUserTag(String appId, String openid, List tagIds) { + WxMpService mpService = mpServiceFactory.getRequiredMpService(appId); + try { + // 第一步,先取消原来的标签 + List oldTagIds = mpService.getUserTagService().userTagList(openid); + for (Long tagId : oldTagIds) { + mpService.getUserTagService().batchUntagging(tagId, new String[]{openid}); + } + // 第二步,再设置新的标签 + if (CollUtil.isEmpty(tagIds)) { + return; + } + for (Long tagId: tagIds) { + mpService.getUserTagService().batchTagging(tagId, new String[]{openid}); + } + } catch (WxErrorException e) { + throw exception(USER_UPDATE_TAG_FAIL, e.getError().getErrorMsg()); + } + } + +}