From f8f3cc82771077535d186910e721398d7c05afa9 Mon Sep 17 00:00:00 2001 From: aiotagro Date: Sat, 4 Oct 2025 20:19:47 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=BC=80=E5=8F=91=E5=92=8C?= =?UTF-8?q?=E7=94=9F=E4=BA=A7=E7=8E=AF=E5=A2=83=E9=85=8D=E7=BD=AE=EF=BC=8C?= =?UTF-8?q?=E5=90=AF=E7=94=A8=E4=BC=9A=E5=91=98=E3=80=81=E6=8A=A5=E8=A1=A8?= =?UTF-8?q?=E3=80=81=E6=94=AF=E4=BB=98=E5=92=8C=E5=BE=AE=E4=BF=A1=E5=85=AC?= =?UTF-8?q?=E4=BC=97=E5=8F=B7=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- aagro-module-member/pom.xml | 88 ++ .../member/api/address/MemberAddressApi.java | 29 + .../api/address/MemberAddressApiImpl.java | 33 + .../api/address/dto/MemberAddressRespDTO.java | 42 + .../member/api/config/MemberConfigApi.java | 18 + .../api/config/MemberConfigApiImpl.java | 28 + .../api/config/dto/MemberConfigRespDTO.java | 32 + .../member/api/level/MemberLevelApi.java | 41 + .../member/api/level/MemberLevelApiImpl.java | 48 + .../api/level/dto/MemberLevelRespDTO.java | 41 + .../member/api/message/package-info.java | 4 + .../message/user/MemberUserCreateMessage.java | 21 + .../pp/module/member/api/package-info.java | 4 + .../member/api/point/MemberPointApi.java | 36 + .../member/api/point/MemberPointApiImpl.java | 50 + .../module/member/api/user/MemberUserApi.java | 68 + .../member/api/user/MemberUserApiImpl.java | 58 + .../api/user/dto/MemberUserRespDTO.java | 55 + .../admin/address/AddressController.java | 41 + .../admin/address/package-info.java | 1 + .../admin/address/vo/AddressBaseVO.java | 37 + .../admin/address/vo/AddressRespVO.java | 19 + .../admin/config/MemberConfigController.java | 45 + .../admin/config/vo/MemberConfigBaseVO.java | 31 + .../admin/config/vo/MemberConfigRespVO.java | 17 + .../config/vo/MemberConfigSaveReqVO.java | 13 + .../admin/group/MemberGroupController.java | 81 ++ .../admin/group/vo/MemberGroupBaseVO.java | 29 + .../group/vo/MemberGroupCreateReqVO.java | 14 + .../admin/group/vo/MemberGroupPageReqVO.java | 30 + .../admin/group/vo/MemberGroupRespVO.java | 22 + .../group/vo/MemberGroupSimpleRespVO.java | 18 + .../group/vo/MemberGroupUpdateReqVO.java | 20 + .../MemberExperienceRecordController.java | 52 + .../admin/level/MemberLevelController.java | 80 ++ .../level/MemberLevelRecordController.java | 52 + .../MemberExperienceRecordBaseVO.java | 43 + .../MemberExperienceRecordPageReqVO.java | 36 + .../MemberExperienceRecordRespVO.java | 22 + .../level/vo/level/MemberLevelBaseVO.java | 53 + .../vo/level/MemberLevelCreateReqVO.java | 14 + .../level/vo/level/MemberLevelListReqVO.java | 18 + .../level/vo/level/MemberLevelRespVO.java | 22 + .../vo/level/MemberLevelSimpleRespVO.java | 21 + .../vo/level/MemberLevelUpdateReqVO.java | 20 + .../vo/record/MemberLevelRecordBaseVO.java | 47 + .../vo/record/MemberLevelRecordPageReqVO.java | 30 + .../vo/record/MemberLevelRecordRespVO.java | 22 + .../point/MemberPointRecordController.java | 56 + .../vo/recrod/MemberPointRecordPageReqVO.java | 27 + .../vo/recrod/MemberPointRecordRespVO.java | 42 + .../signin/MemberSignInConfigController.java | 74 ++ .../signin/MemberSignInRecordController.java | 55 + .../vo/config/MemberSignInConfigBaseVO.java | 45 + .../config/MemberSignInConfigCreateReqVO.java | 12 + .../vo/config/MemberSignInConfigRespVO.java | 19 + .../config/MemberSignInConfigUpdateReqVO.java | 18 + .../record/MemberSignInRecordPageReqVO.java | 33 + .../vo/record/MemberSignInRecordRespVO.java | 30 + .../admin/tag/MemberTagController.java | 94 ++ .../admin/tag/vo/MemberTagBaseVO.java | 19 + .../admin/tag/vo/MemberTagCreateReqVO.java | 14 + .../admin/tag/vo/MemberTagPageReqVO.java | 27 + .../admin/tag/vo/MemberTagRespVO.java | 22 + .../admin/tag/vo/MemberTagUpdateReqVO.java | 20 + .../admin/user/MemberUserController.java | 113 ++ .../admin/user/vo/MemberUserBaseVO.java | 65 + .../admin/user/vo/MemberUserPageReqVO.java | 48 + .../admin/user/vo/MemberUserRespVO.java | 52 + .../user/vo/MemberUserUpdateLevelReqVO.java | 29 + .../user/vo/MemberUserUpdatePointReqVO.java | 22 + .../admin/user/vo/MemberUserUpdateReqVO.java | 20 + .../app/address/AppAddressController.http | 54 + .../app/address/AppAddressController.java | 75 ++ .../app/address/vo/AppAddressBaseVO.java | 35 + .../app/address/vo/AppAddressCreateReqVO.java | 11 + .../app/address/vo/AppAddressRespVO.java | 20 + .../app/address/vo/AppAddressUpdateReqVO.java | 16 + .../app/auth/AppAuthController.http | 67 + .../app/auth/AppAuthController.java | 135 ++ .../app/auth/vo/AppAuthCheckCodeReqVO.java | 41 + .../app/auth/vo/AppAuthLoginReqVO.java | 56 + .../app/auth/vo/AppAuthLoginRespVO.java | 38 + .../app/auth/vo/AppAuthSmsLoginReqVO.java | 58 + .../app/auth/vo/AppAuthSmsSendReqVO.java | 24 + .../app/auth/vo/AppAuthSmsValidateReqVO.java | 33 + .../app/auth/vo/AppAuthSocialLoginReqVO.java | 34 + .../vo/AppAuthWeixinMiniAppLoginReqVO.java | 30 + .../vo/AuthWeixinJsapiSignatureRespVO.java | 31 + .../AppMemberExperienceRecordController.java | 41 + .../app/level/AppMemberLevelController.java | 38 + .../AppMemberExperienceRecordRespVO.java | 24 + .../level/vo/level/AppMemberLevelRespVO.java | 28 + .../point/AppMemberPointRecordController.java | 40 + .../vo/AppMemberPointRecordPageReqVO.java | 23 + .../point/vo/AppMemberPointRecordRespVO.java | 27 + .../AppMemberSignInConfigController.java | 39 + .../AppMemberSignInRecordController.java | 53 + .../config/AppMemberSignInConfigRespVO.java | 16 + .../record/AppMemberSignInRecordRespVO.java | 24 + .../AppMemberSignInRecordSummaryRespVO.java | 19 + .../app/social/AppSocialUserController.java | 79 ++ .../app/social/vo/AppSocialUserBindReqVO.java | 31 + .../app/social/vo/AppSocialUserRespVO.java | 19 + .../social/vo/AppSocialUserUnbindReqVO.java | 27 + .../social/vo/AppSocialWxaQrcodeReqVO.java | 39 + .../AppSocialWxaSubscribeTemplateRespVO.java | 27 + .../app/user/AppMemberUserController.http | 4 + .../app/user/AppMemberUserController.java | 79 ++ .../app/user/vo/AppMemberUserInfoRespVO.java | 59 + .../vo/AppMemberUserResetPasswordReqVO.java | 38 + ...ppMemberUserUpdateMobileByWeixinReqVO.java | 17 + .../vo/AppMemberUserUpdateMobileReqVO.java | 36 + .../vo/AppMemberUserUpdatePasswordReqVO.java | 31 + .../app/user/vo/AppMemberUserUpdateReqVO.java | 23 + .../member/controller/package-info.java | 6 + .../convert/address/AddressConvert.java | 45 + .../member/convert/auth/AuthConvert.java | 35 + .../convert/config/MemberConfigConvert.java | 25 + .../convert/group/MemberGroupConvert.java | 35 + .../level/MemberExperienceRecordConvert.java | 34 + .../convert/level/MemberLevelConvert.java | 39 + .../level/MemberLevelRecordConvert.java | 37 + .../module/member/convert/package-info.java | 6 + .../point/MemberPointRecordConvert.java | 37 + .../signin/MemberSignInConfigConvert.java | 33 + .../signin/MemberSignInRecordConvert.java | 74 ++ .../member/convert/tag/MemberTagConvert.java | 33 + .../convert/user/MemberUserConvert.java | 68 + ...道 Spring Boot 对象转换 MapStruct 入门》.md | 1 + .../dataobject/address/MemberAddressDO.java | 56 + .../dal/dataobject/config/MemberConfigDO.java | 48 + .../dal/dataobject/group/MemberGroupDO.java | 45 + .../level/MemberExperienceRecordDO.java | 64 + .../dal/dataobject/level/MemberLevelDO.java | 64 + .../dataobject/level/MemberLevelRecordDO.java | 71 ++ .../dataobject/point/MemberPointRecordDO.java | 69 + .../signin/MemberSignInConfigDO.java | 50 + .../signin/MemberSignInRecordDO.java | 46 + .../dal/dataobject/tag/MemberTagDO.java | 34 + .../dal/dataobject/user/MemberUserDO.java | 145 +++ .../mysql/address/MemberAddressMapper.java | 22 + .../dal/mysql/config/MemberConfigMapper.java | 14 + .../dal/mysql/group/MemberGroupMapper.java | 31 + .../level/MemberExperienceRecordMapper.java | 35 + .../dal/mysql/level/MemberLevelMapper.java | 33 + .../mysql/level/MemberLevelRecordMapper.java | 26 + .../mysql/point/MemberPointRecordMapper.java | 42 + .../signin/MemberSignInConfigMapper.java | 24 + .../signin/MemberSignInRecordMapper.java | 65 + .../member/dal/mysql/tag/MemberTagMapper.java | 28 + .../dal/mysql/user/MemberUserMapper.java | 96 ++ .../pp/module/member/dal/package-info.java | 9 + .../module/member/dal/redis/package-info.java | 4 + .../member/enums/DictTypeConstants.java | 15 + .../member/enums/ErrorCodeConstants.java | 58 + .../enums/MemberExperienceBizTypeEnum.java | 51 + .../enums/point/MemberPointBizTypeEnum.java | 58 + .../module/member/framework/package-info.java | 6 + .../web/config/MemberWebConfiguration.java | 24 + .../member/framework/web/package-info.java | 4 + .../member/mq/consumer/package-info.java | 4 + .../member/mq/message/package-info.java | 4 + .../member/mq/producer/package-info.java | 4 + .../mq/producer/user/MemberUserProducer.java | 31 + .../aagro/pp/module/member/package-info.java | 8 + .../service/address/AddressService.java | 67 + .../service/address/AddressServiceImpl.java | 97 ++ .../service/auth/MemberAuthService.java | 88 ++ .../service/auth/MemberAuthServiceImpl.java | 285 +++++ .../service/config/MemberConfigService.java | 29 + .../config/MemberConfigServiceImpl.java | 44 + .../service/group/MemberGroupService.java | 84 ++ .../service/group/MemberGroupServiceImpl.java | 103 ++ .../level/MemberExperienceRecordService.java | 53 + .../MemberExperienceRecordServiceImpl.java | 55 + .../level/MemberLevelRecordService.java | 37 + .../level/MemberLevelRecordServiceImpl.java | 39 + .../service/level/MemberLevelService.java | 102 ++ .../service/level/MemberLevelServiceImpl.java | 299 +++++ .../point/MemberPointRecordService.java | 42 + .../point/MemberPointRecordServiceImpl.java | 96 ++ .../signin/MemberSignInConfigService.java | 62 + .../signin/MemberSignInConfigServiceImpl.java | 106 ++ .../signin/MemberSignInRecordService.java | 50 + .../signin/MemberSignInRecordServiceImpl.java | 145 +++ .../member/service/tag/MemberTagService.java | 73 ++ .../service/tag/MemberTagServiceImpl.java | 125 ++ .../service/user/MemberUserService.java | 190 +++ .../service/user/MemberUserServiceImpl.java | 317 +++++ .../address/MemberAddressServiceImplTest.java | 98 ++ .../service/auth/MemberAuthServiceTest.java | 118 ++ .../group/MemberGroupServiceImplTest.java | 160 +++ .../level/MemberLevelServiceImplTest.java | 268 ++++ .../service/tag/MemberTagServiceImplTest.java | 133 ++ .../user/MemberUserServiceImplTest.java | 115 ++ .../test/resources/application-unit-test.yaml | 47 + .../src/test/resources/logback.xml | 4 + .../src/test/resources/sql/clean.sql | 5 + .../src/test/resources/sql/create_tables.sql | 113 ++ aagro-module-mp/pom.xml | 89 ++ .../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 | 38 + .../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 | 117 ++ .../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 | 30 + .../admin/user/vo/MpUserRespVO.java | 55 + .../admin/user/vo/MpUserUpdateReqVO.java | 26 + .../pp/module/mp/controller/package-info.java | 6 + .../mp/convert/account/MpAccountConvert.java | 31 + .../convert/material/MpMaterialConvert.java | 47 + .../module/mp/convert/menu/MpMenuConvert.java | 50 + .../convert/message/MpAutoReplyConvert.java | 37 + .../mp/convert/message/MpMessageConvert.java | 172 +++ .../statistics/MpStatisticsConvert.java | 52 + .../module/mp/convert/tag/MpTagConvert.java | 44 + .../module/mp/convert/user/MpUserConvert.java | 56 + .../dal/dataobject/account/MpAccountDO.java | 62 + .../dal/dataobject/material/MpMaterialDO.java | 99 ++ .../mp/dal/dataobject/menu/MpMenuDO.java | 185 +++ .../dal/dataobject/message/MpAutoReplyDO.java | 165 +++ .../dal/dataobject/message/MpMessageDO.java | 242 ++++ .../module/mp/dal/dataobject/tag/MpTagDO.java | 58 + .../mp/dal/dataobject/user/MpUserDO.java | 114 ++ .../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 | 23 + .../module/mp/dal/mysql/tag/MpTagMapper.java | 26 + .../mp/dal/mysql/user/MpUserMapper.java | 35 + .../module/mp/enums/ErrorCodeConstants.java | 64 + .../enums/message/MpAutoReplyMatchEnum.java | 28 + .../mp/enums/message/MpAutoReplyTypeEnum.java | 29 + .../enums/message/MpMessageSendFromEnum.java | 28 + .../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 +++ .../pp/module/mp/framework/package-info.java | 6 + .../web/config/MpWebConfiguration.java | 24 + .../module/mp/framework/web/package-info.java | 4 + .../cn/aagro/pp/module/mp/package-info.java | 8 + .../mp/service/account/MpAccountService.java | 110 ++ .../service/account/MpAccountServiceImpl.java | 229 ++++ .../mp/service/handler/menu/MenuHandler.java | 34 + .../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 | 61 + .../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 | 59 + .../service/message/MpMessageServiceImpl.java | 172 +++ .../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 ++++ aagro-module-pay/pom.xml | 91 ++ .../api/notify/dto/PayOrderNotifyReqDTO.java | 34 + .../api/notify/dto/PayRefundNotifyReqDTO.java | 40 + .../notify/dto/PayTransferNotifyReqDTO.java | 34 + .../module/pay/api/notify/package-info.java | 4 + .../pp/module/pay/api/order/PayOrderApi.java | 40 + .../module/pay/api/order/PayOrderApiImpl.java | 39 + .../api/order/dto/PayOrderCreateReqDTO.java | 78 ++ .../pay/api/order/dto/PayOrderRespDTO.java | 68 + .../aagro/pp/module/pay/api/package-info.java | 4 + .../module/pay/api/refund/PayRefundApi.java | 31 + .../pay/api/refund/PayRefundApiImpl.java | 36 + .../api/refund/dto/PayRefundCreateReqDTO.java | 71 ++ .../pay/api/refund/dto/PayRefundRespDTO.java | 65 + .../pay/api/transfer/PayTransferApi.java | 31 + .../pay/api/transfer/PayTransferApiImpl.java | 50 + .../transfer/dto/PayTransferCreateReqDTO.java | 133 ++ .../dto/PayTransferCreateRespDTO.java | 28 + .../api/transfer/dto/PayTransferRespDTO.java | 81 ++ .../module/pay/api/wallet/PayWalletApi.java | 29 + .../pay/api/wallet/PayWalletApiImpl.java | 42 + .../wallet/dto/PayWalletAddBalanceReqDTO.java | 50 + .../pay/api/wallet/dto/PayWalletRespDTO.java | 52 + .../admin/app/PayAppController.java | 108 ++ .../controller/admin/app/vo/PayAppBaseVO.java | 47 + .../admin/app/vo/PayAppCreateReqVO.java | 14 + .../admin/app/vo/PayAppPageItemRespVO.java | 26 + .../admin/app/vo/PayAppPageReqVO.java | 33 + .../controller/admin/app/vo/PayAppRespVO.java | 25 + .../admin/app/vo/PayAppUpdateReqVO.java | 17 + .../admin/app/vo/PayAppUpdateStatusReqVO.java | 20 + .../admin/channel/PayChannelController.java | 82 ++ .../admin/channel/vo/PayChannelBaseVO.java | 31 + .../channel/vo/PayChannelCreateReqVO.java | 25 + .../admin/channel/vo/PayChannelRespVO.java | 25 + .../channel/vo/PayChannelUpdateReqVO.java | 20 + .../admin/demo/PayDemoOrderController.java | 77 ++ .../admin/demo/PayDemoWithdrawController.http | 50 + .../admin/demo/PayDemoWithdrawController.java | 65 + .../vo/order/PayDemoOrderCreateReqVO.java | 16 + .../demo/vo/order/PayDemoOrderRespVO.java | 54 + .../withdraw/PayDemoWithdrawCreateReqVO.java | 43 + .../vo/withdraw/PayDemoWithdrawRespVO.java | 47 + .../admin/notify/PayNotifyController.java | 169 +++ .../notify/vo/PayNotifyTaskDetailRespVO.java | 42 + .../notify/vo/PayNotifyTaskPageReqVO.java | 45 + .../admin/notify/vo/PayNotifyTaskRespVO.java | 56 + .../admin/order/PayOrderController.java | 145 +++ .../admin/order/vo/PayOrderBaseVO.java | 89 ++ .../admin/order/vo/PayOrderDetailsRespVO.java | 45 + .../admin/order/vo/PayOrderExcelVO.java | 67 + .../admin/order/vo/PayOrderExportReqVO.java | 37 + .../order/vo/PayOrderPageItemRespVO.java | 25 + .../admin/order/vo/PayOrderPageReqVO.java | 42 + .../admin/order/vo/PayOrderRespVO.java | 22 + .../admin/order/vo/PayOrderSubmitReqVO.java | 33 + .../admin/order/vo/PayOrderSubmitRespVO.java | 18 + .../admin/refund/PayRefundController.java | 90 ++ .../admin/refund/vo/PayRefundBaseVO.java | 78 ++ .../refund/vo/PayRefundDetailsRespVO.java | 40 + .../admin/refund/vo/PayRefundExcelVO.java | 61 + .../admin/refund/vo/PayRefundExportReqVO.java | 40 + .../refund/vo/PayRefundPageItemRespVO.java | 25 + .../admin/refund/vo/PayRefundPageReqVO.java | 45 + .../admin/transfer/PayTransferController.java | 91 ++ .../transfer/vo/PayTransferCreateRespVO.java | 16 + .../transfer/vo/PayTransferPageReqVO.java | 45 + .../admin/transfer/vo/PayTransferRespVO.java | 100 ++ .../admin/wallet/PayWalletController.java | 71 ++ .../wallet/PayWalletRechargeController.java | 59 + .../PayWalletRechargePackageController.java | 75 ++ .../PayWalletTransactionController.java | 43 + .../WalletRechargePackageBaseVO.java | 31 + .../WalletRechargePackageCreateReqVO.java | 14 + .../WalletRechargePackagePageReqVO.java | 30 + .../WalletRechargePackageRespVO.java | 19 + .../WalletRechargePackageUpdateReqVO.java | 20 + .../PayWalletTransactionPageReqVO.java | 23 + .../PayWalletTransactionRespVO.java | 35 + .../wallet/vo/wallet/PayWalletBaseVO.java | 39 + .../wallet/vo/wallet/PayWalletPageReqVO.java | 33 + .../wallet/vo/wallet/PayWalletRespVO.java | 22 + .../wallet/PayWalletUpdateBalanceReqVO.java | 20 + .../wallet/vo/wallet/PayWalletUserReqVO.java | 16 + .../app/channel/AppPayChannelController.java | 39 + .../app/order/AppPayOrderController.http | 63 + .../app/order/AppPayOrderController.java | 90 ++ .../app/order/vo/AppPayOrderSubmitReqVO.java | 14 + .../app/order/vo/AppPayOrderSubmitRespVO.java | 14 + .../transfer/AppPayTransferController.java | 37 + .../app/wallet/AppPayWalletController.java | 42 + .../AppPayWalletRechargeController.java | 72 ++ ...AppPayWalletRechargePackageController.java | 42 + .../AppPayWalletTransactionController.java | 60 + .../recharge/AppPayWalletPackageRespVO.java | 20 + .../AppPayWalletRechargeCreateReqVO.java | 26 + .../AppPayWalletRechargeCreateRespVO.java | 16 + .../recharge/AppPayWalletRechargeRespVO.java | 42 + .../AppPayWalletTransactionPageReqVO.java | 31 + .../AppPayWalletTransactionRespVO.java | 24 + .../AppPayWalletTransactionSummaryRespVO.java | 16 + .../wallet/vo/wallet/AppPayWalletRespVO.java | 19 + .../module/pay/controller/package-info.java | 6 + .../module/pay/convert/app/PayAppConvert.java | 49 + .../convert/channel/PayChannelConvert.java | 28 + .../pay/convert/order/PayOrderConvert.java | 71 ++ .../pp/module/pay/convert/package-info.java | 6 + .../pay/convert/refund/PayRefundConvert.java | 53 + .../pay/convert/wallet/PayWalletConvert.java | 21 + .../wallet/PayWalletRechargeConvert.java | 43 + .../PayWalletRechargePackageConvert.java | 29 + .../wallet/PayWalletTransactionConvert.java | 20 + ...道 Spring Boot 对象转换 MapStruct 入门》.md | 1 + .../pay/dal/dataobject/app/PayAppDO.java | 66 + .../dal/dataobject/channel/PayChannelDO.java | 116 ++ .../dal/dataobject/demo/PayDemoOrderDO.java | 87 ++ .../dataobject/demo/PayDemoWithdrawDO.java | 84 ++ .../dal/dataobject/notify/PayNotifyLogDO.java | 51 + .../dataobject/notify/PayNotifyTaskDO.java | 102 ++ .../pay/dal/dataobject/order/PayOrderDO.java | 147 +++ .../dataobject/order/PayOrderExtensionDO.java | 96 ++ .../dal/dataobject/refund/PayRefundDO.java | 169 +++ .../dataobject/transfer/PayTransferDO.java | 152 +++ .../dal/dataobject/wallet/PayWalletDO.java | 59 + .../wallet/PayWalletRechargeDO.java | 116 ++ .../wallet/PayWalletRechargePackageDO.java | 47 + .../wallet/PayWalletTransactionDO.java | 66 + .../pay/dal/mysql/app/PayAppMapper.java | 26 + .../dal/mysql/channel/PayChannelMapper.java | 31 + .../dal/mysql/demo/PayDemoOrderMapper.java | 28 + .../dal/mysql/demo/PayDemoWithdrawMapper.java | 24 + .../dal/mysql/notify/PayNotifyLogMapper.java | 16 + .../dal/mysql/notify/PayNotifyTaskMapper.java | 46 + .../mysql/order/PayOrderExtensionMapper.java | 38 + .../pay/dal/mysql/order/PayOrderMapper.java | 62 + .../pay/dal/mysql/refund/PayRefundMapper.java | 78 ++ .../dal/mysql/transfer/PayTransferMapper.java | 65 + .../pay/dal/mysql/wallet/PayWalletMapper.java | 134 ++ .../mysql/wallet/PayWalletRechargeMapper.java | 31 + .../PayWalletRechargePackageMapper.java | 32 + .../wallet/PayWalletTransactionMapper.java | 67 + .../pay/dal/redis/RedisKeyConstants.java | 36 + .../pay/dal/redis/no/PayNoRedisDAO.java | 39 + .../redis/notify/PayNotifyLockRedisDAO.java | 39 + .../redis/wallet/PayWalletLockRedisDAO.java | 42 + .../module/pay/enums/DictTypeConstants.java | 20 + .../module/pay/enums/ErrorCodeConstants.java | 97 ++ .../pay/enums/MessageTemplateConstants.java | 14 + .../pp/module/pay/enums/PayChannelEnum.java | 67 + .../enums/demo/PayDemoWithdrawStatusEnum.java | 42 + .../enums/demo/PayDemoWithdrawTypeEnum.java | 39 + .../pay/enums/notify/PayNotifyStatusEnum.java | 32 + .../pay/enums/notify/PayNotifyTypeEnum.java | 29 + .../pay/enums/order/PayOrderStatusEnum.java | 84 ++ .../pay/enums/refund/PayRefundStatusEnum.java | 32 + .../enums/transfer/PayTransferStatusEnum.java | 67 + .../enums/wallet/PayWalletBizTypeEnum.java | 45 + .../job/config/PayJobConfiguration.java | 28 + .../pay/framework/job/core/package-info.java | 4 + .../pp/module/pay/framework/package-info.java | 6 + .../pay/config/PayConfiguration.java | 18 + .../framework/pay/config/PayProperties.java | 69 + .../framework/pay/core/client/PayClient.java | 118 ++ .../pay/core/client/PayClientConfig.java | 27 + .../pay/core/client/PayClientFactory.java | 28 + .../client/dto/order/PayOrderRespDTO.java | 141 +++ .../dto/order/PayOrderUnifiedReqDTO.java | 92 ++ .../client/dto/refund/PayRefundRespDTO.java | 115 ++ .../dto/refund/PayRefundUnifiedReqDTO.java | 68 + .../dto/transfer/PayTransferRespDTO.java | 116 ++ .../transfer/PayTransferUnifiedReqDTO.java | 73 ++ .../client/exception/PayClientException.java | 17 + .../core/client/impl/AbstractPayClient.java | 251 ++++ .../core/client/impl/NonePayClientConfig.java | 31 + .../client/impl/PayClientFactoryImpl.java | 97 ++ .../impl/alipay/AbstractAlipayPayClient.java | 379 ++++++ .../impl/alipay/AlipayAppPayClient.java | 60 + .../impl/alipay/AlipayBarPayClient.java | 86 ++ .../impl/alipay/AlipayPayClientConfig.java | 130 ++ .../client/impl/alipay/AlipayPcPayClient.java | 70 ++ .../client/impl/alipay/AlipayQrPayClient.java | 66 + .../impl/alipay/AlipayWapPayClient.java | 59 + .../core/client/impl/mock/MockPayClient.java | 84 ++ .../client/impl/wallet/WalletPayClient.java | 251 ++++ .../impl/weixin/AbstractWxPayClient.java | 599 +++++++++ .../client/impl/weixin/WxAppPayClient.java | 63 + .../client/impl/weixin/WxBarPayClient.java | 107 ++ .../client/impl/weixin/WxLitePayClient.java | 22 + .../client/impl/weixin/WxNativePayClient.java | 59 + .../client/impl/weixin/WxPayClientConfig.java | 107 ++ .../client/impl/weixin/WxPubPayClient.java | 81 ++ .../client/impl/weixin/WxWapPayClient.java | 62 + .../core/enums/PayOrderDisplayModeEnum.java | 29 + .../pay/framework/pay/package-info.java | 6 + .../web/config/PayWebConfiguration.java | 24 + .../pay/framework/web/package-info.java | 4 + .../module/pay/job/notify/PayNotifyJob.java | 31 + .../pay/job/order/PayOrderExpireJob.java | 31 + .../module/pay/job/order/PayOrderSyncJob.java | 43 + .../pay/job/refund/PayRefundSyncJob.java | 31 + .../pay/job/transfer/PayTransferSyncJob.java | 30 + .../cn/aagro/pp/module/pay/package-info.java | 10 + .../module/pay/service/app/PayAppService.java | 115 ++ .../pay/service/app/PayAppServiceImpl.java | 167 +++ .../service/channel/PayChannelService.java | 104 ++ .../channel/PayChannelServiceImpl.java | 165 +++ .../pay/service/demo/PayDemoOrderService.java | 67 + .../service/demo/PayDemoOrderServiceImpl.java | 268 ++++ .../service/demo/PayDemoWithdrawService.java | 49 + .../demo/PayDemoWithdrawServiceImpl.java | 198 +++ .../pay/service/notify/PayNotifyService.java | 57 + .../service/notify/PayNotifyServiceImpl.java | 323 +++++ .../pay/service/order/PayOrderService.java | 159 +++ .../service/order/PayOrderServiceImpl.java | 605 +++++++++ .../pay/service/refund/PayRefundService.java | 82 ++ .../service/refund/PayRefundServiceImpl.java | 331 +++++ .../service/transfer/PayTransferService.java | 70 ++ .../transfer/PayTransferServiceImpl.java | 316 +++++ .../PayWalletRechargePackageService.java | 71 ++ .../PayWalletRechargePackageServiceImpl.java | 113 ++ .../wallet/PayWalletRechargeService.java | 64 + .../wallet/PayWalletRechargeServiceImpl.java | 351 ++++++ .../pay/service/wallet/PayWalletService.java | 99 ++ .../service/wallet/PayWalletServiceImpl.java | 233 ++++ .../wallet/PayWalletTransactionService.java | 74 ++ .../PayWalletTransactionServiceImpl.java | 95 ++ .../bo/WalletTransactionCreateReqBO.java | 59 + .../PayClientFactoryImplIntegrationTest.java | 134 ++ .../impl/alipay/AbstractAlipayClientTest.java | 221 ++++ .../impl/alipay/AlipayBarPayClientTest.java | 170 +++ .../impl/alipay/AlipayPcPayClientTest.java | 131 ++ .../impl/alipay/AlipayQrPayClientTest.java | 147 +++ .../impl/alipay/AlipayWapPayClientTest.java | 111 ++ .../weixin/WxBarPayClientIntegrationTest.java | 123 ++ .../WxNativePayClientIntegrationTest.java | 84 ++ .../pay/service/app/PayAppServiceTest.java | 260 ++++ .../channel/PayChannelServiceTest.java | 338 +++++ .../service/notify/PayNotifyServiceTest.java | 353 ++++++ .../service/order/PayOrderServiceTest.java | 1108 +++++++++++++++++ .../service/refund/PayRefundServiceTest.java | 699 +++++++++++ .../test/resources/application-unit-test.yaml | 48 + .../src/test/resources/logback.xml | 4 + .../src/test/resources/sql/clean.sql | 8 + .../src/test/resources/sql/create_tables.sql | 180 +++ aagro-module-report/pom.xml | 74 ++ .../admin/ajreport/package-info.java | 1 + .../admin/goview/GoViewDataController.java | 63 + .../admin/goview/GoViewProjectController.java | 77 ++ .../vo/data/GoViewDataGetBySqlReqVO.java | 16 + .../goview/vo/data/GoViewDataRespVO.java | 19 + .../vo/project/GoViewProjectCreateReqVO.java | 15 + .../vo/project/GoViewProjectRespVO.java | 36 + .../vo/project/GoViewProjectUpdateReqVO.java | 34 + .../report/controller/package-info.java | 6 + .../report/convert/ajreport/package-info.java | 4 + .../convert/goview/GoViewProjectConvert.java | 24 + .../dal/dataobject/ajreport/package-info.java | 4 + .../dataobject/goview/GoViewProjectDO.java | 57 + .../dal/mysql/ajreport/package-info.java | 4 + .../dal/mysql/goview/GoViewProjectMapper.java | 19 + .../report/enums/ErrorCodeConstants.java | 15 + .../config/JmReportConfiguration.java | 37 + .../service/JmOnlDragExternalServiceImpl.java | 68 + .../service/JmReportTokenServiceImpl.java | 161 +++ .../jmreport/core/web/package-info.java | 4 + .../module/report/framework/package-info.java | 6 + .../config/SecurityConfiguration.java | 32 + .../framework/security/core/package-info.java | 4 + .../aagro/pp/module/report/package-info.java | 9 + .../report/service/ajreport/package-info.java | 4 + .../service/goview/GoViewDataService.java | 20 + .../service/goview/GoViewDataServiceImpl.java | 55 + .../service/goview/GoViewProjectService.java | 57 + .../goview/GoViewProjectServiceImpl.java | 74 ++ .../goview/GoViewDataServiceImplTest.java | 58 + .../goview/GoViewProjectServiceImplTest.java | 135 ++ .../test/resources/application-unit-test.yaml | 47 + .../src/test/resources/logback.xml | 4 + .../src/test/resources/sql/clean.sql | 1 + .../src/test/resources/sql/create_tables.sql | 14 + aagro-server/pom.xml | 40 +- .../src/main/resources/application-dev.yaml | 16 +- .../src/main/resources/application-local.yaml | 29 +- .../src/main/resources/application-prod.yaml | 6 +- pom.xml | 8 +- 610 files changed, 38505 insertions(+), 56 deletions(-) create mode 100644 aagro-module-member/pom.xml create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/address/MemberAddressApi.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/address/MemberAddressApiImpl.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/address/dto/MemberAddressRespDTO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/config/MemberConfigApi.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/config/MemberConfigApiImpl.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/config/dto/MemberConfigRespDTO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/level/MemberLevelApi.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/level/MemberLevelApiImpl.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/level/dto/MemberLevelRespDTO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/message/package-info.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/message/user/MemberUserCreateMessage.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/package-info.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/point/MemberPointApi.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/point/MemberPointApiImpl.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/user/MemberUserApi.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/user/MemberUserApiImpl.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/user/dto/MemberUserRespDTO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/address/AddressController.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/address/package-info.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/address/vo/AddressBaseVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/address/vo/AddressRespVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/config/MemberConfigController.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/config/vo/MemberConfigBaseVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/config/vo/MemberConfigRespVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/config/vo/MemberConfigSaveReqVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/group/MemberGroupController.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/group/vo/MemberGroupBaseVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/group/vo/MemberGroupCreateReqVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/group/vo/MemberGroupPageReqVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/group/vo/MemberGroupRespVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/group/vo/MemberGroupSimpleRespVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/group/vo/MemberGroupUpdateReqVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/MemberExperienceRecordController.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/MemberLevelController.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/MemberLevelRecordController.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/vo/experience/MemberExperienceRecordBaseVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/vo/experience/MemberExperienceRecordPageReqVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/vo/experience/MemberExperienceRecordRespVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/vo/level/MemberLevelBaseVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/vo/level/MemberLevelCreateReqVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/vo/level/MemberLevelListReqVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/vo/level/MemberLevelRespVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/vo/level/MemberLevelSimpleRespVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/vo/level/MemberLevelUpdateReqVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/vo/record/MemberLevelRecordBaseVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/vo/record/MemberLevelRecordPageReqVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/vo/record/MemberLevelRecordRespVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/point/MemberPointRecordController.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/point/vo/recrod/MemberPointRecordPageReqVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/point/vo/recrod/MemberPointRecordRespVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/signin/MemberSignInConfigController.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/signin/MemberSignInRecordController.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/signin/vo/config/MemberSignInConfigBaseVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/signin/vo/config/MemberSignInConfigCreateReqVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/signin/vo/config/MemberSignInConfigRespVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/signin/vo/config/MemberSignInConfigUpdateReqVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/signin/vo/record/MemberSignInRecordPageReqVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/signin/vo/record/MemberSignInRecordRespVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/tag/MemberTagController.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/tag/vo/MemberTagBaseVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/tag/vo/MemberTagCreateReqVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/tag/vo/MemberTagPageReqVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/tag/vo/MemberTagRespVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/tag/vo/MemberTagUpdateReqVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/user/MemberUserController.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/user/vo/MemberUserBaseVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/user/vo/MemberUserPageReqVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/user/vo/MemberUserRespVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/user/vo/MemberUserUpdateLevelReqVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/user/vo/MemberUserUpdatePointReqVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/user/vo/MemberUserUpdateReqVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/address/AppAddressController.http create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/address/AppAddressController.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/address/vo/AppAddressBaseVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/address/vo/AppAddressCreateReqVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/address/vo/AppAddressRespVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/address/vo/AppAddressUpdateReqVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/auth/AppAuthController.http create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/auth/AppAuthController.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/auth/vo/AppAuthCheckCodeReqVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/auth/vo/AppAuthLoginReqVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/auth/vo/AppAuthLoginRespVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/auth/vo/AppAuthSmsLoginReqVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/auth/vo/AppAuthSmsSendReqVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/auth/vo/AppAuthSmsValidateReqVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/auth/vo/AppAuthSocialLoginReqVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/auth/vo/AppAuthWeixinMiniAppLoginReqVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/auth/vo/AuthWeixinJsapiSignatureRespVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/level/AppMemberExperienceRecordController.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/level/AppMemberLevelController.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/level/vo/experience/AppMemberExperienceRecordRespVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/level/vo/level/AppMemberLevelRespVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/point/AppMemberPointRecordController.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/point/vo/AppMemberPointRecordPageReqVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/point/vo/AppMemberPointRecordRespVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/signin/AppMemberSignInConfigController.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/signin/AppMemberSignInRecordController.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/signin/vo/config/AppMemberSignInConfigRespVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/signin/vo/record/AppMemberSignInRecordRespVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/signin/vo/record/AppMemberSignInRecordSummaryRespVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/social/AppSocialUserController.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/social/vo/AppSocialUserBindReqVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/social/vo/AppSocialUserRespVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/social/vo/AppSocialUserUnbindReqVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/social/vo/AppSocialWxaQrcodeReqVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/social/vo/AppSocialWxaSubscribeTemplateRespVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/user/AppMemberUserController.http create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/user/AppMemberUserController.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/user/vo/AppMemberUserInfoRespVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/user/vo/AppMemberUserResetPasswordReqVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/user/vo/AppMemberUserUpdateMobileByWeixinReqVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/user/vo/AppMemberUserUpdateMobileReqVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/user/vo/AppMemberUserUpdatePasswordReqVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/user/vo/AppMemberUserUpdateReqVO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/package-info.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/convert/address/AddressConvert.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/convert/auth/AuthConvert.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/convert/config/MemberConfigConvert.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/convert/group/MemberGroupConvert.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/convert/level/MemberExperienceRecordConvert.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/convert/level/MemberLevelConvert.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/convert/level/MemberLevelRecordConvert.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/convert/package-info.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/convert/point/MemberPointRecordConvert.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/convert/signin/MemberSignInConfigConvert.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/convert/signin/MemberSignInRecordConvert.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/convert/tag/MemberTagConvert.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/convert/user/MemberUserConvert.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/convert/《芋道 Spring Boot 对象转换 MapStruct 入门》.md create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/dataobject/address/MemberAddressDO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/dataobject/config/MemberConfigDO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/dataobject/group/MemberGroupDO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/dataobject/level/MemberExperienceRecordDO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/dataobject/level/MemberLevelDO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/dataobject/level/MemberLevelRecordDO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/dataobject/point/MemberPointRecordDO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/dataobject/signin/MemberSignInConfigDO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/dataobject/signin/MemberSignInRecordDO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/dataobject/tag/MemberTagDO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/dataobject/user/MemberUserDO.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/mysql/address/MemberAddressMapper.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/mysql/config/MemberConfigMapper.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/mysql/group/MemberGroupMapper.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/mysql/level/MemberExperienceRecordMapper.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/mysql/level/MemberLevelMapper.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/mysql/level/MemberLevelRecordMapper.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/mysql/point/MemberPointRecordMapper.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/mysql/signin/MemberSignInConfigMapper.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/mysql/signin/MemberSignInRecordMapper.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/mysql/tag/MemberTagMapper.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/mysql/user/MemberUserMapper.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/package-info.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/redis/package-info.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/enums/DictTypeConstants.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/enums/ErrorCodeConstants.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/enums/MemberExperienceBizTypeEnum.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/enums/point/MemberPointBizTypeEnum.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/framework/package-info.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/framework/web/config/MemberWebConfiguration.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/framework/web/package-info.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/mq/consumer/package-info.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/mq/message/package-info.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/mq/producer/package-info.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/mq/producer/user/MemberUserProducer.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/package-info.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/address/AddressService.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/address/AddressServiceImpl.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/auth/MemberAuthService.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/auth/MemberAuthServiceImpl.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/config/MemberConfigService.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/config/MemberConfigServiceImpl.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/group/MemberGroupService.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/group/MemberGroupServiceImpl.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/level/MemberExperienceRecordService.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/level/MemberExperienceRecordServiceImpl.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/level/MemberLevelRecordService.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/level/MemberLevelRecordServiceImpl.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/level/MemberLevelService.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/level/MemberLevelServiceImpl.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/point/MemberPointRecordService.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/point/MemberPointRecordServiceImpl.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/signin/MemberSignInConfigService.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/signin/MemberSignInConfigServiceImpl.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/signin/MemberSignInRecordService.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/signin/MemberSignInRecordServiceImpl.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/tag/MemberTagService.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/tag/MemberTagServiceImpl.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/user/MemberUserService.java create mode 100644 aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/user/MemberUserServiceImpl.java create mode 100644 aagro-module-member/src/test/java/cn/aagro/pp/module/member/service/address/MemberAddressServiceImplTest.java create mode 100644 aagro-module-member/src/test/java/cn/aagro/pp/module/member/service/auth/MemberAuthServiceTest.java create mode 100644 aagro-module-member/src/test/java/cn/aagro/pp/module/member/service/group/MemberGroupServiceImplTest.java create mode 100644 aagro-module-member/src/test/java/cn/aagro/pp/module/member/service/level/MemberLevelServiceImplTest.java create mode 100644 aagro-module-member/src/test/java/cn/aagro/pp/module/member/service/tag/MemberTagServiceImplTest.java create mode 100644 aagro-module-member/src/test/java/cn/aagro/pp/module/member/service/user/MemberUserServiceImplTest.java create mode 100644 aagro-module-member/src/test/resources/application-unit-test.yaml create mode 100644 aagro-module-member/src/test/resources/logback.xml create mode 100644 aagro-module-member/src/test/resources/sql/clean.sql create mode 100644 aagro-module-member/src/test/resources/sql/create_tables.sql create mode 100644 aagro-module-mp/pom.xml create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/account/MpAccountController.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/account/vo/MpAccountBaseVO.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/account/vo/MpAccountCreateReqVO.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/account/vo/MpAccountPageReqVO.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/account/vo/MpAccountRespVO.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/account/vo/MpAccountSimpleRespVO.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/account/vo/MpAccountUpdateReqVO.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/material/MpMaterialController.http create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/material/MpMaterialController.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/material/vo/MpMaterialPageReqVO.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/material/vo/MpMaterialRespVO.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/material/vo/MpMaterialUploadNewsImageReqVO.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/material/vo/MpMaterialUploadPermanentReqVO.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/material/vo/MpMaterialUploadRespVO.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/material/vo/MpMaterialUploadTemporaryReqVO.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/menu/MpMenuController.http create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/menu/MpMenuController.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/menu/vo/MpMenuBaseVO.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/menu/vo/MpMenuRespVO.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/menu/vo/MpMenuSaveReqVO.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/message/MpAutoReplyController.http create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/message/MpAutoReplyController.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/message/MpMessageController.http create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/message/MpMessageController.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyBaseVO.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyCreateReqVO.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyPageReqVO.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyRespVO.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyUpdateReqVO.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/message/vo/message/MpMessagePageReqVO.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/message/vo/message/MpMessageRespVO.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/message/vo/message/MpMessageSendReqVO.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/news/MpDraftController.http create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/news/MpDraftController.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/news/MpFreePublishController.http create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/news/MpFreePublishController.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/news/vo/MpDraftPageReqVO.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/news/vo/MpFreePublishPageReqVO.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/open/MpOpenController.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/open/vo/MpOpenCheckSignatureReqVO.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/open/vo/MpOpenHandleMessageReqVO.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/statistics/MpStatisticsController.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/statistics/vo/MpStatisticsGetReqVO.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/statistics/vo/MpStatisticsInterfaceSummaryRespVO.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/statistics/vo/MpStatisticsUpstreamMessageRespVO.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/statistics/vo/MpStatisticsUserCumulateRespVO.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/statistics/vo/MpStatisticsUserSummaryRespVO.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/tag/MpTagController.http create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/tag/MpTagController.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/tag/vo/MpTagBaseVO.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/tag/vo/MpTagCreateReqVO.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/tag/vo/MpTagPageReqVO.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/tag/vo/MpTagRespVO.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/tag/vo/MpTagSimpleRespVO.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/tag/vo/MpTagUpdateReqVO.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/user/MpUserController.http create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/user/MpUserController.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/user/vo/MpUserPageReqVO.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/user/vo/MpUserRespVO.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/user/vo/MpUserUpdateReqVO.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/package-info.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/convert/account/MpAccountConvert.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/convert/material/MpMaterialConvert.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/convert/menu/MpMenuConvert.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/convert/message/MpAutoReplyConvert.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/convert/message/MpMessageConvert.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/convert/statistics/MpStatisticsConvert.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/convert/tag/MpTagConvert.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/convert/user/MpUserConvert.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/dal/dataobject/account/MpAccountDO.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/dal/dataobject/material/MpMaterialDO.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/dal/dataobject/menu/MpMenuDO.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/dal/dataobject/message/MpAutoReplyDO.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/dal/dataobject/message/MpMessageDO.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/dal/dataobject/tag/MpTagDO.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/dal/dataobject/user/MpUserDO.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/dal/mysql/account/MpAccountMapper.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/dal/mysql/material/MpMaterialMapper.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/dal/mysql/menu/MpMenuMapper.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/dal/mysql/message/MpAutoReplyMapper.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/dal/mysql/message/MpMessageMapper.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/dal/mysql/tag/MpTagMapper.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/dal/mysql/user/MpUserMapper.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/enums/ErrorCodeConstants.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/enums/message/MpAutoReplyMatchEnum.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/enums/message/MpAutoReplyTypeEnum.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/enums/message/MpMessageSendFromEnum.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/framework/mp/config/MpConfiguration.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/framework/mp/core/DefaultMpServiceFactory.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/framework/mp/core/MpServiceFactory.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/framework/mp/core/context/MpContextHolder.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/framework/mp/core/util/MpUtils.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/framework/package-info.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/framework/web/config/MpWebConfiguration.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/framework/web/package-info.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/package-info.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/account/MpAccountService.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/account/MpAccountServiceImpl.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/handler/menu/MenuHandler.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/handler/message/MessageAutoReplyHandler.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/handler/message/MessageReceiveHandler.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/handler/other/KfSessionHandler.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/handler/other/NullHandler.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/handler/other/ScanHandler.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/handler/other/StoreCheckNotifyHandler.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/handler/other/package-info.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/handler/user/LocationHandler.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/handler/user/SubscribeHandler.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/handler/user/UnsubscribeHandler.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/material/MpMaterialService.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/material/MpMaterialServiceImpl.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/menu/MpMenuService.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/menu/MpMenuServiceImpl.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/message/MpAutoReplyService.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/message/MpAutoReplyServiceImpl.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/message/MpMessageService.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/message/MpMessageServiceImpl.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/message/bo/MpMessageSendOutReqBO.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/statistics/MpStatisticsService.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/statistics/MpStatisticsServiceImpl.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/tag/MpTagService.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/tag/MpTagServiceImpl.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/user/MpUserService.java create mode 100644 aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/user/MpUserServiceImpl.java create mode 100644 aagro-module-pay/pom.xml create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/notify/dto/PayOrderNotifyReqDTO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/notify/dto/PayRefundNotifyReqDTO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/notify/dto/PayTransferNotifyReqDTO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/notify/package-info.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/order/PayOrderApi.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/order/PayOrderApiImpl.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/order/dto/PayOrderCreateReqDTO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/order/dto/PayOrderRespDTO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/package-info.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/refund/PayRefundApi.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/refund/PayRefundApiImpl.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/refund/dto/PayRefundCreateReqDTO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/refund/dto/PayRefundRespDTO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/transfer/PayTransferApi.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/transfer/PayTransferApiImpl.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/transfer/dto/PayTransferCreateReqDTO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/transfer/dto/PayTransferCreateRespDTO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/transfer/dto/PayTransferRespDTO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/wallet/PayWalletApi.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/wallet/PayWalletApiImpl.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/wallet/dto/PayWalletAddBalanceReqDTO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/wallet/dto/PayWalletRespDTO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/app/PayAppController.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/app/vo/PayAppBaseVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/app/vo/PayAppCreateReqVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/app/vo/PayAppPageItemRespVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/app/vo/PayAppPageReqVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/app/vo/PayAppRespVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/app/vo/PayAppUpdateReqVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/app/vo/PayAppUpdateStatusReqVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/channel/PayChannelController.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/channel/vo/PayChannelBaseVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/channel/vo/PayChannelCreateReqVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/channel/vo/PayChannelRespVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/channel/vo/PayChannelUpdateReqVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/demo/PayDemoOrderController.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/demo/PayDemoWithdrawController.http create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/demo/PayDemoWithdrawController.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/demo/vo/order/PayDemoOrderCreateReqVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/demo/vo/order/PayDemoOrderRespVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/demo/vo/withdraw/PayDemoWithdrawCreateReqVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/demo/vo/withdraw/PayDemoWithdrawRespVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/notify/PayNotifyController.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/notify/vo/PayNotifyTaskDetailRespVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/notify/vo/PayNotifyTaskPageReqVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/notify/vo/PayNotifyTaskRespVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/order/PayOrderController.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/order/vo/PayOrderBaseVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/order/vo/PayOrderDetailsRespVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/order/vo/PayOrderExcelVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/order/vo/PayOrderExportReqVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/order/vo/PayOrderPageItemRespVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/order/vo/PayOrderPageReqVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/order/vo/PayOrderRespVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/order/vo/PayOrderSubmitReqVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/order/vo/PayOrderSubmitRespVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/refund/PayRefundController.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/refund/vo/PayRefundBaseVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/refund/vo/PayRefundDetailsRespVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/refund/vo/PayRefundExcelVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/refund/vo/PayRefundExportReqVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/refund/vo/PayRefundPageItemRespVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/refund/vo/PayRefundPageReqVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/transfer/PayTransferController.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/transfer/vo/PayTransferCreateRespVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/transfer/vo/PayTransferPageReqVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/transfer/vo/PayTransferRespVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/PayWalletController.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/PayWalletRechargeController.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/PayWalletRechargePackageController.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/PayWalletTransactionController.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/vo/rechargepackage/WalletRechargePackageBaseVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/vo/rechargepackage/WalletRechargePackageCreateReqVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/vo/rechargepackage/WalletRechargePackagePageReqVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/vo/rechargepackage/WalletRechargePackageRespVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/vo/rechargepackage/WalletRechargePackageUpdateReqVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/vo/transaction/PayWalletTransactionPageReqVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/vo/transaction/PayWalletTransactionRespVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/vo/wallet/PayWalletBaseVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/vo/wallet/PayWalletPageReqVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/vo/wallet/PayWalletRespVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/vo/wallet/PayWalletUpdateBalanceReqVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/vo/wallet/PayWalletUserReqVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/channel/AppPayChannelController.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/order/AppPayOrderController.http create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/order/AppPayOrderController.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/order/vo/AppPayOrderSubmitReqVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/order/vo/AppPayOrderSubmitRespVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/transfer/AppPayTransferController.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/wallet/AppPayWalletController.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/wallet/AppPayWalletRechargeController.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/wallet/AppPayWalletRechargePackageController.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/wallet/AppPayWalletTransactionController.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/wallet/vo/recharge/AppPayWalletPackageRespVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/wallet/vo/recharge/AppPayWalletRechargeCreateReqVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/wallet/vo/recharge/AppPayWalletRechargeCreateRespVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/wallet/vo/recharge/AppPayWalletRechargeRespVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/wallet/vo/transaction/AppPayWalletTransactionPageReqVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/wallet/vo/transaction/AppPayWalletTransactionRespVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/wallet/vo/transaction/AppPayWalletTransactionSummaryRespVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/wallet/vo/wallet/AppPayWalletRespVO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/package-info.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/convert/app/PayAppConvert.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/convert/channel/PayChannelConvert.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/convert/order/PayOrderConvert.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/convert/package-info.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/convert/refund/PayRefundConvert.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/convert/wallet/PayWalletConvert.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/convert/wallet/PayWalletRechargeConvert.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/convert/wallet/PayWalletRechargePackageConvert.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/convert/wallet/PayWalletTransactionConvert.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/convert/《芋道 Spring Boot 对象转换 MapStruct 入门》.md create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/dataobject/app/PayAppDO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/dataobject/channel/PayChannelDO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/dataobject/demo/PayDemoOrderDO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/dataobject/demo/PayDemoWithdrawDO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/dataobject/notify/PayNotifyLogDO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/dataobject/notify/PayNotifyTaskDO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/dataobject/order/PayOrderDO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/dataobject/order/PayOrderExtensionDO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/dataobject/refund/PayRefundDO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/dataobject/transfer/PayTransferDO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/dataobject/wallet/PayWalletDO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/dataobject/wallet/PayWalletRechargeDO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/dataobject/wallet/PayWalletRechargePackageDO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/dataobject/wallet/PayWalletTransactionDO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/mysql/app/PayAppMapper.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/mysql/channel/PayChannelMapper.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/mysql/demo/PayDemoOrderMapper.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/mysql/demo/PayDemoWithdrawMapper.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/mysql/notify/PayNotifyLogMapper.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/mysql/notify/PayNotifyTaskMapper.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/mysql/order/PayOrderExtensionMapper.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/mysql/order/PayOrderMapper.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/mysql/refund/PayRefundMapper.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/mysql/transfer/PayTransferMapper.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/mysql/wallet/PayWalletMapper.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/mysql/wallet/PayWalletRechargeMapper.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/mysql/wallet/PayWalletRechargePackageMapper.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/mysql/wallet/PayWalletTransactionMapper.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/redis/RedisKeyConstants.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/redis/no/PayNoRedisDAO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/redis/notify/PayNotifyLockRedisDAO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/redis/wallet/PayWalletLockRedisDAO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/enums/DictTypeConstants.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/enums/ErrorCodeConstants.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/enums/MessageTemplateConstants.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/enums/PayChannelEnum.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/enums/demo/PayDemoWithdrawStatusEnum.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/enums/demo/PayDemoWithdrawTypeEnum.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/enums/notify/PayNotifyStatusEnum.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/enums/notify/PayNotifyTypeEnum.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/enums/order/PayOrderStatusEnum.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/enums/refund/PayRefundStatusEnum.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/enums/transfer/PayTransferStatusEnum.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/enums/wallet/PayWalletBizTypeEnum.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/job/config/PayJobConfiguration.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/job/core/package-info.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/package-info.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/config/PayConfiguration.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/config/PayProperties.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/PayClient.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/PayClientConfig.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/PayClientFactory.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/dto/order/PayOrderRespDTO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/dto/order/PayOrderUnifiedReqDTO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/dto/refund/PayRefundRespDTO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/dto/refund/PayRefundUnifiedReqDTO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/dto/transfer/PayTransferRespDTO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/dto/transfer/PayTransferUnifiedReqDTO.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/exception/PayClientException.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/AbstractPayClient.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/NonePayClientConfig.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/PayClientFactoryImpl.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/alipay/AlipayAppPayClient.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/alipay/AlipayBarPayClient.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/alipay/AlipayPayClientConfig.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/alipay/AlipayPcPayClient.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/alipay/AlipayQrPayClient.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/alipay/AlipayWapPayClient.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/mock/MockPayClient.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/wallet/WalletPayClient.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/weixin/WxAppPayClient.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/weixin/WxBarPayClient.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/weixin/WxLitePayClient.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/weixin/WxNativePayClient.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/weixin/WxPayClientConfig.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/weixin/WxPubPayClient.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/weixin/WxWapPayClient.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/enums/PayOrderDisplayModeEnum.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/package-info.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/web/config/PayWebConfiguration.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/web/package-info.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/job/notify/PayNotifyJob.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/job/order/PayOrderExpireJob.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/job/order/PayOrderSyncJob.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/job/refund/PayRefundSyncJob.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/job/transfer/PayTransferSyncJob.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/package-info.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/app/PayAppService.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/app/PayAppServiceImpl.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/channel/PayChannelService.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/channel/PayChannelServiceImpl.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/demo/PayDemoOrderService.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/demo/PayDemoOrderServiceImpl.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/demo/PayDemoWithdrawService.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/demo/PayDemoWithdrawServiceImpl.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/notify/PayNotifyService.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/notify/PayNotifyServiceImpl.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/order/PayOrderService.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/order/PayOrderServiceImpl.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/refund/PayRefundService.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/refund/PayRefundServiceImpl.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/transfer/PayTransferService.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/transfer/PayTransferServiceImpl.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/wallet/PayWalletRechargePackageService.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/wallet/PayWalletRechargePackageServiceImpl.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/wallet/PayWalletRechargeService.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/wallet/PayWalletRechargeServiceImpl.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/wallet/PayWalletService.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/wallet/PayWalletServiceImpl.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/wallet/PayWalletTransactionService.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/wallet/PayWalletTransactionServiceImpl.java create mode 100644 aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/wallet/bo/WalletTransactionCreateReqBO.java create mode 100644 aagro-module-pay/src/test/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/PayClientFactoryImplIntegrationTest.java create mode 100644 aagro-module-pay/src/test/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/alipay/AbstractAlipayClientTest.java create mode 100644 aagro-module-pay/src/test/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/alipay/AlipayBarPayClientTest.java create mode 100644 aagro-module-pay/src/test/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/alipay/AlipayPcPayClientTest.java create mode 100644 aagro-module-pay/src/test/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/alipay/AlipayQrPayClientTest.java create mode 100644 aagro-module-pay/src/test/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/alipay/AlipayWapPayClientTest.java create mode 100644 aagro-module-pay/src/test/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/weixin/WxBarPayClientIntegrationTest.java create mode 100644 aagro-module-pay/src/test/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/weixin/WxNativePayClientIntegrationTest.java create mode 100644 aagro-module-pay/src/test/java/cn/aagro/pp/module/pay/service/app/PayAppServiceTest.java create mode 100644 aagro-module-pay/src/test/java/cn/aagro/pp/module/pay/service/channel/PayChannelServiceTest.java create mode 100644 aagro-module-pay/src/test/java/cn/aagro/pp/module/pay/service/notify/PayNotifyServiceTest.java create mode 100644 aagro-module-pay/src/test/java/cn/aagro/pp/module/pay/service/order/PayOrderServiceTest.java create mode 100644 aagro-module-pay/src/test/java/cn/aagro/pp/module/pay/service/refund/PayRefundServiceTest.java create mode 100644 aagro-module-pay/src/test/resources/application-unit-test.yaml create mode 100644 aagro-module-pay/src/test/resources/logback.xml create mode 100644 aagro-module-pay/src/test/resources/sql/clean.sql create mode 100644 aagro-module-pay/src/test/resources/sql/create_tables.sql create mode 100644 aagro-module-report/pom.xml create mode 100644 aagro-module-report/src/main/java/cn/aagro/pp/module/report/controller/admin/ajreport/package-info.java create mode 100644 aagro-module-report/src/main/java/cn/aagro/pp/module/report/controller/admin/goview/GoViewDataController.java create mode 100644 aagro-module-report/src/main/java/cn/aagro/pp/module/report/controller/admin/goview/GoViewProjectController.java create mode 100644 aagro-module-report/src/main/java/cn/aagro/pp/module/report/controller/admin/goview/vo/data/GoViewDataGetBySqlReqVO.java create mode 100644 aagro-module-report/src/main/java/cn/aagro/pp/module/report/controller/admin/goview/vo/data/GoViewDataRespVO.java create mode 100644 aagro-module-report/src/main/java/cn/aagro/pp/module/report/controller/admin/goview/vo/project/GoViewProjectCreateReqVO.java create mode 100644 aagro-module-report/src/main/java/cn/aagro/pp/module/report/controller/admin/goview/vo/project/GoViewProjectRespVO.java create mode 100644 aagro-module-report/src/main/java/cn/aagro/pp/module/report/controller/admin/goview/vo/project/GoViewProjectUpdateReqVO.java create mode 100644 aagro-module-report/src/main/java/cn/aagro/pp/module/report/controller/package-info.java create mode 100644 aagro-module-report/src/main/java/cn/aagro/pp/module/report/convert/ajreport/package-info.java create mode 100644 aagro-module-report/src/main/java/cn/aagro/pp/module/report/convert/goview/GoViewProjectConvert.java create mode 100644 aagro-module-report/src/main/java/cn/aagro/pp/module/report/dal/dataobject/ajreport/package-info.java create mode 100644 aagro-module-report/src/main/java/cn/aagro/pp/module/report/dal/dataobject/goview/GoViewProjectDO.java create mode 100644 aagro-module-report/src/main/java/cn/aagro/pp/module/report/dal/mysql/ajreport/package-info.java create mode 100644 aagro-module-report/src/main/java/cn/aagro/pp/module/report/dal/mysql/goview/GoViewProjectMapper.java create mode 100644 aagro-module-report/src/main/java/cn/aagro/pp/module/report/enums/ErrorCodeConstants.java create mode 100644 aagro-module-report/src/main/java/cn/aagro/pp/module/report/framework/jmreport/config/JmReportConfiguration.java create mode 100644 aagro-module-report/src/main/java/cn/aagro/pp/module/report/framework/jmreport/core/service/JmOnlDragExternalServiceImpl.java create mode 100644 aagro-module-report/src/main/java/cn/aagro/pp/module/report/framework/jmreport/core/service/JmReportTokenServiceImpl.java create mode 100644 aagro-module-report/src/main/java/cn/aagro/pp/module/report/framework/jmreport/core/web/package-info.java create mode 100644 aagro-module-report/src/main/java/cn/aagro/pp/module/report/framework/package-info.java create mode 100644 aagro-module-report/src/main/java/cn/aagro/pp/module/report/framework/security/config/SecurityConfiguration.java create mode 100644 aagro-module-report/src/main/java/cn/aagro/pp/module/report/framework/security/core/package-info.java create mode 100644 aagro-module-report/src/main/java/cn/aagro/pp/module/report/package-info.java create mode 100644 aagro-module-report/src/main/java/cn/aagro/pp/module/report/service/ajreport/package-info.java create mode 100644 aagro-module-report/src/main/java/cn/aagro/pp/module/report/service/goview/GoViewDataService.java create mode 100644 aagro-module-report/src/main/java/cn/aagro/pp/module/report/service/goview/GoViewDataServiceImpl.java create mode 100644 aagro-module-report/src/main/java/cn/aagro/pp/module/report/service/goview/GoViewProjectService.java create mode 100644 aagro-module-report/src/main/java/cn/aagro/pp/module/report/service/goview/GoViewProjectServiceImpl.java create mode 100644 aagro-module-report/src/test/java/cn/aagro/pp/module/report/service/goview/GoViewDataServiceImplTest.java create mode 100644 aagro-module-report/src/test/java/cn/aagro/pp/module/report/service/goview/GoViewProjectServiceImplTest.java create mode 100644 aagro-module-report/src/test/resources/application-unit-test.yaml create mode 100644 aagro-module-report/src/test/resources/logback.xml create mode 100644 aagro-module-report/src/test/resources/sql/clean.sql create mode 100644 aagro-module-report/src/test/resources/sql/create_tables.sql diff --git a/aagro-module-member/pom.xml b/aagro-module-member/pom.xml new file mode 100644 index 0000000..b0fc0b7 --- /dev/null +++ b/aagro-module-member/pom.xml @@ -0,0 +1,88 @@ + + + + cn.aagro.gg + aiot + ${revision} + ../pom.xml + + 4.0.0 + aagro-module-member + jar + + ${project.artifactId} + + member 模块,我们放会员业务。 + 例如说:会员中心等等 + + + + + cn.aagro.gg + aagro-module-system + ${revision} + + + cn.aagro.gg + aagro-module-infra + ${revision} + + + + + cn.aagro.gg + aagro-spring-boot-starter-biz-tenant + + + + + cn.aagro.gg + aagro-spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-validation + + + + + cn.aagro.gg + aagro-spring-boot-starter-mybatis + + + + cn.aagro.gg + aagro-spring-boot-starter-redis + + + + + cn.aagro.gg + aagro-spring-boot-starter-mq + + + + + cn.aagro.gg + aagro-spring-boot-starter-test + ${revision} + test + + + + + cn.aagro.gg + aagro-spring-boot-starter-excel + + + + cn.aagro.gg + aagro-spring-boot-starter-biz-ip + + + + + diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/address/MemberAddressApi.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/address/MemberAddressApi.java new file mode 100644 index 0000000..1e29020 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/address/MemberAddressApi.java @@ -0,0 +1,29 @@ +package cn.aagro.pp.module.member.api.address; + +import cn.aagro.pp.module.member.api.address.dto.MemberAddressRespDTO; + +/** + * 用户收件地址 API 接口 + * + * @author 芋道源码 + */ +public interface MemberAddressApi { + + /** + * 获得用户收件地址 + * + * @param id 收件地址编号 + * @param userId 用户编号 + * @return 用户收件地址 + */ + MemberAddressRespDTO getAddress(Long id, Long userId); + + /** + * 获得用户默认收件地址 + * + * @param userId 用户编号 + * @return 用户收件地址 + */ + MemberAddressRespDTO getDefaultAddress(Long userId); + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/address/MemberAddressApiImpl.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/address/MemberAddressApiImpl.java new file mode 100644 index 0000000..77c2bda --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/address/MemberAddressApiImpl.java @@ -0,0 +1,33 @@ +package cn.aagro.pp.module.member.api.address; + +import cn.aagro.pp.module.member.api.address.dto.MemberAddressRespDTO; +import cn.aagro.pp.module.member.convert.address.AddressConvert; +import cn.aagro.pp.module.member.service.address.AddressService; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; + +/** + * 用户收件地址 API 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class MemberAddressApiImpl implements MemberAddressApi { + + @Resource + private AddressService addressService; + + @Override + public MemberAddressRespDTO getAddress(Long id, Long userId) { + return AddressConvert.INSTANCE.convert02(addressService.getAddress(userId, id)); + } + + @Override + public MemberAddressRespDTO getDefaultAddress(Long userId) { + return AddressConvert.INSTANCE.convert02(addressService.getDefaultUserAddress(userId)); + } + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/address/dto/MemberAddressRespDTO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/address/dto/MemberAddressRespDTO.java new file mode 100644 index 0000000..4833d17 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/address/dto/MemberAddressRespDTO.java @@ -0,0 +1,42 @@ +package cn.aagro.pp.module.member.api.address.dto; + +import lombok.Data; + +/** + * 用户收件地址 Response DTO + * + * @author 芋道源码 + */ +@Data +public class MemberAddressRespDTO { + + /** + * 编号 + */ + private Long id; + /** + * 用户编号 + */ + private Long userId; + /** + * 收件人名称 + */ + private String name; + /** + * 手机号 + */ + private String mobile; + /** + * 地区编号 + */ + private Integer areaId; + /** + * 收件详细地址 + */ + private String detailAddress; + /** + * 是否默认 + */ + private Boolean defaultStatus; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/config/MemberConfigApi.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/config/MemberConfigApi.java new file mode 100644 index 0000000..4ed8507 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/config/MemberConfigApi.java @@ -0,0 +1,18 @@ +package cn.aagro.pp.module.member.api.config; + +import cn.aagro.pp.module.member.api.config.dto.MemberConfigRespDTO; + +/** + * 用户配置 API 接口 + * + * @author owen + */ +public interface MemberConfigApi { + + /** + * 获得积分配置 + * + * @return 积分配置 + */ + MemberConfigRespDTO getConfig(); +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/config/MemberConfigApiImpl.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/config/MemberConfigApiImpl.java new file mode 100644 index 0000000..2140e60 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/config/MemberConfigApiImpl.java @@ -0,0 +1,28 @@ +package cn.aagro.pp.module.member.api.config; + +import cn.aagro.pp.module.member.api.config.dto.MemberConfigRespDTO; +import cn.aagro.pp.module.member.convert.config.MemberConfigConvert; +import cn.aagro.pp.module.member.service.config.MemberConfigService; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; + +/** + * 用户配置 API 实现类 + * + * @author owen + */ +@Service +@Validated +public class MemberConfigApiImpl implements MemberConfigApi { + + @Resource + private MemberConfigService memberConfigService; + + @Override + public MemberConfigRespDTO getConfig() { + return MemberConfigConvert.INSTANCE.convert01(memberConfigService.getConfig()); + } + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/config/dto/MemberConfigRespDTO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/config/dto/MemberConfigRespDTO.java new file mode 100644 index 0000000..14df4e1 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/config/dto/MemberConfigRespDTO.java @@ -0,0 +1,32 @@ +package cn.aagro.pp.module.member.api.config.dto; + +import lombok.Data; + +/** + * 用户信息 Response DTO + * + * @author 芋道源码 + */ +@Data +public class MemberConfigRespDTO { + + /** + * 积分抵扣开关 + */ + private Boolean pointTradeDeductEnable; + /** + * 积分抵扣,单位:分 + *

+ * 1 积分抵扣多少分 + */ + private Integer pointTradeDeductUnitPrice; + /** + * 积分抵扣最大值 + */ + private Integer pointTradeDeductMaxPrice; + /** + * 1 元赠送多少分 + */ + private Integer pointTradeGivePoint; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/level/MemberLevelApi.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/level/MemberLevelApi.java new file mode 100644 index 0000000..a541b3f --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/level/MemberLevelApi.java @@ -0,0 +1,41 @@ +package cn.aagro.pp.module.member.api.level; + +import cn.aagro.pp.module.member.api.level.dto.MemberLevelRespDTO; +import cn.aagro.pp.module.member.enums.MemberExperienceBizTypeEnum; + +/** + * 会员等级 API 接口 + * + * @author owen + */ +public interface MemberLevelApi { + + /** + * 获得会员等级 + * + * @param id 会员等级编号 + * @return 会员等级 + */ + MemberLevelRespDTO getMemberLevel(Long id); + + /** + * 增加会员经验 + * + * @param userId 会员ID + * @param experience 经验 + * @param bizType 业务类型 {@link MemberExperienceBizTypeEnum} + * @param bizId 业务编号 + */ + void addExperience(Long userId, Integer experience, Integer bizType, String bizId); + + /** + * 扣减会员经验 + * + * @param userId 会员ID + * @param experience 经验 + * @param bizType 业务类型 {@link MemberExperienceBizTypeEnum} + * @param bizId 业务编号 + */ + void reduceExperience(Long userId, Integer experience, Integer bizType, String bizId); + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/level/MemberLevelApiImpl.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/level/MemberLevelApiImpl.java new file mode 100644 index 0000000..323e0c1 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/level/MemberLevelApiImpl.java @@ -0,0 +1,48 @@ +package cn.aagro.pp.module.member.api.level; + +import cn.aagro.pp.module.member.api.level.dto.MemberLevelRespDTO; +import cn.aagro.pp.module.member.convert.level.MemberLevelConvert; +import cn.aagro.pp.module.member.enums.MemberExperienceBizTypeEnum; +import cn.aagro.pp.module.member.service.level.MemberLevelService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; + +import static cn.aagro.pp.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.aagro.pp.module.member.enums.ErrorCodeConstants.EXPERIENCE_BIZ_NOT_SUPPORT; + +/** + * 会员等级 API 实现类 + * + * @author owen + */ +@Slf4j +@Service +@Validated +public class MemberLevelApiImpl implements MemberLevelApi { + + @Resource + private MemberLevelService memberLevelService; + + @Override + public MemberLevelRespDTO getMemberLevel(Long id) { + return MemberLevelConvert.INSTANCE.convert02(memberLevelService.getLevel(id)); + } + + @Override + public void addExperience(Long userId, Integer experience, Integer bizType, String bizId) { + MemberExperienceBizTypeEnum bizTypeEnum = MemberExperienceBizTypeEnum.getByType(bizType); + if (bizTypeEnum == null) { + throw exception(EXPERIENCE_BIZ_NOT_SUPPORT); + } + memberLevelService.addExperience(userId, experience, bizTypeEnum, bizId); + } + + @Override + public void reduceExperience(Long userId, Integer experience, Integer bizType, String bizId) { + addExperience(userId, -experience, bizType, bizId); + } + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/level/dto/MemberLevelRespDTO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/level/dto/MemberLevelRespDTO.java new file mode 100644 index 0000000..9af2322 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/level/dto/MemberLevelRespDTO.java @@ -0,0 +1,41 @@ +package cn.aagro.pp.module.member.api.level.dto; + +import cn.aagro.pp.framework.common.enums.CommonStatusEnum; +import lombok.Data; + +/** + * 会员等级 Resp DTO + * + * @author 芋道源码 + */ +@Data +public class MemberLevelRespDTO { + + /** + * 编号 + */ + private Long id; + /** + * 等级名称 + */ + private String name; + /** + * 等级 + */ + private Integer level; + /** + * 升级经验 + */ + private Integer experience; + /** + * 享受折扣 + */ + private Integer discountPercent; + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/message/package-info.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/message/package-info.java new file mode 100644 index 0000000..37aa891 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/message/package-info.java @@ -0,0 +1,4 @@ +/** + * 消息队列的消息 + */ +package cn.aagro.pp.module.member.api.message; diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/message/user/MemberUserCreateMessage.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/message/user/MemberUserCreateMessage.java new file mode 100644 index 0000000..d50eb9b --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/message/user/MemberUserCreateMessage.java @@ -0,0 +1,21 @@ +package cn.aagro.pp.module.member.api.message.user; + +import lombok.Data; + +import javax.validation.constraints.NotNull; + +/** + * 会员用户创建消息 + * + * @author owen + */ +@Data +public class MemberUserCreateMessage { + + /** + * 用户编号 + */ + @NotNull(message = "用户编号不能为空") + private Long userId; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/package-info.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/package-info.java new file mode 100644 index 0000000..5988208 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/package-info.java @@ -0,0 +1,4 @@ +/** + * member API 包,定义并实现提供给其它模块的 API + */ +package cn.aagro.pp.module.member.api; diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/point/MemberPointApi.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/point/MemberPointApi.java new file mode 100644 index 0000000..8f33d0b --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/point/MemberPointApi.java @@ -0,0 +1,36 @@ +package cn.aagro.pp.module.member.api.point; + +import cn.aagro.pp.module.member.enums.point.MemberPointBizTypeEnum; + +import javax.validation.constraints.Min; + +/** + * 用户积分的 API 接口 + * + * @author owen + */ +public interface MemberPointApi { + + /** + * 增加用户积分 + * + * @param userId 用户编号 + * @param point 积分 + * @param bizType 业务类型 {@link MemberPointBizTypeEnum} + * @param bizId 业务编号 + */ + void addPoint(Long userId, @Min(value = 1L, message = "积分必须是正数") Integer point, + Integer bizType, String bizId); + + /** + * 减少用户积分 + * + * @param userId 用户编号 + * @param point 积分 + * @param bizType 业务类型 {@link MemberPointBizTypeEnum} + * @param bizId 业务编号 + */ + void reducePoint(Long userId, @Min(value = 1L, message = "积分必须是正数") Integer point, + Integer bizType, String bizId); + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/point/MemberPointApiImpl.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/point/MemberPointApiImpl.java new file mode 100644 index 0000000..0645847 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/point/MemberPointApiImpl.java @@ -0,0 +1,50 @@ +package cn.aagro.pp.module.member.api.point; + +import cn.hutool.core.lang.Assert; +import cn.aagro.pp.module.member.enums.point.MemberPointBizTypeEnum; +import cn.aagro.pp.module.member.service.point.MemberPointRecordService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; + +import static cn.aagro.pp.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.aagro.pp.module.member.enums.ErrorCodeConstants.POINT_RECORD_BIZ_NOT_SUPPORT; + +/** + * 用户积分的 API 实现类 + * + * @author owen + */ +@Slf4j +@Service +@Validated +public class MemberPointApiImpl implements MemberPointApi { + + @Resource + private MemberPointRecordService memberPointRecordService; + + @Override + public void addPoint(Long userId, Integer point, Integer bizType, String bizId) { + Assert.isTrue(point > 0); + MemberPointBizTypeEnum bizTypeEnum = MemberPointBizTypeEnum.getByType(bizType); + if (bizTypeEnum == null) { + log.error("[addPoint][userId({}) point({}) bizType({}) bizId({}) {}]", userId, point, bizType, bizId, + POINT_RECORD_BIZ_NOT_SUPPORT); + return; + } + memberPointRecordService.createPointRecord(userId, point, bizTypeEnum, bizId); + } + + @Override + public void reducePoint(Long userId, Integer point, Integer bizType, String bizId) { + Assert.isTrue(point > 0); + MemberPointBizTypeEnum bizTypeEnum = MemberPointBizTypeEnum.getByType(bizType); + if (bizTypeEnum == null) { + throw exception(POINT_RECORD_BIZ_NOT_SUPPORT); + } + memberPointRecordService.createPointRecord(userId, -point, bizTypeEnum, bizId); + } + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/user/MemberUserApi.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/user/MemberUserApi.java new file mode 100644 index 0000000..83c49be --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/user/MemberUserApi.java @@ -0,0 +1,68 @@ +package cn.aagro.pp.module.member.api.user; + +import cn.aagro.pp.module.member.api.user.dto.MemberUserRespDTO; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import static cn.aagro.pp.framework.common.util.collection.CollectionUtils.convertMap; + +/** + * 会员用户的 API 接口 + * + * @author 芋道源码 + */ +public interface MemberUserApi { + + /** + * 获得会员用户信息 + * + * @param id 用户编号 + * @return 用户信息 + */ + MemberUserRespDTO getUser(Long id); + + /** + * 获得会员用户信息们 + * + * @param ids 用户编号的数组 + * @return 用户信息们 + */ + List getUserList(Collection ids); + + /** + * 获得会员用户 Map + * + * @param ids 用户编号的数组 + * @return 会员用户 Map + */ + default Map getUserMap(Collection ids) { + List list = getUserList(ids); + return convertMap(list, MemberUserRespDTO::getId); + } + + /** + * 基于用户昵称,模糊匹配用户列表 + * + * @param nickname 用户昵称,模糊匹配 + * @return 用户信息的列表 + */ + List getUserListByNickname(String nickname); + + /** + * 基于手机号,精准匹配用户 + * + * @param mobile 手机号 + * @return 用户信息 + */ + MemberUserRespDTO getUserByMobile(String mobile); + + /** + * 校验用户是否存在 + * + * @param id 用户编号 + */ + void validateUser(Long id); + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/user/MemberUserApiImpl.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/user/MemberUserApiImpl.java new file mode 100644 index 0000000..51b30fd --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/user/MemberUserApiImpl.java @@ -0,0 +1,58 @@ +package cn.aagro.pp.module.member.api.user; + +import cn.aagro.pp.module.member.api.user.dto.MemberUserRespDTO; +import cn.aagro.pp.module.member.convert.user.MemberUserConvert; +import cn.aagro.pp.module.member.dal.dataobject.user.MemberUserDO; +import cn.aagro.pp.module.member.service.user.MemberUserService; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.List; + +import static cn.aagro.pp.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.aagro.pp.module.member.enums.ErrorCodeConstants.USER_MOBILE_NOT_EXISTS; + +/** + * 会员用户的 API 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class MemberUserApiImpl implements MemberUserApi { + + @Resource + private MemberUserService userService; + + @Override + public MemberUserRespDTO getUser(Long id) { + MemberUserDO user = userService.getUser(id); + return MemberUserConvert.INSTANCE.convert2(user); + } + + @Override + public List getUserList(Collection ids) { + return MemberUserConvert.INSTANCE.convertList2(userService.getUserList(ids)); + } + + @Override + public List getUserListByNickname(String nickname) { + return MemberUserConvert.INSTANCE.convertList2(userService.getUserListByNickname(nickname)); + } + + @Override + public MemberUserRespDTO getUserByMobile(String mobile) { + return MemberUserConvert.INSTANCE.convert2(userService.getUserByMobile(mobile)); + } + + @Override + public void validateUser(Long id) { + MemberUserDO user = userService.getUser(id); + if (user == null) { + throw exception(USER_MOBILE_NOT_EXISTS); + } + } + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/user/dto/MemberUserRespDTO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/user/dto/MemberUserRespDTO.java new file mode 100644 index 0000000..dfad7a9 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/api/user/dto/MemberUserRespDTO.java @@ -0,0 +1,55 @@ +package cn.aagro.pp.module.member.api.user.dto; + +import cn.aagro.pp.framework.common.enums.CommonStatusEnum; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 用户信息 Response DTO + * + * @author 芋道源码 + */ +@Data +public class MemberUserRespDTO { + + /** + * 用户ID + */ + private Long id; + /** + * 用户昵称 + */ + private String nickname; + /** + * 帐号状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 用户头像 + */ + private String avatar; + /** + * 手机 + */ + private String mobile; + /** + * 创建时间(注册时间) + */ + private LocalDateTime createTime; + + // ========== 其它信息 ========== + + /** + * 会员级别编号 + */ + private Long levelId; + + /** + * 积分 + */ + private Integer point; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/address/AddressController.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/address/AddressController.java new file mode 100644 index 0000000..1ebbe24 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/address/AddressController.java @@ -0,0 +1,41 @@ +package cn.aagro.pp.module.member.controller.admin.address; + +import cn.aagro.pp.framework.common.pojo.CommonResult; +import cn.aagro.pp.module.member.controller.admin.address.vo.AddressRespVO; +import cn.aagro.pp.module.member.convert.address.AddressConvert; +import cn.aagro.pp.module.member.dal.dataobject.address.MemberAddressDO; +import cn.aagro.pp.module.member.service.address.AddressService; +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.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import java.util.List; + +import static cn.aagro.pp.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 用户收件地址") +@RestController +@RequestMapping("/member/address") +@Validated +public class AddressController { + + @Resource + private AddressService addressService; + + @GetMapping("/list") + @Operation(summary = "获得用户收件地址列表") + @Parameter(name = "userId", description = "用户编号", required = true) + @PreAuthorize("@ss.hasPermission('member:user:query')") + public CommonResult> getAddressList(@RequestParam("userId") Long userId) { + List list = addressService.getAddressList(userId); + return success(AddressConvert.INSTANCE.convertList2(list)); + } + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/address/package-info.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/address/package-info.java new file mode 100644 index 0000000..2ba94f2 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/address/package-info.java @@ -0,0 +1 @@ +package cn.aagro.pp.module.member.controller.admin.address; diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/address/vo/AddressBaseVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/address/vo/AddressBaseVO.java new file mode 100644 index 0000000..03ea064 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/address/vo/AddressBaseVO.java @@ -0,0 +1,37 @@ +package cn.aagro.pp.module.member.controller.admin.address.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import java.time.LocalDateTime; +import java.time.LocalDateTime; +import java.util.*; +import javax.validation.constraints.*; + +/** + * 用户收件地址 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class AddressBaseVO { + + @Schema(description = "收件人名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三") + @NotNull(message = "收件人名称不能为空") + private String name; + + @Schema(description = "手机号", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "手机号不能为空") + private String mobile; + + @Schema(description = "地区编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "15716") + @NotNull(message = "地区编码不能为空") + private Long areaId; + + @Schema(description = "收件详细地址", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "收件详细地址不能为空") + private String detailAddress; + + @Schema(description = "是否默认", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + @NotNull(message = "是否默认不能为空") + private Boolean defaultStatus; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/address/vo/AddressRespVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/address/vo/AddressRespVO.java new file mode 100644 index 0000000..fd795ba --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/address/vo/AddressRespVO.java @@ -0,0 +1,19 @@ +package cn.aagro.pp.module.member.controller.admin.address.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 用户收件地址 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class AddressRespVO extends AddressBaseVO { + + @Schema(description = "收件地址编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "7380") + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/config/MemberConfigController.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/config/MemberConfigController.java new file mode 100644 index 0000000..8c0a1fa --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/config/MemberConfigController.java @@ -0,0 +1,45 @@ +package cn.aagro.pp.module.member.controller.admin.config; + +import cn.aagro.pp.framework.common.pojo.CommonResult; +import cn.aagro.pp.module.member.controller.admin.config.vo.MemberConfigRespVO; +import cn.aagro.pp.module.member.controller.admin.config.vo.MemberConfigSaveReqVO; +import cn.aagro.pp.module.member.convert.config.MemberConfigConvert; +import cn.aagro.pp.module.member.dal.dataobject.config.MemberConfigDO; +import cn.aagro.pp.module.member.service.config.MemberConfigService; +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.aagro.pp.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 会员设置") +@RestController +@RequestMapping("/member/config") +@Validated +public class MemberConfigController { + + @Resource + private MemberConfigService memberConfigService; + + @PutMapping("/save") + @Operation(summary = "保存会员配置") + @PreAuthorize("@ss.hasPermission('member:config:save')") + public CommonResult saveConfig(@Valid @RequestBody MemberConfigSaveReqVO saveReqVO) { + memberConfigService.saveConfig(saveReqVO); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得会员配置") + @PreAuthorize("@ss.hasPermission('member:config:query')") + public CommonResult getConfig() { + MemberConfigDO config = memberConfigService.getConfig(); + return success(MemberConfigConvert.INSTANCE.convert(config)); + } + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/config/vo/MemberConfigBaseVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/config/vo/MemberConfigBaseVO.java new file mode 100644 index 0000000..22f5167 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/config/vo/MemberConfigBaseVO.java @@ -0,0 +1,31 @@ +package cn.aagro.pp.module.member.controller.admin.config.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +/** + * 会员配置 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class MemberConfigBaseVO { + + @Schema(description = "积分抵扣开关", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "积分抵扣开发不能为空") + private Boolean pointTradeDeductEnable; + + @Schema(description = "积分抵扣,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "13506") + @NotNull(message = "积分抵扣不能为空") + private Integer pointTradeDeductUnitPrice; + + @Schema(description = "积分抵扣最大值", requiredMode = Schema.RequiredMode.REQUIRED, example = "32428") + @NotNull(message = "积分抵扣最大值不能为空") + private Integer pointTradeDeductMaxPrice; + + @Schema(description = "1 元赠送多少分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + @NotNull(message = "1 元赠送积分不能为空") + private Integer pointTradeGivePoint; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/config/vo/MemberConfigRespVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/config/vo/MemberConfigRespVO.java new file mode 100644 index 0000000..24d9b9a --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/config/vo/MemberConfigRespVO.java @@ -0,0 +1,17 @@ +package cn.aagro.pp.module.member.controller.admin.config.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - 会员配置 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MemberConfigRespVO extends MemberConfigBaseVO { + + @Schema(description = "自增主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/config/vo/MemberConfigSaveReqVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/config/vo/MemberConfigSaveReqVO.java new file mode 100644 index 0000000..d165886 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/config/vo/MemberConfigSaveReqVO.java @@ -0,0 +1,13 @@ +package cn.aagro.pp.module.member.controller.admin.config.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 MemberConfigSaveReqVO extends MemberConfigBaseVO { +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/group/MemberGroupController.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/group/MemberGroupController.java new file mode 100644 index 0000000..70d6e30 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/group/MemberGroupController.java @@ -0,0 +1,81 @@ +package cn.aagro.pp.module.member.controller.admin.group; + +import cn.aagro.pp.framework.common.pojo.CommonResult; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.member.controller.admin.group.vo.*; +import cn.aagro.pp.module.member.convert.group.MemberGroupConvert; +import cn.aagro.pp.module.member.dal.dataobject.group.MemberGroupDO; +import cn.aagro.pp.module.member.service.group.MemberGroupService; +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.aagro.pp.framework.common.pojo.CommonResult.success; + + +@Tag(name = "管理后台 - 用户分组") +@RestController +@RequestMapping("/member/group") +@Validated +public class MemberGroupController { + + @Resource + private MemberGroupService groupService; + + @PostMapping("/create") + @Operation(summary = "创建用户分组") + @PreAuthorize("@ss.hasPermission('member:group:create')") + public CommonResult createGroup(@Valid @RequestBody MemberGroupCreateReqVO createReqVO) { + return success(groupService.createGroup(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新用户分组") + @PreAuthorize("@ss.hasPermission('member:group:update')") + public CommonResult updateGroup(@Valid @RequestBody MemberGroupUpdateReqVO updateReqVO) { + groupService.updateGroup(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除用户分组") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('member:group:delete')") + public CommonResult deleteGroup(@RequestParam("id") Long id) { + groupService.deleteGroup(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得用户分组") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('member:group:query')") + public CommonResult getGroup(@RequestParam("id") Long id) { + MemberGroupDO group = groupService.getGroup(id); + return success(MemberGroupConvert.INSTANCE.convert(group)); + } + + @GetMapping("/list-all-simple") + @Operation(summary = "获取会员分组精简信息列表", description = "只包含被开启的会员分组,主要用于前端的下拉选项") + public CommonResult> getSimpleGroupList() { + // 获用户列表,只要开启状态的 + List list = groupService.getEnableGroupList(); + return success(MemberGroupConvert.INSTANCE.convertSimpleList(list)); + } + + @GetMapping("/page") + @Operation(summary = "获得用户分组分页") + @PreAuthorize("@ss.hasPermission('member:group:query')") + public CommonResult> getGroupPage(@Valid MemberGroupPageReqVO pageVO) { + PageResult pageResult = groupService.getGroupPage(pageVO); + return success(MemberGroupConvert.INSTANCE.convertPage(pageResult)); + } + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/group/vo/MemberGroupBaseVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/group/vo/MemberGroupBaseVO.java new file mode 100644 index 0000000..8ff91e1 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/group/vo/MemberGroupBaseVO.java @@ -0,0 +1,29 @@ +package cn.aagro.pp.module.member.controller.admin.group.vo; + +import cn.aagro.pp.framework.common.enums.CommonStatusEnum; +import cn.aagro.pp.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +/** + * 用户分组 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class MemberGroupBaseVO { + + @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "购物达人") + @NotNull(message = "名称不能为空") + private String name; + + @Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "你猜") + private String remark; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "状态不能为空") + @InEnum(CommonStatusEnum.class) + private Integer status; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/group/vo/MemberGroupCreateReqVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/group/vo/MemberGroupCreateReqVO.java new file mode 100644 index 0000000..9389f56 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/group/vo/MemberGroupCreateReqVO.java @@ -0,0 +1,14 @@ +package cn.aagro.pp.module.member.controller.admin.group.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 MemberGroupCreateReqVO extends MemberGroupBaseVO { + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/group/vo/MemberGroupPageReqVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/group/vo/MemberGroupPageReqVO.java new file mode 100644 index 0000000..7447c12 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/group/vo/MemberGroupPageReqVO.java @@ -0,0 +1,30 @@ +package cn.aagro.pp.module.member.controller.admin.group.vo; + +import cn.aagro.pp.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 java.time.LocalDateTime; + +import static cn.aagro.pp.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 MemberGroupPageReqVO extends PageParam { + + @Schema(description = "名称", example = "购物达人") + private String name; + + @Schema(description = "状态", example = "1") + private Integer status; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/group/vo/MemberGroupRespVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/group/vo/MemberGroupRespVO.java new file mode 100644 index 0000000..1fcfc00 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/group/vo/MemberGroupRespVO.java @@ -0,0 +1,22 @@ +package cn.aagro.pp.module.member.controller.admin.group.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 用户分组 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MemberGroupRespVO extends MemberGroupBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "20357") + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/group/vo/MemberGroupSimpleRespVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/group/vo/MemberGroupSimpleRespVO.java new file mode 100644 index 0000000..8650865 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/group/vo/MemberGroupSimpleRespVO.java @@ -0,0 +1,18 @@ +package cn.aagro.pp.module.member.controller.admin.group.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.ToString; + +@Schema(description = "管理后台 - 用户分组 Response VO") +@Data +@ToString(callSuper = true) +public class MemberGroupSimpleRespVO { + + @Schema(description = "编号", example = "6103") + private Long id; + + @Schema(description = "等级名称", example = "芋艿") + private String name; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/group/vo/MemberGroupUpdateReqVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/group/vo/MemberGroupUpdateReqVO.java new file mode 100644 index 0000000..4b8a3a8 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/group/vo/MemberGroupUpdateReqVO.java @@ -0,0 +1,20 @@ +package cn.aagro.pp.module.member.controller.admin.group.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 MemberGroupUpdateReqVO extends MemberGroupBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "20357") + @NotNull(message = "编号不能为空") + private Long id; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/MemberExperienceRecordController.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/MemberExperienceRecordController.java new file mode 100644 index 0000000..d6ba6cf --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/MemberExperienceRecordController.java @@ -0,0 +1,52 @@ +package cn.aagro.pp.module.member.controller.admin.level; + +import cn.aagro.pp.framework.common.pojo.CommonResult; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.member.controller.admin.level.vo.experience.MemberExperienceRecordPageReqVO; +import cn.aagro.pp.module.member.controller.admin.level.vo.experience.MemberExperienceRecordRespVO; +import cn.aagro.pp.module.member.convert.level.MemberExperienceRecordConvert; +import cn.aagro.pp.module.member.dal.dataobject.level.MemberExperienceRecordDO; +import cn.aagro.pp.module.member.service.level.MemberExperienceRecordService; +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.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import javax.validation.Valid; + +import static cn.aagro.pp.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 会员经验记录") +@RestController +@RequestMapping("/member/experience-record") +@Validated +public class MemberExperienceRecordController { + + @Resource + private MemberExperienceRecordService experienceLogService; + + @GetMapping("/get") + @Operation(summary = "获得会员经验记录") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('member:experience-record:query')") + public CommonResult getExperienceRecord(@RequestParam("id") Long id) { + MemberExperienceRecordDO experienceLog = experienceLogService.getExperienceRecord(id); + return success(MemberExperienceRecordConvert.INSTANCE.convert(experienceLog)); + } + + @GetMapping("/page") + @Operation(summary = "获得会员经验记录分页") + @PreAuthorize("@ss.hasPermission('member:experience-record:query')") + public CommonResult> getExperienceRecordPage( + @Valid MemberExperienceRecordPageReqVO pageVO) { + PageResult pageResult = experienceLogService.getExperienceRecordPage(pageVO); + return success(MemberExperienceRecordConvert.INSTANCE.convertPage(pageResult)); + } + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/MemberLevelController.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/MemberLevelController.java new file mode 100644 index 0000000..a4d0d92 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/MemberLevelController.java @@ -0,0 +1,80 @@ +package cn.aagro.pp.module.member.controller.admin.level; + +import cn.aagro.pp.framework.common.pojo.CommonResult; +import cn.aagro.pp.module.member.controller.admin.level.vo.level.*; +import cn.aagro.pp.module.member.convert.level.MemberLevelConvert; +import cn.aagro.pp.module.member.dal.dataobject.level.MemberLevelDO; +import cn.aagro.pp.module.member.service.level.MemberLevelService; +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.aagro.pp.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 会员等级") +@RestController +@RequestMapping("/member/level") +@Validated +public class MemberLevelController { + + @Resource + private MemberLevelService levelService; + + @PostMapping("/create") + @Operation(summary = "创建会员等级") + @PreAuthorize("@ss.hasPermission('member:level:create')") + public CommonResult createLevel(@Valid @RequestBody MemberLevelCreateReqVO createReqVO) { + return success(levelService.createLevel(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新会员等级") + @PreAuthorize("@ss.hasPermission('member:level:update')") + public CommonResult updateLevel(@Valid @RequestBody MemberLevelUpdateReqVO updateReqVO) { + levelService.updateLevel(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除会员等级") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('member:level:delete')") + public CommonResult deleteLevel(@RequestParam("id") Long id) { + levelService.deleteLevel(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得会员等级") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('member:level:query')") + public CommonResult getLevel(@RequestParam("id") Long id) { + MemberLevelDO level = levelService.getLevel(id); + return success(MemberLevelConvert.INSTANCE.convert(level)); + } + + @GetMapping("/list-all-simple") + @Operation(summary = "获取会员等级精简信息列表", description = "只包含被开启的会员等级,主要用于前端的下拉选项") + public CommonResult> getSimpleLevelList() { + // 获用户列表,只要开启状态的 + List list = levelService.getEnableLevelList(); + // 排序后,返回给前端 + return success(MemberLevelConvert.INSTANCE.convertSimpleList(list)); + } + + @GetMapping("/list") + @Operation(summary = "获得会员等级列表") + @PreAuthorize("@ss.hasPermission('member:level:query')") + public CommonResult> getLevelList(@Valid MemberLevelListReqVO listReqVO) { + List result = levelService.getLevelList(listReqVO); + return success(MemberLevelConvert.INSTANCE.convertList(result)); + } + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/MemberLevelRecordController.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/MemberLevelRecordController.java new file mode 100644 index 0000000..ea83d9d --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/MemberLevelRecordController.java @@ -0,0 +1,52 @@ +package cn.aagro.pp.module.member.controller.admin.level; + +import cn.aagro.pp.framework.common.pojo.CommonResult; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.member.controller.admin.level.vo.record.MemberLevelRecordPageReqVO; +import cn.aagro.pp.module.member.controller.admin.level.vo.record.MemberLevelRecordRespVO; +import cn.aagro.pp.module.member.convert.level.MemberLevelRecordConvert; +import cn.aagro.pp.module.member.dal.dataobject.level.MemberLevelRecordDO; +import cn.aagro.pp.module.member.service.level.MemberLevelRecordService; +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.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import javax.validation.Valid; + +import static cn.aagro.pp.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 会员等级记录") +@RestController +@RequestMapping("/member/level-record") +@Validated +public class MemberLevelRecordController { + + @Resource + private MemberLevelRecordService levelLogService; + + @GetMapping("/get") + @Operation(summary = "获得会员等级记录") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('member:level-record:query')") + public CommonResult getLevelRecord(@RequestParam("id") Long id) { + MemberLevelRecordDO levelLog = levelLogService.getLevelRecord(id); + return success(MemberLevelRecordConvert.INSTANCE.convert(levelLog)); + } + + @GetMapping("/page") + @Operation(summary = "获得会员等级记录分页") + @PreAuthorize("@ss.hasPermission('member:level-record:query')") + public CommonResult> getLevelRecordPage( + @Valid MemberLevelRecordPageReqVO pageVO) { + PageResult pageResult = levelLogService.getLevelRecordPage(pageVO); + return success(MemberLevelRecordConvert.INSTANCE.convertPage(pageResult)); + } + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/vo/experience/MemberExperienceRecordBaseVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/vo/experience/MemberExperienceRecordBaseVO.java new file mode 100644 index 0000000..165c9ff --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/vo/experience/MemberExperienceRecordBaseVO.java @@ -0,0 +1,43 @@ +package cn.aagro.pp.module.member.controller.admin.level.vo.experience; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +/** + * 会员经验记录 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class MemberExperienceRecordBaseVO { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3638") + @NotNull(message = "用户编号不能为空") + private Long userId; + + @Schema(description = "业务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "12164") + @NotNull(message = "业务编号不能为空") + private String bizId; + + @Schema(description = "业务类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "业务类型不能为空") + private Integer bizType; + + @Schema(description = "标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "增加经验") + @NotNull(message = "标题不能为空") + private String title; + + @Schema(description = "经验", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + @NotNull(message = "经验不能为空") + private Integer experience; + + @Schema(description = "变更后的经验", requiredMode = Schema.RequiredMode.REQUIRED, example = "200") + @NotNull(message = "变更后的经验不能为空") + private Integer totalExperience; + + @Schema(description = "描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "下单增加 100 经验") + @NotNull(message = "描述不能为空") + private String description; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/vo/experience/MemberExperienceRecordPageReqVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/vo/experience/MemberExperienceRecordPageReqVO.java new file mode 100644 index 0000000..ca7e719 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/vo/experience/MemberExperienceRecordPageReqVO.java @@ -0,0 +1,36 @@ +package cn.aagro.pp.module.member.controller.admin.level.vo.experience; + +import cn.aagro.pp.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 java.time.LocalDateTime; + +import static cn.aagro.pp.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 MemberExperienceRecordPageReqVO extends PageParam { + + @Schema(description = "用户编号", example = "3638") + private Long userId; + + @Schema(description = "业务编号", example = "12164") + private String bizId; + + @Schema(description = "业务类型", example = "1") + private Integer bizType; + + @Schema(description = "标题", example = "增加经验") + private String title; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/vo/experience/MemberExperienceRecordRespVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/vo/experience/MemberExperienceRecordRespVO.java new file mode 100644 index 0000000..cd36cb5 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/vo/experience/MemberExperienceRecordRespVO.java @@ -0,0 +1,22 @@ +package cn.aagro.pp.module.member.controller.admin.level.vo.experience; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 会员经验记录 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MemberExperienceRecordRespVO extends MemberExperienceRecordBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "19610") + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/vo/level/MemberLevelBaseVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/vo/level/MemberLevelBaseVO.java new file mode 100644 index 0000000..4570d0c --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/vo/level/MemberLevelBaseVO.java @@ -0,0 +1,53 @@ +package cn.aagro.pp.module.member.controller.admin.level.vo.level; + +import cn.aagro.pp.framework.common.enums.CommonStatusEnum; +import cn.aagro.pp.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.hibernate.validator.constraints.Range; +import org.hibernate.validator.constraints.URL; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Positive; + +/** + * 会员等级 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class MemberLevelBaseVO { + + @Schema(description = "等级名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿") + @NotBlank(message = "等级名称不能为空") + private String name; + + @Schema(description = "升级经验", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + @NotNull(message = "升级经验不能为空") + @Positive(message = "升级经验必须大于 0") + private Integer experience; + + @Schema(description = "等级", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "等级不能为空") + @Positive(message = "等级必须大于 0") + private Integer level; + + @Schema(description = "享受折扣", requiredMode = Schema.RequiredMode.REQUIRED, example = "98") + @NotNull(message = "享受折扣不能为空") + @Range(min = 0, max = 100, message = "享受折扣的范围为 0-100") + private Integer discountPercent; + + @Schema(description = "等级图标", example = "https://www.iocoder.cn/aagro.jpg") + @URL(message = "等级图标必须是 URL 格式") + private String icon; + + @Schema(description = "等级背景图", example = "https://www.iocoder.cn/aagro.jpg") + @URL(message = "等级背景图必须是 URL 格式") + private String backgroundUrl; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "状态不能为空") + @InEnum(CommonStatusEnum.class) + private Integer status; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/vo/level/MemberLevelCreateReqVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/vo/level/MemberLevelCreateReqVO.java new file mode 100644 index 0000000..a9042b7 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/vo/level/MemberLevelCreateReqVO.java @@ -0,0 +1,14 @@ +package cn.aagro.pp.module.member.controller.admin.level.vo.level; + +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 MemberLevelCreateReqVO extends MemberLevelBaseVO { + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/vo/level/MemberLevelListReqVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/vo/level/MemberLevelListReqVO.java new file mode 100644 index 0000000..9e653ab --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/vo/level/MemberLevelListReqVO.java @@ -0,0 +1,18 @@ +package cn.aagro.pp.module.member.controller.admin.level.vo.level; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.ToString; + +@Schema(description = "管理后台 - 会员等级列表筛选 Request VO") +@Data +@ToString(callSuper = true) +public class MemberLevelListReqVO { + + @Schema(description = "等级名称", example = "芋艿") + private String name; + + @Schema(description = "状态", example = "1") + private Integer status; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/vo/level/MemberLevelRespVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/vo/level/MemberLevelRespVO.java new file mode 100644 index 0000000..31e4b0a --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/vo/level/MemberLevelRespVO.java @@ -0,0 +1,22 @@ +package cn.aagro.pp.module.member.controller.admin.level.vo.level; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 会员等级 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MemberLevelRespVO extends MemberLevelBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "6103") + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/vo/level/MemberLevelSimpleRespVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/vo/level/MemberLevelSimpleRespVO.java new file mode 100644 index 0000000..81ffb86 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/vo/level/MemberLevelSimpleRespVO.java @@ -0,0 +1,21 @@ +package cn.aagro.pp.module.member.controller.admin.level.vo.level; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.ToString; + +@Schema(description = "管理后台 - 会员等级 Response VO") +@Data +@ToString(callSuper = true) +public class MemberLevelSimpleRespVO { + + @Schema(description = "编号", example = "6103") + private Long id; + + @Schema(description = "等级名称", example = "芋艿") + private String name; + + @Schema(description = "等级图标", example = "https://www.iocoder.cn/aagro.jpg") + private String icon; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/vo/level/MemberLevelUpdateReqVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/vo/level/MemberLevelUpdateReqVO.java new file mode 100644 index 0000000..44b3823 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/vo/level/MemberLevelUpdateReqVO.java @@ -0,0 +1,20 @@ +package cn.aagro.pp.module.member.controller.admin.level.vo.level; + +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 MemberLevelUpdateReqVO extends MemberLevelBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "6103") + @NotNull(message = "编号不能为空") + private Long id; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/vo/record/MemberLevelRecordBaseVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/vo/record/MemberLevelRecordBaseVO.java new file mode 100644 index 0000000..5d41c42 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/vo/record/MemberLevelRecordBaseVO.java @@ -0,0 +1,47 @@ +package cn.aagro.pp.module.member.controller.admin.level.vo.record; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +/** + * 会员等级记录 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class MemberLevelRecordBaseVO { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "25923") + @NotNull(message = "用户编号不能为空") + private Long userId; + + @Schema(description = "等级编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "25985") + @NotNull(message = "等级编号不能为空") + private Long levelId; + + @Schema(description = "会员等级", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "会员等级不能为空") + private Integer level; + + @Schema(description = "享受折扣", requiredMode = Schema.RequiredMode.REQUIRED, example = "13319") + @NotNull(message = "享受折扣不能为空") + private Integer discountPercent; + + @Schema(description = "升级经验", requiredMode = Schema.RequiredMode.REQUIRED, example = "13319") + @NotNull(message = "升级经验不能为空") + private Integer experience; + + @Schema(description = "会员此时的经验", requiredMode = Schema.RequiredMode.REQUIRED, example = "13319") + @NotNull(message = "会员此时的经验不能为空") + private Integer userExperience; + + @Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "推广需要") + @NotNull(message = "备注不能为空") + private String remark; + + @Schema(description = "描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "升级为金牌会员") + @NotNull(message = "描述不能为空") + private String description; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/vo/record/MemberLevelRecordPageReqVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/vo/record/MemberLevelRecordPageReqVO.java new file mode 100644 index 0000000..8eabb95 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/vo/record/MemberLevelRecordPageReqVO.java @@ -0,0 +1,30 @@ +package cn.aagro.pp.module.member.controller.admin.level.vo.record; + +import cn.aagro.pp.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 java.time.LocalDateTime; + +import static cn.aagro.pp.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 MemberLevelRecordPageReqVO extends PageParam { + + @Schema(description = "用户编号", example = "25923") + private Long userId; + + @Schema(description = "等级编号", example = "25985") + private Long levelId; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/vo/record/MemberLevelRecordRespVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/vo/record/MemberLevelRecordRespVO.java new file mode 100644 index 0000000..7a11aaf --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/level/vo/record/MemberLevelRecordRespVO.java @@ -0,0 +1,22 @@ +package cn.aagro.pp.module.member.controller.admin.level.vo.record; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 会员等级记录 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MemberLevelRecordRespVO extends MemberLevelRecordBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "8741") + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/point/MemberPointRecordController.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/point/MemberPointRecordController.java new file mode 100644 index 0000000..de3bed4 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/point/MemberPointRecordController.java @@ -0,0 +1,56 @@ +package cn.aagro.pp.module.member.controller.admin.point; + +import cn.aagro.pp.framework.common.pojo.CommonResult; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.member.controller.admin.point.vo.recrod.MemberPointRecordPageReqVO; +import cn.aagro.pp.module.member.controller.admin.point.vo.recrod.MemberPointRecordRespVO; +import cn.aagro.pp.module.member.convert.point.MemberPointRecordConvert; +import cn.aagro.pp.module.member.dal.dataobject.point.MemberPointRecordDO; +import cn.aagro.pp.module.member.dal.dataobject.user.MemberUserDO; +import cn.aagro.pp.module.member.service.point.MemberPointRecordService; +import cn.aagro.pp.module.member.service.user.MemberUserService; +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.util.CollectionUtils; +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 javax.validation.Valid; +import java.util.List; + +import static cn.aagro.pp.framework.common.pojo.CommonResult.success; +import static cn.aagro.pp.framework.common.util.collection.CollectionUtils.convertSet; + +@Tag(name = "管理后台 - 签到记录") +@RestController +@RequestMapping("/member/point/record") +@Validated +public class MemberPointRecordController { + + @Resource + private MemberPointRecordService pointRecordService; + + @Resource + private MemberUserService memberUserService; + + @GetMapping("/page") + @Operation(summary = "获得用户积分记录分页") + @PreAuthorize("@ss.hasPermission('point:record:query')") + public CommonResult> getPointRecordPage(@Valid MemberPointRecordPageReqVO pageVO) { + // 执行分页查询 + PageResult pageResult = pointRecordService.getPointRecordPage(pageVO); + if (CollectionUtils.isEmpty(pageResult.getList())) { + return success(PageResult.empty(pageResult.getTotal())); + } + + // 拼接结果返回 + List users = memberUserService.getUserList( + convertSet(pageResult.getList(), MemberPointRecordDO::getUserId)); + return success(MemberPointRecordConvert.INSTANCE.convertPage(pageResult, users)); + } + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/point/vo/recrod/MemberPointRecordPageReqVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/point/vo/recrod/MemberPointRecordPageReqVO.java new file mode 100644 index 0000000..b084825 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/point/vo/recrod/MemberPointRecordPageReqVO.java @@ -0,0 +1,27 @@ +package cn.aagro.pp.module.member.controller.admin.point.vo.recrod; + +import cn.aagro.pp.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 MemberPointRecordPageReqVO extends PageParam { + + @Schema(description = "用户昵称", example = "张三") + private String nickname; + + @Schema(description = "用户编号", example = "123") + private Long userId; + + @Schema(description = "业务类型", example = "1") + private Integer bizType; + + @Schema(description = "积分标题", example = "呵呵") + private String title; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/point/vo/recrod/MemberPointRecordRespVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/point/vo/recrod/MemberPointRecordRespVO.java new file mode 100644 index 0000000..7c32e97 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/point/vo/recrod/MemberPointRecordRespVO.java @@ -0,0 +1,42 @@ +package cn.aagro.pp.module.member.controller.admin.point.vo.recrod; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 用户积分记录 Response VO") +@Data +public class MemberPointRecordRespVO { + + @Schema(description = "自增主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "31457") + private Long id; + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long userId; + + @Schema(description = "昵称", example = "张三") + private String nickname; + + @Schema(description = "业务编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "22706") + private String bizId; + + @Schema(description = "业务类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer bizType; + + @Schema(description = "积分标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "你猜") + private String title; + + @Schema(description = "积分描述", example = "你猜") + private String description; + + @Schema(description = "积分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer point; + + @Schema(description = "变动后的积分", requiredMode = Schema.RequiredMode.REQUIRED, example = "200") + private Integer totalPoint; + + @Schema(description = "发生时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/signin/MemberSignInConfigController.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/signin/MemberSignInConfigController.java new file mode 100644 index 0000000..d6e39e6 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/signin/MemberSignInConfigController.java @@ -0,0 +1,74 @@ +package cn.aagro.pp.module.member.controller.admin.signin; + +import cn.aagro.pp.framework.common.pojo.CommonResult; +import cn.aagro.pp.module.member.controller.admin.signin.vo.config.MemberSignInConfigCreateReqVO; +import cn.aagro.pp.module.member.controller.admin.signin.vo.config.MemberSignInConfigRespVO; +import cn.aagro.pp.module.member.controller.admin.signin.vo.config.MemberSignInConfigUpdateReqVO; +import cn.aagro.pp.module.member.convert.signin.MemberSignInConfigConvert; +import cn.aagro.pp.module.member.dal.dataobject.signin.MemberSignInConfigDO; +import cn.aagro.pp.module.member.service.signin.MemberSignInConfigService; +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.aagro.pp.framework.common.pojo.CommonResult.success; + +// TODO 芋艿:url +@Tag(name = "管理后台 - 签到规则") +@RestController +@RequestMapping("/member/sign-in/config") +@Validated +public class MemberSignInConfigController { + + @Resource + private MemberSignInConfigService signInConfigService; + + @PostMapping("/create") + @Operation(summary = "创建签到规则") + @PreAuthorize("@ss.hasPermission('point:sign-in-config:create')") + public CommonResult createSignInConfig(@Valid @RequestBody MemberSignInConfigCreateReqVO createReqVO) { + return success(signInConfigService.createSignInConfig(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新签到规则") + @PreAuthorize("@ss.hasPermission('point:sign-in-config:update')") + public CommonResult updateSignInConfig(@Valid @RequestBody MemberSignInConfigUpdateReqVO updateReqVO) { + signInConfigService.updateSignInConfig(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除签到规则") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('point:sign-in-config:delete')") + public CommonResult deleteSignInConfig(@RequestParam("id") Long id) { + signInConfigService.deleteSignInConfig(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得签到规则") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('point:sign-in-config:query')") + public CommonResult getSignInConfig(@RequestParam("id") Long id) { + MemberSignInConfigDO signInConfig = signInConfigService.getSignInConfig(id); + return success(MemberSignInConfigConvert.INSTANCE.convert(signInConfig)); + } + + @GetMapping("/list") + @Operation(summary = "获得签到规则列表") + @PreAuthorize("@ss.hasPermission('point:sign-in-config:query')") + public CommonResult> getSignInConfigList() { + List list = signInConfigService.getSignInConfigList(); + return success(MemberSignInConfigConvert.INSTANCE.convertList(list)); + } + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/signin/MemberSignInRecordController.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/signin/MemberSignInRecordController.java new file mode 100644 index 0000000..7a6dbf2 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/signin/MemberSignInRecordController.java @@ -0,0 +1,55 @@ +package cn.aagro.pp.module.member.controller.admin.signin; + +import cn.aagro.pp.framework.common.pojo.CommonResult; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.member.controller.admin.signin.vo.record.MemberSignInRecordPageReqVO; +import cn.aagro.pp.module.member.controller.admin.signin.vo.record.MemberSignInRecordRespVO; +import cn.aagro.pp.module.member.convert.signin.MemberSignInRecordConvert; +import cn.aagro.pp.module.member.dal.dataobject.signin.MemberSignInRecordDO; +import cn.aagro.pp.module.member.dal.dataobject.user.MemberUserDO; +import cn.aagro.pp.module.member.service.signin.MemberSignInRecordService; +import cn.aagro.pp.module.member.service.user.MemberUserService; +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.util.CollectionUtils; +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 javax.validation.Valid; +import java.util.List; + +import static cn.aagro.pp.framework.common.pojo.CommonResult.success; +import static cn.aagro.pp.framework.common.util.collection.CollectionUtils.convertSet; + +@Tag(name = "管理后台 - 签到记录") +@RestController +@RequestMapping("/member/sign-in/record") +@Validated +public class MemberSignInRecordController { + + @Resource + private MemberSignInRecordService signInRecordService; + + @Resource + private MemberUserService memberUserService; + + @GetMapping("/page") + @Operation(summary = "获得签到记录分页") + @PreAuthorize("@ss.hasPermission('point:sign-in-record:query')") + public CommonResult> getSignInRecordPage(@Valid MemberSignInRecordPageReqVO pageVO) { + // 执行分页查询 + PageResult pageResult = signInRecordService.getSignInRecordPage(pageVO); + if (CollectionUtils.isEmpty(pageResult.getList())) { + return success(PageResult.empty(pageResult.getTotal())); + } + + // 拼接结果返回 + List users = memberUserService.getUserList( + convertSet(pageResult.getList(), MemberSignInRecordDO::getUserId)); + return success(MemberSignInRecordConvert.INSTANCE.convertPage(pageResult, users)); + } +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/signin/vo/config/MemberSignInConfigBaseVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/signin/vo/config/MemberSignInConfigBaseVO.java new file mode 100644 index 0000000..c6b90fb --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/signin/vo/config/MemberSignInConfigBaseVO.java @@ -0,0 +1,45 @@ +package cn.aagro.pp.module.member.controller.admin.signin.vo.config; + +import cn.hutool.core.util.ObjUtil; +import cn.aagro.pp.framework.common.enums.CommonStatusEnum; +import cn.aagro.pp.framework.common.validation.InEnum; +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.AssertTrue; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.PositiveOrZero; + +/** + * 签到规则 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class MemberSignInConfigBaseVO { + + @Schema(description = "签到第 x 天", requiredMode = Schema.RequiredMode.REQUIRED, example = "7") + @NotNull(message = "签到天数不能为空") + private Integer day; + + @Schema(description = "奖励积分", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + @NotNull(message = "奖励积分不能为空") + @PositiveOrZero(message = "奖励积分不能小于 0") + private Integer point; + + @Schema(description = "奖励经验", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + @NotNull(message = "奖励经验不能为空") + @PositiveOrZero(message = "奖励经验不能小于 0") + private Integer experience; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "状态不能为空") + @InEnum(CommonStatusEnum.class) + private Integer status; + + @AssertTrue(message = "签到奖励积分和经验不能同时为空") + @JsonIgnore + public boolean isConfigAward() { + return ObjUtil.notEqual(point, 0) || ObjUtil.notEqual(experience, 0); + } +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/signin/vo/config/MemberSignInConfigCreateReqVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/signin/vo/config/MemberSignInConfigCreateReqVO.java new file mode 100644 index 0000000..0987ba4 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/signin/vo/config/MemberSignInConfigCreateReqVO.java @@ -0,0 +1,12 @@ +package cn.aagro.pp.module.member.controller.admin.signin.vo.config; + +import lombok.*; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "管理后台 - 签到规则创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MemberSignInConfigCreateReqVO extends MemberSignInConfigBaseVO { + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/signin/vo/config/MemberSignInConfigRespVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/signin/vo/config/MemberSignInConfigRespVO.java new file mode 100644 index 0000000..4b5fd9e --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/signin/vo/config/MemberSignInConfigRespVO.java @@ -0,0 +1,19 @@ +package cn.aagro.pp.module.member.controller.admin.signin.vo.config; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 签到规则 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MemberSignInConfigRespVO extends MemberSignInConfigBaseVO { + + @Schema(description = "自增主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "20937") + private Integer id; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/signin/vo/config/MemberSignInConfigUpdateReqVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/signin/vo/config/MemberSignInConfigUpdateReqVO.java new file mode 100644 index 0000000..b1421c0 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/signin/vo/config/MemberSignInConfigUpdateReqVO.java @@ -0,0 +1,18 @@ +package cn.aagro.pp.module.member.controller.admin.signin.vo.config; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +import javax.validation.constraints.*; + +@Schema(description = "管理后台 - 签到规则更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MemberSignInConfigUpdateReqVO extends MemberSignInConfigBaseVO { + + @Schema(description = "规则自增主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "13653") + @NotNull(message = "规则自增主键不能为空") + private Long id; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/signin/vo/record/MemberSignInRecordPageReqVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/signin/vo/record/MemberSignInRecordPageReqVO.java new file mode 100644 index 0000000..ea78ef8 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/signin/vo/record/MemberSignInRecordPageReqVO.java @@ -0,0 +1,33 @@ +package cn.aagro.pp.module.member.controller.admin.signin.vo.record; + +import cn.aagro.pp.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 java.time.LocalDateTime; + +import static cn.aagro.pp.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 MemberSignInRecordPageReqVO extends PageParam { + + @Schema(description = "签到用户", example = "土豆") + private String nickname; + + @Schema(description = "第几天签到", example = "10") + private Integer day; + + @Schema(description = "用户编号", example = "123") + private Long userId; + + @Schema(description = "签到时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/signin/vo/record/MemberSignInRecordRespVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/signin/vo/record/MemberSignInRecordRespVO.java new file mode 100644 index 0000000..e82c093 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/signin/vo/record/MemberSignInRecordRespVO.java @@ -0,0 +1,30 @@ +package cn.aagro.pp.module.member.controller.admin.signin.vo.record; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 签到记录 Response VO") +@Data +public class MemberSignInRecordRespVO { + + @Schema(description = "签到自增 id", requiredMode = Schema.RequiredMode.REQUIRED, example = "11903") + private Long id; + + @Schema(description = "签到用户", requiredMode = Schema.RequiredMode.REQUIRED, example = "6507") + private Long userId; + + @Schema(description = "昵称", example = "张三") + private String nickname; + + @Schema(description = "第几天签到", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer day; + + @Schema(description = "签到的积分", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Integer point; + + @Schema(description = "签到时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/tag/MemberTagController.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/tag/MemberTagController.java new file mode 100644 index 0000000..42c2ce3 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/tag/MemberTagController.java @@ -0,0 +1,94 @@ +package cn.aagro.pp.module.member.controller.admin.tag; + +import cn.aagro.pp.framework.common.pojo.CommonResult; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.member.controller.admin.tag.vo.MemberTagCreateReqVO; +import cn.aagro.pp.module.member.controller.admin.tag.vo.MemberTagPageReqVO; +import cn.aagro.pp.module.member.controller.admin.tag.vo.MemberTagRespVO; +import cn.aagro.pp.module.member.controller.admin.tag.vo.MemberTagUpdateReqVO; +import cn.aagro.pp.module.member.convert.tag.MemberTagConvert; +import cn.aagro.pp.module.member.dal.dataobject.tag.MemberTagDO; +import cn.aagro.pp.module.member.service.tag.MemberTagService; +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.Collection; +import java.util.List; + +import static cn.aagro.pp.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 会员标签") +@RestController +@RequestMapping("/member/tag") +@Validated +public class MemberTagController { + + @Resource + private MemberTagService tagService; + + @PostMapping("/create") + @Operation(summary = "创建会员标签") + @PreAuthorize("@ss.hasPermission('member:tag:create')") + public CommonResult createTag(@Valid @RequestBody MemberTagCreateReqVO createReqVO) { + return success(tagService.createTag(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新会员标签") + @PreAuthorize("@ss.hasPermission('member:tag:update')") + public CommonResult updateTag(@Valid @RequestBody MemberTagUpdateReqVO updateReqVO) { + tagService.updateTag(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除会员标签") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('member:tag:delete')") + public CommonResult deleteTag(@RequestParam("id") Long id) { + tagService.deleteTag(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得会员标签") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('member:tag:query')") + public CommonResult getMemberTag(@RequestParam("id") Long id) { + MemberTagDO tag = tagService.getTag(id); + return success(MemberTagConvert.INSTANCE.convert(tag)); + } + + @GetMapping("/list-all-simple") + @Operation(summary = "获取会员标签精简信息列表", description = "只包含被开启的会员标签,主要用于前端的下拉选项") + public CommonResult> getSimpleTagList() { + // 获用户列表,只要开启状态的 + List list = tagService.getTagList(); + // 排序后,返回给前端 + return success(MemberTagConvert.INSTANCE.convertList(list)); + } + + @GetMapping("/list") + @Operation(summary = "获得会员标签列表") + @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048") + @PreAuthorize("@ss.hasPermission('member:tag:query')") + public CommonResult> getMemberTagList(@RequestParam("ids") Collection ids) { + List list = tagService.getTagList(ids); + return success(MemberTagConvert.INSTANCE.convertList(list)); + } + + @GetMapping("/page") + @Operation(summary = "获得会员标签分页") + @PreAuthorize("@ss.hasPermission('member:tag:query')") + public CommonResult> getTagPage(@Valid MemberTagPageReqVO pageVO) { + PageResult pageResult = tagService.getTagPage(pageVO); + return success(MemberTagConvert.INSTANCE.convertPage(pageResult)); + } + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/tag/vo/MemberTagBaseVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/tag/vo/MemberTagBaseVO.java new file mode 100644 index 0000000..3c5db83 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/tag/vo/MemberTagBaseVO.java @@ -0,0 +1,19 @@ +package cn.aagro.pp.module.member.controller.admin.tag.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +/** + * 会员标签 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class MemberTagBaseVO { + + @Schema(description = "标签名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四") + @NotNull(message = "标签名称不能为空") + private String name; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/tag/vo/MemberTagCreateReqVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/tag/vo/MemberTagCreateReqVO.java new file mode 100644 index 0000000..228467e --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/tag/vo/MemberTagCreateReqVO.java @@ -0,0 +1,14 @@ +package cn.aagro.pp.module.member.controller.admin.tag.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 MemberTagCreateReqVO extends MemberTagBaseVO { + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/tag/vo/MemberTagPageReqVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/tag/vo/MemberTagPageReqVO.java new file mode 100644 index 0000000..3daf77d --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/tag/vo/MemberTagPageReqVO.java @@ -0,0 +1,27 @@ +package cn.aagro.pp.module.member.controller.admin.tag.vo; + +import cn.aagro.pp.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 java.time.LocalDateTime; + +import static cn.aagro.pp.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 MemberTagPageReqVO extends PageParam { + + @Schema(description = "标签名称", example = "李四") + private String name; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/tag/vo/MemberTagRespVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/tag/vo/MemberTagRespVO.java new file mode 100644 index 0000000..666df92 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/tag/vo/MemberTagRespVO.java @@ -0,0 +1,22 @@ +package cn.aagro.pp.module.member.controller.admin.tag.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 会员标签 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MemberTagRespVO extends MemberTagBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "907") + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/tag/vo/MemberTagUpdateReqVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/tag/vo/MemberTagUpdateReqVO.java new file mode 100644 index 0000000..4ba31f0 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/tag/vo/MemberTagUpdateReqVO.java @@ -0,0 +1,20 @@ +package cn.aagro.pp.module.member.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 MemberTagUpdateReqVO extends MemberTagBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "907") + @NotNull(message = "编号不能为空") + private Long id; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/user/MemberUserController.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/user/MemberUserController.java new file mode 100644 index 0000000..248158b --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/user/MemberUserController.java @@ -0,0 +1,113 @@ +package cn.aagro.pp.module.member.controller.admin.user; + +import cn.hutool.core.collection.CollUtil; +import cn.aagro.pp.framework.common.pojo.CommonResult; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.member.controller.admin.user.vo.*; +import cn.aagro.pp.module.member.convert.user.MemberUserConvert; +import cn.aagro.pp.module.member.dal.dataobject.group.MemberGroupDO; +import cn.aagro.pp.module.member.dal.dataobject.level.MemberLevelDO; +import cn.aagro.pp.module.member.dal.dataobject.tag.MemberTagDO; +import cn.aagro.pp.module.member.dal.dataobject.user.MemberUserDO; +import cn.aagro.pp.module.member.enums.point.MemberPointBizTypeEnum; +import cn.aagro.pp.module.member.service.group.MemberGroupService; +import cn.aagro.pp.module.member.service.level.MemberLevelService; +import cn.aagro.pp.module.member.service.point.MemberPointRecordService; +import cn.aagro.pp.module.member.service.tag.MemberTagService; +import cn.aagro.pp.module.member.service.user.MemberUserService; +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.Collection; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import static cn.aagro.pp.framework.common.pojo.CommonResult.success; +import static cn.aagro.pp.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.aagro.pp.framework.web.core.util.WebFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - 会员用户") +@RestController +@RequestMapping("/member/user") +@Validated +public class MemberUserController { + + @Resource + private MemberUserService memberUserService; + @Resource + private MemberTagService memberTagService; + @Resource + private MemberLevelService memberLevelService; + @Resource + private MemberGroupService memberGroupService; + @Resource + private MemberPointRecordService memberPointRecordService; + + @PutMapping("/update") + @Operation(summary = "更新会员用户") + @PreAuthorize("@ss.hasPermission('member:user:update')") + public CommonResult updateUser(@Valid @RequestBody MemberUserUpdateReqVO updateReqVO) { + memberUserService.updateUser(updateReqVO); + return success(true); + } + + @PutMapping("/update-level") + @Operation(summary = "更新会员用户等级") + @PreAuthorize("@ss.hasPermission('member:user:update-level')") + public CommonResult updateUserLevel(@Valid @RequestBody MemberUserUpdateLevelReqVO updateReqVO) { + memberLevelService.updateUserLevel(updateReqVO); + return success(true); + } + + @PutMapping("/update-point") + @Operation(summary = "更新会员用户积分") + @PreAuthorize("@ss.hasPermission('member:user:update-point')") + public CommonResult updateUserPoint(@Valid @RequestBody MemberUserUpdatePointReqVO updateReqVO) { + memberPointRecordService.createPointRecord(updateReqVO.getId(), updateReqVO.getPoint(), + MemberPointBizTypeEnum.ADMIN, String.valueOf(getLoginUserId())); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得会员用户") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('member:user:query')") + public CommonResult getUser(@RequestParam("id") Long id) { + MemberUserDO user = memberUserService.getUser(id); + return success(MemberUserConvert.INSTANCE.convert03(user)); + } + + @GetMapping("/page") + @Operation(summary = "获得会员用户分页") + @PreAuthorize("@ss.hasPermission('member:user:query')") + public CommonResult> getUserPage(@Valid MemberUserPageReqVO pageVO) { + PageResult pageResult = memberUserService.getUserPage(pageVO); + if (CollUtil.isEmpty(pageResult.getList())) { + return success(PageResult.empty()); + } + + // 处理用户标签返显 + Set tagIds = pageResult.getList().stream() + .map(MemberUserDO::getTagIds) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .collect(Collectors.toSet()); + List tags = memberTagService.getTagList(tagIds); + // 处理用户级别返显 + List levels = memberLevelService.getLevelList( + convertSet(pageResult.getList(), MemberUserDO::getLevelId)); + // 处理用户分组返显 + List groups = memberGroupService.getGroupList( + convertSet(pageResult.getList(), MemberUserDO::getGroupId)); + return success(MemberUserConvert.INSTANCE.convertPage(pageResult, tags, levels, groups)); + } + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/user/vo/MemberUserBaseVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/user/vo/MemberUserBaseVO.java new file mode 100644 index 0000000..4fd62e9 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/user/vo/MemberUserBaseVO.java @@ -0,0 +1,65 @@ +package cn.aagro.pp.module.member.controller.admin.user.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.hibernate.validator.constraints.URL; +import org.springframework.format.annotation.DateTimeFormat; + +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; +import java.util.List; + +import static cn.aagro.pp.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY; + +/** + * 会员用户 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class MemberUserBaseVO { + + @Schema(description = "手机号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15601691300") + @NotNull(message = "手机号不能为空") + private String mobile; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + @NotNull(message = "状态不能为空") + private Byte status; + + @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四") + @NotNull(message = "用户昵称不能为空") + private String nickname; + + @Schema(description = "头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/x.png") + @URL(message = "头像必须是 URL 格式") + private String avatar; + + @Schema(description = "用户昵称", example = "李四") + private String name; + + @Schema(description = "用户性别", example = "1") + private Integer sex; + + @Schema(description = "所在地编号", example = "4371") + private Long areaId; + + @Schema(description = "所在地全程", example = "上海上海市普陀区") + private String areaName; + + @Schema(description = "出生日期", example = "2023-03-12") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY) + private LocalDateTime birthday; + + @Schema(description = "会员备注", example = "我是小备注") + private String mark; + + @Schema(description = "会员标签", example = "[1, 2]") + private List tagIds; + + @Schema(description = "会员等级编号", example = "1") + private Long levelId; + + @Schema(description = "用户分组编号", example = "1") + private Long groupId; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/user/vo/MemberUserPageReqVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/user/vo/MemberUserPageReqVO.java new file mode 100644 index 0000000..a0ab2d6 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/user/vo/MemberUserPageReqVO.java @@ -0,0 +1,48 @@ +package cn.aagro.pp.module.member.controller.admin.user.vo; + +import cn.aagro.pp.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 java.time.LocalDateTime; +import java.util.List; + +import static cn.aagro.pp.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 MemberUserPageReqVO extends PageParam { + + @Schema(description = "手机号", example = "15601691300") + private String mobile; + + @Schema(description = "用户昵称", example = "李四") + private String nickname; + + @Schema(description = "最后登录时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] loginDate; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + + @Schema(description = "会员标签编号列表", example = "[1, 2]") + private List tagIds; + + @Schema(description = "会员等级编号", example = "1") + private Long levelId; + + @Schema(description = "用户分组编号", example = "1") + private Long groupId; + + // TODO 芋艿:注册用户类型; + + // TODO 芋艿:登录用户类型; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/user/vo/MemberUserRespVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/user/vo/MemberUserRespVO.java new file mode 100644 index 0000000..058c1f1 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/user/vo/MemberUserRespVO.java @@ -0,0 +1,52 @@ +package cn.aagro.pp.module.member.controller.admin.user.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - 会员用户 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MemberUserRespVO extends MemberUserBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "23788") + private Long id; + + @Schema(description = "注册 IP", requiredMode = Schema.RequiredMode.REQUIRED, example = "127.0.0.1") + private String registerIp; + + @Schema(description = "最后登录IP", requiredMode = Schema.RequiredMode.REQUIRED, example = "127.0.0.1") + private String loginIp; + + @Schema(description = "最后登录时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime loginDate; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + // ========== 其它信息 ========== + + @Schema(description = "积分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer point; + + @Schema(description = "总积分", requiredMode = Schema.RequiredMode.REQUIRED, example = "2000") + private Integer totalPoint; + + @Schema(description = "会员标签", example = "[红色, 快乐]") + private List tagNames; + + @Schema(description = "会员等级", example = "黄金会员") + private String levelName; + + @Schema(description = "用户分组", example = "购物达人") + private String groupName; + + @Schema(description = "用户经验值", requiredMode = Schema.RequiredMode.REQUIRED, example = "200") + private Integer experience; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/user/vo/MemberUserUpdateLevelReqVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/user/vo/MemberUserUpdateLevelReqVO.java new file mode 100644 index 0000000..5d59afd --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/user/vo/MemberUserUpdateLevelReqVO.java @@ -0,0 +1,29 @@ +package cn.aagro.pp.module.member.controller.admin.user.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.ToString; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 用户修改等级 Request VO") +@Data +@ToString(callSuper = true) +public class MemberUserUpdateLevelReqVO { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "23788") + @NotNull(message = "用户编号不能为空") + private Long id; + + /** + * 取消用户等级时,值为空 + */ + @Schema(description = "用户等级编号", example = "1") + private Long levelId; + + @Schema(description = "修改原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "推广需要") + @NotBlank(message = "修改原因不能为空") + private String reason; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/user/vo/MemberUserUpdatePointReqVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/user/vo/MemberUserUpdatePointReqVO.java new file mode 100644 index 0000000..36c0a41 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/user/vo/MemberUserUpdatePointReqVO.java @@ -0,0 +1,22 @@ +package cn.aagro.pp.module.member.controller.admin.user.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.ToString; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 用户修改积分 Request VO") +@Data +@ToString(callSuper = true) +public class MemberUserUpdatePointReqVO { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "23788") + @NotNull(message = "用户编号不能为空") + private Long id; + + @Schema(description = "变动积分,正数为增加,负数为减少", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + @NotNull(message = "变动积分不能为空") + private Integer point; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/user/vo/MemberUserUpdateReqVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/user/vo/MemberUserUpdateReqVO.java new file mode 100644 index 0000000..60fa809 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/admin/user/vo/MemberUserUpdateReqVO.java @@ -0,0 +1,20 @@ +package cn.aagro.pp.module.member.controller.admin.user.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 MemberUserUpdateReqVO extends MemberUserBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "23788") + @NotNull(message = "编号不能为空") + private Long id; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/address/AppAddressController.http b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/address/AppAddressController.http new file mode 100644 index 0000000..a0582e6 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/address/AppAddressController.http @@ -0,0 +1,54 @@ +### 请求 /create 接口 => 成功 +POST {{appApi}}//member/address/create +Content-Type: application/json +tenant-id: {{appTenantId}} +Authorization: Bearer {{appToken}} + +{ + "name": "yunai", + "mobile": "15601691300", + "areaId": "610632", + "postCode": "200000", + "detailAddress": "芋道源码 233 号 666 室", + "defaulted": true +} + +### 请求 /update 接口 => 成功 +PUT {{appApi}}//member/address/update +Content-Type: application/json +tenant-id: {{appTenantId}} +Authorization: Bearer {{appToken}} + +{ + "id": "1", + "name": "yunai888", + "mobile": "15601691300", + "areaId": "610632", + "postCode": "200000", + "detailAddress": "芋道源码 233 号 666 室", + "defaulted": false +} + +### 请求 /delete 接口 => 成功 +DELETE {{appApi}}//member/address/delete?id=2 +Content-Type: application/json +tenant-id: {{appTenantId}} +Authorization: Bearer {{appToken}} + +### 请求 /get 接口 => 成功 +GET {{appApi}}//member/address/get?id=1 +Content-Type: application/json +tenant-id: {{appTenantId}} +Authorization: Bearer {{appToken}} + +### 请求 /get-default 接口 => 成功 +GET {{appApi}}//member/address/get-default +Content-Type: application/json +tenant-id: {{appTenantId}} +Authorization: Bearer {{appToken}} + +### 请求 /list 接口 => 成功 +GET {{appApi}}//member/address/list +Content-Type: application/json +tenant-id: {{appTenantId}} +Authorization: Bearer {{appToken}} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/address/AppAddressController.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/address/AppAddressController.java new file mode 100644 index 0000000..7a9fd52 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/address/AppAddressController.java @@ -0,0 +1,75 @@ +package cn.aagro.pp.module.member.controller.app.address; + +import cn.aagro.pp.framework.common.pojo.CommonResult; +import cn.aagro.pp.module.member.controller.app.address.vo.AppAddressCreateReqVO; +import cn.aagro.pp.module.member.controller.app.address.vo.AppAddressRespVO; +import cn.aagro.pp.module.member.controller.app.address.vo.AppAddressUpdateReqVO; +import cn.aagro.pp.module.member.convert.address.AddressConvert; +import cn.aagro.pp.module.member.dal.dataobject.address.MemberAddressDO; +import cn.aagro.pp.module.member.service.address.AddressService; +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.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; + +import static cn.aagro.pp.framework.common.pojo.CommonResult.success; +import static cn.aagro.pp.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "用户 APP - 用户收件地址") +@RestController +@RequestMapping("/member/address") +@Validated +public class AppAddressController { + + @Resource + private AddressService addressService; + + @PostMapping("/create") + @Operation(summary = "创建用户收件地址") + public CommonResult createAddress(@Valid @RequestBody AppAddressCreateReqVO createReqVO) { + return success(addressService.createAddress(getLoginUserId(), createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新用户收件地址") + public CommonResult updateAddress(@Valid @RequestBody AppAddressUpdateReqVO updateReqVO) { + addressService.updateAddress(getLoginUserId(), updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除用户收件地址") + @Parameter(name = "id", description = "编号", required = true) + public CommonResult deleteAddress(@RequestParam("id") Long id) { + addressService.deleteAddress(getLoginUserId(), id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得用户收件地址") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + public CommonResult getAddress(@RequestParam("id") Long id) { + MemberAddressDO address = addressService.getAddress(getLoginUserId(), id); + return success(AddressConvert.INSTANCE.convert(address)); + } + + @GetMapping("/get-default") + @Operation(summary = "获得默认的用户收件地址") + public CommonResult getDefaultUserAddress() { + MemberAddressDO address = addressService.getDefaultUserAddress(getLoginUserId()); + return success(AddressConvert.INSTANCE.convert(address)); + } + + @GetMapping("/list") + @Operation(summary = "获得用户收件地址列表") + public CommonResult> getAddressList() { + List list = addressService.getAddressList(getLoginUserId()); + return success(AddressConvert.INSTANCE.convertList(list)); + } + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/address/vo/AppAddressBaseVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/address/vo/AppAddressBaseVO.java new file mode 100644 index 0000000..2167647 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/address/vo/AppAddressBaseVO.java @@ -0,0 +1,35 @@ +package cn.aagro.pp.module.member.controller.app.address.vo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +// TODO 芋艿:example 缺失 +/** +* 用户收件地址 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class AppAddressBaseVO { + + @Schema(description = "收件人名称", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "收件人名称不能为空") + private String name; + + @Schema(description = "手机号", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "手机号不能为空") + private String mobile; + + @Schema(description = "地区编号", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "地区编号不能为空") + private Long areaId; + + @Schema(description = "收件详细地址", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "收件详细地址不能为空") + private String detailAddress; + + @Schema(description = "是否默认地址", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "是否默认地址不能为空") + private Boolean defaultStatus; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/address/vo/AppAddressCreateReqVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/address/vo/AppAddressCreateReqVO.java new file mode 100644 index 0000000..dd9465d --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/address/vo/AppAddressCreateReqVO.java @@ -0,0 +1,11 @@ +package cn.aagro.pp.module.member.controller.app.address.vo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +@Schema(description = "用户 APP - 用户收件地址创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class AppAddressCreateReqVO extends AppAddressBaseVO { + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/address/vo/AppAddressRespVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/address/vo/AppAddressRespVO.java new file mode 100644 index 0000000..8a676e9 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/address/vo/AppAddressRespVO.java @@ -0,0 +1,20 @@ +package cn.aagro.pp.module.member.controller.app.address.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "用户 APP - 用户收件地址 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class AppAddressRespVO extends AppAddressBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "地区名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "上海上海市普陀区") + private String areaName; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/address/vo/AppAddressUpdateReqVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/address/vo/AppAddressUpdateReqVO.java new file mode 100644 index 0000000..9d4a353 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/address/vo/AppAddressUpdateReqVO.java @@ -0,0 +1,16 @@ +package cn.aagro.pp.module.member.controller.app.address.vo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import javax.validation.constraints.*; + +@Schema(description = "用户 APP - 用户收件地址更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class AppAddressUpdateReqVO extends AppAddressBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "编号不能为空") + private Long id; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/auth/AppAuthController.http b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/auth/AppAuthController.http new file mode 100644 index 0000000..391aa92 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/auth/AppAuthController.http @@ -0,0 +1,67 @@ +### 请求 /login 接口 => 成功 +POST {{appApi}}/member/auth/login +Content-Type: application/json +tenant-id: {{appTenantId}} + +{ + "mobile": "15601691388", + "password": "admin123" +} + +### 请求 /send-sms-code 接口 => 成功 +POST {{appApi}}/member/auth/send-sms-code +Content-Type: application/json +tenant-id: {{appTenantId}} + +{ + "mobile": "15601691388", + "scene": 1 +} + +### 请求 /sms-login 接口 => 成功 +POST {{appApi}}/member/auth/sms-login +Content-Type: application/json +tenant-id: {{appTenantId}} +terminal: 30 + +{ + "mobile": "15601691388", + "code": 9999 +} + +### 请求 /social-login 接口 => 成功 +POST {{appApi}}/member/auth/social-login +Content-Type: application/json +tenant-id: {{appTenantId}} + +{ + "type": 34, + "code": "0e1oc9000CTjFQ1oim200bhtb61oc90g", + "state": "default" +} + +### 请求 /weixin-mini-app-login 接口 => 成功 +POST {{appApi}}/member/auth/weixin-mini-app-login +Content-Type: application/json +tenant-id: {{appTenantId}} + +{ + "phoneCode": "618e6412e0c728f5b8fc7164497463d0158a923c9e7fd86af8bba393b9decbc5", + "loginCode": "001frTkl21JUf94VGxol2hSlff1frTkR" +} + +### 请求 /logout 接口 => 成功 +POST {{appApi}}/member/auth/logout +Content-Type: application/json +Authorization: Bearer c1b76bdaf2c146c581caa4d7fd81ee66 +tenant-id: {{appTenantId}} + +### 请求 /auth/refresh-token 接口 => 成功 +POST {{appApi}}/member/auth/refresh-token?refreshToken=bc43d929094849a28b3a69f6e6940d70 +Content-Type: application/json +tenant-id: {{appTenantId}} + +### 请求 /auth/create-weixin-jsapi-signature 接口 => 成功 +POST {{appApi}}/member/auth/create-weixin-jsapi-signature?url=http://www.iocoder.cn +Authorization: Bearer {{appToken}} +tenant-id: {{appTenantId}} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/auth/AppAuthController.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/auth/AppAuthController.java new file mode 100644 index 0000000..858e7e1 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/auth/AppAuthController.java @@ -0,0 +1,135 @@ +package cn.aagro.pp.module.member.controller.app.auth; + +import cn.hutool.core.util.StrUtil; +import cn.aagro.pp.framework.common.enums.UserTypeEnum; +import cn.aagro.pp.framework.common.pojo.CommonResult; +import cn.aagro.pp.framework.security.config.SecurityProperties; +import cn.aagro.pp.framework.security.core.util.SecurityFrameworkUtils; +import cn.aagro.pp.module.member.controller.app.auth.vo.*; +import cn.aagro.pp.module.member.convert.auth.AuthConvert; +import cn.aagro.pp.module.member.service.auth.MemberAuthService; +import cn.aagro.pp.module.system.api.social.SocialClientApi; +import cn.aagro.pp.module.system.api.social.dto.SocialWxJsapiSignatureRespDTO; +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 lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.annotation.security.PermitAll; +import javax.servlet.http.HttpServletRequest; +import javax.validation.Valid; + +import static cn.aagro.pp.framework.common.pojo.CommonResult.success; +import static cn.aagro.pp.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "用户 APP - 认证") +@RestController +@RequestMapping("/member/auth") +@Validated +@Slf4j +public class AppAuthController { + + @Resource + private MemberAuthService authService; + + @Resource + private SocialClientApi socialClientApi; + + @Resource + private SecurityProperties securityProperties; + + @PostMapping("/login") + @Operation(summary = "使用手机 + 密码登录") + @PermitAll + public CommonResult login(@RequestBody @Valid AppAuthLoginReqVO reqVO) { + return success(authService.login(reqVO)); + } + + @PostMapping("/logout") + @Operation(summary = "登出系统") + @PermitAll + public CommonResult logout(HttpServletRequest request) { + String token = SecurityFrameworkUtils.obtainAuthorization(request, + securityProperties.getTokenHeader(), securityProperties.getTokenParameter()); + if (StrUtil.isNotBlank(token)) { + authService.logout(token); + } + return success(true); + } + + @PostMapping("/refresh-token") + @Operation(summary = "刷新令牌") + @Parameter(name = "refreshToken", description = "刷新令牌", required = true) + @PermitAll + public CommonResult refreshToken(@RequestParam("refreshToken") String refreshToken) { + return success(authService.refreshToken(refreshToken)); + } + + // ========== 短信登录相关 ========== + + @PostMapping("/sms-login") + @Operation(summary = "使用手机 + 验证码登录") + @PermitAll + public CommonResult smsLogin(@RequestBody @Valid AppAuthSmsLoginReqVO reqVO) { + return success(authService.smsLogin(reqVO)); + } + + @PostMapping("/send-sms-code") + @Operation(summary = "发送手机验证码") + @PermitAll + public CommonResult sendSmsCode(@RequestBody @Valid AppAuthSmsSendReqVO reqVO) { + authService.sendSmsCode(getLoginUserId(), reqVO); + return success(true); + } + + @PostMapping("/validate-sms-code") + @Operation(summary = "校验手机验证码") + @PermitAll + public CommonResult validateSmsCode(@RequestBody @Valid AppAuthSmsValidateReqVO reqVO) { + authService.validateSmsCode(getLoginUserId(), reqVO); + return success(true); + } + + // ========== 社交登录相关 ========== + + @GetMapping("/social-auth-redirect") + @Operation(summary = "社交授权的跳转") + @Parameters({ + @Parameter(name = "type", description = "社交类型", required = true), + @Parameter(name = "redirectUri", description = "回调路径") + }) + @PermitAll + public CommonResult socialAuthRedirect(@RequestParam("type") Integer type, + @RequestParam("redirectUri") String redirectUri) { + return CommonResult.success(authService.getSocialAuthorizeUrl(type, redirectUri)); + } + + @PostMapping("/social-login") + @Operation(summary = "社交快捷登录,使用 code 授权码", description = "适合未登录的用户,但是社交账号已绑定用户") + @PermitAll + public CommonResult socialLogin(@RequestBody @Valid AppAuthSocialLoginReqVO reqVO) { + return success(authService.socialLogin(reqVO)); + } + + @PostMapping("/weixin-mini-app-login") + @Operation(summary = "微信小程序的一键登录") + @PermitAll + public CommonResult weixinMiniAppLogin(@RequestBody @Valid AppAuthWeixinMiniAppLoginReqVO reqVO) { + return success(authService.weixinMiniAppLogin(reqVO)); + } + + @PostMapping("/create-weixin-jsapi-signature") + @Operation(summary = "创建微信 JS SDK 初始化所需的签名", + description = "参考 https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html 文档") + @PermitAll + public CommonResult createWeixinMpJsapiSignature(@RequestParam("url") String url) { + SocialWxJsapiSignatureRespDTO signature = socialClientApi.createWxMpJsapiSignature( + UserTypeEnum.MEMBER.getValue(), url); + return success(AuthConvert.INSTANCE.convert(signature)); + } + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/auth/vo/AppAuthCheckCodeReqVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/auth/vo/AppAuthCheckCodeReqVO.java new file mode 100644 index 0000000..ff79dfb --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/auth/vo/AppAuthCheckCodeReqVO.java @@ -0,0 +1,41 @@ +package cn.aagro.pp.module.member.controller.app.auth.vo; + +import cn.aagro.pp.framework.common.validation.InEnum; +import cn.aagro.pp.framework.common.validation.Mobile; +import cn.aagro.pp.module.system.enums.sms.SmsSceneEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; + +// TODO 芋艿:code review 相关逻辑 +@Schema(description = "用户 APP - 校验验证码 Request VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AppAuthCheckCodeReqVO { + + @Schema(description = "手机号", example = "15601691234") + @NotBlank(message = "手机号不能为空") + @Mobile + private String mobile; + + @Schema(description = "手机验证码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotBlank(message = "手机验证码不能为空") + @Length(min = 4, max = 6, message = "手机验证码长度为 4-6 位") + @Pattern(regexp = "^[0-9]+$", message = "手机验证码必须都是数字") + private String code; + + @Schema(description = "发送场景,对应 SmsSceneEnum 枚举", example = "1") + @NotNull(message = "发送场景不能为空") + @InEnum(SmsSceneEnum.class) + private Integer scene; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/auth/vo/AppAuthLoginReqVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/auth/vo/AppAuthLoginReqVO.java new file mode 100644 index 0000000..4603902 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/auth/vo/AppAuthLoginReqVO.java @@ -0,0 +1,56 @@ +package cn.aagro.pp.module.member.controller.app.auth.vo; + +import cn.hutool.core.util.StrUtil; +import cn.aagro.pp.framework.common.validation.InEnum; +import cn.aagro.pp.framework.common.validation.Mobile; +import cn.aagro.pp.module.system.enums.social.SocialTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.AssertTrue; +import javax.validation.constraints.NotEmpty; + +@Schema(description = "用户 APP - 手机 + 密码登录 Request VO,如果登录并绑定社交用户,需要传递 social 开头的参数") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AppAuthLoginReqVO { + + @Schema(description = "手机号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15601691300") + @NotEmpty(message = "手机号不能为空") + @Mobile + private String mobile; + + @Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "buzhidao") + @NotEmpty(message = "密码不能为空") + @Length(min = 4, max = 16, message = "密码长度为 4-16 位") + private String password; + + // ========== 绑定社交登录时,需要传递如下参数 ========== + + @Schema(description = "社交平台的类型,参见 SocialTypeEnum 枚举值", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + @InEnum(SocialTypeEnum.class) + private Integer socialType; + + @Schema(description = "授权码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private String socialCode; + + @Schema(description = "state", requiredMode = Schema.RequiredMode.REQUIRED, example = "9b2ffbc1-7425-4155-9894-9d5c08541d62") + private String socialState; + + @AssertTrue(message = "授权码不能为空") + public boolean isSocialCodeValid() { + return socialType == null || StrUtil.isNotEmpty(socialCode); + } + + @AssertTrue(message = "授权 state 不能为空") + public boolean isSocialState() { + return socialType == null || StrUtil.isNotEmpty(socialState); + } + +} \ No newline at end of file diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/auth/vo/AppAuthLoginRespVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/auth/vo/AppAuthLoginRespVO.java new file mode 100644 index 0000000..e94690f --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/auth/vo/AppAuthLoginRespVO.java @@ -0,0 +1,38 @@ +package cn.aagro.pp.module.member.controller.app.auth.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Schema(description = "用户 APP - 登录 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AppAuthLoginRespVO { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long userId; + + @Schema(description = "访问令牌", requiredMode = Schema.RequiredMode.REQUIRED, example = "happy") + private String accessToken; + + @Schema(description = "刷新令牌", requiredMode = Schema.RequiredMode.REQUIRED, example = "nice") + private String refreshToken; + + @Schema(description = "过期时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime expiresTime; + + /** + * 仅社交登录、社交绑定时会返回 + * + * 为什么需要返回?微信公众号、微信小程序支付需要传递 openid 给支付接口 + */ + @Schema(description = "社交用户 openid", example = "qq768") + private String openid; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/auth/vo/AppAuthSmsLoginReqVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/auth/vo/AppAuthSmsLoginReqVO.java new file mode 100644 index 0000000..f43bfc4 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/auth/vo/AppAuthSmsLoginReqVO.java @@ -0,0 +1,58 @@ +package cn.aagro.pp.module.member.controller.app.auth.vo; + +import cn.hutool.core.util.StrUtil; +import cn.aagro.pp.framework.common.validation.InEnum; +import cn.aagro.pp.framework.common.validation.Mobile; +import cn.aagro.pp.module.system.enums.social.SocialTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.AssertTrue; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.Pattern; + +@Schema(description = "用户 APP - 手机 + 验证码登录 Request VO,如果登录并绑定社交用户,需要传递 social 开头的参数") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AppAuthSmsLoginReqVO { + + @Schema(description = "手机号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15601691300") + @NotEmpty(message = "手机号不能为空") + @Mobile + private String mobile; + + @Schema(description = "手机验证码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotEmpty(message = "手机验证码不能为空") + @Length(min = 4, max = 6, message = "手机验证码长度为 4-6 位") + @Pattern(regexp = "^[0-9]+$", message = "手机验证码必须都是数字") + private String code; + + // ========== 绑定社交登录时,需要传递如下参数 ========== + + @Schema(description = "社交平台的类型,参见 SocialTypeEnum 枚举值", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + @InEnum(SocialTypeEnum.class) + private Integer socialType; + + @Schema(description = "授权码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private String socialCode; + + @Schema(description = "state", requiredMode = Schema.RequiredMode.REQUIRED, example = "9b2ffbc1-7425-4155-9894-9d5c08541d62") + private String socialState; + + @AssertTrue(message = "授权码不能为空") + public boolean isSocialCodeValid() { + return socialType == null || StrUtil.isNotEmpty(socialCode); + } + + @AssertTrue(message = "授权 state 不能为空") + public boolean isSocialState() { + return socialType == null || StrUtil.isNotEmpty(socialState); + } + +} \ No newline at end of file diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/auth/vo/AppAuthSmsSendReqVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/auth/vo/AppAuthSmsSendReqVO.java new file mode 100644 index 0000000..57c5dbd --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/auth/vo/AppAuthSmsSendReqVO.java @@ -0,0 +1,24 @@ +package cn.aagro.pp.module.member.controller.app.auth.vo; + +import cn.aagro.pp.framework.common.validation.InEnum; +import cn.aagro.pp.framework.common.validation.Mobile; +import cn.aagro.pp.module.system.enums.sms.SmsSceneEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +@Schema(description = "用户 APP - 发送手机验证码 Request VO") +@Data +public class AppAuthSmsSendReqVO { + + @Schema(description = "手机号", example = "15601691234") + @Mobile + private String mobile; + + @Schema(description = "发送场景,对应 SmsSceneEnum 枚举", example = "1") + @NotNull(message = "发送场景不能为空") + @InEnum(SmsSceneEnum.class) + private Integer scene; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/auth/vo/AppAuthSmsValidateReqVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/auth/vo/AppAuthSmsValidateReqVO.java new file mode 100644 index 0000000..ced846f --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/auth/vo/AppAuthSmsValidateReqVO.java @@ -0,0 +1,33 @@ +package cn.aagro.pp.module.member.controller.app.auth.vo; + +import cn.aagro.pp.framework.common.validation.InEnum; +import cn.aagro.pp.framework.common.validation.Mobile; +import cn.aagro.pp.module.system.enums.sms.SmsSceneEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; + +@Schema(description = "用户 APP - 校验手机验证码 Request VO") +@Data +public class AppAuthSmsValidateReqVO { + + @Schema(description = "手机号", example = "15601691234") + @Mobile + private String mobile; + + @Schema(description = "发送场景,对应 SmsSceneEnum 枚举", example = "1") + @NotNull(message = "发送场景不能为空") + @InEnum(SmsSceneEnum.class) + private Integer scene; + + @Schema(description = "手机验证码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotEmpty(message = "手机验证码不能为空") + @Length(min = 4, max = 6, message = "手机验证码长度为 4-6 位") + @Pattern(regexp = "^[0-9]+$", message = "手机验证码必须都是数字") + private String code; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/auth/vo/AppAuthSocialLoginReqVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/auth/vo/AppAuthSocialLoginReqVO.java new file mode 100644 index 0000000..668204b --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/auth/vo/AppAuthSocialLoginReqVO.java @@ -0,0 +1,34 @@ +package cn.aagro.pp.module.member.controller.app.auth.vo; + +import cn.aagro.pp.framework.common.validation.InEnum; +import cn.aagro.pp.module.system.enums.social.SocialTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +@Schema(description = "用户 APP - 社交快捷登录 Request VO,使用 code 授权码") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AppAuthSocialLoginReqVO { + + @Schema(description = "社交平台的类型,参见 SocialTypeEnum 枚举值", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + @InEnum(SocialTypeEnum.class) + @NotNull(message = "社交平台的类型不能为空") + private Integer type; + + @Schema(description = "授权码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotEmpty(message = "授权码不能为空") + private String code; + + @Schema(description = "state", requiredMode = Schema.RequiredMode.REQUIRED, example = "9b2ffbc1-7425-4155-9894-9d5c08541d62") + @NotEmpty(message = "state 不能为空") + private String state; + +} \ No newline at end of file diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/auth/vo/AppAuthWeixinMiniAppLoginReqVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/auth/vo/AppAuthWeixinMiniAppLoginReqVO.java new file mode 100644 index 0000000..03df0f4 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/auth/vo/AppAuthWeixinMiniAppLoginReqVO.java @@ -0,0 +1,30 @@ +package cn.aagro.pp.module.member.controller.app.auth.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotEmpty; + +@Schema(description = "用户 APP - 微信小程序手机登录 Request VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AppAuthWeixinMiniAppLoginReqVO { + + @Schema(description = "手机 code,小程序通过 wx.getPhoneNumber 方法获得", requiredMode = Schema.RequiredMode.REQUIRED, example = "hello") + @NotEmpty(message = "手机 code 不能为空") + private String phoneCode; + + @Schema(description = "登录 code,小程序通过 wx.login 方法获得", requiredMode = Schema.RequiredMode.REQUIRED, example = "word") + @NotEmpty(message = "登录 code 不能为空") + private String loginCode; + + @Schema(description = "state", requiredMode = Schema.RequiredMode.REQUIRED, example = "9b2ffbc1-7425-4155-9894-9d5c08541d62") + @NotEmpty(message = "state 不能为空") + private String state; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/auth/vo/AuthWeixinJsapiSignatureRespVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/auth/vo/AuthWeixinJsapiSignatureRespVO.java new file mode 100644 index 0000000..0128a6f --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/auth/vo/AuthWeixinJsapiSignatureRespVO.java @@ -0,0 +1,31 @@ +package cn.aagro.pp.module.member.controller.app.auth.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Schema(description = "用户 APP - 微信公众号 JSAPI 签名 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AuthWeixinJsapiSignatureRespVO { + + @Schema(description = "微信公众号的 appId", requiredMode = Schema.RequiredMode.REQUIRED, example = "hello") + private String appId; + + @Schema(description = "匿名串", requiredMode = Schema.RequiredMode.REQUIRED, example = "world") + private String nonceStr; + + @Schema(description = "时间戳", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long timestamp; + + @Schema(description = "URL", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn") + private String url; + + @Schema(description = "签名", requiredMode = Schema.RequiredMode.REQUIRED, example = "阿巴阿巴") + private String signature; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/level/AppMemberExperienceRecordController.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/level/AppMemberExperienceRecordController.java new file mode 100644 index 0000000..4fe3235 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/level/AppMemberExperienceRecordController.java @@ -0,0 +1,41 @@ +package cn.aagro.pp.module.member.controller.app.level; + +import cn.aagro.pp.framework.common.pojo.CommonResult; +import cn.aagro.pp.framework.common.pojo.PageParam; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.member.controller.app.level.vo.experience.AppMemberExperienceRecordRespVO; +import cn.aagro.pp.module.member.convert.level.MemberExperienceRecordConvert; +import cn.aagro.pp.module.member.dal.dataobject.level.MemberExperienceRecordDO; +import cn.aagro.pp.module.member.service.level.MemberExperienceRecordService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +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 javax.validation.Valid; + +import static cn.aagro.pp.framework.common.pojo.CommonResult.success; +import static cn.aagro.pp.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "用户 App - 会员经验记录") +@RestController +@RequestMapping("/member/experience-record") +@Validated +public class AppMemberExperienceRecordController { + + @Resource + private MemberExperienceRecordService experienceLogService; + + @GetMapping("/page") + @Operation(summary = "获得会员经验记录分页") + public CommonResult> getExperienceRecordPage( + @Valid PageParam pageParam) { + PageResult pageResult = experienceLogService.getExperienceRecordPage( + getLoginUserId(), pageParam); + return success(MemberExperienceRecordConvert.INSTANCE.convertPage02(pageResult)); + } + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/level/AppMemberLevelController.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/level/AppMemberLevelController.java new file mode 100644 index 0000000..a3c3600 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/level/AppMemberLevelController.java @@ -0,0 +1,38 @@ +package cn.aagro.pp.module.member.controller.app.level; + +import cn.aagro.pp.framework.common.pojo.CommonResult; +import cn.aagro.pp.module.member.controller.app.level.vo.level.AppMemberLevelRespVO; +import cn.aagro.pp.module.member.convert.level.MemberLevelConvert; +import cn.aagro.pp.module.member.dal.dataobject.level.MemberLevelDO; +import cn.aagro.pp.module.member.service.level.MemberLevelService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +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 javax.annotation.security.PermitAll; +import java.util.List; + +import static cn.aagro.pp.framework.common.pojo.CommonResult.success; + +@Tag(name = "用户 App - 会员等级") +@RestController +@RequestMapping("/member/level") +@Validated +public class AppMemberLevelController { + + @Resource + private MemberLevelService levelService; + + @GetMapping("/list") + @Operation(summary = "获得会员等级列表") + @PermitAll + public CommonResult> getLevelList() { + List result = levelService.getEnableLevelList(); + return success(MemberLevelConvert.INSTANCE.convertList02(result)); + } + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/level/vo/experience/AppMemberExperienceRecordRespVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/level/vo/experience/AppMemberExperienceRecordRespVO.java new file mode 100644 index 0000000..2e56f0f --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/level/vo/experience/AppMemberExperienceRecordRespVO.java @@ -0,0 +1,24 @@ +package cn.aagro.pp.module.member.controller.app.level.vo.experience; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "用户 App - 会员经验记录 Response VO") +@Data +public class AppMemberExperienceRecordRespVO { + + @Schema(description = "标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "增加经验") + private String title; + + @Schema(description = "经验", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer experience; + + @Schema(description = "描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "下单增加 100 经验") + private String description; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/level/vo/level/AppMemberLevelRespVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/level/vo/level/AppMemberLevelRespVO.java new file mode 100644 index 0000000..7893b97 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/level/vo/level/AppMemberLevelRespVO.java @@ -0,0 +1,28 @@ +package cn.aagro.pp.module.member.controller.app.level.vo.level; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "用户 App - 会员等级 Response VO") +@Data +public class AppMemberLevelRespVO { + + @Schema(description = "等级名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿") + private String name; + + @Schema(description = "等级", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer level; + + @Schema(description = "升级经验", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer experience; + + @Schema(description = "享受折扣", requiredMode = Schema.RequiredMode.REQUIRED, example = "98") + private Integer discountPercent; + + @Schema(description = "等级图标", example = "https://www.iocoder.cn/aagro.jpg") + private String icon; + + @Schema(description = "等级背景图", example = "https://www.iocoder.cn/aagro.jpg") + private String backgroundUrl; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/point/AppMemberPointRecordController.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/point/AppMemberPointRecordController.java new file mode 100644 index 0000000..12697f7 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/point/AppMemberPointRecordController.java @@ -0,0 +1,40 @@ +package cn.aagro.pp.module.member.controller.app.point; + +import cn.aagro.pp.framework.common.pojo.CommonResult; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.common.util.object.BeanUtils; +import cn.aagro.pp.module.member.controller.app.point.vo.AppMemberPointRecordPageReqVO; +import cn.aagro.pp.module.member.controller.app.point.vo.AppMemberPointRecordRespVO; +import cn.aagro.pp.module.member.dal.dataobject.point.MemberPointRecordDO; +import cn.aagro.pp.module.member.service.point.MemberPointRecordService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +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 javax.validation.Valid; + +import static cn.aagro.pp.framework.common.pojo.CommonResult.success; +import static cn.aagro.pp.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "用户 App - 签到记录") +@RestController +@RequestMapping("/member/point/record") +@Validated +public class AppMemberPointRecordController { + + @Resource + private MemberPointRecordService pointRecordService; + + @GetMapping("/page") + @Operation(summary = "获得用户积分记录分页") + public CommonResult> getPointRecordPage( + @Valid AppMemberPointRecordPageReqVO pageReqVO) { + PageResult pageResult = pointRecordService.getPointRecordPage(getLoginUserId(), pageReqVO); + return success(BeanUtils.toBean(pageResult, AppMemberPointRecordRespVO.class)); + } + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/point/vo/AppMemberPointRecordPageReqVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/point/vo/AppMemberPointRecordPageReqVO.java new file mode 100644 index 0000000..49a2482 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/point/vo/AppMemberPointRecordPageReqVO.java @@ -0,0 +1,23 @@ +package cn.aagro.pp.module.member.controller.app.point.vo; + +import cn.aagro.pp.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.aagro.pp.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "用户 App - 用户积分记录分页 Request VO") +@Data +public class AppMemberPointRecordPageReqVO extends PageParam { + + @Schema(description = "是否增加积分", example = "true") + private Boolean addStatus; // true - 增加;false - 减少;null - 不筛选 + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "创建时间") + private LocalDateTime[] createTime; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/point/vo/AppMemberPointRecordRespVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/point/vo/AppMemberPointRecordRespVO.java new file mode 100644 index 0000000..9ea7fdf --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/point/vo/AppMemberPointRecordRespVO.java @@ -0,0 +1,27 @@ +package cn.aagro.pp.module.member.controller.app.point.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "用户 App - 用户积分记录 Response VO") +@Data +public class AppMemberPointRecordRespVO { + + @Schema(description = "自增主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "31457") + private Long id; + + @Schema(description = "积分标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "你猜") + private String title; + + @Schema(description = "积分描述", example = "你猜") + private String description; + + @Schema(description = "积分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer point; + + @Schema(description = "发生时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/signin/AppMemberSignInConfigController.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/signin/AppMemberSignInConfigController.java new file mode 100644 index 0000000..47bb31e --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/signin/AppMemberSignInConfigController.java @@ -0,0 +1,39 @@ +package cn.aagro.pp.module.member.controller.app.signin; + +import cn.aagro.pp.framework.common.enums.CommonStatusEnum; +import cn.aagro.pp.framework.common.pojo.CommonResult; +import cn.aagro.pp.module.member.controller.app.signin.vo.config.AppMemberSignInConfigRespVO; +import cn.aagro.pp.module.member.convert.signin.MemberSignInConfigConvert; +import cn.aagro.pp.module.member.dal.dataobject.signin.MemberSignInConfigDO; +import cn.aagro.pp.module.member.service.signin.MemberSignInConfigService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +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 javax.annotation.security.PermitAll; +import java.util.List; + +import static cn.aagro.pp.framework.common.pojo.CommonResult.success; + +@Tag(name = "用户 App - 签到规则") +@RestController +@RequestMapping("/member/sign-in/config") +@Validated +public class AppMemberSignInConfigController { + + @Resource + private MemberSignInConfigService signInConfigService; + + @GetMapping("/list") + @Operation(summary = "获得签到规则列表") + @PermitAll + public CommonResult> getSignInConfigList() { + List pageResult = signInConfigService.getSignInConfigList(CommonStatusEnum.ENABLE.getStatus()); + return success(MemberSignInConfigConvert.INSTANCE.convertList02(pageResult)); + } + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/signin/AppMemberSignInRecordController.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/signin/AppMemberSignInRecordController.java new file mode 100644 index 0000000..f3f2780 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/signin/AppMemberSignInRecordController.java @@ -0,0 +1,53 @@ +package cn.aagro.pp.module.member.controller.app.signin; + +import cn.aagro.pp.framework.common.pojo.CommonResult; +import cn.aagro.pp.framework.common.pojo.PageParam; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.member.controller.app.signin.vo.record.AppMemberSignInRecordRespVO; +import cn.aagro.pp.module.member.controller.app.signin.vo.record.AppMemberSignInRecordSummaryRespVO; +import cn.aagro.pp.module.member.convert.signin.MemberSignInRecordConvert; +import cn.aagro.pp.module.member.dal.dataobject.signin.MemberSignInRecordDO; +import cn.aagro.pp.module.member.service.signin.MemberSignInRecordService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; + +import static cn.aagro.pp.framework.common.pojo.CommonResult.success; +import static cn.aagro.pp.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "用户 App - 签到记录") +@RestController +@RequestMapping("/member/sign-in/record") +@Validated +public class AppMemberSignInRecordController { + + @Resource + private MemberSignInRecordService signInRecordService; + + @GetMapping("/get-summary") + @Operation(summary = "获得个人签到统计") + public CommonResult getSignInRecordSummary() { + return success(signInRecordService.getSignInRecordSummary(getLoginUserId())); + } + + @PostMapping("/create") + @Operation(summary = "签到") + public CommonResult createSignInRecord() { + MemberSignInRecordDO recordDO = signInRecordService.createSignRecord(getLoginUserId()); + return success(MemberSignInRecordConvert.INSTANCE.coverRecordToAppRecordVo(recordDO)); + } + + @GetMapping("/page") + @Operation(summary = "获得签到记录分页") + public CommonResult> getSignRecordPage(PageParam pageParam) { + PageResult pageResult = signInRecordService.getSignRecordPage(getLoginUserId(), pageParam); + return success(MemberSignInRecordConvert.INSTANCE.convertPage02(pageResult)); + } + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/signin/vo/config/AppMemberSignInConfigRespVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/signin/vo/config/AppMemberSignInConfigRespVO.java new file mode 100644 index 0000000..3133818 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/signin/vo/config/AppMemberSignInConfigRespVO.java @@ -0,0 +1,16 @@ +package cn.aagro.pp.module.member.controller.app.signin.vo.config; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "用户 App - 签到规则 Response VO") +@Data +public class AppMemberSignInConfigRespVO { + + @Schema(description = "签到第 x 天", requiredMode = Schema.RequiredMode.REQUIRED, example = "7") + private Integer day; + + @Schema(description = "奖励积分", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Integer point; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/signin/vo/record/AppMemberSignInRecordRespVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/signin/vo/record/AppMemberSignInRecordRespVO.java new file mode 100644 index 0000000..9b5a973 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/signin/vo/record/AppMemberSignInRecordRespVO.java @@ -0,0 +1,24 @@ +package cn.aagro.pp.module.member.controller.app.signin.vo.record; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "用户 App - 签到记录 Response VO") +@Data +public class AppMemberSignInRecordRespVO { + + @Schema(description = "第几天签到", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer day; + + @Schema(description = "签到的分数", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Integer point; + + @Schema(description = "签到的经验", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Integer experience; + + @Schema(description = "签到时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/signin/vo/record/AppMemberSignInRecordSummaryRespVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/signin/vo/record/AppMemberSignInRecordSummaryRespVO.java new file mode 100644 index 0000000..e1bf601 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/signin/vo/record/AppMemberSignInRecordSummaryRespVO.java @@ -0,0 +1,19 @@ +package cn.aagro.pp.module.member.controller.app.signin.vo.record; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "用户 App - 个人签到统计 Response VO") +@Data +public class AppMemberSignInRecordSummaryRespVO { + + @Schema(description = "总签到天数", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Integer totalDay; + + @Schema(description = "连续签到第 x 天", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") + private Integer continuousDay; + + @Schema(description = "今天是否已签到", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean todaySignIn; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/social/AppSocialUserController.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/social/AppSocialUserController.java new file mode 100644 index 0000000..c7c6df9 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/social/AppSocialUserController.java @@ -0,0 +1,79 @@ +package cn.aagro.pp.module.member.controller.app.social; + +import cn.hutool.core.codec.Base64; +import cn.aagro.pp.framework.common.enums.UserTypeEnum; +import cn.aagro.pp.framework.common.pojo.CommonResult; +import cn.aagro.pp.framework.common.util.object.BeanUtils; +import cn.aagro.pp.module.member.controller.app.social.vo.*; +import cn.aagro.pp.module.system.api.social.SocialClientApi; +import cn.aagro.pp.module.system.api.social.SocialUserApi; +import cn.aagro.pp.module.system.api.social.dto.*; +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.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.annotation.security.PermitAll; +import javax.validation.Valid; +import java.util.List; + +import static cn.aagro.pp.framework.common.pojo.CommonResult.success; +import static cn.aagro.pp.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "用户 App - 社交用户") +@RestController +@RequestMapping("/member/social-user") +@Validated +public class AppSocialUserController { + + @Resource + private SocialUserApi socialUserApi; + @Resource + private SocialClientApi socialClientApi; + + @PostMapping("/bind") + @Operation(summary = "社交绑定,使用 code 授权码") + @PermitAll + public CommonResult socialBind(@RequestBody @Valid AppSocialUserBindReqVO reqVO) { + SocialUserBindReqDTO reqDTO = new SocialUserBindReqDTO(getLoginUserId(), UserTypeEnum.MEMBER.getValue(), + reqVO.getType(), reqVO.getCode(), reqVO.getState()); + String openid = socialUserApi.bindSocialUser(reqDTO); + return success(openid); + } + + @DeleteMapping("/unbind") + @Operation(summary = "取消社交绑定") + public CommonResult socialUnbind(@RequestBody AppSocialUserUnbindReqVO reqVO) { + SocialUserUnbindReqDTO reqDTO = new SocialUserUnbindReqDTO(getLoginUserId(), UserTypeEnum.MEMBER.getValue(), + reqVO.getType(), reqVO.getOpenid()); + socialUserApi.unbindSocialUser(reqDTO); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得社交用户") + @Parameter(name = "type", description = "社交平台的类型,参见 SocialTypeEnum 枚举值", required = true, example = "10") + public CommonResult getSocialUser(@RequestParam("type") Integer type) { + SocialUserRespDTO socialUser = socialUserApi.getSocialUserByUserId(UserTypeEnum.MEMBER.getValue(), getLoginUserId(), type); + return success(BeanUtils.toBean(socialUser, AppSocialUserRespVO.class)); + } + + @PostMapping("/wxa-qrcode") + @Operation(summary = "获得微信小程序码(base64 image)") + @PermitAll + public CommonResult getWxaQrcode(@RequestBody @Valid AppSocialWxaQrcodeReqVO reqVO) { + byte[] wxQrcode = socialClientApi.getWxaQrcode(BeanUtils.toBean(reqVO, SocialWxQrcodeReqDTO.class)); + return success(Base64.encode(wxQrcode)); + } + + @GetMapping("/get-subscribe-template-list") + @Operation(summary = "获得微信小程订阅模板列表") + @PermitAll + public CommonResult> getSubscribeTemplateList() { + List template = socialClientApi.getWxaSubscribeTemplateList(UserTypeEnum.MEMBER.getValue()); + return success(BeanUtils.toBean(template, AppSocialWxaSubscribeTemplateRespVO.class)); + } + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/social/vo/AppSocialUserBindReqVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/social/vo/AppSocialUserBindReqVO.java new file mode 100644 index 0000000..2f708e2 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/social/vo/AppSocialUserBindReqVO.java @@ -0,0 +1,31 @@ +package cn.aagro.pp.module.member.controller.app.social.vo; + +import cn.aagro.pp.framework.common.validation.InEnum; +import cn.aagro.pp.module.system.enums.social.SocialTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +@Schema(description = "用户 APP - 社交绑定 Request VO,使用 code 授权码") +@Data +public class AppSocialUserBindReqVO { + + @Schema(description = "社交平台的类型,参见 SocialTypeEnum 枚举值", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + @InEnum(SocialTypeEnum.class) + @NotNull(message = "社交平台的类型不能为空") + private Integer type; + + @Schema(description = "授权码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotEmpty(message = "授权码不能为空") + private String code; + + @Schema(description = "state", requiredMode = Schema.RequiredMode.REQUIRED, example = "9b2ffbc1-7425-4155-9894-9d5c08541d62") + @NotEmpty(message = "state 不能为空") + private String state; + +} \ No newline at end of file diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/social/vo/AppSocialUserRespVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/social/vo/AppSocialUserRespVO.java new file mode 100644 index 0000000..b31d78c --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/social/vo/AppSocialUserRespVO.java @@ -0,0 +1,19 @@ +package cn.aagro.pp.module.member.controller.app.social.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "用户 APP - 社交用户 Response VO") +@Data +public class AppSocialUserRespVO { + + @Schema(description = "社交用户的 openid", requiredMode = Schema.RequiredMode.REQUIRED, example = "IPRmJ0wvBptiPIlGEZiPewGwiEiE") + private String openid; + + @Schema(description = "社交用户的昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道源码") + private String nickname; + + @Schema(description = "社交用户的头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png") + private String avatar; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/social/vo/AppSocialUserUnbindReqVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/social/vo/AppSocialUserUnbindReqVO.java new file mode 100644 index 0000000..1c2d07f --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/social/vo/AppSocialUserUnbindReqVO.java @@ -0,0 +1,27 @@ +package cn.aagro.pp.module.member.controller.app.social.vo; + +import cn.aagro.pp.framework.common.validation.InEnum; +import cn.aagro.pp.module.system.enums.social.SocialTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +@Schema(description = "用户 APP - 取消社交绑定 Request VO") +@Data +public class AppSocialUserUnbindReqVO { + + @Schema(description = "社交平台的类型,参见 SocialTypeEnum 枚举值", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + @InEnum(SocialTypeEnum.class) + @NotNull(message = "社交平台的类型不能为空") + private Integer type; + + @Schema(description = "社交用户的 openid", requiredMode = Schema.RequiredMode.REQUIRED, example = "IPRmJ0wvBptiPIlGEZiPewGwiEiE") + @NotEmpty(message = "社交用户的 openid 不能为空") + private String openid; + +} \ No newline at end of file diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/social/vo/AppSocialWxaQrcodeReqVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/social/vo/AppSocialWxaQrcodeReqVO.java new file mode 100644 index 0000000..db9eab3 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/social/vo/AppSocialWxaQrcodeReqVO.java @@ -0,0 +1,39 @@ +package cn.aagro.pp.module.member.controller.app.social.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; + + +@Schema(description = "用户 APP - 获得获取小程序码 Request VO") +@Data +public class AppSocialWxaQrcodeReqVO { + + /** + * 页面路径不能携带参数(参数请放在scene字段里) + */ + @Schema(description = "场景值", requiredMode = Schema.RequiredMode.REQUIRED, example = "1001") + private String scene; + + /** + * 默认是主页,页面 page,例如 pages/index/index,根路径前不要填加 /,不能携带参数(参数请放在scene字段里), + * 如果不填写这个字段,默认跳主页面。scancode_time为系统保留参数,不允许配置 + */ + @Schema(description = "页面路径", requiredMode = Schema.RequiredMode.REQUIRED, example = "pages/goods/index") + @NotEmpty(message = "页面路径不能为空") + private String path; + + @Schema(description = "二维码宽度", requiredMode = Schema.RequiredMode.REQUIRED, example = "430") + private Integer width; + + @Schema(description = "是/否自动配置线条颜色", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean autoColor; + + @Schema(description = "是/否检查 page 是否存在", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean checkPath; + + @Schema(description = "是/否需要透明底色", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean hyaline; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/social/vo/AppSocialWxaSubscribeTemplateRespVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/social/vo/AppSocialWxaSubscribeTemplateRespVO.java new file mode 100644 index 0000000..c334ce8 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/social/vo/AppSocialWxaSubscribeTemplateRespVO.java @@ -0,0 +1,27 @@ +package cn.aagro.pp.module.member.controller.app.social.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "用户 APP - 获得小程序订阅模版 Response VO") +@Data +public class AppSocialWxaSubscribeTemplateRespVO { + + @Schema(description = "模版编号", requiredMode = Schema.RequiredMode.REQUIRED, + example = "9Aw5ZV1j9xdWTFEkqCpZ7mIBbSC34khK55OtzUPl0rU") + private String id; + + @Schema(description = "模版标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "订单支付通知") + private String title; + + @Schema(description = "模版内容", requiredMode = Schema.RequiredMode.REQUIRED, + example = "{ {result.DATA} }\\n\\n领奖金额:{ {withdrawMoney.DATA} }\\n领奖时间: { {withdrawTime.DATA} }") + private String content; + + @Schema(description = "模板内容示例", requiredMode = Schema.RequiredMode.REQUIRED, example = "下单时间:2016年8月8日") + private String example; + + @Schema(description = "模版类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + private Integer type; // 2 为一次性订阅,3 为长期订阅 + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/user/AppMemberUserController.http b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/user/AppMemberUserController.http new file mode 100644 index 0000000..8ffb70c --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/user/AppMemberUserController.http @@ -0,0 +1,4 @@ +### 请求 /member/user/profile/get 接口 => 没有权限 +GET {{appApi}}/member/user/get +Authorization: Bearer test245 +tenant-id: {{appTenantId}} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/user/AppMemberUserController.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/user/AppMemberUserController.java new file mode 100644 index 0000000..2e7ebc6 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/user/AppMemberUserController.java @@ -0,0 +1,79 @@ +package cn.aagro.pp.module.member.controller.app.user; + +import cn.aagro.pp.framework.common.pojo.CommonResult; +import cn.aagro.pp.module.member.controller.app.user.vo.*; +import cn.aagro.pp.module.member.convert.user.MemberUserConvert; +import cn.aagro.pp.module.member.dal.dataobject.level.MemberLevelDO; +import cn.aagro.pp.module.member.dal.dataobject.user.MemberUserDO; +import cn.aagro.pp.module.member.service.level.MemberLevelService; +import cn.aagro.pp.module.member.service.user.MemberUserService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.annotation.security.PermitAll; +import javax.validation.Valid; + +import static cn.aagro.pp.framework.common.pojo.CommonResult.success; +import static cn.aagro.pp.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "用户 APP - 用户个人中心") +@RestController +@RequestMapping("/member/user") +@Validated +@Slf4j +public class AppMemberUserController { + + @Resource + private MemberUserService userService; + @Resource + private MemberLevelService levelService; + + @GetMapping("/get") + @Operation(summary = "获得基本信息") + public CommonResult getUserInfo() { + MemberUserDO user = userService.getUser(getLoginUserId()); + MemberLevelDO level = levelService.getLevel(user.getLevelId()); + return success(MemberUserConvert.INSTANCE.convert(user, level)); + } + + @PutMapping("/update") + @Operation(summary = "修改基本信息") + public CommonResult updateUser(@RequestBody @Valid AppMemberUserUpdateReqVO reqVO) { + userService.updateUser(getLoginUserId(), reqVO); + return success(true); + } + + @PutMapping("/update-mobile") + @Operation(summary = "修改用户手机") + public CommonResult updateUserMobile(@RequestBody @Valid AppMemberUserUpdateMobileReqVO reqVO) { + userService.updateUserMobile(getLoginUserId(), reqVO); + return success(true); + } + + @PutMapping("/update-mobile-by-weixin") + @Operation(summary = "基于微信小程序的授权码,修改用户手机") + public CommonResult updateUserMobileByWeixin(@RequestBody @Valid AppMemberUserUpdateMobileByWeixinReqVO reqVO) { + userService.updateUserMobileByWeixin(getLoginUserId(), reqVO); + return success(true); + } + + @PutMapping("/update-password") + @Operation(summary = "修改用户密码", description = "用户修改密码时使用") + public CommonResult updateUserPassword(@RequestBody @Valid AppMemberUserUpdatePasswordReqVO reqVO) { + userService.updateUserPassword(getLoginUserId(), reqVO); + return success(true); + } + + @PutMapping("/reset-password") + @Operation(summary = "重置密码", description = "用户忘记密码时使用") + @PermitAll + public CommonResult resetUserPassword(@RequestBody @Valid AppMemberUserResetPasswordReqVO reqVO) { + userService.resetUserPassword(reqVO); + return success(true); + } + +} \ No newline at end of file diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/user/vo/AppMemberUserInfoRespVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/user/vo/AppMemberUserInfoRespVO.java new file mode 100644 index 0000000..ac0247f --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/user/vo/AppMemberUserInfoRespVO.java @@ -0,0 +1,59 @@ +package cn.aagro.pp.module.member.controller.app.user.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Schema(description = "用户 APP - 用户个人信息 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class AppMemberUserInfoRespVO { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿") + private String nickname; + + @Schema(description = "用户头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/xxx.png") + private String avatar; + + @Schema(description = "用户手机号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15601691300") + private String mobile; + + @Schema(description = "用户性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer sex; + + @Schema(description = "积分", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Integer point; + + @Schema(description = "经验值", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Integer experience; + + @Schema(description = "用户等级") + private Level level; + + @Schema(description = "是否成为推广员", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean brokerageEnabled; + + @Schema(description = "用户 App - 会员等级") + @Data + public static class Level { + + @Schema(description = "等级编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "等级名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿") + private String name; + + @Schema(description = "等级", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer level; + + @Schema(description = "等级图标", example = "https://www.iocoder.cn/aagro.jpg") + private String icon; + + } + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/user/vo/AppMemberUserResetPasswordReqVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/user/vo/AppMemberUserResetPasswordReqVO.java new file mode 100644 index 0000000..4f73a0e --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/user/vo/AppMemberUserResetPasswordReqVO.java @@ -0,0 +1,38 @@ +package cn.aagro.pp.module.member.controller.app.user.vo; + +import cn.aagro.pp.framework.common.validation.Mobile; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.Pattern; + +@Schema(description = "用户 APP - 重置密码 Request VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AppMemberUserResetPasswordReqVO { + + @Schema(description = "新密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "buzhidao") + @NotEmpty(message = "新密码不能为空") + @Length(min = 4, max = 16, message = "密码长度为 4-16 位") + private String password; + + @Schema(description = "手机验证码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotEmpty(message = "手机验证码不能为空") + @Length(min = 4, max = 6, message = "手机验证码长度为 4-6 位") + @Pattern(regexp = "^[0-9]+$", message = "手机验证码必须都是数字") + private String code; + + @Schema(description = "手机号",requiredMode = Schema.RequiredMode.REQUIRED,example = "15878962356") + @NotBlank(message = "手机号不能为空") + @Mobile + private String mobile; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/user/vo/AppMemberUserUpdateMobileByWeixinReqVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/user/vo/AppMemberUserUpdateMobileByWeixinReqVO.java new file mode 100644 index 0000000..26fca03 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/user/vo/AppMemberUserUpdateMobileByWeixinReqVO.java @@ -0,0 +1,17 @@ +package cn.aagro.pp.module.member.controller.app.user.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; + +@Schema(description = "用户 APP - 基于微信小程序的授权码,修改手机 Request VO") +@Data +public class AppMemberUserUpdateMobileByWeixinReqVO { + + @Schema(description = "手机 code,小程序通过 wx.getPhoneNumber 方法获得", + requiredMode = Schema.RequiredMode.REQUIRED, example = "hello") + @NotEmpty(message = "手机 code 不能为空") + private String code; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/user/vo/AppMemberUserUpdateMobileReqVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/user/vo/AppMemberUserUpdateMobileReqVO.java new file mode 100644 index 0000000..1739ae5 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/user/vo/AppMemberUserUpdateMobileReqVO.java @@ -0,0 +1,36 @@ +package cn.aagro.pp.module.member.controller.app.user.vo; + +import cn.aagro.pp.framework.common.validation.Mobile; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.Pattern; + +@Schema(description = "用户 APP - 修改手机 Request VO") +@Data +public class AppMemberUserUpdateMobileReqVO { + + @Schema(description = "手机验证码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotEmpty(message = "手机验证码不能为空") + @Length(min = 4, max = 6, message = "手机验证码长度为 4-6 位") + @Pattern(regexp = "^[0-9]+$", message = "手机验证码必须都是数字") + private String code; + + @Schema(description = "手机号",requiredMode = Schema.RequiredMode.REQUIRED, example = "15823654487") + @NotBlank(message = "手机号不能为空") + @Length(min = 8, max = 11, message = "手机号码长度为 8-11 位") + @Mobile + private String mobile; + + @Schema(description = "原手机验证码", example = "1024") + @Length(min = 4, max = 6, message = "手机验证码长度为 4-6 位") + @Pattern(regexp = "^[0-9]+$", message = "手机验证码必须都是数字") + private String oldCode; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/user/vo/AppMemberUserUpdatePasswordReqVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/user/vo/AppMemberUserUpdatePasswordReqVO.java new file mode 100644 index 0000000..cede806 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/user/vo/AppMemberUserUpdatePasswordReqVO.java @@ -0,0 +1,31 @@ +package cn.aagro.pp.module.member.controller.app.user.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.Pattern; + +@Schema(description = "用户 APP - 修改密码 Request VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AppMemberUserUpdatePasswordReqVO { + + @Schema(description = "新密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "buzhidao") + @NotEmpty(message = "新密码不能为空") + @Length(min = 4, max = 16, message = "密码长度为 4-16 位") + private String password; + + @Schema(description = "手机验证码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotEmpty(message = "手机验证码不能为空") + @Length(min = 4, max = 6, message = "手机验证码长度为 4-6 位") + @Pattern(regexp = "^[0-9]+$", message = "手机验证码必须都是数字") + private String code; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/user/vo/AppMemberUserUpdateReqVO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/user/vo/AppMemberUserUpdateReqVO.java new file mode 100644 index 0000000..3bf6a69 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/app/user/vo/AppMemberUserUpdateReqVO.java @@ -0,0 +1,23 @@ +package cn.aagro.pp.module.member.controller.app.user.vo; + +import cn.aagro.pp.framework.common.validation.InEnum; +import cn.aagro.pp.module.system.enums.common.SexEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.hibernate.validator.constraints.URL; + +@Schema(description = "用户 App - 会员用户更新 Request VO") +@Data +public class AppMemberUserUpdateReqVO { + + @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四") + private String nickname; + + @Schema(description = "头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/x.png") + @URL(message = "头像必须是 URL 格式") + private String avatar; + + @Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer sex; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/package-info.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/package-info.java new file mode 100644 index 0000000..9f6bc7e --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/controller/package-info.java @@ -0,0 +1,6 @@ +/** + * 提供 RESTful API 给前端: + * 1. admin 包:提供给管理后台 aagro-ui-admin 前端项目 + * 2. app 包:提供给用户 APP aagro-ui-app 前端项目,它的 Controller 和 VO 都要添加 App 前缀,用于和管理后台进行区分 + */ +package cn.aagro.pp.module.member.controller; diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/convert/address/AddressConvert.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/convert/address/AddressConvert.java new file mode 100644 index 0000000..b2a84f4 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/convert/address/AddressConvert.java @@ -0,0 +1,45 @@ +package cn.aagro.pp.module.member.convert.address; + +import cn.aagro.pp.framework.ip.core.utils.AreaUtils; +import cn.aagro.pp.module.member.api.address.dto.MemberAddressRespDTO; +import cn.aagro.pp.module.member.controller.admin.address.vo.AddressRespVO; +import cn.aagro.pp.module.member.controller.app.address.vo.AppAddressCreateReqVO; +import cn.aagro.pp.module.member.controller.app.address.vo.AppAddressRespVO; +import cn.aagro.pp.module.member.controller.app.address.vo.AppAddressUpdateReqVO; +import cn.aagro.pp.module.member.dal.dataobject.address.MemberAddressDO; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Named; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * 用户收件地址 Convert + * + * @author 芋道源码 + */ +@Mapper +public interface AddressConvert { + + AddressConvert INSTANCE = Mappers.getMapper(AddressConvert.class); + + MemberAddressDO convert(AppAddressCreateReqVO bean); + + MemberAddressDO convert(AppAddressUpdateReqVO bean); + + @Mapping(source = "areaId", target = "areaName", qualifiedByName = "convertAreaIdToAreaName") + AppAddressRespVO convert(MemberAddressDO bean); + + List convertList(List list); + + MemberAddressRespDTO convert02(MemberAddressDO bean); + + @Named("convertAreaIdToAreaName") + default String convertAreaIdToAreaName(Integer areaId) { + return AreaUtils.format(areaId); + } + + List convertList2(List list); + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/convert/auth/AuthConvert.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/convert/auth/AuthConvert.java new file mode 100644 index 0000000..8899e66 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/convert/auth/AuthConvert.java @@ -0,0 +1,35 @@ +package cn.aagro.pp.module.member.convert.auth; + +import cn.aagro.pp.module.member.controller.app.auth.vo.*; +import cn.aagro.pp.module.member.controller.app.social.vo.AppSocialUserUnbindReqVO; +import cn.aagro.pp.module.member.controller.app.user.vo.AppMemberUserResetPasswordReqVO; +import cn.aagro.pp.framework.common.biz.system.oauth2.dto.OAuth2AccessTokenRespDTO; +import cn.aagro.pp.module.system.api.sms.dto.code.SmsCodeSendReqDTO; +import cn.aagro.pp.module.system.api.sms.dto.code.SmsCodeUseReqDTO; +import cn.aagro.pp.module.system.api.sms.dto.code.SmsCodeValidateReqDTO; +import cn.aagro.pp.module.system.api.social.dto.SocialUserBindReqDTO; +import cn.aagro.pp.module.system.api.social.dto.SocialUserUnbindReqDTO; +import cn.aagro.pp.module.system.api.social.dto.SocialWxJsapiSignatureRespDTO; +import cn.aagro.pp.module.system.enums.sms.SmsSceneEnum; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface AuthConvert { + + AuthConvert INSTANCE = Mappers.getMapper(AuthConvert.class); + + SocialUserBindReqDTO convert(Long userId, Integer userType, AppAuthSocialLoginReqVO reqVO); + SocialUserUnbindReqDTO convert(Long userId, Integer userType, AppSocialUserUnbindReqVO reqVO); + + SmsCodeSendReqDTO convert(AppAuthSmsSendReqVO reqVO); + SmsCodeUseReqDTO convert(AppMemberUserResetPasswordReqVO reqVO, SmsSceneEnum scene, String usedIp); + SmsCodeUseReqDTO convert(AppAuthSmsLoginReqVO reqVO, Integer scene, String usedIp); + + AppAuthLoginRespVO convert(OAuth2AccessTokenRespDTO bean, String openid); + + SmsCodeValidateReqDTO convert(AppAuthSmsValidateReqVO bean); + + SocialWxJsapiSignatureRespDTO convert(SocialWxJsapiSignatureRespDTO bean); + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/convert/config/MemberConfigConvert.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/convert/config/MemberConfigConvert.java new file mode 100644 index 0000000..41ec4e4 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/convert/config/MemberConfigConvert.java @@ -0,0 +1,25 @@ +package cn.aagro.pp.module.member.convert.config; + +import cn.aagro.pp.module.member.api.config.dto.MemberConfigRespDTO; +import cn.aagro.pp.module.member.controller.admin.config.vo.MemberConfigRespVO; +import cn.aagro.pp.module.member.controller.admin.config.vo.MemberConfigSaveReqVO; +import cn.aagro.pp.module.member.dal.dataobject.config.MemberConfigDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * 会员配置 Convert + * + * @author QingX + */ +@Mapper +public interface MemberConfigConvert { + + MemberConfigConvert INSTANCE = Mappers.getMapper(MemberConfigConvert.class); + + MemberConfigRespVO convert(MemberConfigDO bean); + + MemberConfigDO convert(MemberConfigSaveReqVO bean); + + MemberConfigRespDTO convert01(MemberConfigDO config); +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/convert/group/MemberGroupConvert.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/convert/group/MemberGroupConvert.java new file mode 100644 index 0000000..6eb35b0 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/convert/group/MemberGroupConvert.java @@ -0,0 +1,35 @@ +package cn.aagro.pp.module.member.convert.group; + +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.member.controller.admin.group.vo.MemberGroupCreateReqVO; +import cn.aagro.pp.module.member.controller.admin.group.vo.MemberGroupRespVO; +import cn.aagro.pp.module.member.controller.admin.group.vo.MemberGroupSimpleRespVO; +import cn.aagro.pp.module.member.controller.admin.group.vo.MemberGroupUpdateReqVO; +import cn.aagro.pp.module.member.dal.dataobject.group.MemberGroupDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * 用户分组 Convert + * + * @author owen + */ +@Mapper +public interface MemberGroupConvert { + + MemberGroupConvert INSTANCE = Mappers.getMapper(MemberGroupConvert.class); + + MemberGroupDO convert(MemberGroupCreateReqVO bean); + + MemberGroupDO convert(MemberGroupUpdateReqVO bean); + + MemberGroupRespVO convert(MemberGroupDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + + List convertSimpleList(List list); +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/convert/level/MemberExperienceRecordConvert.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/convert/level/MemberExperienceRecordConvert.java new file mode 100644 index 0000000..b7afe27 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/convert/level/MemberExperienceRecordConvert.java @@ -0,0 +1,34 @@ +package cn.aagro.pp.module.member.convert.level; + +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.member.controller.admin.level.vo.experience.MemberExperienceRecordRespVO; +import cn.aagro.pp.module.member.controller.app.level.vo.experience.AppMemberExperienceRecordRespVO; +import cn.aagro.pp.module.member.dal.dataobject.level.MemberExperienceRecordDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * 会员经验记录 Convert + * + * @author owen + */ +@Mapper +public interface MemberExperienceRecordConvert { + + MemberExperienceRecordConvert INSTANCE = Mappers.getMapper(MemberExperienceRecordConvert.class); + + MemberExperienceRecordRespVO convert(MemberExperienceRecordDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + + MemberExperienceRecordDO convert(Long userId, Integer experience, Integer totalExperience, + String bizId, Integer bizType, + String title, String description); + + PageResult convertPage02(PageResult page); + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/convert/level/MemberLevelConvert.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/convert/level/MemberLevelConvert.java new file mode 100644 index 0000000..7ccd6ca --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/convert/level/MemberLevelConvert.java @@ -0,0 +1,39 @@ +package cn.aagro.pp.module.member.convert.level; + +import cn.aagro.pp.module.member.api.level.dto.MemberLevelRespDTO; +import cn.aagro.pp.module.member.controller.admin.level.vo.level.MemberLevelCreateReqVO; +import cn.aagro.pp.module.member.controller.admin.level.vo.level.MemberLevelRespVO; +import cn.aagro.pp.module.member.controller.admin.level.vo.level.MemberLevelSimpleRespVO; +import cn.aagro.pp.module.member.controller.admin.level.vo.level.MemberLevelUpdateReqVO; +import cn.aagro.pp.module.member.controller.app.level.vo.level.AppMemberLevelRespVO; +import cn.aagro.pp.module.member.dal.dataobject.level.MemberLevelDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * 会员等级 Convert + * + * @author owen + */ +@Mapper +public interface MemberLevelConvert { + + MemberLevelConvert INSTANCE = Mappers.getMapper(MemberLevelConvert.class); + + MemberLevelDO convert(MemberLevelCreateReqVO bean); + + MemberLevelDO convert(MemberLevelUpdateReqVO bean); + + MemberLevelRespVO convert(MemberLevelDO bean); + + List convertList(List list); + + List convertSimpleList(List list); + + List convertList02(List list); + + MemberLevelRespDTO convert02(MemberLevelDO bean); + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/convert/level/MemberLevelRecordConvert.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/convert/level/MemberLevelRecordConvert.java new file mode 100644 index 0000000..dcf7c22 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/convert/level/MemberLevelRecordConvert.java @@ -0,0 +1,37 @@ +package cn.aagro.pp.module.member.convert.level; + +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.member.controller.admin.level.vo.record.MemberLevelRecordRespVO; +import cn.aagro.pp.module.member.dal.dataobject.level.MemberLevelDO; +import cn.aagro.pp.module.member.dal.dataobject.level.MemberLevelRecordDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * 会员等级记录 Convert + * + * @author owen + */ +@Mapper +public interface MemberLevelRecordConvert { + + MemberLevelRecordConvert INSTANCE = Mappers.getMapper(MemberLevelRecordConvert.class); + + MemberLevelRecordRespVO convert(MemberLevelRecordDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + + default MemberLevelRecordDO copyTo(MemberLevelDO from, MemberLevelRecordDO to) { + if (from != null) { + to.setLevelId(from.getId()); + to.setLevel(from.getLevel()); + to.setDiscountPercent(from.getDiscountPercent()); + to.setExperience(from.getExperience()); + } + return to; + } +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/convert/package-info.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/convert/package-info.java new file mode 100644 index 0000000..381baf0 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/convert/package-info.java @@ -0,0 +1,6 @@ +/** + * 提供 POJO 类的实体转换 + * + * 目前使用 MapStruct 框架 + */ +package cn.aagro.pp.module.member.convert; diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/convert/point/MemberPointRecordConvert.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/convert/point/MemberPointRecordConvert.java new file mode 100644 index 0000000..d02d908 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/convert/point/MemberPointRecordConvert.java @@ -0,0 +1,37 @@ +package cn.aagro.pp.module.member.convert.point; + +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.common.util.collection.MapUtils; +import cn.aagro.pp.module.member.controller.admin.point.vo.recrod.MemberPointRecordRespVO; +import cn.aagro.pp.module.member.controller.app.point.vo.AppMemberPointRecordRespVO; +import cn.aagro.pp.module.member.dal.dataobject.point.MemberPointRecordDO; +import cn.aagro.pp.module.member.dal.dataobject.user.MemberUserDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; +import java.util.Map; + +import static cn.aagro.pp.framework.common.util.collection.CollectionUtils.convertMap; + +/** + * 用户积分记录 Convert + * + * @author QingX + */ +@Mapper +public interface MemberPointRecordConvert { + + MemberPointRecordConvert INSTANCE = Mappers.getMapper(MemberPointRecordConvert.class); + + default PageResult convertPage(PageResult pageResult, List users) { + PageResult voPageResult = convertPage(pageResult); + // user 拼接 + Map userMap = convertMap(users, MemberUserDO::getId); + voPageResult.getList().forEach(record -> MapUtils.findAndThen(userMap, record.getUserId(), + memberUserRespDTO -> record.setNickname(memberUserRespDTO.getNickname()))); + return voPageResult; + } + PageResult convertPage(PageResult pageResult); + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/convert/signin/MemberSignInConfigConvert.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/convert/signin/MemberSignInConfigConvert.java new file mode 100644 index 0000000..570d9b7 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/convert/signin/MemberSignInConfigConvert.java @@ -0,0 +1,33 @@ +package cn.aagro.pp.module.member.convert.signin; + +import cn.aagro.pp.module.member.controller.admin.signin.vo.config.MemberSignInConfigCreateReqVO; +import cn.aagro.pp.module.member.controller.admin.signin.vo.config.MemberSignInConfigRespVO; +import cn.aagro.pp.module.member.controller.admin.signin.vo.config.MemberSignInConfigUpdateReqVO; +import cn.aagro.pp.module.member.controller.app.signin.vo.config.AppMemberSignInConfigRespVO; +import cn.aagro.pp.module.member.dal.dataobject.signin.MemberSignInConfigDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * 签到规则 Convert + * + * @author QingX + */ +@Mapper +public interface MemberSignInConfigConvert { + + MemberSignInConfigConvert INSTANCE = Mappers.getMapper(MemberSignInConfigConvert.class); + + MemberSignInConfigDO convert(MemberSignInConfigCreateReqVO bean); + + MemberSignInConfigDO convert(MemberSignInConfigUpdateReqVO bean); + + MemberSignInConfigRespVO convert(MemberSignInConfigDO bean); + + List convertList(List list); + + List convertList02(List list); + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/convert/signin/MemberSignInRecordConvert.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/convert/signin/MemberSignInRecordConvert.java new file mode 100644 index 0000000..2cce92f --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/convert/signin/MemberSignInRecordConvert.java @@ -0,0 +1,74 @@ +package cn.aagro.pp.module.member.convert.signin; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjUtil; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.common.util.collection.MapUtils; +import cn.aagro.pp.framework.common.util.date.DateUtils; +import cn.aagro.pp.module.member.controller.admin.signin.vo.record.MemberSignInRecordRespVO; +import cn.aagro.pp.module.member.controller.app.signin.vo.record.AppMemberSignInRecordRespVO; +import cn.aagro.pp.module.member.dal.dataobject.signin.MemberSignInConfigDO; +import cn.aagro.pp.module.member.dal.dataobject.signin.MemberSignInRecordDO; +import cn.aagro.pp.module.member.dal.dataobject.user.MemberUserDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.Comparator; +import java.util.List; +import java.util.Map; + +import static cn.aagro.pp.framework.common.util.collection.CollectionUtils.convertMap; + +/** + * 签到记录 Convert + * + * @author 芋道源码 + */ +@Mapper +public interface MemberSignInRecordConvert { + + MemberSignInRecordConvert INSTANCE = Mappers.getMapper(MemberSignInRecordConvert.class); + + default PageResult convertPage(PageResult pageResult, List users) { + PageResult voPageResult = convertPage(pageResult); + // user 拼接 + Map userMap = convertMap(users, MemberUserDO::getId); + voPageResult.getList().forEach(record -> MapUtils.findAndThen(userMap, record.getUserId(), + memberUserRespDTO -> record.setNickname(memberUserRespDTO.getNickname()))); + return voPageResult; + } + + PageResult convertPage(PageResult pageResult); + + PageResult convertPage02(PageResult pageResult); + + AppMemberSignInRecordRespVO coverRecordToAppRecordVo(MemberSignInRecordDO memberSignInRecordDO); + + default MemberSignInRecordDO convert(Long userId, MemberSignInRecordDO lastRecord, List configs) { + // 1. 计算是第几天签到 + configs.sort(Comparator.comparing(MemberSignInConfigDO::getDay)); + MemberSignInConfigDO lastConfig = CollUtil.getLast(configs); // 最大签到天数配置 + // 1.2. 计算今天是第几天签到 (只有连续签到才加否则重置为 1) + int day = 1; + if (lastRecord != null && DateUtils.isYesterday(lastRecord.getCreateTime())) { + day = lastRecord.getDay() + 1; + } + // 1.3 判断是否超出了最大签到配置 + if (day > lastConfig.getDay()) { + day = 1; // 超过最大配置的天数,重置到第一天。(也就是说开启下一轮签到) + } + + // 2.1 初始化签到信息 + MemberSignInRecordDO record = new MemberSignInRecordDO().setUserId(userId) + .setDay(day).setPoint(0).setExperience(0); + // 2.2 获取签到对应的积分 + MemberSignInConfigDO config = CollUtil.findOne(configs, item -> ObjUtil.equal(item.getDay(), record.getDay())); + if (config == null) { + return record; + } + record.setPoint(config.getPoint()); + record.setExperience(config.getExperience()); + return record; + } + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/convert/tag/MemberTagConvert.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/convert/tag/MemberTagConvert.java new file mode 100644 index 0000000..cf4b556 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/convert/tag/MemberTagConvert.java @@ -0,0 +1,33 @@ +package cn.aagro.pp.module.member.convert.tag; + +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.member.controller.admin.tag.vo.MemberTagCreateReqVO; +import cn.aagro.pp.module.member.controller.admin.tag.vo.MemberTagRespVO; +import cn.aagro.pp.module.member.controller.admin.tag.vo.MemberTagUpdateReqVO; +import cn.aagro.pp.module.member.dal.dataobject.tag.MemberTagDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * 会员标签 Convert + * + * @author 芋道源码 + */ +@Mapper +public interface MemberTagConvert { + + MemberTagConvert INSTANCE = Mappers.getMapper(MemberTagConvert.class); + + MemberTagDO convert(MemberTagCreateReqVO bean); + + MemberTagDO convert(MemberTagUpdateReqVO bean); + + MemberTagRespVO convert(MemberTagDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/convert/user/MemberUserConvert.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/convert/user/MemberUserConvert.java new file mode 100644 index 0000000..c6afb7a --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/convert/user/MemberUserConvert.java @@ -0,0 +1,68 @@ +package cn.aagro.pp.module.member.convert.user; + +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.member.api.user.dto.MemberUserRespDTO; +import cn.aagro.pp.module.member.controller.admin.user.vo.MemberUserRespVO; +import cn.aagro.pp.module.member.controller.admin.user.vo.MemberUserUpdateReqVO; +import cn.aagro.pp.module.member.controller.app.user.vo.AppMemberUserInfoRespVO; +import cn.aagro.pp.module.member.convert.address.AddressConvert; +import cn.aagro.pp.module.member.dal.dataobject.group.MemberGroupDO; +import cn.aagro.pp.module.member.dal.dataobject.level.MemberLevelDO; +import cn.aagro.pp.module.member.dal.dataobject.tag.MemberTagDO; +import cn.aagro.pp.module.member.dal.dataobject.user.MemberUserDO; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Mappings; +import org.mapstruct.factory.Mappers; + +import java.util.List; +import java.util.Map; + +import static cn.aagro.pp.framework.common.util.collection.CollectionUtils.convertList; +import static cn.aagro.pp.framework.common.util.collection.CollectionUtils.convertMap; + +@Mapper(uses = {AddressConvert.class}) +public interface MemberUserConvert { + + MemberUserConvert INSTANCE = Mappers.getMapper(MemberUserConvert.class); + + AppMemberUserInfoRespVO convert(MemberUserDO bean); + + + @Mappings({ + @Mapping(source = "level", target = "level"), + @Mapping(source = "bean.id", target = "id"), + @Mapping(source = "bean.experience", target = "experience") + }) + AppMemberUserInfoRespVO convert(MemberUserDO bean, MemberLevelDO level); + + MemberUserRespDTO convert2(MemberUserDO bean); + + List convertList2(List list); + + MemberUserDO convert(MemberUserUpdateReqVO bean); + + PageResult convertPage(PageResult page); + + @Mapping(source = "areaId", target = "areaName", qualifiedByName = "convertAreaIdToAreaName") + MemberUserRespVO convert03(MemberUserDO bean); + + default PageResult convertPage(PageResult pageResult, + List tags, + List levels, + List groups) { + PageResult result = convertPage(pageResult); + // 处理关联数据 + Map tagMap = convertMap(tags, MemberTagDO::getId, MemberTagDO::getName); + Map levelMap = convertMap(levels, MemberLevelDO::getId, MemberLevelDO::getName); + Map groupMap = convertMap(groups, MemberGroupDO::getId, MemberGroupDO::getName); + // 填充关联数据 + result.getList().forEach(user -> { + user.setTagNames(convertList(user.getTagIds(), tagMap::get)); + user.setLevelName(levelMap.get(user.getLevelId())); + user.setGroupName(groupMap.get(user.getGroupId())); + }); + return result; + } + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/convert/《芋道 Spring Boot 对象转换 MapStruct 入门》.md b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/convert/《芋道 Spring Boot 对象转换 MapStruct 入门》.md new file mode 100644 index 0000000..a209217 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/convert/《芋道 Spring Boot 对象转换 MapStruct 入门》.md @@ -0,0 +1 @@ + diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/dataobject/address/MemberAddressDO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/dataobject/address/MemberAddressDO.java new file mode 100644 index 0000000..ed7ac0c --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/dataobject/address/MemberAddressDO.java @@ -0,0 +1,56 @@ +package cn.aagro.pp.module.member.dal.dataobject.address; + +import cn.aagro.pp.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * 用户收件地址 DO + * + * @author 芋道源码 + */ +@TableName("member_address") +@KeySequence("member_address_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MemberAddressDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 用户编号 + */ + private Long userId; + /** + * 收件人名称 + */ + private String name; + /** + * 手机号 + */ + private String mobile; + /** + * 地区编号 + */ + private Long areaId; + /** + * 收件详细地址 + */ + private String detailAddress; + /** + * 是否默认 + * + * true - 默认收件地址 + */ + private Boolean defaultStatus; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/dataobject/config/MemberConfigDO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/dataobject/config/MemberConfigDO.java new file mode 100644 index 0000000..43e0867 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/dataobject/config/MemberConfigDO.java @@ -0,0 +1,48 @@ +package cn.aagro.pp.module.member.dal.dataobject.config; + +import cn.aagro.pp.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * 会员配置 DO + * + * @author QingX + */ +@TableName(value = "member_config", autoResultMap = true) +@KeySequence("member_config_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MemberConfigDO extends BaseDO { + + /** + * 自增主键 + */ + @TableId + private Long id; + /** + * 积分抵扣开关 + */ + private Boolean pointTradeDeductEnable; + /** + * 积分抵扣,单位:分 + * + * 1 积分抵扣多少分 + */ + private Integer pointTradeDeductUnitPrice; + /** + * 积分抵扣最大值 + */ + private Integer pointTradeDeductMaxPrice; + /** + * 1 元赠送多少分 + */ + private Integer pointTradeGivePoint; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/dataobject/group/MemberGroupDO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/dataobject/group/MemberGroupDO.java new file mode 100644 index 0000000..3604dd0 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/dataobject/group/MemberGroupDO.java @@ -0,0 +1,45 @@ +package cn.aagro.pp.module.member.dal.dataobject.group; + +import cn.aagro.pp.framework.common.enums.CommonStatusEnum; +import cn.aagro.pp.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * 用户分组 DO + * + * @author owen + */ +@TableName("member_group") +@KeySequence("member_group_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MemberGroupDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 名称 + */ + private String name; + /** + * 备注 + */ + private String remark; + /** + * 状态 + *

+ * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/dataobject/level/MemberExperienceRecordDO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/dataobject/level/MemberExperienceRecordDO.java new file mode 100644 index 0000000..d8b667e --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/dataobject/level/MemberExperienceRecordDO.java @@ -0,0 +1,64 @@ +package cn.aagro.pp.module.member.dal.dataobject.level; + +import cn.aagro.pp.framework.mybatis.core.dataobject.BaseDO; +import cn.aagro.pp.module.member.dal.dataobject.user.MemberUserDO; +import cn.aagro.pp.module.member.enums.MemberExperienceBizTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * 会员经验记录 DO + * + * @author owen + */ +@TableName("member_experience_record") +@KeySequence("member_experience_record_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MemberExperienceRecordDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 用户编号 + * + * 关联 {@link MemberUserDO#getId()} 字段 + */ + private Long userId; + /** + * 业务类型 + *

+ * 枚举 {@link MemberExperienceBizTypeEnum} + */ + private Integer bizType; + /** + * 业务编号 + */ + private String bizId; + /** + * 标题 + */ + private String title; + /** + * 描述 + */ + private String description; + /** + * 经验 + */ + private Integer experience; + /** + * 变更后的经验 + */ + private Integer totalExperience; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/dataobject/level/MemberLevelDO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/dataobject/level/MemberLevelDO.java new file mode 100644 index 0000000..579268e --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/dataobject/level/MemberLevelDO.java @@ -0,0 +1,64 @@ +package cn.aagro.pp.module.member.dal.dataobject.level; + +import cn.aagro.pp.framework.common.enums.CommonStatusEnum; +import cn.aagro.pp.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * 会员等级 DO + * + * 配置每个等级需要的积分 + * + * @author owen + */ +@TableName("member_level") +@KeySequence("member_level_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MemberLevelDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 等级名称 + */ + private String name; + /** + * 等级 + */ + private Integer level; + /** + * 升级经验 + */ + private Integer experience; + /** + * 享受折扣 + */ + private Integer discountPercent; + + /** + * 等级图标 + */ + private String icon; + /** + * 等级背景图 + */ + private String backgroundUrl; + /** + * 状态 + *

+ * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/dataobject/level/MemberLevelRecordDO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/dataobject/level/MemberLevelRecordDO.java new file mode 100644 index 0000000..d3660e8 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/dataobject/level/MemberLevelRecordDO.java @@ -0,0 +1,71 @@ +package cn.aagro.pp.module.member.dal.dataobject.level; + +import cn.aagro.pp.framework.mybatis.core.dataobject.BaseDO; +import cn.aagro.pp.module.member.dal.dataobject.user.MemberUserDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * 会员等级记录 DO + * + * 用户每次等级发生变更时,记录一条日志 + * + * @author owen + */ +@TableName("member_level_record") +@KeySequence("member_level_record_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MemberLevelRecordDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 用户编号 + * + * 关联 {@link MemberUserDO#getId()} 字段 + */ + private Long userId; + /** + * 等级编号 + * + * 关联 {@link MemberLevelDO#getId()} 字段 + */ + private Long levelId; + /** + * 会员等级 + * + * 冗余 {@link MemberLevelDO#getLevel()} 字段 + */ + private Integer level; + /** + * 享受折扣 + */ + private Integer discountPercent; + /** + * 升级经验 + */ + private Integer experience; + /** + * 会员此时的经验 + */ + private Integer userExperience; + /** + * 备注 + */ + private String remark; + /** + * 描述 + */ + private String description; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/dataobject/point/MemberPointRecordDO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/dataobject/point/MemberPointRecordDO.java new file mode 100644 index 0000000..642aef0 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/dataobject/point/MemberPointRecordDO.java @@ -0,0 +1,69 @@ +package cn.aagro.pp.module.member.dal.dataobject.point; + +import cn.aagro.pp.framework.mybatis.core.dataobject.BaseDO; +import cn.aagro.pp.module.member.enums.point.MemberPointBizTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * 用户积分记录 DO + * + * @author QingX + */ +@TableName("member_point_record") +@KeySequence("member_point_record_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MemberPointRecordDO extends BaseDO { + + /** + * 自增主键 + */ + @TableId + private Long id; + /** + * 用户编号 + * + * 对应 MemberUserDO 的 id 属性 + */ + private Long userId; + + /** + * 业务编码 + */ + private String bizId; + /** + * 业务类型 + * + * 枚举 {@link MemberPointBizTypeEnum} + */ + private Integer bizType; + + /** + * 积分标题 + */ + private String title; + /** + * 积分描述 + */ + private String description; + + /** + * 变动积分 + * + * 1、正数表示获得积分 + * 2、负数表示消耗积分 + */ + private Integer point; + /** + * 变动后的积分 + */ + private Integer totalPoint; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/dataobject/signin/MemberSignInConfigDO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/dataobject/signin/MemberSignInConfigDO.java new file mode 100644 index 0000000..c85b0d7 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/dataobject/signin/MemberSignInConfigDO.java @@ -0,0 +1,50 @@ +package cn.aagro.pp.module.member.dal.dataobject.signin; + +import cn.aagro.pp.framework.common.enums.CommonStatusEnum; +import cn.aagro.pp.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * 签到规则 DO + * + * @author QingX + */ +@TableName("member_sign_in_config") +@KeySequence("member_sign_in_config_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MemberSignInConfigDO extends BaseDO { + + /** + * 规则自增主键 + */ + @TableId + private Long id; + /** + * 签到第 x 天 + */ + private Integer day; + /** + * 奖励积分 + */ + private Integer point; + /** + * 奖励经验 + */ + private Integer experience; + + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/dataobject/signin/MemberSignInRecordDO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/dataobject/signin/MemberSignInRecordDO.java new file mode 100644 index 0000000..5eab082 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/dataobject/signin/MemberSignInRecordDO.java @@ -0,0 +1,46 @@ +package cn.aagro.pp.module.member.dal.dataobject.signin; + +import cn.aagro.pp.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * 签到记录 DO + * + * @author 芋道源码 + */ +@TableName("member_sign_in_record") +@KeySequence("member_sign_in_record_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MemberSignInRecordDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 签到用户 + */ + private Long userId; + /** + * 第几天签到 + */ + private Integer day; + /** + * 签到的积分 + */ + private Integer point; + /** + * 签到的经验 + */ + private Integer experience; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/dataobject/tag/MemberTagDO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/dataobject/tag/MemberTagDO.java new file mode 100644 index 0000000..a4bdb9c --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/dataobject/tag/MemberTagDO.java @@ -0,0 +1,34 @@ +package cn.aagro.pp.module.member.dal.dataobject.tag; + +import cn.aagro.pp.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * 会员标签 DO + * + * @author 芋道源码 + */ +@TableName("member_tag") +@KeySequence("member_tag_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MemberTagDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 标签名称 + */ + private String name; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/dataobject/user/MemberUserDO.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/dataobject/user/MemberUserDO.java new file mode 100644 index 0000000..64adf4d --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/dataobject/user/MemberUserDO.java @@ -0,0 +1,145 @@ +package cn.aagro.pp.module.member.dal.dataobject.user; + +import cn.aagro.pp.framework.common.enums.CommonStatusEnum; +import cn.aagro.pp.framework.common.enums.TerminalEnum; +import cn.aagro.pp.framework.ip.core.Area; +import cn.aagro.pp.framework.mybatis.core.type.LongListTypeHandler; +import cn.aagro.pp.framework.tenant.core.db.TenantBaseDO; +import cn.aagro.pp.module.member.dal.dataobject.group.MemberGroupDO; +import cn.aagro.pp.module.member.dal.dataobject.level.MemberLevelDO; +import cn.aagro.pp.module.system.enums.common.SexEnum; +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 org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 会员用户 DO + * + * uk_mobile 索引:基于 {@link #mobile} 字段 + * + * @author 芋道源码 + */ +@TableName(value = "member_user", autoResultMap = true) +@KeySequence("member_user_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MemberUserDO extends TenantBaseDO { + + // ========== 账号信息 ========== + + /** + * 用户ID + */ + @TableId + private Long id; + /** + * 手机 + */ + private String mobile; + /** + * 加密后的密码 + * + * 因为目前使用 {@link BCryptPasswordEncoder} 加密器,所以无需自己处理 salt 盐 + */ + private String password; + /** + * 帐号状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 注册 IP + */ + private String registerIp; + /** + * 注册终端 + * 枚举 {@link TerminalEnum} + */ + private Integer registerTerminal; + /** + * 最后登录IP + */ + private String loginIp; + /** + * 最后登录时间 + */ + private LocalDateTime loginDate; + + // ========== 基础信息 ========== + + /** + * 用户昵称 + */ + private String nickname; + /** + * 用户头像 + */ + private String avatar; + + /** + * 真实名字 + */ + private String name; + /** + * 性别 + * + * 枚举 {@link SexEnum} + */ + private Integer sex; + /** + * 出生日期 + */ + private LocalDateTime birthday; + /** + * 所在地 + * + * 关联 {@link Area#getId()} 字段 + */ + private Integer areaId; + /** + * 用户备注 + */ + private String mark; + + // ========== 其它信息 ========== + + /** + * 积分 + */ + private Integer point; + // TODO 疯狂:增加一个 totalPoint;个人信息接口要返回 + + /** + * 会员标签列表,以逗号分隔 + */ + @TableField(typeHandler = LongListTypeHandler.class) + private List tagIds; + + /** + * 会员级别编号 + * + * 关联 {@link MemberLevelDO#getId()} 字段 + */ + private Long levelId; + /** + * 会员经验 + */ + private Integer experience; + /** + * 用户分组编号 + * + * 关联 {@link MemberGroupDO#getId()} 字段 + */ + private Long groupId; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/mysql/address/MemberAddressMapper.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/mysql/address/MemberAddressMapper.java new file mode 100644 index 0000000..eea1df0 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/mysql/address/MemberAddressMapper.java @@ -0,0 +1,22 @@ +package cn.aagro.pp.module.member.dal.mysql.address; + +import cn.aagro.pp.framework.mybatis.core.mapper.BaseMapperX; +import cn.aagro.pp.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.aagro.pp.module.member.dal.dataobject.address.MemberAddressDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface MemberAddressMapper extends BaseMapperX { + + default MemberAddressDO selectByIdAndUserId(Long id, Long userId) { + return selectOne(MemberAddressDO::getId, id, MemberAddressDO::getUserId, userId); + } + + default List selectListByUserIdAndDefaulted(Long userId, Boolean defaulted) { + return selectList(new LambdaQueryWrapperX().eq(MemberAddressDO::getUserId, userId) + .eqIfPresent(MemberAddressDO::getDefaultStatus, defaulted)); + } + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/mysql/config/MemberConfigMapper.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/mysql/config/MemberConfigMapper.java new file mode 100644 index 0000000..ea297cc --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/mysql/config/MemberConfigMapper.java @@ -0,0 +1,14 @@ +package cn.aagro.pp.module.member.dal.mysql.config; + +import cn.aagro.pp.framework.mybatis.core.mapper.BaseMapperX; +import cn.aagro.pp.module.member.dal.dataobject.config.MemberConfigDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * 积分设置 Mapper + * + * @author QingX + */ +@Mapper +public interface MemberConfigMapper extends BaseMapperX { +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/mysql/group/MemberGroupMapper.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/mysql/group/MemberGroupMapper.java new file mode 100644 index 0000000..30057e2 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/mysql/group/MemberGroupMapper.java @@ -0,0 +1,31 @@ +package cn.aagro.pp.module.member.dal.mysql.group; + +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.mybatis.core.mapper.BaseMapperX; +import cn.aagro.pp.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.aagro.pp.module.member.controller.admin.group.vo.MemberGroupPageReqVO; +import cn.aagro.pp.module.member.dal.dataobject.group.MemberGroupDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * 用户分组 Mapper + * + * @author owen + */ +@Mapper +public interface MemberGroupMapper extends BaseMapperX { + + default PageResult selectPage(MemberGroupPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(MemberGroupDO::getName, reqVO.getName()) + .eqIfPresent(MemberGroupDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(MemberGroupDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(MemberGroupDO::getId)); + } + + default List selectListByStatus(Integer status) { + return selectList(MemberGroupDO::getStatus, status); + } +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/mysql/level/MemberExperienceRecordMapper.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/mysql/level/MemberExperienceRecordMapper.java new file mode 100644 index 0000000..ab7eadc --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/mysql/level/MemberExperienceRecordMapper.java @@ -0,0 +1,35 @@ +package cn.aagro.pp.module.member.dal.mysql.level; + +import cn.aagro.pp.framework.common.pojo.PageParam; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.mybatis.core.mapper.BaseMapperX; +import cn.aagro.pp.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.aagro.pp.module.member.controller.admin.level.vo.experience.MemberExperienceRecordPageReqVO; +import cn.aagro.pp.module.member.dal.dataobject.level.MemberExperienceRecordDO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.apache.ibatis.annotations.Mapper; + +/** + * 会员经验记录 Mapper + * + * @author owen + */ +@Mapper +public interface MemberExperienceRecordMapper extends BaseMapperX { + + default PageResult selectPage(MemberExperienceRecordPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(MemberExperienceRecordDO::getUserId, reqVO.getUserId()) + .eqIfPresent(MemberExperienceRecordDO::getBizId, reqVO.getBizId()) + .eqIfPresent(MemberExperienceRecordDO::getBizType, reqVO.getBizType()) + .eqIfPresent(MemberExperienceRecordDO::getTitle, reqVO.getTitle()) + .betweenIfPresent(MemberExperienceRecordDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(MemberExperienceRecordDO::getId)); + } + + default PageResult selectPage(Long userId, PageParam pageParam) { + return selectPage(pageParam, new LambdaQueryWrapper() + .eq(MemberExperienceRecordDO::getUserId, userId) + .orderByDesc(MemberExperienceRecordDO::getId)); + } +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/mysql/level/MemberLevelMapper.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/mysql/level/MemberLevelMapper.java new file mode 100644 index 0000000..51258c2 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/mysql/level/MemberLevelMapper.java @@ -0,0 +1,33 @@ +package cn.aagro.pp.module.member.dal.mysql.level; + +import cn.aagro.pp.framework.mybatis.core.mapper.BaseMapperX; +import cn.aagro.pp.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.aagro.pp.module.member.controller.admin.level.vo.level.MemberLevelListReqVO; +import cn.aagro.pp.module.member.dal.dataobject.level.MemberLevelDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * 会员等级 Mapper + * + * @author owen + */ +@Mapper +public interface MemberLevelMapper extends BaseMapperX { + + default List selectList(MemberLevelListReqVO reqVO) { + return selectList(new LambdaQueryWrapperX() + .likeIfPresent(MemberLevelDO::getName, reqVO.getName()) + .eqIfPresent(MemberLevelDO::getStatus, reqVO.getStatus()) + .orderByAsc(MemberLevelDO::getLevel)); + } + + + default List selectListByStatus(Integer status) { + return selectList(new LambdaQueryWrapperX() + .eq(MemberLevelDO::getStatus, status) + .orderByAsc(MemberLevelDO::getLevel)); + } + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/mysql/level/MemberLevelRecordMapper.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/mysql/level/MemberLevelRecordMapper.java new file mode 100644 index 0000000..d330f22 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/mysql/level/MemberLevelRecordMapper.java @@ -0,0 +1,26 @@ +package cn.aagro.pp.module.member.dal.mysql.level; + +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.mybatis.core.mapper.BaseMapperX; +import cn.aagro.pp.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.aagro.pp.module.member.controller.admin.level.vo.record.MemberLevelRecordPageReqVO; +import cn.aagro.pp.module.member.dal.dataobject.level.MemberLevelRecordDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * 会员等级记录 Mapper + * + * @author owen + */ +@Mapper +public interface MemberLevelRecordMapper extends BaseMapperX { + + default PageResult selectPage(MemberLevelRecordPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(MemberLevelRecordDO::getUserId, reqVO.getUserId()) + .eqIfPresent(MemberLevelRecordDO::getLevelId, reqVO.getLevelId()) + .betweenIfPresent(MemberLevelRecordDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(MemberLevelRecordDO::getId)); + } + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/mysql/point/MemberPointRecordMapper.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/mysql/point/MemberPointRecordMapper.java new file mode 100644 index 0000000..2542a15 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/mysql/point/MemberPointRecordMapper.java @@ -0,0 +1,42 @@ +package cn.aagro.pp.module.member.dal.mysql.point; + +import cn.aagro.pp.framework.common.pojo.PageParam; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.mybatis.core.mapper.BaseMapperX; +import cn.aagro.pp.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.aagro.pp.module.member.controller.admin.point.vo.recrod.MemberPointRecordPageReqVO; +import cn.aagro.pp.module.member.controller.app.point.vo.AppMemberPointRecordPageReqVO; +import cn.aagro.pp.module.member.dal.dataobject.point.MemberPointRecordDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Set; + +/** + * 用户积分记录 Mapper + * + * @author QingX + */ +@Mapper +public interface MemberPointRecordMapper extends BaseMapperX { + + default PageResult selectPage(MemberPointRecordPageReqVO reqVO, Set userIds) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .inIfPresent(MemberPointRecordDO::getUserId, userIds) + .eqIfPresent(MemberPointRecordDO::getUserId, reqVO.getUserId()) + .eqIfPresent(MemberPointRecordDO::getBizType, reqVO.getBizType()) + .likeIfPresent(MemberPointRecordDO::getTitle, reqVO.getTitle()) + .orderByDesc(MemberPointRecordDO::getId)); + } + + default PageResult selectPage(Long userId, AppMemberPointRecordPageReqVO pageReqVO) { + return selectPage(pageReqVO, new LambdaQueryWrapperX() + .eq(MemberPointRecordDO::getUserId, userId) + .betweenIfPresent(MemberPointRecordDO::getCreateTime, pageReqVO.getCreateTime()) + .gt(Boolean.TRUE.equals(pageReqVO.getAddStatus()), + MemberPointRecordDO::getPoint, 0) + .lt(Boolean.FALSE.equals(pageReqVO.getAddStatus()), + MemberPointRecordDO::getPoint, 0) + .orderByDesc(MemberPointRecordDO::getId)); + } + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/mysql/signin/MemberSignInConfigMapper.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/mysql/signin/MemberSignInConfigMapper.java new file mode 100644 index 0000000..36340a9 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/mysql/signin/MemberSignInConfigMapper.java @@ -0,0 +1,24 @@ +package cn.aagro.pp.module.member.dal.mysql.signin; + +import cn.aagro.pp.framework.mybatis.core.mapper.BaseMapperX; +import cn.aagro.pp.module.member.dal.dataobject.signin.MemberSignInConfigDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * 签到规则 Mapper + * + * @author QingX + */ +@Mapper +public interface MemberSignInConfigMapper extends BaseMapperX { + + default MemberSignInConfigDO selectByDay(Integer day) { + return selectOne(MemberSignInConfigDO::getDay, day); + } + + default List selectListByStatus(Integer status) { + return selectList(MemberSignInConfigDO::getStatus, status); + } +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/mysql/signin/MemberSignInRecordMapper.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/mysql/signin/MemberSignInRecordMapper.java new file mode 100644 index 0000000..a25f808 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/mysql/signin/MemberSignInRecordMapper.java @@ -0,0 +1,65 @@ +package cn.aagro.pp.module.member.dal.mysql.signin; + +import cn.aagro.pp.framework.common.pojo.PageParam; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.mybatis.core.mapper.BaseMapperX; +import cn.aagro.pp.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.aagro.pp.module.member.controller.admin.signin.vo.record.MemberSignInRecordPageReqVO; +import cn.aagro.pp.module.member.dal.dataobject.signin.MemberSignInRecordDO; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; +import java.util.Set; + +/** + * 签到记录 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface MemberSignInRecordMapper extends BaseMapperX { + + default PageResult selectPage(MemberSignInRecordPageReqVO reqVO, Set userIds) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .inIfPresent(MemberSignInRecordDO::getUserId, userIds) + .eqIfPresent(MemberSignInRecordDO::getUserId, reqVO.getUserId()) + .eqIfPresent(MemberSignInRecordDO::getDay, reqVO.getDay()) + .betweenIfPresent(MemberSignInRecordDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(MemberSignInRecordDO::getId)); + } + + default PageResult selectPage(Long userId, PageParam pageParam) { + return selectPage(pageParam, new LambdaQueryWrapperX() + .eq(MemberSignInRecordDO::getUserId, userId) + .orderByDesc(MemberSignInRecordDO::getId)); + } + + /** + * 获取用户最近的签到记录信息,根据签到时间倒序 + * + * @param userId 用户编号 + * @return 签到记录列表 + */ + default MemberSignInRecordDO selectLastRecordByUserId(Long userId) { + return selectOne(new QueryWrapper() + .eq("user_id", userId) + .orderByDesc("create_time") + .last("limit 1")); + } + + default Long selectCountByUserId(Long userId) { + return selectCount(MemberSignInRecordDO::getUserId, userId); + } + + /** + * 获取用户的签到记录列表信息 + * + * @param userId 用户编号 + * @return 签到记录信息 + */ + default List selectListByUserId(Long userId) { + return selectList(MemberSignInRecordDO::getUserId, userId); + } + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/mysql/tag/MemberTagMapper.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/mysql/tag/MemberTagMapper.java new file mode 100644 index 0000000..d329f6a --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/mysql/tag/MemberTagMapper.java @@ -0,0 +1,28 @@ +package cn.aagro.pp.module.member.dal.mysql.tag; + +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.mybatis.core.mapper.BaseMapperX; +import cn.aagro.pp.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.aagro.pp.module.member.controller.admin.tag.vo.MemberTagPageReqVO; +import cn.aagro.pp.module.member.dal.dataobject.tag.MemberTagDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * 会员标签 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface MemberTagMapper extends BaseMapperX { + + default PageResult selectPage(MemberTagPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(MemberTagDO::getName, reqVO.getName()) + .betweenIfPresent(MemberTagDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(MemberTagDO::getId)); + } + + default MemberTagDO selelctByName(String name) { + return selectOne(MemberTagDO::getName, name); + } +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/mysql/user/MemberUserMapper.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/mysql/user/MemberUserMapper.java new file mode 100644 index 0000000..64fc30b --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/mysql/user/MemberUserMapper.java @@ -0,0 +1,96 @@ +package cn.aagro.pp.module.member.dal.mysql.user; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.mybatis.core.mapper.BaseMapperX; +import cn.aagro.pp.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.aagro.pp.module.member.controller.admin.user.vo.MemberUserPageReqVO; +import cn.aagro.pp.module.member.dal.dataobject.user.MemberUserDO; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * 会员 User Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface MemberUserMapper extends BaseMapperX { + + default MemberUserDO selectByMobile(String mobile) { + return selectOne(MemberUserDO::getMobile, mobile); + } + + default List selectListByNicknameLike(String nickname) { + return selectList(new LambdaQueryWrapperX() + .likeIfPresent(MemberUserDO::getNickname, nickname)); + } + + default PageResult selectPage(MemberUserPageReqVO reqVO) { + // 处理 tagIds 过滤条件 + String tagIdSql = ""; + if (CollUtil.isNotEmpty(reqVO.getTagIds())) { + tagIdSql = reqVO.getTagIds().stream() + .map(tagId -> "FIND_IN_SET(" + tagId + ", tag_ids)") + .collect(Collectors.joining(" OR ")); + } + // 分页查询 + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(MemberUserDO::getMobile, reqVO.getMobile()) + .betweenIfPresent(MemberUserDO::getLoginDate, reqVO.getLoginDate()) + .likeIfPresent(MemberUserDO::getNickname, reqVO.getNickname()) + .betweenIfPresent(MemberUserDO::getCreateTime, reqVO.getCreateTime()) + .eqIfPresent(MemberUserDO::getLevelId, reqVO.getLevelId()) + .eqIfPresent(MemberUserDO::getGroupId, reqVO.getGroupId()) + .apply(StrUtil.isNotEmpty(tagIdSql), tagIdSql) + .orderByDesc(MemberUserDO::getId)); + } + + default Long selectCountByGroupId(Long groupId) { + return selectCount(MemberUserDO::getGroupId, groupId); + } + + default Long selectCountByLevelId(Long levelId) { + return selectCount(MemberUserDO::getLevelId, levelId); + } + + default Long selectCountByTagId(Long tagId) { + return selectCount(new LambdaQueryWrapperX() + .apply("FIND_IN_SET({0}, tag_ids)", tagId)); + } + + /** + * 更新用户积分(增加) + * + * @param id 用户编号 + * @param incrCount 增加积分(正数) + */ + default void updatePointIncr(Long id, Integer incrCount) { + Assert.isTrue(incrCount > 0); + LambdaUpdateWrapper lambdaUpdateWrapper = new LambdaUpdateWrapper() + .setSql(" point = point + " + incrCount) + .eq(MemberUserDO::getId, id); + update(null, lambdaUpdateWrapper); + } + + /** + * 更新用户积分(减少) + * + * @param id 用户编号 + * @param incrCount 增加积分(负数) + * @return 更新行数 + */ + default int updatePointDecr(Long id, Integer incrCount) { + Assert.isTrue(incrCount < 0); + LambdaUpdateWrapper lambdaUpdateWrapper = new LambdaUpdateWrapper() + .setSql(" point = point + " + incrCount) // 负数,所以使用 + 号 + .eq(MemberUserDO::getId, id); + return update(null, lambdaUpdateWrapper); + } + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/package-info.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/package-info.java new file mode 100644 index 0000000..594ecfd --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/package-info.java @@ -0,0 +1,9 @@ +/** + * DAL = Data Access Layer 数据访问层 + * 1. data object:数据对象 + * 2. redis:Redis 的 CRUD 操作 + * 3. mysql:MySQL 的 CRUD 操作 + * + * 其中,MySQL 的表以 member_ 作为前缀 + */ +package cn.aagro.pp.module.member.dal; diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/redis/package-info.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/redis/package-info.java new file mode 100644 index 0000000..5a322de --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/dal/redis/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位,后续有类后,可以删除,避免 package 无法提交到 Git 上 + */ +package cn.aagro.pp.module.member.dal.redis; diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/enums/DictTypeConstants.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/enums/DictTypeConstants.java new file mode 100644 index 0000000..d2569f2 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/enums/DictTypeConstants.java @@ -0,0 +1,15 @@ +package cn.aagro.pp.module.member.enums; + +/** + * Member 字典类型的枚举类 + * + * @author owen + */ +public interface DictTypeConstants { + + /** + * 会员经验记录 - 业务类型 + */ + String MEMBER_EXPERIENCE_BIZ_TYPE = "member_experience_biz_type"; + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/enums/ErrorCodeConstants.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/enums/ErrorCodeConstants.java new file mode 100644 index 0000000..1eb44d0 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/enums/ErrorCodeConstants.java @@ -0,0 +1,58 @@ +package cn.aagro.pp.module.member.enums; + +import cn.aagro.pp.framework.common.exception.ErrorCode; + +/** + * Member 错误码枚举类 + *

+ * member 系统,使用 1-004-000-000 段 + */ +public interface ErrorCodeConstants { + + // ========== 用户相关 1-004-001-000 ============ + ErrorCode USER_NOT_EXISTS = new ErrorCode(1_004_001_000, "用户不存在"); + ErrorCode USER_MOBILE_NOT_EXISTS = new ErrorCode(1_004_001_001, "手机号未注册用户"); + ErrorCode USER_MOBILE_USED = new ErrorCode(1_004_001_002, "修改手机失败,该手机号({})已经被使用"); + ErrorCode USER_POINT_NOT_ENOUGH = new ErrorCode(1_004_001_003, "用户积分余额不足"); + + // ========== AUTH 模块 1-004-003-000 ========== + ErrorCode AUTH_LOGIN_BAD_CREDENTIALS = new ErrorCode(1_004_003_000, "登录失败,账号密码不正确"); + ErrorCode AUTH_LOGIN_USER_DISABLED = new ErrorCode(1_004_003_001, "登录失败,账号被禁用"); + ErrorCode AUTH_SOCIAL_USER_NOT_FOUND = new ErrorCode(1_004_003_005, "登录失败,解析不到三方登录信息"); + ErrorCode AUTH_MOBILE_USED = new ErrorCode(1_004_003_007, "手机号已经被使用"); + + // ========== 用户收件地址 1-004-004-000 ========== + ErrorCode ADDRESS_NOT_EXISTS = new ErrorCode(1_004_004_000, "用户收件地址不存在"); + + //========== 用户标签 1-004-006-000 ========== + ErrorCode TAG_NOT_EXISTS = new ErrorCode(1_004_006_000, "用户标签不存在"); + ErrorCode TAG_NAME_EXISTS = new ErrorCode(1_004_006_001, "用户标签已经存在"); + ErrorCode TAG_HAS_USER = new ErrorCode(1_004_006_002, "用户标签下存在用户,无法删除"); + + //========== 积分配置 1-004-007-000 ========== + + //========== 积分记录 1-004-008-000 ========== + ErrorCode POINT_RECORD_BIZ_NOT_SUPPORT = new ErrorCode(1_004_008_000, "用户积分记录业务类型不支持"); + + //========== 签到配置 1-004-009-000 ========== + ErrorCode SIGN_IN_CONFIG_NOT_EXISTS = new ErrorCode(1_004_009_000, "签到天数规则不存在"); + ErrorCode SIGN_IN_CONFIG_EXISTS = new ErrorCode(1_004_009_001, "签到天数规则已存在"); + + //========== 签到配置 1-004-010-000 ========== + ErrorCode SIGN_IN_RECORD_TODAY_EXISTS = new ErrorCode(1_004_010_000, "今日已签到,请勿重复签到"); + + //========== 用户等级 1-004-011-000 ========== + ErrorCode LEVEL_NOT_EXISTS = new ErrorCode(1_004_011_000, "用户等级不存在"); + ErrorCode LEVEL_NAME_EXISTS = new ErrorCode(1_004_011_001, "用户等级名称[{}]已被使用"); + ErrorCode LEVEL_VALUE_EXISTS = new ErrorCode(1_004_011_002, "用户等级值[{}]已被[{}]使用"); + ErrorCode LEVEL_EXPERIENCE_MIN = new ErrorCode(1_004_011_003, "升级经验必须大于上一个等级[{}]设置的升级经验[{}]"); + ErrorCode LEVEL_EXPERIENCE_MAX = new ErrorCode(1_004_011_004, "升级经验必须小于下一个等级[{}]设置的升级经验[{}]"); + ErrorCode LEVEL_HAS_USER = new ErrorCode(1_004_011_005, "用户等级下存在用户,无法删除"); + + ErrorCode EXPERIENCE_BIZ_NOT_SUPPORT = new ErrorCode(1_004_011_201, "用户经验业务类型不支持"); + + //========== 用户分组 1-004-012-000 ========== + ErrorCode GROUP_NOT_EXISTS = new ErrorCode(1_004_012_000, "用户分组不存在"); + ErrorCode GROUP_HAS_USER = new ErrorCode(1_004_012_001, "用户分组下存在用户,无法删除"); + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/enums/MemberExperienceBizTypeEnum.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/enums/MemberExperienceBizTypeEnum.java new file mode 100644 index 0000000..13203e1 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/enums/MemberExperienceBizTypeEnum.java @@ -0,0 +1,51 @@ +package cn.aagro.pp.module.member.enums; + +import cn.hutool.core.util.EnumUtil; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Objects; + +/** + * 会员经验 - 业务类型 + * + * @author owen + */ +@Getter +@AllArgsConstructor +public enum MemberExperienceBizTypeEnum { + + /** + * 管理员调整、邀请新用户、下单、退单、签到、抽奖 + */ + ADMIN(0, "管理员调整", "管理员调整获得 {} 经验", true), + INVITE_REGISTER(1, "邀新奖励", "邀请好友获得 {} 经验", true), + SIGN_IN(4, "签到奖励", "签到获得 {} 经验", true), + LOTTERY(5, "抽奖奖励", "抽奖获得 {} 经验", true), + ORDER_GIVE(11, "下单奖励", "下单获得 {} 经验", true), + ORDER_GIVE_CANCEL(12, "下单奖励(整单取消)", "取消订单获得 {} 经验", false), // ORDER_GIVE 的取消 + ORDER_GIVE_CANCEL_ITEM(13, "下单奖励(单个退款)", "退款订单获得 {} 经验", false), // ORDER_GIVE 的取消 + ; + + /** + * 业务类型 + */ + private final int type; + /** + * 标题 + */ + private final String title; + /** + * 描述 + */ + private final String description; + /** + * 是否为扣减积分 + */ + private final boolean add; + + public static MemberExperienceBizTypeEnum getByType(Integer type) { + return EnumUtil.getBy(MemberExperienceBizTypeEnum.class, + e -> Objects.equals(type, e.getType())); + } +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/enums/point/MemberPointBizTypeEnum.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/enums/point/MemberPointBizTypeEnum.java new file mode 100644 index 0000000..e8da942 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/enums/point/MemberPointBizTypeEnum.java @@ -0,0 +1,58 @@ +package cn.aagro.pp.module.member.enums.point; + +import cn.hutool.core.util.EnumUtil; +import cn.aagro.pp.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Objects; + +/** + * 会员积分的业务类型枚举 + * + * @author 芋道源码 + */ +@AllArgsConstructor +@Getter +public enum MemberPointBizTypeEnum implements ArrayValuable { + + SIGN(1, "签到", "签到获得 {} 积分", true), + ADMIN(2, "管理员修改", "管理员修改 {} 积分", true), + + ORDER_USE(11, "订单积分抵扣", "下单使用 {} 积分", false), // 下单时,扣减积分 + ORDER_USE_CANCEL(12, "订单积分抵扣(整单取消)", "订单取消,退还 {} 积分", true), // ORDER_USE 的取消 + ORDER_USE_CANCEL_ITEM(13, "订单积分抵扣(单个退款)", "订单退款,退还 {} 积分", true), // ORDER_USE 的取消 + + ORDER_GIVE(21, "订单积分奖励", "下单获得 {} 积分", true), // 支付订单时,赠送积分 + ORDER_GIVE_CANCEL(22, "订单积分奖励(整单取消)", "订单取消,退还 {} 积分", false), // ORDER_GIVE 的取消 + ORDER_GIVE_CANCEL_ITEM(23, "订单积分奖励(单个退款)", "订单退款,扣除赠送的 {} 积分", false) // ORDER_GIVE 的取消 + ; + + /** + * 类型 + */ + private final Integer type; + /** + * 名字 + */ + private final String name; + /** + * 描述 + */ + private final String description; + /** + * 是否为扣减积分 + */ + private final boolean add; + + @Override + public Integer[] array() { + return new Integer[0]; + } + + public static MemberPointBizTypeEnum getByType(Integer type) { + return EnumUtil.getBy(MemberPointBizTypeEnum.class, + e -> Objects.equals(type, e.getType())); + } + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/framework/package-info.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/framework/package-info.java new file mode 100644 index 0000000..6994007 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/framework/package-info.java @@ -0,0 +1,6 @@ +/** + * 属于 member 模块的 framework 封装 + * + * @author 芋道源码 + */ +package cn.aagro.pp.module.member.framework; diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/framework/web/config/MemberWebConfiguration.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/framework/web/config/MemberWebConfiguration.java new file mode 100644 index 0000000..4f721c4 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/framework/web/config/MemberWebConfiguration.java @@ -0,0 +1,24 @@ +package cn.aagro.pp.module.member.framework.web.config; + +import cn.aagro.pp.framework.swagger.config.AagroSwaggerAutoConfiguration; +import org.springdoc.core.GroupedOpenApi; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * member 模块的 web 组件的 Configuration + * + * @author 芋道源码 + */ +@Configuration(proxyBeanMethods = false) +public class MemberWebConfiguration { + + /** + * member 模块的 API 分组 + */ + @Bean + public GroupedOpenApi memberGroupedOpenApi() { + return AagroSwaggerAutoConfiguration.buildGroupedOpenApi("member"); + } + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/framework/web/package-info.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/framework/web/package-info.java new file mode 100644 index 0000000..1ec42bf --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/framework/web/package-info.java @@ -0,0 +1,4 @@ +/** + * member 模块的 web 配置 + */ +package cn.aagro.pp.module.member.framework.web; diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/mq/consumer/package-info.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/mq/consumer/package-info.java new file mode 100644 index 0000000..6b5e39e --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/mq/consumer/package-info.java @@ -0,0 +1,4 @@ +/** + * 消息队列的消费者 + */ +package cn.aagro.pp.module.member.mq.consumer; diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/mq/message/package-info.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/mq/message/package-info.java new file mode 100644 index 0000000..9796c27 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/mq/message/package-info.java @@ -0,0 +1,4 @@ +/** + * 消息队列的消息 + */ +package cn.aagro.pp.module.member.mq.message; diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/mq/producer/package-info.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/mq/producer/package-info.java new file mode 100644 index 0000000..4dc9e50 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/mq/producer/package-info.java @@ -0,0 +1,4 @@ +/** + * 消息队列的生产者 + */ +package cn.aagro.pp.module.member.mq.producer; diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/mq/producer/user/MemberUserProducer.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/mq/producer/user/MemberUserProducer.java new file mode 100644 index 0000000..1fc54a0 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/mq/producer/user/MemberUserProducer.java @@ -0,0 +1,31 @@ +package cn.aagro.pp.module.member.mq.producer.user; + +import cn.aagro.pp.module.member.api.message.user.MemberUserCreateMessage; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * 会员用户 Producer + * + * @author owen + */ +@Slf4j +@Component +public class MemberUserProducer { + + @Resource + private ApplicationContext applicationContext; + + /** + * 发送 {@link MemberUserCreateMessage} 消息 + * + * @param userId 用户编号 + */ + public void sendUserCreateMessage(Long userId) { + applicationContext.publishEvent(new MemberUserCreateMessage().setUserId(userId)); + } + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/package-info.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/package-info.java new file mode 100644 index 0000000..9146c96 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/package-info.java @@ -0,0 +1,8 @@ +/** + * member 模块,我们放会员业务。 + * 例如说:会员中心等等 + * + * 1. Controller URL:以 /member/ 开头,避免和其它 Module 冲突 + * 2. DataObject 表名:以 member_ 开头,方便在数据库中区分 + */ +package cn.aagro.pp.module.member; diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/address/AddressService.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/address/AddressService.java new file mode 100644 index 0000000..a081d4f --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/address/AddressService.java @@ -0,0 +1,67 @@ +package cn.aagro.pp.module.member.service.address; + +import cn.aagro.pp.module.member.controller.app.address.vo.AppAddressCreateReqVO; +import cn.aagro.pp.module.member.controller.app.address.vo.AppAddressUpdateReqVO; +import cn.aagro.pp.module.member.dal.dataobject.address.MemberAddressDO; + +import javax.validation.Valid; +import java.util.List; + +/** + * 用户收件地址 Service 接口 + * + * @author 芋道源码 + */ +public interface AddressService { + + /** + * 创建用户收件地址 + * + * + * @param userId 用户编号 + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createAddress(Long userId, @Valid AppAddressCreateReqVO createReqVO); + + /** + * 更新用户收件地址 + * + * @param userId 用户编号 + * @param updateReqVO 更新信息 + */ + void updateAddress(Long userId, @Valid AppAddressUpdateReqVO updateReqVO); + + /** + * 删除用户收件地址 + * + * @param userId 用户编号 + * @param id 编号 + */ + void deleteAddress(Long userId, Long id); + + /** + * 获得用户收件地址 + * + * @param id 编号 + * @return 用户收件地址 + */ + MemberAddressDO getAddress(Long userId, Long id); + + /** + * 获得用户收件地址列表 + * + * @param userId 用户编号 + * @return 用户收件地址列表 + */ + List getAddressList(Long userId); + + /** + * 获得用户默认的收件地址 + * + * @param userId 用户编号 + * @return 用户收件地址 + */ + MemberAddressDO getDefaultUserAddress(Long userId); + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/address/AddressServiceImpl.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/address/AddressServiceImpl.java new file mode 100644 index 0000000..70bf861 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/address/AddressServiceImpl.java @@ -0,0 +1,97 @@ +package cn.aagro.pp.module.member.service.address; + +import cn.hutool.core.collection.CollUtil; +import cn.aagro.pp.module.member.controller.app.address.vo.AppAddressCreateReqVO; +import cn.aagro.pp.module.member.controller.app.address.vo.AppAddressUpdateReqVO; +import cn.aagro.pp.module.member.convert.address.AddressConvert; +import cn.aagro.pp.module.member.dal.dataobject.address.MemberAddressDO; +import cn.aagro.pp.module.member.dal.mysql.address.MemberAddressMapper; +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 static cn.aagro.pp.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.aagro.pp.module.member.enums.ErrorCodeConstants.ADDRESS_NOT_EXISTS; + +/** + * 用户收件地址 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class AddressServiceImpl implements AddressService { + + @Resource + private MemberAddressMapper memberAddressMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createAddress(Long userId, AppAddressCreateReqVO createReqVO) { + // 如果添加的是默认收件地址,则将原默认地址修改为非默认 + if (Boolean.TRUE.equals(createReqVO.getDefaultStatus())) { + List addresses = memberAddressMapper.selectListByUserIdAndDefaulted(userId, true); + addresses.forEach(address -> memberAddressMapper.updateById(new MemberAddressDO().setId(address.getId()).setDefaultStatus(false))); + } + + // 插入 + MemberAddressDO address = AddressConvert.INSTANCE.convert(createReqVO); + address.setUserId(userId); + memberAddressMapper.insert(address); + // 返回 + return address.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateAddress(Long userId, AppAddressUpdateReqVO updateReqVO) { + // 校验存在,校验是否能够操作 + validAddressExists(userId, updateReqVO.getId()); + + // 如果修改的是默认收件地址,则将原默认地址修改为非默认 + if (Boolean.TRUE.equals(updateReqVO.getDefaultStatus())) { + List addresses = memberAddressMapper.selectListByUserIdAndDefaulted(userId, true); + addresses.stream().filter(u -> !u.getId().equals(updateReqVO.getId())) // 排除自己 + .forEach(address -> memberAddressMapper.updateById(new MemberAddressDO().setId(address.getId()).setDefaultStatus(false))); + } + + // 更新 + MemberAddressDO updateObj = AddressConvert.INSTANCE.convert(updateReqVO); + memberAddressMapper.updateById(updateObj); + } + + @Override + public void deleteAddress(Long userId, Long id) { + // 校验存在,校验是否能够操作 + validAddressExists(userId, id); + // 删除 + memberAddressMapper.deleteById(id); + } + + private void validAddressExists(Long userId, Long id) { + MemberAddressDO addressDO = getAddress(userId, id); + if (addressDO == null) { + throw exception(ADDRESS_NOT_EXISTS); + } + } + + @Override + public MemberAddressDO getAddress(Long userId, Long id) { + return memberAddressMapper.selectByIdAndUserId(id, userId); + } + + @Override + public List getAddressList(Long userId) { + return memberAddressMapper.selectListByUserIdAndDefaulted(userId, null); + } + + @Override + public MemberAddressDO getDefaultUserAddress(Long userId) { + List addresses = memberAddressMapper.selectListByUserIdAndDefaulted(userId, true); + return CollUtil.getFirst(addresses); + } + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/auth/MemberAuthService.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/auth/MemberAuthService.java new file mode 100644 index 0000000..1ad2542 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/auth/MemberAuthService.java @@ -0,0 +1,88 @@ +package cn.aagro.pp.module.member.service.auth; + +import cn.aagro.pp.module.member.controller.app.auth.vo.*; + +import javax.validation.Valid; + +/** + * 会员的认证 Service 接口 + * + * 提供用户的账号密码登录、token 的校验等认证相关的功能 + * + * @author 芋道源码 + */ +public interface MemberAuthService { + + /** + * 手机 + 密码登录 + * + * @param reqVO 登录信息 + * @return 登录结果 + */ + AppAuthLoginRespVO login(@Valid AppAuthLoginReqVO reqVO); + + /** + * 基于 token 退出登录 + * + * @param token token + */ + void logout(String token); + + /** + * 手机 + 验证码登陆 + * + * @param reqVO 登陆信息 + * @return 登录结果 + */ + AppAuthLoginRespVO smsLogin(@Valid AppAuthSmsLoginReqVO reqVO); + + /** + * 社交登录,使用 code 授权码 + * + * @param reqVO 登录信息 + * @return 登录结果 + */ + AppAuthLoginRespVO socialLogin(@Valid AppAuthSocialLoginReqVO reqVO); + + /** + * 微信小程序的一键登录 + * + * @param reqVO 登录信息 + * @return 登录结果 + */ + AppAuthLoginRespVO weixinMiniAppLogin(AppAuthWeixinMiniAppLoginReqVO reqVO); + + /** + * 获得社交认证 URL + * + * @param type 社交平台类型 + * @param redirectUri 跳转地址 + * @return 认证 URL + */ + String getSocialAuthorizeUrl(Integer type, String redirectUri); + + /** + * 给用户发送短信验证码 + * + * @param userId 用户编号 + * @param reqVO 发送信息 + */ + void sendSmsCode(Long userId, AppAuthSmsSendReqVO reqVO); + + /** + * 校验短信验证码是否正确 + * + * @param userId 用户编号 + * @param reqVO 校验信息 + */ + void validateSmsCode(Long userId, AppAuthSmsValidateReqVO reqVO); + + /** + * 刷新访问令牌 + * + * @param refreshToken 刷新令牌 + * @return 登录结果 + */ + AppAuthLoginRespVO refreshToken(String refreshToken); + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/auth/MemberAuthServiceImpl.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/auth/MemberAuthServiceImpl.java new file mode 100644 index 0000000..b6840e2 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/auth/MemberAuthServiceImpl.java @@ -0,0 +1,285 @@ +package cn.aagro.pp.module.member.service.auth; + +import cn.hutool.core.lang.Assert; +import cn.aagro.pp.framework.common.enums.CommonStatusEnum; +import cn.aagro.pp.framework.common.enums.TerminalEnum; +import cn.aagro.pp.framework.common.enums.UserTypeEnum; +import cn.aagro.pp.framework.common.util.monitor.TracerUtils; +import cn.aagro.pp.framework.common.util.servlet.ServletUtils; +import cn.aagro.pp.module.member.controller.app.auth.vo.*; +import cn.aagro.pp.module.member.convert.auth.AuthConvert; +import cn.aagro.pp.module.member.dal.dataobject.user.MemberUserDO; +import cn.aagro.pp.module.member.service.user.MemberUserService; +import cn.aagro.pp.module.system.api.logger.LoginLogApi; +import cn.aagro.pp.module.system.api.logger.dto.LoginLogCreateReqDTO; +import cn.aagro.pp.framework.common.biz.system.oauth2.OAuth2TokenCommonApi; +import cn.aagro.pp.framework.common.biz.system.oauth2.dto.OAuth2AccessTokenCreateReqDTO; +import cn.aagro.pp.framework.common.biz.system.oauth2.dto.OAuth2AccessTokenRespDTO; +import cn.aagro.pp.module.system.api.sms.SmsCodeApi; +import cn.aagro.pp.module.system.api.social.SocialClientApi; +import cn.aagro.pp.module.system.api.social.SocialUserApi; +import cn.aagro.pp.module.system.api.social.dto.SocialUserBindReqDTO; +import cn.aagro.pp.module.system.api.social.dto.SocialUserRespDTO; +import cn.aagro.pp.module.system.api.social.dto.SocialWxPhoneNumberInfoRespDTO; +import cn.aagro.pp.module.system.enums.logger.LoginLogTypeEnum; +import cn.aagro.pp.module.system.enums.logger.LoginResultEnum; +import cn.aagro.pp.module.system.enums.oauth2.OAuth2ClientConstants; +import cn.aagro.pp.module.system.enums.sms.SmsSceneEnum; +import cn.aagro.pp.module.system.enums.social.SocialTypeEnum; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.Objects; + +import static cn.aagro.pp.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.aagro.pp.framework.common.util.servlet.ServletUtils.getClientIP; +import static cn.aagro.pp.framework.web.core.util.WebFrameworkUtils.getTerminal; +import static cn.aagro.pp.module.member.enums.ErrorCodeConstants.*; + +/** + * 会员的认证 Service 接口 + * + * @author 芋道源码 + */ +@Service +@Slf4j +public class MemberAuthServiceImpl implements MemberAuthService { + + @Resource + private MemberUserService userService; + @Resource + private SmsCodeApi smsCodeApi; + @Resource + private LoginLogApi loginLogApi; + @Resource + private SocialUserApi socialUserApi; + @Resource + private SocialClientApi socialClientApi; + @Resource + private OAuth2TokenCommonApi oauth2TokenApi; + + @Override + public AppAuthLoginRespVO login(AppAuthLoginReqVO reqVO) { + // 使用手机 + 密码,进行登录。 + MemberUserDO user = login0(reqVO.getMobile(), reqVO.getPassword()); + + // 如果 socialType 非空,说明需要绑定社交用户 + String openid = null; + if (reqVO.getSocialType() != null) { + openid = socialUserApi.bindSocialUser(new SocialUserBindReqDTO(user.getId(), getUserType().getValue(), + reqVO.getSocialType(), reqVO.getSocialCode(), reqVO.getSocialState())); + } + + // 创建 Token 令牌,记录登录日志 + return createTokenAfterLoginSuccess(user, reqVO.getMobile(), LoginLogTypeEnum.LOGIN_MOBILE, openid); + } + + @Override + @Transactional + public AppAuthLoginRespVO smsLogin(AppAuthSmsLoginReqVO reqVO) { + // 校验验证码 + String userIp = getClientIP(); + smsCodeApi.useSmsCode(AuthConvert.INSTANCE.convert(reqVO, SmsSceneEnum.MEMBER_LOGIN.getScene(), userIp)); + + // 获得获得注册用户 + MemberUserDO user = userService.createUserIfAbsent(reqVO.getMobile(), userIp, getTerminal()); + Assert.notNull(user, "获取用户失败,结果为空"); + + // 校验是否禁用 + if (CommonStatusEnum.isDisable(user.getStatus())) { + createLoginLog(user.getId(), reqVO.getMobile(), LoginLogTypeEnum.LOGIN_SMS, LoginResultEnum.USER_DISABLED); + throw exception(AUTH_LOGIN_USER_DISABLED); + } + + // 如果 socialType 非空,说明需要绑定社交用户 + String openid = null; + if (reqVO.getSocialType() != null) { + openid = socialUserApi.bindSocialUser(new SocialUserBindReqDTO(user.getId(), getUserType().getValue(), + reqVO.getSocialType(), reqVO.getSocialCode(), reqVO.getSocialState())); + } + + // 创建 Token 令牌,记录登录日志 + return createTokenAfterLoginSuccess(user, reqVO.getMobile(), LoginLogTypeEnum.LOGIN_SMS, openid); + } + + @Override + @Transactional + public AppAuthLoginRespVO socialLogin(AppAuthSocialLoginReqVO reqVO) { + // 使用 code 授权码,进行登录。然后,获得到绑定的用户编号 + SocialUserRespDTO socialUser = socialUserApi.getSocialUserByCode(UserTypeEnum.MEMBER.getValue(), reqVO.getType(), + reqVO.getCode(), reqVO.getState()); + if (socialUser == null) { + throw exception(AUTH_SOCIAL_USER_NOT_FOUND); + } + + // 情况一:已绑定,直接读取用户信息 + MemberUserDO user; + if (socialUser.getUserId() != null) { + user = userService.getUser(socialUser.getUserId()); + // 情况二:未绑定,注册用户 + 绑定用户 + } else { + user = userService.createUser(socialUser.getNickname(), socialUser.getAvatar(), getClientIP(), getTerminal()); + socialUserApi.bindSocialUser(new SocialUserBindReqDTO(user.getId(), getUserType().getValue(), + reqVO.getType(), reqVO.getCode(), reqVO.getState())); + } + if (user == null) { + throw exception(USER_NOT_EXISTS); + } + + // 创建 Token 令牌,记录登录日志 + return createTokenAfterLoginSuccess(user, user.getMobile(), LoginLogTypeEnum.LOGIN_SOCIAL, socialUser.getOpenid()); + } + + @Override + public AppAuthLoginRespVO weixinMiniAppLogin(AppAuthWeixinMiniAppLoginReqVO reqVO) { + // 获得对应的手机号信息 + SocialWxPhoneNumberInfoRespDTO phoneNumberInfo = socialClientApi.getWxMaPhoneNumberInfo( + UserTypeEnum.MEMBER.getValue(), reqVO.getPhoneCode()); + Assert.notNull(phoneNumberInfo, "获得手机信息失败,结果为空"); + + // 获得获得注册用户 + MemberUserDO user = userService.createUserIfAbsent(phoneNumberInfo.getPurePhoneNumber(), + getClientIP(), TerminalEnum.WECHAT_MINI_PROGRAM.getTerminal()); + Assert.notNull(user, "获取用户失败,结果为空"); + + // 绑定社交用户 + String openid = socialUserApi.bindSocialUser(new SocialUserBindReqDTO(user.getId(), getUserType().getValue(), + SocialTypeEnum.WECHAT_MINI_PROGRAM.getType(), reqVO.getLoginCode(), reqVO.getState())); + + // 创建 Token 令牌,记录登录日志 + return createTokenAfterLoginSuccess(user, user.getMobile(), LoginLogTypeEnum.LOGIN_SOCIAL, openid); + } + + private AppAuthLoginRespVO createTokenAfterLoginSuccess(MemberUserDO user, String mobile, + LoginLogTypeEnum logType, String openid) { + // 插入登陆日志 + createLoginLog(user.getId(), mobile, logType, LoginResultEnum.SUCCESS); + // 创建 Token 令牌 + OAuth2AccessTokenRespDTO accessTokenRespDTO = oauth2TokenApi.createAccessToken(new OAuth2AccessTokenCreateReqDTO() + .setUserId(user.getId()).setUserType(getUserType().getValue()) + .setClientId(OAuth2ClientConstants.CLIENT_ID_DEFAULT)); + // 构建返回结果 + return AuthConvert.INSTANCE.convert(accessTokenRespDTO, openid); + } + + @Override + public String getSocialAuthorizeUrl(Integer type, String redirectUri) { + return socialClientApi.getAuthorizeUrl(type, UserTypeEnum.MEMBER.getValue(), redirectUri); + } + + private MemberUserDO login0(String mobile, String password) { + final LoginLogTypeEnum logTypeEnum = LoginLogTypeEnum.LOGIN_MOBILE; + // 校验账号是否存在 + MemberUserDO user = userService.getUserByMobile(mobile); + if (user == null) { + createLoginLog(null, mobile, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS); + throw exception(AUTH_LOGIN_BAD_CREDENTIALS); + } + if (!userService.isPasswordMatch(password, user.getPassword())) { + createLoginLog(user.getId(), mobile, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS); + throw exception(AUTH_LOGIN_BAD_CREDENTIALS); + } + // 校验是否禁用 + if (CommonStatusEnum.isDisable(user.getStatus())) { + createLoginLog(user.getId(), mobile, logTypeEnum, LoginResultEnum.USER_DISABLED); + throw exception(AUTH_LOGIN_USER_DISABLED); + } + return user; + } + + private void createLoginLog(Long userId, String mobile, LoginLogTypeEnum logType, LoginResultEnum loginResult) { + // 插入登录日志 + LoginLogCreateReqDTO reqDTO = new LoginLogCreateReqDTO(); + reqDTO.setLogType(logType.getType()); + reqDTO.setTraceId(TracerUtils.getTraceId()); + reqDTO.setUserId(userId); + reqDTO.setUserType(getUserType().getValue()); + reqDTO.setUsername(mobile); + reqDTO.setUserAgent(ServletUtils.getUserAgent()); + reqDTO.setUserIp(getClientIP()); + reqDTO.setResult(loginResult.getResult()); + loginLogApi.createLoginLog(reqDTO); + // 更新最后登录时间 + if (userId != null && Objects.equals(LoginResultEnum.SUCCESS.getResult(), loginResult.getResult())) { + userService.updateUserLogin(userId, getClientIP()); + } + } + + @Override + public void logout(String token) { + // 删除访问令牌 + OAuth2AccessTokenRespDTO accessTokenRespDTO = oauth2TokenApi.removeAccessToken(token); + if (accessTokenRespDTO == null) { + return; + } + // 删除成功,则记录登出日志 + createLogoutLog(accessTokenRespDTO.getUserId()); + } + + @Override + public void sendSmsCode(Long userId, AppAuthSmsSendReqVO reqVO) { + // 情况 1:如果是修改手机场景,需要校验新手机号是否已经注册,说明不能使用该手机了 + if (Objects.equals(reqVO.getScene(), SmsSceneEnum.MEMBER_UPDATE_MOBILE.getScene())) { + MemberUserDO user = userService.getUserByMobile(reqVO.getMobile()); + if (user != null && !Objects.equals(user.getId(), userId)) { + throw exception(AUTH_MOBILE_USED); + } + } + // 情况 2:如果是重置密码场景,需要校验手机号是存在的 + if (Objects.equals(reqVO.getScene(), SmsSceneEnum.MEMBER_RESET_PASSWORD.getScene())) { + MemberUserDO user = userService.getUserByMobile(reqVO.getMobile()); + if (user == null) { + throw exception(USER_MOBILE_NOT_EXISTS); + } + } + // 情况 3:如果是修改密码场景,需要查询手机号,无需前端传递 + if (Objects.equals(reqVO.getScene(), SmsSceneEnum.MEMBER_UPDATE_PASSWORD.getScene())) { + MemberUserDO user = userService.getUser(userId); + // TODO 芋艿:后续 member user 手机非强绑定,这块需要做下调整; + reqVO.setMobile(user.getMobile()); + } + + // 执行发送 + smsCodeApi.sendSmsCode(AuthConvert.INSTANCE.convert(reqVO).setCreateIp(getClientIP())); + } + + @Override + public void validateSmsCode(Long userId, AppAuthSmsValidateReqVO reqVO) { + smsCodeApi.validateSmsCode(AuthConvert.INSTANCE.convert(reqVO)); + } + + @Override + public AppAuthLoginRespVO refreshToken(String refreshToken) { + OAuth2AccessTokenRespDTO accessTokenDO = oauth2TokenApi.refreshAccessToken(refreshToken, + OAuth2ClientConstants.CLIENT_ID_DEFAULT); + return AuthConvert.INSTANCE.convert(accessTokenDO, null); + } + + private void createLogoutLog(Long userId) { + LoginLogCreateReqDTO reqDTO = new LoginLogCreateReqDTO(); + reqDTO.setLogType(LoginLogTypeEnum.LOGOUT_SELF.getType()); + reqDTO.setTraceId(TracerUtils.getTraceId()); + reqDTO.setUserId(userId); + reqDTO.setUserType(getUserType().getValue()); + reqDTO.setUsername(getMobile(userId)); + reqDTO.setUserAgent(ServletUtils.getUserAgent()); + reqDTO.setUserIp(getClientIP()); + reqDTO.setResult(LoginResultEnum.SUCCESS.getResult()); + loginLogApi.createLoginLog(reqDTO); + } + + private String getMobile(Long userId) { + if (userId == null) { + return null; + } + MemberUserDO user = userService.getUser(userId); + return user != null ? user.getMobile() : null; + } + + private UserTypeEnum getUserType() { + return UserTypeEnum.MEMBER; + } + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/config/MemberConfigService.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/config/MemberConfigService.java new file mode 100644 index 0000000..45bad2a --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/config/MemberConfigService.java @@ -0,0 +1,29 @@ +package cn.aagro.pp.module.member.service.config; + +import cn.aagro.pp.module.member.controller.admin.config.vo.MemberConfigSaveReqVO; +import cn.aagro.pp.module.member.dal.dataobject.config.MemberConfigDO; + +import javax.validation.Valid; + +/** + * 会员配置 Service 接口 + * + * @author QingX + */ +public interface MemberConfigService { + + /** + * 保存会员配置 + * + * @param saveReqVO 更新信息 + */ + void saveConfig(@Valid MemberConfigSaveReqVO saveReqVO); + + /** + * 获得会员配置 + * + * @return 积分配置 + */ + MemberConfigDO getConfig(); + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/config/MemberConfigServiceImpl.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/config/MemberConfigServiceImpl.java new file mode 100644 index 0000000..29385be --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/config/MemberConfigServiceImpl.java @@ -0,0 +1,44 @@ +package cn.aagro.pp.module.member.service.config; + +import cn.aagro.pp.framework.common.util.collection.CollectionUtils; +import cn.aagro.pp.module.member.controller.admin.config.vo.MemberConfigSaveReqVO; +import cn.aagro.pp.module.member.convert.config.MemberConfigConvert; +import cn.aagro.pp.module.member.dal.dataobject.config.MemberConfigDO; +import cn.aagro.pp.module.member.dal.mysql.config.MemberConfigMapper; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.List; + +/** + * 会员配置 Service 实现类 + * + * @author QingX + */ +@Service +@Validated +public class MemberConfigServiceImpl implements MemberConfigService { + + @Resource + private MemberConfigMapper memberConfigMapper; + + @Override + public void saveConfig(MemberConfigSaveReqVO saveReqVO) { + // 存在,则进行更新 + MemberConfigDO dbConfig = getConfig(); + if (dbConfig != null) { + memberConfigMapper.updateById(MemberConfigConvert.INSTANCE.convert(saveReqVO).setId(dbConfig.getId())); + return; + } + // 不存在,则进行插入 + memberConfigMapper.insert(MemberConfigConvert.INSTANCE.convert(saveReqVO)); + } + + @Override + public MemberConfigDO getConfig() { + List list = memberConfigMapper.selectList(); + return CollectionUtils.getFirst(list); + } + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/group/MemberGroupService.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/group/MemberGroupService.java new file mode 100644 index 0000000..39cd187 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/group/MemberGroupService.java @@ -0,0 +1,84 @@ +package cn.aagro.pp.module.member.service.group; + +import cn.aagro.pp.framework.common.enums.CommonStatusEnum; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.member.controller.admin.group.vo.MemberGroupCreateReqVO; +import cn.aagro.pp.module.member.controller.admin.group.vo.MemberGroupPageReqVO; +import cn.aagro.pp.module.member.controller.admin.group.vo.MemberGroupUpdateReqVO; +import cn.aagro.pp.module.member.dal.dataobject.group.MemberGroupDO; + +import javax.validation.Valid; +import java.util.Collection; +import java.util.List; + +/** + * 用户分组 Service 接口 + * + * @author owen + */ +public interface MemberGroupService { + + /** + * 创建用户分组 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createGroup(@Valid MemberGroupCreateReqVO createReqVO); + + /** + * 更新用户分组 + * + * @param updateReqVO 更新信息 + */ + void updateGroup(@Valid MemberGroupUpdateReqVO updateReqVO); + + /** + * 删除用户分组 + * + * @param id 编号 + */ + void deleteGroup(Long id); + + /** + * 获得用户分组 + * + * @param id 编号 + * @return 用户分组 + */ + MemberGroupDO getGroup(Long id); + + /** + * 获得用户分组列表 + * + * @param ids 编号 + * @return 用户分组列表 + */ + List getGroupList(Collection ids); + + /** + * 获得用户分组分页 + * + * @param pageReqVO 分页查询 + * @return 用户分组分页 + */ + PageResult getGroupPage(MemberGroupPageReqVO pageReqVO); + + /** + * 获得指定状态的用户分组列表 + * + * @param status 状态 + * @return 用户分组列表 + */ + List getGroupListByStatus(Integer status); + + /** + * 获得开启状态的用户分组列表 + * + * @return 用户分组列表 + */ + default List getEnableGroupList() { + return getGroupListByStatus(CommonStatusEnum.ENABLE.getStatus()); + } + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/group/MemberGroupServiceImpl.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/group/MemberGroupServiceImpl.java new file mode 100644 index 0000000..b8c01e2 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/group/MemberGroupServiceImpl.java @@ -0,0 +1,103 @@ +package cn.aagro.pp.module.member.service.group; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.ListUtil; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.member.controller.admin.group.vo.MemberGroupCreateReqVO; +import cn.aagro.pp.module.member.controller.admin.group.vo.MemberGroupPageReqVO; +import cn.aagro.pp.module.member.controller.admin.group.vo.MemberGroupUpdateReqVO; +import cn.aagro.pp.module.member.convert.group.MemberGroupConvert; +import cn.aagro.pp.module.member.dal.dataobject.group.MemberGroupDO; +import cn.aagro.pp.module.member.dal.mysql.group.MemberGroupMapper; +import cn.aagro.pp.module.member.service.user.MemberUserService; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.List; + +import static cn.aagro.pp.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.aagro.pp.module.member.enums.ErrorCodeConstants.GROUP_HAS_USER; +import static cn.aagro.pp.module.member.enums.ErrorCodeConstants.GROUP_NOT_EXISTS; + +/** + * 用户分组 Service 实现类 + * + * @author owen + */ +@Service +@Validated +public class MemberGroupServiceImpl implements MemberGroupService { + + @Resource + private MemberGroupMapper memberGroupMapper; + + @Resource + private MemberUserService memberUserService; + + @Override + public Long createGroup(MemberGroupCreateReqVO createReqVO) { + // 插入 + MemberGroupDO group = MemberGroupConvert.INSTANCE.convert(createReqVO); + memberGroupMapper.insert(group); + // 返回 + return group.getId(); + } + + @Override + public void updateGroup(MemberGroupUpdateReqVO updateReqVO) { + // 校验存在 + validateGroupExists(updateReqVO.getId()); + // 更新 + MemberGroupDO updateObj = MemberGroupConvert.INSTANCE.convert(updateReqVO); + memberGroupMapper.updateById(updateObj); + } + + @Override + public void deleteGroup(Long id) { + // 校验存在 + validateGroupExists(id); + // 校验分组下是否有用户 + validateGroupHasUser(id); + // 删除 + memberGroupMapper.deleteById(id); + } + + void validateGroupExists(Long id) { + if (memberGroupMapper.selectById(id) == null) { + throw exception(GROUP_NOT_EXISTS); + } + } + + void validateGroupHasUser(Long id) { + Long count = memberUserService.getUserCountByGroupId(id); + if (count > 0) { + throw exception(GROUP_HAS_USER); + } + } + + @Override + public MemberGroupDO getGroup(Long id) { + return memberGroupMapper.selectById(id); + } + + @Override + public List getGroupList(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return ListUtil.empty(); + } + return memberGroupMapper.selectByIds(ids); + } + + @Override + public PageResult getGroupPage(MemberGroupPageReqVO pageReqVO) { + return memberGroupMapper.selectPage(pageReqVO); + } + + @Override + public List getGroupListByStatus(Integer status) { + return memberGroupMapper.selectListByStatus(status); + } + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/level/MemberExperienceRecordService.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/level/MemberExperienceRecordService.java new file mode 100644 index 0000000..991bbfd --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/level/MemberExperienceRecordService.java @@ -0,0 +1,53 @@ +package cn.aagro.pp.module.member.service.level; + +import cn.aagro.pp.framework.common.pojo.PageParam; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.member.controller.admin.level.vo.experience.MemberExperienceRecordPageReqVO; +import cn.aagro.pp.module.member.dal.dataobject.level.MemberExperienceRecordDO; +import cn.aagro.pp.module.member.enums.MemberExperienceBizTypeEnum; + +/** + * 会员经验记录 Service 接口 + * + * @author owen + */ +public interface MemberExperienceRecordService { + + /** + * 获得会员经验记录 + * + * @param id 编号 + * @return 会员经验记录 + */ + MemberExperienceRecordDO getExperienceRecord(Long id); + + /** + * 【管理员】获得会员经验记录分页 + * + * @param pageReqVO 分页查询 + * @return 会员经验记录分页 + */ + PageResult getExperienceRecordPage(MemberExperienceRecordPageReqVO pageReqVO); + + /** + * 【会员】获得会员经验记录分页 + * + * @param userId 用户编号 + * @param pageParam 分页查询 + * @return 会员经验记录分页 + */ + PageResult getExperienceRecordPage(Long userId, PageParam pageParam); + + /** + * 根据业务类型, 创建 经验变动记录 + * + * @param userId 会员编号 + * @param experience 变动经验值 + * @param totalExperience 会员当前的经验 + * @param bizType 业务类型 + * @param bizId 业务ID + */ + void createExperienceRecord(Long userId, Integer experience, Integer totalExperience, + MemberExperienceBizTypeEnum bizType, String bizId); + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/level/MemberExperienceRecordServiceImpl.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/level/MemberExperienceRecordServiceImpl.java new file mode 100644 index 0000000..f96c9f5 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/level/MemberExperienceRecordServiceImpl.java @@ -0,0 +1,55 @@ +package cn.aagro.pp.module.member.service.level; + +import cn.hutool.core.util.StrUtil; +import cn.aagro.pp.framework.common.pojo.PageParam; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.member.controller.admin.level.vo.experience.MemberExperienceRecordPageReqVO; +import cn.aagro.pp.module.member.convert.level.MemberExperienceRecordConvert; +import cn.aagro.pp.module.member.dal.dataobject.level.MemberExperienceRecordDO; +import cn.aagro.pp.module.member.dal.mysql.level.MemberExperienceRecordMapper; +import cn.aagro.pp.module.member.enums.MemberExperienceBizTypeEnum; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.List; + +/** + * 会员经验记录 Service 实现类 + * + * @author owen + */ +@Service +@Validated +public class MemberExperienceRecordServiceImpl implements MemberExperienceRecordService { + + @Resource + private MemberExperienceRecordMapper experienceLogMapper; + + @Override + public MemberExperienceRecordDO getExperienceRecord(Long id) { + return experienceLogMapper.selectById(id); + } + + @Override + public PageResult getExperienceRecordPage(MemberExperienceRecordPageReqVO pageReqVO) { + return experienceLogMapper.selectPage(pageReqVO); + } + + @Override + public PageResult getExperienceRecordPage(Long userId, PageParam pageParam) { + return experienceLogMapper.selectPage(userId, pageParam); + } + + @Override + public void createExperienceRecord(Long userId, Integer experience, Integer totalExperience, + MemberExperienceBizTypeEnum bizType, String bizId) { + String description = StrUtil.format(bizType.getDescription(), experience); + MemberExperienceRecordDO record = MemberExperienceRecordConvert.INSTANCE.convert( + userId, experience, totalExperience, + bizId, bizType.getType(), bizType.getTitle(), description); + experienceLogMapper.insert(record); + } + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/level/MemberLevelRecordService.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/level/MemberLevelRecordService.java new file mode 100644 index 0000000..0bd48a6 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/level/MemberLevelRecordService.java @@ -0,0 +1,37 @@ +package cn.aagro.pp.module.member.service.level; + +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.member.controller.admin.level.vo.record.MemberLevelRecordPageReqVO; +import cn.aagro.pp.module.member.dal.dataobject.level.MemberLevelRecordDO; + +/** + * 会员等级记录 Service 接口 + * + * @author owen + */ +public interface MemberLevelRecordService { + + /** + * 获得会员等级记录 + * + * @param id 编号 + * @return 会员等级记录 + */ + MemberLevelRecordDO getLevelRecord(Long id); + + /** + * 获得会员等级记录分页 + * + * @param pageReqVO 分页查询 + * @return 会员等级记录分页 + */ + PageResult getLevelRecordPage(MemberLevelRecordPageReqVO pageReqVO); + + /** + * 创建会员等级记录 + * + * @param levelRecord 会员等级记录 + */ + void createLevelRecord(MemberLevelRecordDO levelRecord); + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/level/MemberLevelRecordServiceImpl.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/level/MemberLevelRecordServiceImpl.java new file mode 100644 index 0000000..d7a43c8 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/level/MemberLevelRecordServiceImpl.java @@ -0,0 +1,39 @@ +package cn.aagro.pp.module.member.service.level; + +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.member.controller.admin.level.vo.record.MemberLevelRecordPageReqVO; +import cn.aagro.pp.module.member.dal.dataobject.level.MemberLevelRecordDO; +import cn.aagro.pp.module.member.dal.mysql.level.MemberLevelRecordMapper; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; + +/** + * 会员等级记录 Service 实现类 + * + * @author owen + */ +@Service +@Validated +public class MemberLevelRecordServiceImpl implements MemberLevelRecordService { + + @Resource + private MemberLevelRecordMapper levelLogMapper; + + @Override + public MemberLevelRecordDO getLevelRecord(Long id) { + return levelLogMapper.selectById(id); + } + + @Override + public PageResult getLevelRecordPage(MemberLevelRecordPageReqVO pageReqVO) { + return levelLogMapper.selectPage(pageReqVO); + } + + @Override + public void createLevelRecord(MemberLevelRecordDO levelRecord) { + levelLogMapper.insert(levelRecord); + } + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/level/MemberLevelService.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/level/MemberLevelService.java new file mode 100644 index 0000000..42e3d71 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/level/MemberLevelService.java @@ -0,0 +1,102 @@ +package cn.aagro.pp.module.member.service.level; + +import cn.aagro.pp.framework.common.enums.CommonStatusEnum; +import cn.aagro.pp.module.member.controller.admin.level.vo.level.MemberLevelCreateReqVO; +import cn.aagro.pp.module.member.controller.admin.level.vo.level.MemberLevelListReqVO; +import cn.aagro.pp.module.member.controller.admin.level.vo.level.MemberLevelUpdateReqVO; +import cn.aagro.pp.module.member.controller.admin.user.vo.MemberUserUpdateLevelReqVO; +import cn.aagro.pp.module.member.dal.dataobject.level.MemberLevelDO; +import cn.aagro.pp.module.member.enums.MemberExperienceBizTypeEnum; + +import javax.validation.Valid; +import java.util.Collection; +import java.util.List; + +/** + * 会员等级 Service 接口 + * + * @author owen + */ +public interface MemberLevelService { + + /** + * 创建会员等级 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createLevel(@Valid MemberLevelCreateReqVO createReqVO); + + /** + * 更新会员等级 + * + * @param updateReqVO 更新信息 + */ + void updateLevel(@Valid MemberLevelUpdateReqVO updateReqVO); + + /** + * 删除会员等级 + * + * @param id 编号 + */ + void deleteLevel(Long id); + + /** + * 获得会员等级 + * + * @param id 编号 + * @return 会员等级 + */ + MemberLevelDO getLevel(Long id); + + /** + * 获得会员等级列表 + * + * @param ids 编号 + * @return 会员等级列表 + */ + List getLevelList(Collection ids); + + /** + * 获得会员等级列表 + * + * @param listReqVO 查询参数 + * @return 会员等级列表 + */ + List getLevelList(MemberLevelListReqVO listReqVO); + + /** + * 获得指定状态的会员等级列表 + * + * @param status 状态 + * @return 会员等级列表 + */ + List getLevelListByStatus(Integer status); + + /** + * 获得开启状态的会员等级列表 + * + * @return 会员等级列表 + */ + default List getEnableLevelList() { + return getLevelListByStatus(CommonStatusEnum.ENABLE.getStatus()); + } + + /** + * 修改会员的等级 + * + * @param updateReqVO 修改参数 + */ + void updateUserLevel(MemberUserUpdateLevelReqVO updateReqVO); + + /** + * 增加会员经验 + * + * @param userId 会员ID + * @param experience 经验 + * @param bizType 业务类型 + * @param bizId 业务编号 + */ + void addExperience(Long userId, Integer experience, MemberExperienceBizTypeEnum bizType, String bizId); + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/level/MemberLevelServiceImpl.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/level/MemberLevelServiceImpl.java new file mode 100644 index 0000000..a6e925d --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/level/MemberLevelServiceImpl.java @@ -0,0 +1,299 @@ +package cn.aagro.pp.module.member.service.level; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.NumberUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.aagro.pp.module.member.controller.admin.level.vo.level.MemberLevelCreateReqVO; +import cn.aagro.pp.module.member.controller.admin.level.vo.level.MemberLevelListReqVO; +import cn.aagro.pp.module.member.controller.admin.level.vo.level.MemberLevelUpdateReqVO; +import cn.aagro.pp.module.member.controller.admin.user.vo.MemberUserUpdateLevelReqVO; +import cn.aagro.pp.module.member.convert.level.MemberLevelConvert; +import cn.aagro.pp.module.member.convert.level.MemberLevelRecordConvert; +import cn.aagro.pp.module.member.dal.dataobject.level.MemberLevelDO; +import cn.aagro.pp.module.member.dal.dataobject.level.MemberLevelRecordDO; +import cn.aagro.pp.module.member.dal.dataobject.user.MemberUserDO; +import cn.aagro.pp.module.member.dal.mysql.level.MemberLevelMapper; +import cn.aagro.pp.module.member.enums.MemberExperienceBizTypeEnum; +import cn.aagro.pp.module.member.service.user.MemberUserService; +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; + +import static cn.aagro.pp.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.aagro.pp.module.member.enums.ErrorCodeConstants.*; + +/** + * 会员等级 Service 实现类 + * + * @author owen + */ +@Slf4j +@Service +@Validated +public class MemberLevelServiceImpl implements MemberLevelService { + + @Resource + private MemberLevelMapper memberLevelMapper; + + @Resource + private MemberLevelRecordService memberLevelRecordService; + @Resource + private MemberExperienceRecordService memberExperienceRecordService; + @Resource + private MemberUserService memberUserService; + + @Override + public Long createLevel(MemberLevelCreateReqVO createReqVO) { + // 校验配置是否有效 + validateConfigValid(null, createReqVO.getName(), createReqVO.getLevel(), createReqVO.getExperience()); + + // 插入 + MemberLevelDO level = MemberLevelConvert.INSTANCE.convert(createReqVO); + memberLevelMapper.insert(level); + // 返回 + return level.getId(); + } + + @Override + public void updateLevel(MemberLevelUpdateReqVO updateReqVO) { + // 校验存在 + validateLevelExists(updateReqVO.getId()); + // 校验配置是否有效 + validateConfigValid(updateReqVO.getId(), updateReqVO.getName(), updateReqVO.getLevel(), updateReqVO.getExperience()); + + // 更新 + MemberLevelDO updateObj = MemberLevelConvert.INSTANCE.convert(updateReqVO); + memberLevelMapper.updateById(updateObj); + } + + @Override + public void deleteLevel(Long id) { + // 校验存在 + validateLevelExists(id); + // 校验分组下是否有用户 + validateLevelHasUser(id); + // 删除 + memberLevelMapper.deleteById(id); + } + + @VisibleForTesting + MemberLevelDO validateLevelExists(Long id) { + MemberLevelDO levelDO = memberLevelMapper.selectById(id); + if (levelDO == null) { + throw exception(LEVEL_NOT_EXISTS); + } + return levelDO; + } + + @VisibleForTesting + void validateNameUnique(List list, Long id, String name) { + for (MemberLevelDO levelDO : list) { + if (ObjUtil.notEqual(levelDO.getName(), name)) { + continue; + } + if (id == null || !id.equals(levelDO.getId())) { + throw exception(LEVEL_NAME_EXISTS, levelDO.getName()); + } + } + } + + @VisibleForTesting + void validateLevelUnique(List list, Long id, Integer level) { + for (MemberLevelDO levelDO : list) { + if (ObjUtil.notEqual(levelDO.getLevel(), level)) { + continue; + } + + if (id == null || !id.equals(levelDO.getId())) { + throw exception(LEVEL_VALUE_EXISTS, levelDO.getLevel(), levelDO.getName()); + } + } + } + + @VisibleForTesting + void validateExperienceOutRange(List list, Long id, Integer level, Integer experience) { + for (MemberLevelDO levelDO : list) { + if (levelDO.getId().equals(id)) { + continue; + } + + if (levelDO.getLevel() < level) { + // 经验大于前一个等级 + if (experience <= levelDO.getExperience()) { + throw exception(LEVEL_EXPERIENCE_MIN, levelDO.getName(), levelDO.getExperience()); + } + } else if (levelDO.getLevel() > level) { + //小于下一个级别 + if (experience >= levelDO.getExperience()) { + throw exception(LEVEL_EXPERIENCE_MAX, levelDO.getName(), levelDO.getExperience()); + } + } + } + } + + @VisibleForTesting + void validateConfigValid(Long id, String name, Integer level, Integer experience) { + List list = memberLevelMapper.selectList(); + // 校验名称唯一 + validateNameUnique(list, id, name); + // 校验等级唯一 + validateLevelUnique(list, id, level); + // 校验升级所需经验是否有效: 大于前一个等级,小于下一个级别 + validateExperienceOutRange(list, id, level, experience); + } + + @VisibleForTesting + void validateLevelHasUser(Long id) { + Long count = memberUserService.getUserCountByLevelId(id); + if (count > 0) { + throw exception(LEVEL_HAS_USER); + } + } + + @Override + public MemberLevelDO getLevel(Long id) { + return id != null && id > 0 ? memberLevelMapper.selectById(id) : null; + } + + @Override + public List getLevelList(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return Collections.emptyList(); + } + return memberLevelMapper.selectByIds(ids); + } + + @Override + public List getLevelList(MemberLevelListReqVO listReqVO) { + return memberLevelMapper.selectList(listReqVO); + } + + @Override + public List getLevelListByStatus(Integer status) { + return memberLevelMapper.selectListByStatus(status); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateUserLevel(MemberUserUpdateLevelReqVO updateReqVO) { + MemberUserDO user = memberUserService.getUser(updateReqVO.getId()); + if (user == null) { + throw exception(USER_NOT_EXISTS); + } + // 等级未发生变化 + if (ObjUtil.equal(user.getLevelId(), updateReqVO.getLevelId())) { + return; + } + + // 1. 记录等级变动 + MemberLevelRecordDO levelRecord = new MemberLevelRecordDO() + .setUserId(user.getId()).setRemark(updateReqVO.getReason()); + MemberLevelDO memberLevel = null; + if (updateReqVO.getLevelId() == null) { + // 取消用户等级时,需要扣减经验 + levelRecord.setExperience(-user.getExperience()); + levelRecord.setUserExperience(0); + levelRecord.setDescription("管理员取消了等级"); + } else { + // 复制等级配置 + memberLevel = validateLevelExists(updateReqVO.getLevelId()); + MemberLevelRecordConvert.INSTANCE.copyTo(memberLevel, levelRecord); + // 变动经验值 = 等级的升级经验 - 会员当前的经验;正数为增加经验,负数为扣减经验 + levelRecord.setExperience(memberLevel.getExperience() - user.getExperience()); + levelRecord.setUserExperience(memberLevel.getExperience()); // 会员当前的经验 = 等级的升级经验 + levelRecord.setDescription("管理员调整为:" + memberLevel.getName()); + } + memberLevelRecordService.createLevelRecord(levelRecord); + + // 2. 记录会员经验变动 + memberExperienceRecordService.createExperienceRecord(user.getId(), + levelRecord.getExperience(), levelRecord.getUserExperience(), + MemberExperienceBizTypeEnum.ADMIN, String.valueOf(MemberExperienceBizTypeEnum.ADMIN.getType())); + + // 3. 更新会员表上的等级编号、经验值 + memberUserService.updateUserLevel(user.getId(), updateReqVO.getLevelId(), + levelRecord.getUserExperience()); + + // 4. 给会员发送等级变动消息 + notifyMemberLevelChange(user.getId(), memberLevel); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void addExperience(Long userId, Integer experience, MemberExperienceBizTypeEnum bizType, String bizId) { + if (experience == 0) { + return; + } + if (!bizType.isAdd() && experience > 0) { + experience = -experience; + } + + // 1. 创建经验记录 + MemberUserDO user = memberUserService.getUser(userId); + Integer userExperience = ObjUtil.defaultIfNull(user.getExperience(), 0); + userExperience = NumberUtil.max(userExperience + experience, 0); // 防止扣出负数 + MemberLevelRecordDO levelRecord = new MemberLevelRecordDO().setUserId(user.getId()) + .setExperience(experience).setUserExperience(userExperience).setLevelId(user.getLevelId()); + memberExperienceRecordService.createExperienceRecord(userId, experience, userExperience, + bizType, bizId); + + // 2.1 保存等级变更记录 + MemberLevelDO newLevel = calculateNewLevel(user, userExperience); + if (newLevel != null) { + MemberLevelRecordConvert.INSTANCE.copyTo(newLevel, levelRecord); + memberLevelRecordService.createLevelRecord(levelRecord); + + // 2.2 给会员发送等级变动消息 + notifyMemberLevelChange(userId, newLevel); + } + + // 3. 更新会员表上的等级编号、经验值 + memberUserService.updateUserLevel(user.getId(), Optional.ofNullable(levelRecord.getLevelId()).orElse(user.getLevelId()), userExperience); + } + + /** + * 计算会员等级 + * + * @param user 会员 + * @param userExperience 会员当前的经验值 + * @return 会员新的等级,null表示无变化 + */ + private MemberLevelDO calculateNewLevel(MemberUserDO user, int userExperience) { + List list = getEnableLevelList(); + if (CollUtil.isEmpty(list)) { + log.warn("计算会员等级失败:会员等级配置不存在"); + return null; + } + + MemberLevelDO matchLevel = list.stream() + .filter(level -> userExperience >= level.getExperience()) + .max(Comparator.nullsFirst(Comparator.comparing(MemberLevelDO::getLevel))) + .orElse(null); + if (matchLevel == null) { + log.warn("计算会员等级失败:未找到会员{}经验{}对应的等级配置", user.getId(), userExperience); + return null; + } + + // 等级没有变化 + if (ObjectUtil.equal(matchLevel.getId(), user.getLevelId())) { + return null; + } + + return matchLevel; + } + + private void notifyMemberLevelChange(Long userId, MemberLevelDO level) { + //todo: 给会员发消息 + } + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/point/MemberPointRecordService.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/point/MemberPointRecordService.java new file mode 100644 index 0000000..922c411 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/point/MemberPointRecordService.java @@ -0,0 +1,42 @@ +package cn.aagro.pp.module.member.service.point; + +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.member.controller.admin.point.vo.recrod.MemberPointRecordPageReqVO; +import cn.aagro.pp.module.member.controller.app.point.vo.AppMemberPointRecordPageReqVO; +import cn.aagro.pp.module.member.dal.dataobject.point.MemberPointRecordDO; +import cn.aagro.pp.module.member.enums.point.MemberPointBizTypeEnum; + +/** + * 用户积分记录 Service 接口 + * + * @author QingX + */ +public interface MemberPointRecordService { + + /** + * 【管理员】获得积分记录分页 + * + * @param pageReqVO 分页查询 + * @return 签到记录分页 + */ + PageResult getPointRecordPage(MemberPointRecordPageReqVO pageReqVO); + + /** + * 【会员】获得积分记录分页 + * + * @param userId 用户编号 + * @param pageReqVO 分页查询 + * @return 签到记录分页 + */ + PageResult getPointRecordPage(Long userId, AppMemberPointRecordPageReqVO pageReqVO); + + /** + * 创建用户积分记录 + * + * @param userId 用户ID + * @param point 变动积分 + * @param bizType 业务类型 + * @param bizId 业务编号 + */ + void createPointRecord(Long userId, Integer point, MemberPointBizTypeEnum bizType, String bizId); +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/point/MemberPointRecordServiceImpl.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/point/MemberPointRecordServiceImpl.java new file mode 100644 index 0000000..a29e95d --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/point/MemberPointRecordServiceImpl.java @@ -0,0 +1,96 @@ +package cn.aagro.pp.module.member.service.point; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.member.controller.admin.point.vo.recrod.MemberPointRecordPageReqVO; +import cn.aagro.pp.module.member.controller.app.point.vo.AppMemberPointRecordPageReqVO; +import cn.aagro.pp.module.member.dal.dataobject.point.MemberPointRecordDO; +import cn.aagro.pp.module.member.dal.dataobject.user.MemberUserDO; +import cn.aagro.pp.module.member.dal.mysql.point.MemberPointRecordMapper; +import cn.aagro.pp.module.member.enums.point.MemberPointBizTypeEnum; +import cn.aagro.pp.module.member.service.user.MemberUserService; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Set; + +import static cn.aagro.pp.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.aagro.pp.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.aagro.pp.module.member.enums.ErrorCodeConstants.USER_POINT_NOT_ENOUGH; + + +/** + * 积分记录 Service 实现类 + * + * @author QingX + */ +@Slf4j +@Service +@Validated +public class MemberPointRecordServiceImpl implements MemberPointRecordService { + + @Resource + private MemberPointRecordMapper memberPointRecordMapper; + + @Resource + private MemberUserService memberUserService; + + @Override + public PageResult getPointRecordPage(MemberPointRecordPageReqVO pageReqVO) { + // 根据用户昵称查询出用户 ids + Set userIds = null; + if (StringUtils.isNotBlank(pageReqVO.getNickname())) { + List users = memberUserService.getUserListByNickname(pageReqVO.getNickname()); + // 如果查询用户结果为空直接返回无需继续查询 + if (CollectionUtils.isEmpty(users)) { + return PageResult.empty(); + } + userIds = convertSet(users, MemberUserDO::getId); + } + // 执行查询 + return memberPointRecordMapper.selectPage(pageReqVO, userIds); + } + + @Override + public PageResult getPointRecordPage(Long userId, AppMemberPointRecordPageReqVO pageReqVO) { + return memberPointRecordMapper.selectPage(userId, pageReqVO); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void createPointRecord(Long userId, Integer point, MemberPointBizTypeEnum bizType, String bizId) { + if (point == 0) { + return; + } + // 1. 校验用户积分余额 + MemberUserDO user = memberUserService.getUser(userId); + Integer userPoint = ObjectUtil.defaultIfNull(user.getPoint(), 0); + int totalPoint = userPoint + point; // 用户变动后的积分 + if (totalPoint < 0) { + log.error("[createPointRecord][userId({}) point({}) bizType({}) bizId({}) {}]", userId, point, bizType, bizId, + USER_POINT_NOT_ENOUGH); + return; + } + + // 2. 更新用户积分 + boolean success = memberUserService.updateUserPoint(userId, point); + if (!success) { + throw exception(USER_POINT_NOT_ENOUGH); + } + + // 3. 增加积分记录 + MemberPointRecordDO record = new MemberPointRecordDO() + .setUserId(userId).setBizId(bizId).setBizType(bizType.getType()) + .setTitle(bizType.getName()).setDescription(StrUtil.format(bizType.getDescription(), point)) + .setPoint(point).setTotalPoint(totalPoint); + memberPointRecordMapper.insert(record); + } + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/signin/MemberSignInConfigService.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/signin/MemberSignInConfigService.java new file mode 100644 index 0000000..927102b --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/signin/MemberSignInConfigService.java @@ -0,0 +1,62 @@ +package cn.aagro.pp.module.member.service.signin; + +import cn.aagro.pp.module.member.controller.admin.signin.vo.config.MemberSignInConfigCreateReqVO; +import cn.aagro.pp.module.member.controller.admin.signin.vo.config.MemberSignInConfigUpdateReqVO; +import cn.aagro.pp.module.member.dal.dataobject.signin.MemberSignInConfigDO; + +import javax.validation.Valid; +import java.util.List; + +/** + * 签到规则 Service 接口 + * + * @author QingX + */ +public interface MemberSignInConfigService { + + /** + * 创建签到规则 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createSignInConfig(@Valid MemberSignInConfigCreateReqVO createReqVO); + + /** + * 更新签到规则 + * + * @param updateReqVO 更新信息 + */ + void updateSignInConfig(@Valid MemberSignInConfigUpdateReqVO updateReqVO); + + /** + * 删除签到规则 + * + * @param id 编号 + */ + void deleteSignInConfig(Long id); + + /** + * 获得签到规则 + * + * @param id 编号 + * @return 签到规则 + */ + MemberSignInConfigDO getSignInConfig(Long id); + + /** + * 获得签到规则列表 + * + * @return 签到规则分页 + */ + List getSignInConfigList(); + + /** + * 获得签到规则列表 + * + * @param status 状态 + * @return 签到规则分页 + */ + List getSignInConfigList(Integer status); + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/signin/MemberSignInConfigServiceImpl.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/signin/MemberSignInConfigServiceImpl.java new file mode 100644 index 0000000..d46f3c2 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/signin/MemberSignInConfigServiceImpl.java @@ -0,0 +1,106 @@ +package cn.aagro.pp.module.member.service.signin; + +import cn.aagro.pp.module.member.controller.admin.signin.vo.config.MemberSignInConfigCreateReqVO; +import cn.aagro.pp.module.member.controller.admin.signin.vo.config.MemberSignInConfigUpdateReqVO; +import cn.aagro.pp.module.member.convert.signin.MemberSignInConfigConvert; +import cn.aagro.pp.module.member.dal.dataobject.signin.MemberSignInConfigDO; +import cn.aagro.pp.module.member.dal.mysql.signin.MemberSignInConfigMapper; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.Comparator; +import java.util.List; + +import static cn.aagro.pp.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.aagro.pp.module.member.enums.ErrorCodeConstants.SIGN_IN_CONFIG_EXISTS; +import static cn.aagro.pp.module.member.enums.ErrorCodeConstants.SIGN_IN_CONFIG_NOT_EXISTS; + +/** + * 签到规则 Service 实现类 + * + * @author QingX + */ +@Service +@Validated +public class MemberSignInConfigServiceImpl implements MemberSignInConfigService { + + @Resource + private MemberSignInConfigMapper memberSignInConfigMapper; + + @Override + public Long createSignInConfig(MemberSignInConfigCreateReqVO createReqVO) { + // 判断是否重复插入签到天数 + validateSignInConfigDayDuplicate(createReqVO.getDay(), null); + + // 插入 + MemberSignInConfigDO signInConfig = MemberSignInConfigConvert.INSTANCE.convert(createReqVO); + memberSignInConfigMapper.insert(signInConfig); + // 返回 + return signInConfig.getId(); + } + + @Override + public void updateSignInConfig(MemberSignInConfigUpdateReqVO updateReqVO) { + // 校验存在 + validateSignInConfigExists(updateReqVO.getId()); + // 判断是否重复插入签到天数 + validateSignInConfigDayDuplicate(updateReqVO.getDay(), updateReqVO.getId()); + + // 判断更新 + MemberSignInConfigDO updateObj = MemberSignInConfigConvert.INSTANCE.convert(updateReqVO); + memberSignInConfigMapper.updateById(updateObj); + } + + @Override + public void deleteSignInConfig(Long id) { + // 校验存在 + validateSignInConfigExists(id); + // 删除 + memberSignInConfigMapper.deleteById(id); + } + + private void validateSignInConfigExists(Long id) { + if (memberSignInConfigMapper.selectById(id) == null) { + throw exception(SIGN_IN_CONFIG_NOT_EXISTS); + } + } + + /** + * 校验 day 是否重复 + * + * @param day 天 + * @param id 编号,只有更新的时候会传递 + */ + private void validateSignInConfigDayDuplicate(Integer day, Long id) { + MemberSignInConfigDO config = memberSignInConfigMapper.selectByDay(day); + // 1. 新增时,config 非空,则说明重复 + if (id == null && config != null) { + throw exception(SIGN_IN_CONFIG_EXISTS); + } + // 2. 更新时,如果 config 非空,且 id 不相等,则说明重复 + if (id != null && config != null && !config.getId().equals(id)) { + throw exception(SIGN_IN_CONFIG_EXISTS); + } + } + + @Override + public MemberSignInConfigDO getSignInConfig(Long id) { + return memberSignInConfigMapper.selectById(id); + } + + @Override + public List getSignInConfigList() { + List list = memberSignInConfigMapper.selectList(); + list.sort(Comparator.comparing(MemberSignInConfigDO::getDay)); + return list; + } + + @Override + public List getSignInConfigList(Integer status) { + List list = memberSignInConfigMapper.selectListByStatus(status); + list.sort(Comparator.comparing(MemberSignInConfigDO::getDay)); + return list; + } + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/signin/MemberSignInRecordService.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/signin/MemberSignInRecordService.java new file mode 100644 index 0000000..0289e50 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/signin/MemberSignInRecordService.java @@ -0,0 +1,50 @@ +package cn.aagro.pp.module.member.service.signin; + +import cn.aagro.pp.framework.common.pojo.PageParam; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.member.controller.admin.signin.vo.record.MemberSignInRecordPageReqVO; +import cn.aagro.pp.module.member.controller.app.signin.vo.record.AppMemberSignInRecordSummaryRespVO; +import cn.aagro.pp.module.member.dal.dataobject.signin.MemberSignInRecordDO; + +/** + * 签到记录 Service 接口 + * + * @author 芋道源码 + */ +public interface MemberSignInRecordService { + + /** + * 【管理员】获得签到记录分页 + * + * @param pageReqVO 分页查询 + * @return 签到记录分页 + */ + PageResult getSignInRecordPage(MemberSignInRecordPageReqVO pageReqVO); + + /** + * 【会员】获得签到记录分页 + * + * @param userId 用户编号 + * @param pageParam 分页查询 + * @return 签到记录分页 + */ + PageResult getSignRecordPage(Long userId, PageParam pageParam); + + /** + * 创建签到记录 + * + * @param userId 用户编号 + * @return 签到记录 + */ + MemberSignInRecordDO createSignRecord(Long userId); + + /** + * 根据用户编号,获得个人签到统计信息 + * + * @param userId 用户编号 + * @return 个人签到统计信息 + */ + AppMemberSignInRecordSummaryRespVO getSignInRecordSummary(Long userId); + + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/signin/MemberSignInRecordServiceImpl.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/signin/MemberSignInRecordServiceImpl.java new file mode 100644 index 0000000..3f9491c --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/signin/MemberSignInRecordServiceImpl.java @@ -0,0 +1,145 @@ +package cn.aagro.pp.module.member.service.signin; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjUtil; +import cn.aagro.pp.framework.common.enums.CommonStatusEnum; +import cn.aagro.pp.framework.common.pojo.PageParam; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.common.util.date.DateUtils; +import cn.aagro.pp.framework.common.util.object.ObjectUtils; +import cn.aagro.pp.module.member.controller.admin.signin.vo.record.MemberSignInRecordPageReqVO; +import cn.aagro.pp.module.member.controller.app.signin.vo.record.AppMemberSignInRecordSummaryRespVO; +import cn.aagro.pp.module.member.convert.signin.MemberSignInRecordConvert; +import cn.aagro.pp.module.member.dal.dataobject.signin.MemberSignInConfigDO; +import cn.aagro.pp.module.member.dal.dataobject.signin.MemberSignInRecordDO; +import cn.aagro.pp.module.member.dal.dataobject.user.MemberUserDO; +import cn.aagro.pp.module.member.dal.mysql.signin.MemberSignInRecordMapper; +import cn.aagro.pp.module.member.enums.MemberExperienceBizTypeEnum; +import cn.aagro.pp.module.member.enums.point.MemberPointBizTypeEnum; +import cn.aagro.pp.module.member.service.level.MemberLevelService; +import cn.aagro.pp.module.member.service.point.MemberPointRecordService; +import cn.aagro.pp.module.member.service.user.MemberUserService; +import org.apache.commons.lang3.StringUtils; +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.Set; + +import static cn.aagro.pp.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.aagro.pp.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.aagro.pp.module.member.enums.ErrorCodeConstants.SIGN_IN_RECORD_TODAY_EXISTS; + +/** + * 签到记录 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class MemberSignInRecordServiceImpl implements MemberSignInRecordService { + + @Resource + private MemberSignInRecordMapper signInRecordMapper; + @Resource + private MemberSignInConfigService signInConfigService; + @Resource + private MemberPointRecordService pointRecordService; + @Resource + private MemberLevelService memberLevelService; + + @Resource + private MemberUserService memberUserService; + + @Override + public AppMemberSignInRecordSummaryRespVO getSignInRecordSummary(Long userId) { + // 1. 初始化默认返回信息 + AppMemberSignInRecordSummaryRespVO summary = new AppMemberSignInRecordSummaryRespVO(); + summary.setTotalDay(0); + summary.setContinuousDay(0); + summary.setTodaySignIn(false); + + // 2. 获取用户签到的记录数 + Long signCount = signInRecordMapper.selectCountByUserId(userId); + if (ObjUtil.equal(signCount, 0L)) { + return summary; + } + summary.setTotalDay(signCount.intValue()); // 设置总签到天数 + + // 3. 校验当天是否有签到 + MemberSignInRecordDO lastRecord = signInRecordMapper.selectLastRecordByUserId(userId); + if (lastRecord == null) { + return summary; + } + summary.setTodaySignIn(DateUtils.isToday(lastRecord.getCreateTime())); + + // 4.1 检查今天是否未签到且记录不是昨天创建的,如果是则直接返回 + if (!summary.getTodaySignIn() && !DateUtils.isYesterday(lastRecord.getCreateTime())) { + return summary; + } + + // 4.2 要么是今天签到了,要么是昨天的记录,设置连续签到天数 + summary.setContinuousDay(lastRecord.getDay()); + return summary; + } + + @Override + public PageResult getSignInRecordPage(MemberSignInRecordPageReqVO pageReqVO) { + // 根据用户昵称查询出用户ids + Set userIds = null; + if (StringUtils.isNotBlank(pageReqVO.getNickname())) { + List users = memberUserService.getUserListByNickname(pageReqVO.getNickname()); + // 如果查询用户结果为空直接返回无需继续查询 + if (CollUtil.isEmpty(users)) { + return PageResult.empty(); + } + userIds = convertSet(users, MemberUserDO::getId); + } + // 分页查询 + return signInRecordMapper.selectPage(pageReqVO, userIds); + } + + @Override + public PageResult getSignRecordPage(Long userId, PageParam pageParam) { + return signInRecordMapper.selectPage(userId, pageParam); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public MemberSignInRecordDO createSignRecord(Long userId) { + // 1. 获取当前用户最近的签到 + MemberSignInRecordDO lastRecord = signInRecordMapper.selectLastRecordByUserId(userId); + // 1.1. 判断是否重复签到 + validateSigned(lastRecord); + + // 2.1. 获取所有的签到规则 + List signInConfigs = signInConfigService.getSignInConfigList(CommonStatusEnum.ENABLE.getStatus()); + // 2.2. 组合数据 + MemberSignInRecordDO record = MemberSignInRecordConvert.INSTANCE.convert(userId, lastRecord, signInConfigs); + + // 3. 插入签到记录 + signInRecordMapper.insert(record); + + // 4. 增加积分 + if (!ObjectUtils.equalsAny(record.getPoint(), null, 0)) { + pointRecordService.createPointRecord(userId, record.getPoint(), MemberPointBizTypeEnum.SIGN, String.valueOf(record.getId())); + } + // 5. 增加经验 + if (!ObjectUtils.equalsAny(record.getExperience(), null, 0)) { + memberLevelService.addExperience(userId, record.getExperience(), MemberExperienceBizTypeEnum.SIGN_IN, String.valueOf(record.getId())); + } + return record; + } + + private void validateSigned(MemberSignInRecordDO signInRecordDO) { + if (signInRecordDO == null) { + return; + } + if (DateUtils.isToday(signInRecordDO.getCreateTime())) { + throw exception(SIGN_IN_RECORD_TODAY_EXISTS); + } + } + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/tag/MemberTagService.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/tag/MemberTagService.java new file mode 100644 index 0000000..962ebe4 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/tag/MemberTagService.java @@ -0,0 +1,73 @@ +package cn.aagro.pp.module.member.service.tag; + +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.member.controller.admin.tag.vo.MemberTagCreateReqVO; +import cn.aagro.pp.module.member.controller.admin.tag.vo.MemberTagPageReqVO; +import cn.aagro.pp.module.member.controller.admin.tag.vo.MemberTagUpdateReqVO; +import cn.aagro.pp.module.member.dal.dataobject.tag.MemberTagDO; + +import javax.validation.Valid; +import java.util.Collection; +import java.util.List; + +/** + * 会员标签 Service 接口 + * + * @author 芋道源码 + */ +public interface MemberTagService { + + /** + * 创建会员标签 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createTag(@Valid MemberTagCreateReqVO createReqVO); + + /** + * 更新会员标签 + * + * @param updateReqVO 更新信息 + */ + void updateTag(@Valid MemberTagUpdateReqVO updateReqVO); + + /** + * 删除会员标签 + * + * @param id 编号 + */ + void deleteTag(Long id); + + /** + * 获得会员标签 + * + * @param id 编号 + * @return 会员标签 + */ + MemberTagDO getTag(Long id); + + /** + * 获得会员标签列表 + * + * @param ids 编号 + * @return 会员标签列表 + */ + List getTagList(Collection ids); + + /** + * 获得会员标签分页 + * + * @param pageReqVO 分页查询 + * @return 会员标签分页 + */ + PageResult getTagPage(MemberTagPageReqVO pageReqVO); + + /** + * 获取标签列表 + * + * @return 标签列表 + */ + List getTagList(); + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/tag/MemberTagServiceImpl.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/tag/MemberTagServiceImpl.java new file mode 100644 index 0000000..ecb297e --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/tag/MemberTagServiceImpl.java @@ -0,0 +1,125 @@ +package cn.aagro.pp.module.member.service.tag; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.util.StrUtil; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.member.controller.admin.tag.vo.MemberTagCreateReqVO; +import cn.aagro.pp.module.member.controller.admin.tag.vo.MemberTagPageReqVO; +import cn.aagro.pp.module.member.controller.admin.tag.vo.MemberTagUpdateReqVO; +import cn.aagro.pp.module.member.convert.tag.MemberTagConvert; +import cn.aagro.pp.module.member.dal.dataobject.tag.MemberTagDO; +import cn.aagro.pp.module.member.dal.mysql.tag.MemberTagMapper; +import cn.aagro.pp.module.member.service.user.MemberUserService; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.List; + +import static cn.aagro.pp.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.aagro.pp.module.member.enums.ErrorCodeConstants.*; + +/** + * 会员标签 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class MemberTagServiceImpl implements MemberTagService { + + @Resource + private MemberTagMapper memberTagMapper; + + @Resource + private MemberUserService memberUserService; + + @Override + public Long createTag(MemberTagCreateReqVO createReqVO) { + // 校验名称唯一 + validateTagNameUnique(null, createReqVO.getName()); + // 插入 + MemberTagDO tag = MemberTagConvert.INSTANCE.convert(createReqVO); + memberTagMapper.insert(tag); + // 返回 + return tag.getId(); + } + + @Override + public void updateTag(MemberTagUpdateReqVO updateReqVO) { + // 校验存在 + validateTagExists(updateReqVO.getId()); + // 校验名称唯一 + validateTagNameUnique(updateReqVO.getId(), updateReqVO.getName()); + // 更新 + MemberTagDO updateObj = MemberTagConvert.INSTANCE.convert(updateReqVO); + memberTagMapper.updateById(updateObj); + } + + @Override + public void deleteTag(Long id) { + // 校验存在 + validateTagExists(id); + // 校验标签下是否有用户 + validateTagHasUser(id); + // 删除 + memberTagMapper.deleteById(id); + } + + private void validateTagExists(Long id) { + if (memberTagMapper.selectById(id) == null) { + throw exception(TAG_NOT_EXISTS); + } + } + + private void validateTagNameUnique(Long id, String name) { + if (StrUtil.isBlank(name)) { + return; + } + MemberTagDO tag = memberTagMapper.selelctByName(name); + if (tag == null) { + return; + } + + // 如果 id 为空,说明不用比较是否为相同 id 的标签 + if (id == null) { + throw exception(TAG_NAME_EXISTS); + } + if (!tag.getId().equals(id)) { + throw exception(TAG_NAME_EXISTS); + } + } + + void validateTagHasUser(Long id) { + Long count = memberUserService.getUserCountByTagId(id); + if (count > 0) { + throw exception(TAG_HAS_USER); + } + } + + @Override + public MemberTagDO getTag(Long id) { + return memberTagMapper.selectById(id); + } + + @Override + public List getTagList(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return ListUtil.empty(); + } + return memberTagMapper.selectByIds(ids); + } + + @Override + public PageResult getTagPage(MemberTagPageReqVO pageReqVO) { + return memberTagMapper.selectPage(pageReqVO); + } + + @Override + public List getTagList() { + return memberTagMapper.selectList(); + } + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/user/MemberUserService.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/user/MemberUserService.java new file mode 100644 index 0000000..ac0be56 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/user/MemberUserService.java @@ -0,0 +1,190 @@ +package cn.aagro.pp.module.member.service.user; + +import cn.aagro.pp.framework.common.enums.TerminalEnum; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.common.validation.Mobile; +import cn.aagro.pp.module.member.controller.admin.user.vo.MemberUserPageReqVO; +import cn.aagro.pp.module.member.controller.admin.user.vo.MemberUserUpdateReqVO; +import cn.aagro.pp.module.member.controller.app.user.vo.*; +import cn.aagro.pp.module.member.dal.dataobject.user.MemberUserDO; + +import javax.validation.Valid; +import java.util.Collection; +import java.util.List; + +/** + * 会员用户 Service 接口 + * + * @author 芋道源码 + */ +public interface MemberUserService { + + /** + * 通过手机查询用户 + * + * @param mobile 手机 + * @return 用户对象 + */ + MemberUserDO getUserByMobile(String mobile); + + /** + * 基于用户昵称,模糊匹配用户列表 + * + * @param nickname 用户昵称,模糊匹配 + * @return 用户信息的列表 + */ + List getUserListByNickname(String nickname); + + /** + * 基于手机号创建用户。 + * 如果用户已经存在,则直接进行返回 + * + * @param mobile 手机号 + * @param registerIp 注册 IP + * @param terminal 终端 {@link TerminalEnum} + * @return 用户对象 + */ + MemberUserDO createUserIfAbsent(@Mobile String mobile, String registerIp, Integer terminal); + + /** + * 创建用户 + * 目的:三方登录时,如果未绑定用户时,自动创建对应用户 + * + * @param nickname 昵称 + * @param avtar 头像 + * @param registerIp 注册 IP + * @param terminal 终端 {@link TerminalEnum} + * @return 用户对象 + */ + MemberUserDO createUser(String nickname, String avtar, String registerIp, Integer terminal); + + /** + * 更新用户的最后登陆信息 + * + * @param id 用户编号 + * @param loginIp 登陆 IP + */ + void updateUserLogin(Long id, String loginIp); + + /** + * 通过用户 ID 查询用户 + * + * @param id 用户ID + * @return 用户对象信息 + */ + MemberUserDO getUser(Long id); + + /** + * 通过用户 ID 查询用户们 + * + * @param ids 用户 ID + * @return 用户对象信息数组 + */ + List getUserList(Collection ids); + + /** + * 【会员】修改基本信息 + * + * @param userId 用户编号 + * @param reqVO 基本信息 + */ + void updateUser(Long userId, AppMemberUserUpdateReqVO reqVO); + + /** + * 【会员】修改手机,基于手机验证码 + * + * @param userId 用户编号 + * @param reqVO 请求信息 + */ + void updateUserMobile(Long userId, AppMemberUserUpdateMobileReqVO reqVO); + + /** + * 【会员】修改手机,基于微信小程序的授权码 + * + * @param userId 用户编号 + * @param reqVO 请求信息 + */ + void updateUserMobileByWeixin(Long userId, AppMemberUserUpdateMobileByWeixinReqVO reqVO); + + /** + * 【会员】修改密码 + * + * @param userId 用户编号 + * @param reqVO 请求信息 + */ + void updateUserPassword(Long userId, AppMemberUserUpdatePasswordReqVO reqVO); + + /** + * 【会员】忘记密码 + * + * @param reqVO 请求信息 + */ + void resetUserPassword(AppMemberUserResetPasswordReqVO reqVO); + + /** + * 判断密码是否匹配 + * + * @param rawPassword 未加密的密码 + * @param encodedPassword 加密后的密码 + * @return 是否匹配 + */ + boolean isPasswordMatch(String rawPassword, String encodedPassword); + + /** + * 【管理员】更新会员用户 + * + * @param updateReqVO 更新信息 + */ + void updateUser(@Valid MemberUserUpdateReqVO updateReqVO); + + /** + * 【管理员】获得会员用户分页 + * + * @param pageReqVO 分页查询 + * @return 会员用户分页 + */ + PageResult getUserPage(MemberUserPageReqVO pageReqVO); + + /** + * 更新用户的等级和经验 + * + * @param id 用户编号 + * @param levelId 用户等级 + * @param experience 用户经验 + */ + void updateUserLevel(Long id, Long levelId, Integer experience); + + /** + * 获得指定用户分组下的用户数量 + * + * @param groupId 用户分组编号 + * @return 用户数量 + */ + Long getUserCountByGroupId(Long groupId); + + /** + * 获得指定用户等级下的用户数量 + * + * @param levelId 用户等级编号 + * @return 用户数量 + */ + Long getUserCountByLevelId(Long levelId); + + /** + * 获得指定会员标签下的用户数量 + * + * @param tagId 用户标签编号 + * @return 用户数量 + */ + Long getUserCountByTagId(Long tagId); + + /** + * 更新用户的积分 + * + * @param userId 用户编号 + * @param point 积分数量 + * @return 更新结果 + */ + boolean updateUserPoint(Long userId, Integer point); + +} diff --git a/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/user/MemberUserServiceImpl.java b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/user/MemberUserServiceImpl.java new file mode 100644 index 0000000..3eae266 --- /dev/null +++ b/aagro-module-member/src/main/java/cn/aagro/pp/module/member/service/user/MemberUserServiceImpl.java @@ -0,0 +1,317 @@ +package cn.aagro.pp.module.member.service.user; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.*; +import cn.aagro.pp.framework.common.enums.CommonStatusEnum; +import cn.aagro.pp.framework.common.enums.UserTypeEnum; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.common.util.object.BeanUtils; +import cn.aagro.pp.module.member.controller.admin.user.vo.MemberUserPageReqVO; +import cn.aagro.pp.module.member.controller.admin.user.vo.MemberUserUpdateReqVO; +import cn.aagro.pp.module.member.controller.app.user.vo.*; +import cn.aagro.pp.module.member.convert.auth.AuthConvert; +import cn.aagro.pp.module.member.convert.user.MemberUserConvert; +import cn.aagro.pp.module.member.dal.dataobject.user.MemberUserDO; +import cn.aagro.pp.module.member.dal.mysql.user.MemberUserMapper; +import cn.aagro.pp.module.member.mq.producer.user.MemberUserProducer; +import cn.aagro.pp.module.system.api.sms.SmsCodeApi; +import cn.aagro.pp.module.system.api.sms.dto.code.SmsCodeUseReqDTO; +import cn.aagro.pp.module.system.api.social.SocialClientApi; +import cn.aagro.pp.module.system.api.social.dto.SocialWxPhoneNumberInfoRespDTO; +import cn.aagro.pp.module.system.enums.sms.SmsSceneEnum; +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.List; + +import static cn.aagro.pp.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.aagro.pp.framework.common.util.servlet.ServletUtils.getClientIP; +import static cn.aagro.pp.module.member.enums.ErrorCodeConstants.*; + +/** + * 会员 User Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Valid +@Slf4j +public class MemberUserServiceImpl implements MemberUserService { + + @Resource + private MemberUserMapper memberUserMapper; + + @Resource + private SmsCodeApi smsCodeApi; + + @Resource + private SocialClientApi socialClientApi; + + @Resource + private PasswordEncoder passwordEncoder; + + @Resource + private MemberUserProducer memberUserProducer; + + @Override + public MemberUserDO getUserByMobile(String mobile) { + return memberUserMapper.selectByMobile(mobile); + } + + @Override + public List getUserListByNickname(String nickname) { + return memberUserMapper.selectListByNicknameLike(nickname); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public MemberUserDO createUserIfAbsent(String mobile, String registerIp, Integer terminal) { + // 用户已经存在 + MemberUserDO user = memberUserMapper.selectByMobile(mobile); + if (user != null) { + return user; + } + // 用户不存在,则进行创建 + return createUser(mobile, null, null, registerIp, terminal); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public MemberUserDO createUser(String nickname, String avtar, String registerIp, Integer terminal) { + return createUser(null, nickname, avtar, registerIp, terminal); + } + + private MemberUserDO createUser(String mobile, String nickname, String avtar, + String registerIp, Integer terminal) { + // 生成密码 + String password = IdUtil.fastSimpleUUID(); + // 插入用户 + MemberUserDO user = new MemberUserDO(); + user.setMobile(mobile); + user.setStatus(CommonStatusEnum.ENABLE.getStatus()); // 默认开启 + user.setPassword(encodePassword(password)); // 加密密码 + user.setRegisterIp(registerIp).setRegisterTerminal(terminal); + user.setNickname(nickname).setAvatar(avtar); // 基础信息 + if (StrUtil.isEmpty(nickname)) { + // 昵称为空时,随机一个名字,避免一些依赖 nickname 的逻辑报错,或者有点丑。例如说,短信发送有昵称时~ + user.setNickname("用户" + RandomUtil.randomNumbers(6)); + } + memberUserMapper.insert(user); + + // 发送 MQ 消息:用户创建 + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + + @Override + public void afterCommit() { + memberUserProducer.sendUserCreateMessage(user.getId()); + } + + }); + return user; + } + + @Override + public void updateUserLogin(Long id, String loginIp) { + memberUserMapper.updateById(new MemberUserDO().setId(id) + .setLoginIp(loginIp).setLoginDate(LocalDateTime.now())); + } + + @Override + public MemberUserDO getUser(Long id) { + return memberUserMapper.selectById(id); + } + + @Override + public List getUserList(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return ListUtil.empty(); + } + return memberUserMapper.selectByIds(ids); + } + + @Override + public void updateUser(Long userId, AppMemberUserUpdateReqVO reqVO) { + MemberUserDO updateObj = BeanUtils.toBean(reqVO, MemberUserDO.class).setId(userId); + memberUserMapper.updateById(updateObj); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateUserMobile(Long userId, AppMemberUserUpdateMobileReqVO reqVO) { + // 1.1 检测用户是否存在 + MemberUserDO user = validateUserExists(userId); + // 1.2 校验新手机是否已经被绑定 + validateMobileUnique(null, reqVO.getMobile()); + + // 2.1 校验旧手机和旧验证码 + // 补充说明:从安全性来说,老手机也校验 oldCode 验证码会更安全。但是由于 uni-app 商城界面暂时没做,所以这里不强制校验 + if (StrUtil.isNotEmpty(reqVO.getOldCode())) { + smsCodeApi.useSmsCode(new SmsCodeUseReqDTO().setMobile(user.getMobile()).setCode(reqVO.getOldCode()) + .setScene(SmsSceneEnum.MEMBER_UPDATE_MOBILE.getScene()).setUsedIp(getClientIP())); + } + // 2.2 使用新验证码 + smsCodeApi.useSmsCode(new SmsCodeUseReqDTO().setMobile(reqVO.getMobile()).setCode(reqVO.getCode()) + .setScene(SmsSceneEnum.MEMBER_UPDATE_MOBILE.getScene()).setUsedIp(getClientIP())); + + // 3. 更新用户手机 + memberUserMapper.updateById(MemberUserDO.builder().id(userId).mobile(reqVO.getMobile()).build()); + } + + @Override + public void updateUserMobileByWeixin(Long userId, AppMemberUserUpdateMobileByWeixinReqVO reqVO) { + // 1.1 获得对应的手机号信息 + SocialWxPhoneNumberInfoRespDTO phoneNumberInfo = socialClientApi.getWxMaPhoneNumberInfo( + UserTypeEnum.MEMBER.getValue(), reqVO.getCode()); + Assert.notNull(phoneNumberInfo, "获得手机信息失败,结果为空"); + // 1.2 校验新手机是否已经被绑定 + validateMobileUnique(userId, phoneNumberInfo.getPhoneNumber()); + + // 2. 更新用户手机 + memberUserMapper.updateById(MemberUserDO.builder().id(userId).mobile(phoneNumberInfo.getPhoneNumber()).build()); + } + + @Override + public void updateUserPassword(Long userId, AppMemberUserUpdatePasswordReqVO reqVO) { + // 检测用户是否存在 + MemberUserDO user = validateUserExists(userId); + // 校验验证码 + smsCodeApi.useSmsCode(new SmsCodeUseReqDTO().setMobile(user.getMobile()).setCode(reqVO.getCode()) + .setScene(SmsSceneEnum.MEMBER_UPDATE_PASSWORD.getScene()).setUsedIp(getClientIP())); + + // 更新用户密码 + memberUserMapper.updateById(MemberUserDO.builder().id(userId) + .password(passwordEncoder.encode(reqVO.getPassword())).build()); + } + + @Override + public void resetUserPassword(AppMemberUserResetPasswordReqVO reqVO) { + // 检验用户是否存在 + MemberUserDO user = validateUserExists(reqVO.getMobile()); + + // 使用验证码 + smsCodeApi.useSmsCode(AuthConvert.INSTANCE.convert(reqVO, SmsSceneEnum.MEMBER_RESET_PASSWORD, + getClientIP())); + + // 更新密码 + memberUserMapper.updateById(MemberUserDO.builder().id(user.getId()) + .password(passwordEncoder.encode(reqVO.getPassword())).build()); + } + + private MemberUserDO validateUserExists(String mobile) { + MemberUserDO user = memberUserMapper.selectByMobile(mobile); + if (user == null) { + throw exception(USER_MOBILE_NOT_EXISTS); + } + return user; + } + + @Override + public boolean isPasswordMatch(String rawPassword, String encodedPassword) { + return passwordEncoder.matches(rawPassword, encodedPassword); + } + + /** + * 对密码进行加密 + * + * @param password 密码 + * @return 加密后的密码 + */ + private String encodePassword(String password) { + return passwordEncoder.encode(password); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateUser(MemberUserUpdateReqVO updateReqVO) { + // 校验存在 + validateUserExists(updateReqVO.getId()); + // 校验手机唯一 + validateMobileUnique(updateReqVO.getId(), updateReqVO.getMobile()); + + // 更新 + MemberUserDO updateObj = MemberUserConvert.INSTANCE.convert(updateReqVO); + memberUserMapper.updateById(updateObj); + } + + @VisibleForTesting + MemberUserDO validateUserExists(Long id) { + if (id == null) { + return null; + } + MemberUserDO user = memberUserMapper.selectById(id); + if (user == null) { + throw exception(USER_NOT_EXISTS); + } + return user; + } + + @VisibleForTesting + void validateMobileUnique(Long id, String mobile) { + if (StrUtil.isBlank(mobile)) { + return; + } + MemberUserDO user = memberUserMapper.selectByMobile(mobile); + if (user == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的用户 + if (id == null) { + throw exception(USER_MOBILE_USED, mobile); + } + if (!user.getId().equals(id)) { + throw exception(USER_MOBILE_USED, mobile); + } + } + + @Override + public PageResult getUserPage(MemberUserPageReqVO pageReqVO) { + return memberUserMapper.selectPage(pageReqVO); + } + + @Override + public void updateUserLevel(Long id, Long levelId, Integer experience) { + // 0 代表无等级:防止UpdateById时,会被过滤掉的问题 + levelId = ObjectUtil.defaultIfNull(levelId, 0L); + memberUserMapper.updateById(new MemberUserDO() + .setId(id) + .setLevelId(levelId).setExperience(experience) + ); + } + + @Override + public Long getUserCountByGroupId(Long groupId) { + return memberUserMapper.selectCountByGroupId(groupId); + } + + @Override + public Long getUserCountByLevelId(Long levelId) { + return memberUserMapper.selectCountByLevelId(levelId); + } + + @Override + public Long getUserCountByTagId(Long tagId) { + return memberUserMapper.selectCountByTagId(tagId); + } + + @Override + public boolean updateUserPoint(Long id, Integer point) { + if (point > 0) { + memberUserMapper.updatePointIncr(id, point); + } else if (point < 0) { + return memberUserMapper.updatePointDecr(id, point) > 0; + } + return true; + } + +} diff --git a/aagro-module-member/src/test/java/cn/aagro/pp/module/member/service/address/MemberAddressServiceImplTest.java b/aagro-module-member/src/test/java/cn/aagro/pp/module/member/service/address/MemberAddressServiceImplTest.java new file mode 100644 index 0000000..6e22b2a --- /dev/null +++ b/aagro-module-member/src/test/java/cn/aagro/pp/module/member/service/address/MemberAddressServiceImplTest.java @@ -0,0 +1,98 @@ +package cn.aagro.pp.module.member.service.address; + +import cn.aagro.pp.framework.test.core.ut.BaseDbUnitTest; +import cn.aagro.pp.module.member.controller.app.address.vo.AppAddressCreateReqVO; +import cn.aagro.pp.module.member.controller.app.address.vo.AppAddressUpdateReqVO; +import cn.aagro.pp.module.member.dal.dataobject.address.MemberAddressDO; +import cn.aagro.pp.module.member.dal.mysql.address.MemberAddressMapper; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; + +import static cn.aagro.pp.framework.test.core.util.AssertUtils.assertPojoEquals; +import static cn.aagro.pp.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.aagro.pp.framework.test.core.util.RandomUtils.randomLongId; +import static cn.aagro.pp.framework.test.core.util.RandomUtils.randomPojo; +import static cn.aagro.pp.module.member.enums.ErrorCodeConstants.ADDRESS_NOT_EXISTS; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +/** + * {@link AddressServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(AddressServiceImpl.class) +public class MemberAddressServiceImplTest extends BaseDbUnitTest { + + @Resource + private AddressServiceImpl addressService; + + @Resource + private MemberAddressMapper addressMapper; + + @Test + public void testCreateAddress_success() { + // 准备参数 + AppAddressCreateReqVO reqVO = randomPojo(AppAddressCreateReqVO.class); + + // 调用 + Long addressId = addressService.createAddress(randomLongId(), reqVO); + // 断言 + assertNotNull(addressId); + // 校验记录的属性是否正确 + MemberAddressDO address = addressMapper.selectById(addressId); + assertPojoEquals(reqVO, address); + } + + @Test + public void testUpdateAddress_success() { + // mock 数据 + MemberAddressDO dbAddress = randomPojo(MemberAddressDO.class); + addressMapper.insert(dbAddress);// @Sql: 先插入出一条存在的数据 + // 准备参数 + AppAddressUpdateReqVO reqVO = randomPojo(AppAddressUpdateReqVO.class, o -> { + o.setId(dbAddress.getId()); // 设置更新的 ID + }); + + // 调用 + addressService.updateAddress(dbAddress.getUserId(), reqVO); + // 校验是否更新正确 + MemberAddressDO address = addressMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, address); + } + + @Test + public void testUpdateAddress_notExists() { + // 准备参数 + AppAddressUpdateReqVO reqVO = randomPojo(AppAddressUpdateReqVO.class); + + // 调用, 并断言异常 + assertServiceException(() -> addressService.updateAddress(randomLongId(), reqVO), ADDRESS_NOT_EXISTS); + } + + @Test + public void testDeleteAddress_success() { + // mock 数据 + MemberAddressDO dbAddress = randomPojo(MemberAddressDO.class); + addressMapper.insert(dbAddress);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbAddress.getId(); + + // 调用 + addressService.deleteAddress(dbAddress.getUserId(), id); + // 校验数据不存在了 + assertNull(addressMapper.selectById(id)); + } + + @Test + public void testDeleteAddress_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> addressService.deleteAddress(randomLongId(), id), ADDRESS_NOT_EXISTS); + } + +} diff --git a/aagro-module-member/src/test/java/cn/aagro/pp/module/member/service/auth/MemberAuthServiceTest.java b/aagro-module-member/src/test/java/cn/aagro/pp/module/member/service/auth/MemberAuthServiceTest.java new file mode 100644 index 0000000..92b2408 --- /dev/null +++ b/aagro-module-member/src/test/java/cn/aagro/pp/module/member/service/auth/MemberAuthServiceTest.java @@ -0,0 +1,118 @@ +package cn.aagro.pp.module.member.service.auth; + +import cn.aagro.pp.framework.common.biz.system.oauth2.OAuth2TokenCommonApi; +import cn.aagro.pp.framework.common.enums.CommonStatusEnum; +import cn.aagro.pp.framework.common.util.collection.ArrayUtils; +import cn.aagro.pp.framework.redis.config.AagroRedisAutoConfiguration; +import cn.aagro.pp.framework.test.core.ut.BaseDbAndRedisUnitTest; +import cn.aagro.pp.module.member.dal.dataobject.user.MemberUserDO; +import cn.aagro.pp.module.member.dal.mysql.user.MemberUserMapper; +import cn.aagro.pp.module.member.service.user.MemberUserService; +import cn.aagro.pp.module.system.api.logger.LoginLogApi; +import cn.aagro.pp.module.system.api.sms.SmsCodeApi; +import cn.aagro.pp.module.system.api.social.SocialUserApi; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.security.crypto.password.PasswordEncoder; + +import javax.annotation.Resource; +import java.util.function.Consumer; + +import static cn.hutool.core.util.RandomUtil.randomEle; +import static cn.aagro.pp.framework.test.core.util.RandomUtils.randomPojo; +import static cn.aagro.pp.framework.test.core.util.RandomUtils.randomString; + +// TODO @芋艿:单测的 review,等逻辑都达成一致后 +/** + * {@link MemberAuthService} 的单元测试类 + * + * @author 宋天 + */ +@Import({MemberAuthServiceImpl.class, AagroRedisAutoConfiguration.class}) +public class MemberAuthServiceTest extends BaseDbAndRedisUnitTest { + + // TODO @芋艿:登录相关的单测,待补全 + + @Resource + private MemberAuthServiceImpl authService; + + @MockBean + private MemberUserService userService; + @MockBean + private SmsCodeApi smsCodeApi; + @MockBean + private LoginLogApi loginLogApi; + @MockBean + private OAuth2TokenCommonApi oauth2TokenApi; + @MockBean + private SocialUserApi socialUserApi; + @MockBean + private PasswordEncoder passwordEncoder; + + @Resource + private MemberUserMapper memberUserMapper; + + // TODO 芋艿:后续重构这个单测 +// @Test +// public void testUpdatePassword_success(){ +// // 准备参数 +// MemberUserDO userDO = randomUserDO(); +// memberUserMapper.insert(userDO); +// +// // 新密码 +// String newPassword = randomString(); +// +// // 请求实体 +// AppMemberUserUpdatePasswordReqVO reqVO = AppMemberUserUpdatePasswordReqVO.builder() +// .oldPassword(userDO.getPassword()) +// .password(newPassword) +// .build(); +// +// // 测试桩 +// // 这两个相等是为了返回ture这个结果 +// when(passwordEncoder.matches(reqVO.getOldPassword(),reqVO.getOldPassword())).thenReturn(true); +// when(passwordEncoder.encode(newPassword)).thenReturn(newPassword); +// +// // 更新用户密码 +// authService.updatePassword(userDO.getId(), reqVO); +// assertEquals(memberUserMapper.selectById(userDO.getId()).getPassword(),newPassword); +// } + + // TODO 芋艿:后续重构这个单测 +// @Test +// public void testResetPassword_success(){ +// // 准备参数 +// MemberUserDO userDO = randomUserDO(); +// memberUserMapper.insert(userDO); +// +// // 随机密码 +// String password = randomNumbers(11); +// // 随机验证码 +// String code = randomNumbers(4); +// +// // mock +// when(passwordEncoder.encode(password)).thenReturn(password); +// +// // 更新用户密码 +// AppMemberUserResetPasswordReqVO reqVO = new AppMemberUserResetPasswordReqVO(); +// reqVO.setMobile(userDO.getMobile()); +// reqVO.setPassword(password); +// reqVO.setCode(code); +// +// authService.resetPassword(reqVO); +// assertEquals(memberUserMapper.selectById(userDO.getId()).getPassword(),password); +// } + + // ========== 随机对象 ========== + + @SafeVarargs + private static MemberUserDO randomUserDO(Consumer... consumers) { + Consumer consumer = (o) -> { + o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); // 保证 status 的范围 + o.setPassword(randomString()); + }; + return randomPojo(MemberUserDO.class, ArrayUtils.append(consumer, consumers)); + } + + +} diff --git a/aagro-module-member/src/test/java/cn/aagro/pp/module/member/service/group/MemberGroupServiceImplTest.java b/aagro-module-member/src/test/java/cn/aagro/pp/module/member/service/group/MemberGroupServiceImplTest.java new file mode 100644 index 0000000..8930e0d --- /dev/null +++ b/aagro-module-member/src/test/java/cn/aagro/pp/module/member/service/group/MemberGroupServiceImplTest.java @@ -0,0 +1,160 @@ +package cn.aagro.pp.module.member.service.group; + +import cn.aagro.pp.framework.common.enums.CommonStatusEnum; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.test.core.ut.BaseDbUnitTest; +import cn.aagro.pp.module.member.controller.admin.group.vo.MemberGroupCreateReqVO; +import cn.aagro.pp.module.member.controller.admin.group.vo.MemberGroupPageReqVO; +import cn.aagro.pp.module.member.controller.admin.group.vo.MemberGroupUpdateReqVO; +import cn.aagro.pp.module.member.dal.dataobject.group.MemberGroupDO; +import cn.aagro.pp.module.member.dal.mysql.group.MemberGroupMapper; +import cn.aagro.pp.module.member.service.user.MemberUserService; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; + +import static cn.aagro.pp.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime; +import static cn.aagro.pp.framework.common.util.date.LocalDateTimeUtils.buildTime; +import static cn.aagro.pp.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static cn.aagro.pp.framework.test.core.util.AssertUtils.assertPojoEquals; +import static cn.aagro.pp.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.aagro.pp.framework.test.core.util.RandomUtils.*; +import static cn.aagro.pp.module.member.enums.ErrorCodeConstants.GROUP_HAS_USER; +import static cn.aagro.pp.module.member.enums.ErrorCodeConstants.GROUP_NOT_EXISTS; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +// TODO 芋艿:完全 review 完,在去 review 单测 +/** + * {@link MemberGroupServiceImpl} 的单元测试类 + * + * @author owen + */ +@Import(MemberGroupServiceImpl.class) +public class MemberGroupServiceImplTest extends BaseDbUnitTest { + + @Resource + private MemberGroupServiceImpl groupService; + + @Resource + private MemberGroupMapper groupMapper; + + @MockBean + private MemberUserService memberUserService; + + @Test + public void testCreateGroup_success() { + // 准备参数 + MemberGroupCreateReqVO reqVO = randomPojo(MemberGroupCreateReqVO.class, + o -> o.setStatus(randomCommonStatus())); + + // 调用 + Long groupId = groupService.createGroup(reqVO); + // 断言 + assertNotNull(groupId); + // 校验记录的属性是否正确 + MemberGroupDO group = groupMapper.selectById(groupId); + assertPojoEquals(reqVO, group); + } + + @Test + public void testUpdateGroup_success() { + // mock 数据 + MemberGroupDO dbGroup = randomPojo(MemberGroupDO.class); + groupMapper.insert(dbGroup);// @Sql: 先插入出一条存在的数据 + // 准备参数 + MemberGroupUpdateReqVO reqVO = randomPojo(MemberGroupUpdateReqVO.class, o -> { + o.setId(dbGroup.getId()); // 设置更新的 ID + o.setStatus(randomCommonStatus()); + }); + + // 调用 + groupService.updateGroup(reqVO); + // 校验是否更新正确 + MemberGroupDO group = groupMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, group); + } + + @Test + public void testUpdateGroup_notExists() { + // 准备参数 + MemberGroupUpdateReqVO reqVO = randomPojo(MemberGroupUpdateReqVO.class); + + // 调用, 并断言异常 + assertServiceException(() -> groupService.updateGroup(reqVO), GROUP_NOT_EXISTS); + } + + @Test + public void testDeleteGroup_success() { + // mock 数据 + MemberGroupDO dbGroup = randomPojo(MemberGroupDO.class); + groupMapper.insert(dbGroup);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbGroup.getId(); + + // 调用 + groupService.deleteGroup(id); + // 校验数据不存在了 + assertNull(groupMapper.selectById(id)); + } + + @Test + public void testDeleteGroup_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> groupService.deleteGroup(id), GROUP_NOT_EXISTS); + } + + @Test + public void testDeleteGroup_hasUser() { + // mock 数据 + MemberGroupDO dbGroup = randomPojo(MemberGroupDO.class); + groupMapper.insert(dbGroup);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbGroup.getId(); + + // mock 会员数据 + when(memberUserService.getUserCountByGroupId(eq(id))).thenReturn(1L); + + // 调用, 并断言异常 + assertServiceException(() -> groupService.deleteGroup(id), GROUP_HAS_USER); + } + + @Test + public void testGetGroupPage() { + String name = randomString(); + int status = CommonStatusEnum.ENABLE.getStatus(); + + // mock 数据 + MemberGroupDO dbGroup = randomPojo(MemberGroupDO.class, o -> { // 等会查询到 + o.setName(name); + o.setStatus(status); + o.setCreateTime(buildTime(2023, 2, 18)); + }); + groupMapper.insert(dbGroup); + // 测试 name 不匹配 + groupMapper.insert(cloneIgnoreId(dbGroup, o -> o.setName(""))); + // 测试 status 不匹配 + groupMapper.insert(cloneIgnoreId(dbGroup, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + // 测试 createTime 不匹配 + groupMapper.insert(cloneIgnoreId(dbGroup, o -> o.setCreateTime(null))); + // 准备参数 + MemberGroupPageReqVO reqVO = new MemberGroupPageReqVO(); + reqVO.setName(name); + reqVO.setStatus(status); + reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28)); + + // 调用 + PageResult pageResult = groupService.getGroupPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbGroup, pageResult.getList().get(0)); + } + +} diff --git a/aagro-module-member/src/test/java/cn/aagro/pp/module/member/service/level/MemberLevelServiceImplTest.java b/aagro-module-member/src/test/java/cn/aagro/pp/module/member/service/level/MemberLevelServiceImplTest.java new file mode 100644 index 0000000..343639c --- /dev/null +++ b/aagro-module-member/src/test/java/cn/aagro/pp/module/member/service/level/MemberLevelServiceImplTest.java @@ -0,0 +1,268 @@ +package cn.aagro.pp.module.member.service.level; + +import cn.aagro.pp.framework.common.enums.CommonStatusEnum; +import cn.aagro.pp.framework.common.util.collection.ArrayUtils; +import cn.aagro.pp.framework.test.core.ut.BaseDbUnitTest; +import cn.aagro.pp.module.member.controller.admin.level.vo.level.MemberLevelCreateReqVO; +import cn.aagro.pp.module.member.controller.admin.level.vo.level.MemberLevelListReqVO; +import cn.aagro.pp.module.member.controller.admin.level.vo.level.MemberLevelUpdateReqVO; +import cn.aagro.pp.module.member.dal.dataobject.level.MemberLevelDO; +import cn.aagro.pp.module.member.dal.mysql.level.MemberLevelMapper; +import cn.aagro.pp.module.member.service.user.MemberUserService; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.List; +import java.util.function.Consumer; + +import static cn.hutool.core.util.RandomUtil.randomInt; +import static cn.aagro.pp.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static cn.aagro.pp.framework.test.core.util.AssertUtils.assertPojoEquals; +import static cn.aagro.pp.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.aagro.pp.framework.test.core.util.RandomUtils.*; +import static cn.aagro.pp.module.member.enums.ErrorCodeConstants.*; +import static org.junit.jupiter.api.Assertions.*; + +// TODO 芋艿:完全 review 完,在去 review 单测 +/** + * {@link MemberLevelServiceImpl} 的单元测试类 + * + * @author owen + */ +@Import(MemberLevelServiceImpl.class) +public class MemberLevelServiceImplTest extends BaseDbUnitTest { + + @Resource + private MemberLevelServiceImpl levelService; + + @Resource + private MemberLevelMapper memberlevelMapper; + + @MockBean + private MemberLevelRecordService memberLevelRecordService; + @MockBean + private MemberExperienceRecordService memberExperienceRecordService; + @MockBean + private MemberUserService memberUserService; + + @Test + public void testCreateLevel_success() { + // 准备参数 + MemberLevelCreateReqVO reqVO = randomPojo(MemberLevelCreateReqVO.class, o -> { + o.setDiscountPercent(randomInt()); + o.setIcon(randomURL()); + o.setBackgroundUrl(randomURL()); + o.setStatus(randomCommonStatus()); + }); + + // 调用 + Long levelId = levelService.createLevel(reqVO); + // 断言 + assertNotNull(levelId); + // 校验记录的属性是否正确 + MemberLevelDO level = memberlevelMapper.selectById(levelId); + assertPojoEquals(reqVO, level); + } + + @Test + public void testUpdateLevel_success() { + // mock 数据 + MemberLevelDO dbLevel = randomPojo(MemberLevelDO.class); + memberlevelMapper.insert(dbLevel);// @Sql: 先插入出一条存在的数据 + // 准备参数 + MemberLevelUpdateReqVO reqVO = randomPojo(MemberLevelUpdateReqVO.class, o -> { + o.setId(dbLevel.getId()); // 设置更新的 ID + //以下要保持一致 + o.setName(dbLevel.getName()); + o.setLevel(dbLevel.getLevel()); + o.setExperience(dbLevel.getExperience()); + //以下是要修改的字段 + o.setDiscountPercent(randomInt()); + o.setIcon(randomURL()); + o.setBackgroundUrl(randomURL()); + o.setStatus(randomCommonStatus()); + }); + + // 调用 + levelService.updateLevel(reqVO); + // 校验是否更新正确 + MemberLevelDO level = memberlevelMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, level); + } + + @Test + public void testUpdateLevel_notExists() { + // 准备参数 + MemberLevelUpdateReqVO reqVO = randomPojo(MemberLevelUpdateReqVO.class); + + // 调用, 并断言异常 + assertServiceException(() -> levelService.updateLevel(reqVO), LEVEL_NOT_EXISTS); + } + + @Test + public void testDeleteLevel_success() { + // mock 数据 + MemberLevelDO dbLevel = randomPojo(MemberLevelDO.class); + memberlevelMapper.insert(dbLevel);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbLevel.getId(); + + // 调用 + levelService.deleteLevel(id); + // 校验数据不存在了 + assertNull(memberlevelMapper.selectById(id)); + } + + @Test + public void testDeleteLevel_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> levelService.deleteLevel(id), LEVEL_NOT_EXISTS); + } + + @Test + public void testGetLevelList() { + // mock 数据 + MemberLevelDO dbLevel = randomPojo(MemberLevelDO.class, o -> { // 等会查询到 + o.setName("黄金会员"); + o.setStatus(1); + }); + memberlevelMapper.insert(dbLevel); + // 测试 name 不匹配 + memberlevelMapper.insert(cloneIgnoreId(dbLevel, o -> o.setName(""))); + // 测试 status 不匹配 + memberlevelMapper.insert(cloneIgnoreId(dbLevel, o -> o.setStatus(0))); + // 准备参数 + MemberLevelListReqVO reqVO = new MemberLevelListReqVO(); + reqVO.setName("黄金会员"); + reqVO.setStatus(1); + + // 调用 + List list = levelService.getLevelList(reqVO); + // 断言 + assertEquals(1, list.size()); + assertPojoEquals(dbLevel, list.get(0)); + } + + @Test + public void testCreateLevel_nameUnique() { + // 准备参数 + String name = randomString(); + + // mock 数据 + memberlevelMapper.insert(randomLevelDO(o -> o.setName(name))); + + // 调用,校验异常 + List list = memberlevelMapper.selectList(); + assertServiceException(() -> levelService.validateNameUnique(list, null, name), LEVEL_NAME_EXISTS, name); + } + + @Test + public void testUpdateLevel_nameUnique() { + // 准备参数 + Long id = randomLongId(); + String name = randomString(); + + // mock 数据 + memberlevelMapper.insert(randomLevelDO(o -> o.setName(name))); + + // 调用,校验异常 + List list = memberlevelMapper.selectList(); + assertServiceException(() -> levelService.validateNameUnique(list, id, name), LEVEL_NAME_EXISTS, name); + } + + @Test + public void testCreateLevel_levelUnique() { + // 准备参数 + Integer level = randomInteger(); + String name = randomString(); + + // mock 数据 + memberlevelMapper.insert(randomLevelDO(o -> { + o.setLevel(level); + o.setName(name); + })); + + // 调用,校验异常 + List list = memberlevelMapper.selectList(); + assertServiceException(() -> levelService.validateLevelUnique(list, null, level), LEVEL_VALUE_EXISTS, level, name); + } + + @Test + public void testUpdateLevel_levelUnique() { + // 准备参数 + Long id = randomLongId(); + Integer level = randomInteger(); + String name = randomString(); + + // mock 数据 + memberlevelMapper.insert(randomLevelDO(o -> { + o.setLevel(level); + o.setName(name); + })); + + // 调用,校验异常 + List list = memberlevelMapper.selectList(); + assertServiceException(() -> levelService.validateLevelUnique(list, id, level), LEVEL_VALUE_EXISTS, level, name); + } + + @Test + public void testCreateLevel_experienceOutRange() { + // 准备参数 + int level = 10; + int experience = 10; + String name = randomString(); + + // mock 数据 + memberlevelMapper.insert(randomLevelDO(o -> { + o.setLevel(level); + o.setExperience(experience); + o.setName(name); + })); + List list = memberlevelMapper.selectList(); + + // 调用,校验异常 + assertServiceException(() -> levelService.validateExperienceOutRange(list, null, level + 1, experience - 1), LEVEL_EXPERIENCE_MIN, name, level); + // 调用,校验异常 + assertServiceException(() -> levelService.validateExperienceOutRange(list, null, level - 1, experience + 1), LEVEL_EXPERIENCE_MAX, name, level); + } + + @Test + public void testUpdateLevel_experienceOutRange() { + // 准备参数 + int level = 10; + int experience = 10; + Long id = randomLongId(); + String name = randomString(); + + // mock 数据 + memberlevelMapper.insert(randomLevelDO(o -> { + o.setLevel(level); + o.setExperience(experience); + o.setName(name); + })); + List list = memberlevelMapper.selectList(); + + // 调用,校验异常 + assertServiceException(() -> levelService.validateExperienceOutRange(list, id, level + 1, experience - 1), LEVEL_EXPERIENCE_MIN, name, level); + // 调用,校验异常 + assertServiceException(() -> levelService.validateExperienceOutRange(list, id, level - 1, experience + 1), LEVEL_EXPERIENCE_MAX, name, level); + } + + // ========== 随机对象 ========== + + @SafeVarargs + private static MemberLevelDO randomLevelDO(Consumer... consumers) { + Consumer consumer = (o) -> { + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setDiscountPercent(randomInt(0, 100)); + o.setIcon(randomURL()); + o.setBackgroundUrl(randomURL()); + }; + return randomPojo(MemberLevelDO.class, ArrayUtils.append(consumer, consumers)); + } +} diff --git a/aagro-module-member/src/test/java/cn/aagro/pp/module/member/service/tag/MemberTagServiceImplTest.java b/aagro-module-member/src/test/java/cn/aagro/pp/module/member/service/tag/MemberTagServiceImplTest.java new file mode 100644 index 0000000..ac73e22 --- /dev/null +++ b/aagro-module-member/src/test/java/cn/aagro/pp/module/member/service/tag/MemberTagServiceImplTest.java @@ -0,0 +1,133 @@ +package cn.aagro.pp.module.member.service.tag; + +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.test.core.ut.BaseDbUnitTest; +import cn.aagro.pp.module.member.controller.admin.tag.vo.MemberTagCreateReqVO; +import cn.aagro.pp.module.member.controller.admin.tag.vo.MemberTagPageReqVO; +import cn.aagro.pp.module.member.controller.admin.tag.vo.MemberTagUpdateReqVO; +import cn.aagro.pp.module.member.dal.dataobject.tag.MemberTagDO; +import cn.aagro.pp.module.member.dal.mysql.tag.MemberTagMapper; +import cn.aagro.pp.module.member.service.user.MemberUserService; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; + +import static cn.aagro.pp.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime; +import static cn.aagro.pp.framework.common.util.date.LocalDateTimeUtils.buildTime; +import static cn.aagro.pp.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static cn.aagro.pp.framework.test.core.util.AssertUtils.assertPojoEquals; +import static cn.aagro.pp.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.aagro.pp.framework.test.core.util.RandomUtils.randomLongId; +import static cn.aagro.pp.framework.test.core.util.RandomUtils.randomPojo; +import static cn.aagro.pp.module.member.enums.ErrorCodeConstants.TAG_NOT_EXISTS; +import static org.junit.jupiter.api.Assertions.*; + +// TODO 芋艿:完全 review 完,在去 review 单测 +/** + * {@link MemberTagServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(MemberTagServiceImpl.class) +public class MemberTagServiceImplTest extends BaseDbUnitTest { + + @Resource + private MemberTagServiceImpl tagService; + + @Resource + private MemberTagMapper tagMapper; + + @MockBean + private MemberUserService memberUserService; + + @Test + public void testCreateTag_success() { + // 准备参数 + MemberTagCreateReqVO reqVO = randomPojo(MemberTagCreateReqVO.class); + + // 调用 + Long tagId = tagService.createTag(reqVO); + // 断言 + assertNotNull(tagId); + // 校验记录的属性是否正确 + MemberTagDO tag = tagMapper.selectById(tagId); + assertPojoEquals(reqVO, tag); + } + + @Test + public void testUpdateTag_success() { + // mock 数据 + MemberTagDO dbTag = randomPojo(MemberTagDO.class); + tagMapper.insert(dbTag);// @Sql: 先插入出一条存在的数据 + // 准备参数 + MemberTagUpdateReqVO reqVO = randomPojo(MemberTagUpdateReqVO.class, o -> { + o.setId(dbTag.getId()); // 设置更新的 ID + }); + + // 调用 + tagService.updateTag(reqVO); + // 校验是否更新正确 + MemberTagDO tag = tagMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, tag); + } + + @Test + public void testUpdateTag_notExists() { + // 准备参数 + MemberTagUpdateReqVO reqVO = randomPojo(MemberTagUpdateReqVO.class); + + // 调用, 并断言异常 + assertServiceException(() -> tagService.updateTag(reqVO), TAG_NOT_EXISTS); + } + + @Test + public void testDeleteTag_success() { + // mock 数据 + MemberTagDO dbTag = randomPojo(MemberTagDO.class); + tagMapper.insert(dbTag);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbTag.getId(); + + // 调用 + tagService.deleteTag(id); + // 校验数据不存在了 + assertNull(tagMapper.selectById(id)); + } + + @Test + public void testDeleteTag_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> tagService.deleteTag(id), TAG_NOT_EXISTS); + } + + @Test + public void testGetTagPage() { + // mock 数据 + MemberTagDO dbTag = randomPojo(MemberTagDO.class, o -> { // 等会查询到 + o.setName("test"); + o.setCreateTime(buildTime(2023, 2, 18)); + }); + tagMapper.insert(dbTag); + // 测试 name 不匹配 + tagMapper.insert(cloneIgnoreId(dbTag, o -> o.setName("ne"))); + // 测试 createTime 不匹配 + tagMapper.insert(cloneIgnoreId(dbTag, o -> o.setCreateTime(null))); + // 准备参数 + MemberTagPageReqVO reqVO = new MemberTagPageReqVO(); + reqVO.setName("test"); + reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28)); + + // 调用 + PageResult pageResult = tagService.getTagPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbTag, pageResult.getList().get(0)); + } + +} diff --git a/aagro-module-member/src/test/java/cn/aagro/pp/module/member/service/user/MemberUserServiceImplTest.java b/aagro-module-member/src/test/java/cn/aagro/pp/module/member/service/user/MemberUserServiceImplTest.java new file mode 100644 index 0000000..0fc36f9 --- /dev/null +++ b/aagro-module-member/src/test/java/cn/aagro/pp/module/member/service/user/MemberUserServiceImplTest.java @@ -0,0 +1,115 @@ +package cn.aagro.pp.module.member.service.user; + +import cn.hutool.core.util.RandomUtil; +import cn.aagro.pp.framework.common.enums.CommonStatusEnum; +import cn.aagro.pp.framework.common.util.collection.ArrayUtils; +import cn.aagro.pp.framework.redis.config.AagroRedisAutoConfiguration; +import cn.aagro.pp.framework.test.core.ut.BaseDbAndRedisUnitTest; +import cn.aagro.pp.module.member.controller.app.user.vo.AppMemberUserUpdateMobileReqVO; +import cn.aagro.pp.module.member.dal.dataobject.user.MemberUserDO; +import cn.aagro.pp.module.member.dal.mysql.user.MemberUserMapper; +import cn.aagro.pp.module.member.service.auth.MemberAuthServiceImpl; +import cn.aagro.pp.module.system.api.sms.SmsCodeApi; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.security.crypto.password.PasswordEncoder; + +import javax.annotation.Resource; +import java.util.function.Consumer; + +import static cn.hutool.core.util.RandomUtil.randomEle; +import static cn.hutool.core.util.RandomUtil.randomNumbers; +import static cn.aagro.pp.framework.test.core.util.RandomUtils.randomPojo; +import static org.junit.jupiter.api.Assertions.assertEquals; + +// TODO @芋艿:单测的 review,等逻辑都达成一致后 +/** + * {@link MemberUserServiceImpl} 的单元测试类 + * + * @author 宋天 + */ +@Disabled +@Import({MemberUserServiceImpl.class, AagroRedisAutoConfiguration.class}) +public class MemberUserServiceImplTest extends BaseDbAndRedisUnitTest { + + @Resource + private MemberUserServiceImpl memberUserService; + + @Resource + private StringRedisTemplate stringRedisTemplate; + + @Resource + private MemberUserMapper userMapper; + + @MockBean + private MemberAuthServiceImpl authService; + + @MockBean + private PasswordEncoder passwordEncoder; + + @MockBean + private SmsCodeApi smsCodeApi; + + // TODO 芋艿:后续重构这个单测 +// @Test +// public void testUpdateNickName_success(){ +// // mock 数据 +// MemberUserDO userDO = randomUserDO(); +// userMapper.insert(userDO); +// +// // 随机昵称 +// String newNickName = randomString(); +// +// // 调用接口修改昵称 +// memberUserService.updateUser(userDO.getId(),newNickName); +// // 查询新修改后的昵称 +// String nickname = memberUserService.getUser(userDO.getId()).getNickname(); +// // 断言 +// assertEquals(newNickName,nickname); +// } + + @Test + @Disabled // TODO 芋艿:后续再修复 + public void updateMobile_success(){ + // mock数据 + String oldMobile = randomNumbers(11); + MemberUserDO userDO = randomUserDO(); + userDO.setMobile(oldMobile); + userMapper.insert(userDO); + + // TODO 芋艿:需要修复该单元测试,重构多模块带来的 + // 旧手机和旧验证码 +// SmsCodeDO codeDO = new SmsCodeDO(); + String oldCode = RandomUtil.randomString(4); +// codeDO.setMobile(userDO.getMobile()); +// codeDO.setCode(oldCode); +// codeDO.setScene(SmsSceneEnum.MEMBER_UPDATE_MOBILE.getScene()); +// codeDO.setUsed(Boolean.FALSE); +// when(smsCodeService.checkCodeIsExpired(codeDO.getMobile(),codeDO.getCode(),codeDO.getScene())).thenReturn(codeDO); + + // 更新手机号 + String newMobile = randomNumbers(11); + String newCode = randomNumbers(4); + AppMemberUserUpdateMobileReqVO reqVO = new AppMemberUserUpdateMobileReqVO(); + reqVO.setMobile(newMobile); + reqVO.setCode(newCode); + reqVO.setOldCode(oldCode); + memberUserService.updateUserMobile(userDO.getId(),reqVO); + + assertEquals(memberUserService.getUser(userDO.getId()).getMobile(),newMobile); + } + + // ========== 随机对象 ========== + + @SafeVarargs + private static MemberUserDO randomUserDO(Consumer... consumers) { + Consumer consumer = (o) -> { + o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); // 保证 status 的范围 + }; + return randomPojo(MemberUserDO.class, ArrayUtils.append(consumer, consumers)); + } + +} diff --git a/aagro-module-member/src/test/resources/application-unit-test.yaml b/aagro-module-member/src/test/resources/application-unit-test.yaml new file mode 100644 index 0000000..b901462 --- /dev/null +++ b/aagro-module-member/src/test/resources/application-unit-test.yaml @@ -0,0 +1,47 @@ +spring: + main: + lazy-initialization: true # 开启懒加载,加快速度 + banner-mode: off # 单元测试,禁用 Banner + +--- #################### 数据库相关配置 #################### + +spring: + # 数据源配置项 + datasource: + name: ruoyi-vue-pro + url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false;NON_KEYWORDS=value; # MODE 使用 MySQL 模式;DATABASE_TO_UPPER 配置表和字段使用小写 + driver-class-name: org.h2.Driver + username: sa + password: + druid: + async-init: true # 单元测试,异步初始化 Druid 连接池,提升启动速度 + initial-size: 1 # 单元测试,配置为 1,提升启动速度 + sql: + init: + schema-locations: classpath:/sql/create_tables.sql + + # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 + redis: + host: 127.0.0.1 # 地址 + port: 16379 # 端口(单元测试,使用 16379 端口) + database: 0 # 数据库索引 + +mybatis: + lazy-initialization: true # 单元测试,设置 MyBatis Mapper 延迟加载,加速每个单元测试 + +--- #################### 定时任务相关配置 #################### + +--- #################### 配置中心相关配置 #################### + +--- #################### 服务保障相关配置 #################### + +# Lock4j 配置项(单元测试,禁用 Lock4j) + +--- #################### 监控相关配置 #################### + +--- #################### 芋道相关配置 #################### + +# 芋道配置项,设置当前项目所有自定义的配置 +aagro: + info: + base-package: cn.aagro.pp.module diff --git a/aagro-module-member/src/test/resources/logback.xml b/aagro-module-member/src/test/resources/logback.xml new file mode 100644 index 0000000..daf756b --- /dev/null +++ b/aagro-module-member/src/test/resources/logback.xml @@ -0,0 +1,4 @@ + + + + diff --git a/aagro-module-member/src/test/resources/sql/clean.sql b/aagro-module-member/src/test/resources/sql/clean.sql new file mode 100644 index 0000000..f972e04 --- /dev/null +++ b/aagro-module-member/src/test/resources/sql/clean.sql @@ -0,0 +1,5 @@ +DELETE FROM "member_user"; +DELETE FROM "member_address"; +DELETE FROM "member_tag"; +DELETE FROM "member_level"; +DELETE FROM "member_group"; \ No newline at end of file diff --git a/aagro-module-member/src/test/resources/sql/create_tables.sql b/aagro-module-member/src/test/resources/sql/create_tables.sql new file mode 100644 index 0000000..782a818 --- /dev/null +++ b/aagro-module-member/src/test/resources/sql/create_tables.sql @@ -0,0 +1,113 @@ +CREATE TABLE IF NOT EXISTS "member_user" +( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY COMMENT '编号', + "nickname" varchar(30) NOT NULL DEFAULT '' COMMENT '用户昵称', + "name" varchar(30) NULL COMMENT '真实名字', + sex tinyint null comment '性别', + birthday datetime null comment '出生日期', + area_id int null comment '所在地', + mark varchar(255) null comment '用户备注', + point int default 0 null comment '积分', + "avatar" varchar(255) NOT NULL DEFAULT '' COMMENT '头像', + "status" tinyint NOT NULL COMMENT '状态', + "mobile" varchar(11) NOT NULL COMMENT '手机号', + "password" varchar(100) NOT NULL DEFAULT '' COMMENT '密码', + "register_ip" varchar(32) NOT NULL COMMENT '注册 IP', + "login_ip" varchar(50) NULL DEFAULT '' COMMENT '最后登录IP', + "login_date" datetime NULL DEFAULT NULL COMMENT '最后登录时间', + "tag_ids" varchar(255) NULL DEFAULT NULL COMMENT '用户标签编号列表,以逗号分隔', + "level_id" bigint NULL DEFAULT NULL COMMENT '等级编号', + "experience" bigint NULL DEFAULT NULL COMMENT '经验', + "group_id" bigint NULL DEFAULT NULL COMMENT '用户分组编号', + "creator" varchar(64) NULL DEFAULT '' COMMENT '创建者', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + "updater" varchar(64) NULL DEFAULT '' COMMENT '更新者', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + "deleted" bit(1) NOT NULL DEFAULT '0' COMMENT '是否删除', + "tenant_id" bigint not null default '0', + PRIMARY KEY ("id") +) COMMENT '会员表'; + +CREATE TABLE IF NOT EXISTS "member_address" ( + "id" bigint(20) NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "user_id" bigint(20) NOT NULL, + "name" varchar(10) NOT NULL, + "mobile" varchar(20) NOT NULL, + "area_id" bigint(20) NOT NULL, + "detail_address" varchar(250) NOT NULL, + "default_status" bit NOT NULL, + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "creator" varchar(64) DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "updater" varchar(64) DEFAULT '', + PRIMARY KEY ("id") +) COMMENT '用户收件地址'; + +CREATE TABLE IF NOT EXISTS "member_tag" +( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar NOT NULL, + "creator" varchar DEFAULT '', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL default '0', + PRIMARY KEY ("id") +) COMMENT '会员标签'; + +CREATE TABLE IF NOT EXISTS "member_level" +( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar NOT NULL, + "experience" int NOT NULL, + "level" int NOT NULL, + "discount_percent" int NOT NULL, + "icon" varchar NOT NULL, + "background_url" varchar NOT NULL, + "creator" varchar DEFAULT '', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint not null default '0', + "status" tinyint NOT NULL DEFAULT '0', + PRIMARY KEY ("id") +) COMMENT '会员等级'; + +CREATE TABLE IF NOT EXISTS "member_group" +( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar NOT NULL, + "remark" varchar NOT NULL, + "status" tinyint NOT NULL DEFAULT '0', + "creator" varchar DEFAULT '', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint not null default '0', + PRIMARY KEY ("id") +) COMMENT '用户分组'; +CREATE TABLE IF NOT EXISTS "member_brokerage_record" +( + "id" int NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "user_id" bigint NOT NULL, + "biz_id" varchar NOT NULL, + "biz_type" varchar NOT NULL, + "title" varchar NOT NULL, + "price" int NOT NULL, + "total_price" int NOT NULL, + "description" varchar NOT NULL, + "status" varchar NOT NULL, + "frozen_days" int NOT NULL, + "unfreeze_time" varchar, + "creator" varchar DEFAULT '', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint not null default '0', + PRIMARY KEY ("id") +) COMMENT '佣金记录'; diff --git a/aagro-module-mp/pom.xml b/aagro-module-mp/pom.xml new file mode 100644 index 0000000..9651266 --- /dev/null +++ b/aagro-module-mp/pom.xml @@ -0,0 +1,89 @@ + + + + cn.aagro.gg + aiot + ${revision} + ../pom.xml + + 4.0.0 + + aagro-module-mp + jar + + ${project.artifactId} + + mp 模块,我们放微信微信公众号。 + 例如说:提供微信公众号的账号、菜单、粉丝、标签、消息、自动回复、素材、模板通知、运营数据等功能 + + + + + cn.aagro.gg + aagro-module-system + ${revision} + + + cn.aagro.gg + aagro-module-infra + ${revision} + + + + + cn.aagro.gg + aagro-spring-boot-starter-biz-tenant + + + + + cn.aagro.gg + aagro-spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-validation + + + + + cn.aagro.gg + aagro-spring-boot-starter-mybatis + + + + cn.aagro.gg + aagro-spring-boot-starter-redis + + + + + cn.aagro.gg + aagro-spring-boot-starter-mq + + + + + cn.aagro.gg + aagro-spring-boot-starter-test + ${revision} + test + + + + + cn.aagro.gg + aagro-spring-boot-starter-excel + + + + + com.github.binarywang + wx-java-mp-spring-boot-starter + + + + diff --git a/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/account/MpAccountController.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/account/MpAccountController.java new file mode 100644 index 0000000..836e1dc --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/account/MpAccountController.java @@ -0,0 +1,98 @@ +package cn.aagro.pp.module.mp.controller.admin.account; + +import cn.aagro.pp.framework.common.pojo.CommonResult; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.mp.controller.admin.account.vo.*; +import cn.aagro.pp.module.mp.convert.account.MpAccountConvert; +import cn.aagro.pp.module.mp.dal.dataobject.account.MpAccountDO; +import cn.aagro.pp.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.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/account/vo/MpAccountBaseVO.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/account/vo/MpAccountBaseVO.java new file mode 100644 index 0000000..d0f9478 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/account/vo/MpAccountBaseVO.java @@ -0,0 +1,43 @@ +package cn.aagro.pp.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 = "aagroyuanma") + @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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/account/vo/MpAccountCreateReqVO.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/account/vo/MpAccountCreateReqVO.java new file mode 100644 index 0000000..3f4752b --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/account/vo/MpAccountCreateReqVO.java @@ -0,0 +1,14 @@ +package cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/account/vo/MpAccountPageReqVO.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/account/vo/MpAccountPageReqVO.java new file mode 100644 index 0000000..11bfb74 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/account/vo/MpAccountPageReqVO.java @@ -0,0 +1,24 @@ +package cn.aagro.pp.module.mp.controller.admin.account.vo; + +import cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/account/vo/MpAccountRespVO.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/account/vo/MpAccountRespVO.java new file mode 100644 index 0000000..e5bc8ac --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/account/vo/MpAccountRespVO.java @@ -0,0 +1,25 @@ +package cn.aagro.pp.module.mp.controller.admin.account.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 公众号账号 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MpAccountRespVO extends MpAccountBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "二维码图片URL", example = "https://www.iocoder.cn/1024.png") + private String qrCodeUrl; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/account/vo/MpAccountSimpleRespVO.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/account/vo/MpAccountSimpleRespVO.java new file mode 100644 index 0000000..8b1bc96 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/account/vo/MpAccountSimpleRespVO.java @@ -0,0 +1,16 @@ +package cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/account/vo/MpAccountUpdateReqVO.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/account/vo/MpAccountUpdateReqVO.java new file mode 100644 index 0000000..f06c4b8 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/account/vo/MpAccountUpdateReqVO.java @@ -0,0 +1,20 @@ +package cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/material/MpMaterialController.http b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/material/MpMaterialController.http new file mode 100644 index 0000000..c093adf --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/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: {{adminTenantId}} diff --git a/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/material/MpMaterialController.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/material/MpMaterialController.java new file mode 100644 index 0000000..e55b927 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/material/MpMaterialController.java @@ -0,0 +1,74 @@ +package cn.aagro.pp.module.mp.controller.admin.material; + +import cn.aagro.pp.framework.common.pojo.CommonResult; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.mp.controller.admin.material.vo.*; +import cn.aagro.pp.module.mp.convert.material.MpMaterialConvert; +import cn.aagro.pp.module.mp.dal.dataobject.material.MpMaterialDO; +import cn.aagro.pp.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.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/material/vo/MpMaterialPageReqVO.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/material/vo/MpMaterialPageReqVO.java new file mode 100644 index 0000000..8da25f4 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/material/vo/MpMaterialPageReqVO.java @@ -0,0 +1,27 @@ +package cn.aagro.pp.module.mp.controller.admin.material.vo; + +import cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/material/vo/MpMaterialRespVO.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/material/vo/MpMaterialRespVO.java new file mode 100644 index 0000000..5065746 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/material/vo/MpMaterialRespVO.java @@ -0,0 +1,47 @@ +package cn.aagro.pp.module.mp.controller.admin.material.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 公众号素材 Response VO") +@Data +public class MpMaterialRespVO { + + @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long accountId; + @Schema(description = "公众号账号的 appId", requiredMode = Schema.RequiredMode.REQUIRED, example = "wx1234567890") + private String appId; + + @Schema(description = "素材的 media_id", requiredMode = Schema.RequiredMode.REQUIRED, example = "123") + private String mediaId; + + @Schema(description = "文件类型 参见 WxConsts.MediaFileType 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "image") + private String type; + + @Schema(description = "是否永久 true - 永久;false - 临时", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean permanent; + + @Schema(description = "素材的 URL", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png") + private String url; + + + @Schema(description = "名字", example = "yunai.png") + private String name; + + @Schema(description = "公众号文件 URL 只有【永久素材】使用", example = "https://mmbiz.qpic.cn/xxx.mp3") + private String mpUrl; + + @Schema(description = "视频素材的标题 只有【永久素材】使用", example = "我是标题") + private String title; + @Schema(description = "视频素材的描述 只有【永久素材】使用", example = "我是介绍") + private String introduction; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/material/vo/MpMaterialUploadNewsImageReqVO.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/material/vo/MpMaterialUploadNewsImageReqVO.java new file mode 100644 index 0000000..0441f45 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/material/vo/MpMaterialUploadNewsImageReqVO.java @@ -0,0 +1,23 @@ +package cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/material/vo/MpMaterialUploadPermanentReqVO.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/material/vo/MpMaterialUploadPermanentReqVO.java new file mode 100644 index 0000000..2e05a25 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/material/vo/MpMaterialUploadPermanentReqVO.java @@ -0,0 +1,53 @@ +package cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/material/vo/MpMaterialUploadRespVO.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/material/vo/MpMaterialUploadRespVO.java new file mode 100644 index 0000000..61c7bfd --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/material/vo/MpMaterialUploadRespVO.java @@ -0,0 +1,16 @@ +package cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/material/vo/MpMaterialUploadTemporaryReqVO.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/material/vo/MpMaterialUploadTemporaryReqVO.java new file mode 100644 index 0000000..f19e847 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/material/vo/MpMaterialUploadTemporaryReqVO.java @@ -0,0 +1,28 @@ +package cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/menu/MpMenuController.http b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/menu/MpMenuController.http new file mode 100644 index 0000000..defd7ec --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/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: {{adminTenantId}} + +{ + "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: {{adminTenantId}} + +{ + "accountId": "1", + "menus": [] +} + +### 请求 /mp/menu/list 接口 => 成功 +GET {{baseUrl}}/mp/menu/list?accountId=1 +Authorization: Bearer {{token}} +tenant-id: {{adminTenantId}} diff --git a/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/menu/MpMenuController.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/menu/MpMenuController.java new file mode 100644 index 0000000..464e2fd --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/menu/MpMenuController.java @@ -0,0 +1,57 @@ +package cn.aagro.pp.module.mp.controller.admin.menu; + +import cn.aagro.pp.framework.common.pojo.CommonResult; +import cn.aagro.pp.module.mp.controller.admin.menu.vo.MpMenuRespVO; +import cn.aagro.pp.module.mp.controller.admin.menu.vo.MpMenuSaveReqVO; +import cn.aagro.pp.module.mp.convert.menu.MpMenuConvert; +import cn.aagro.pp.module.mp.dal.dataobject.menu.MpMenuDO; +import cn.aagro.pp.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.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/menu/vo/MpMenuBaseVO.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/menu/vo/MpMenuBaseVO.java new file mode 100644 index 0000000..789fe9e --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/menu/vo/MpMenuBaseVO.java @@ -0,0 +1,115 @@ +package cn.aagro.pp.module.mp.controller.admin.menu.vo; + +import cn.aagro.pp.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.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/menu/vo/MpMenuRespVO.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/menu/vo/MpMenuRespVO.java new file mode 100644 index 0000000..6b6962a --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/menu/vo/MpMenuRespVO.java @@ -0,0 +1,28 @@ +package cn.aagro.pp.module.mp.controller.admin.menu.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 公众号菜单 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MpMenuRespVO extends MpMenuBaseVO { + + @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + private Long accountId; + + @Schema(description = "公众号 appId", requiredMode = Schema.RequiredMode.REQUIRED, example = "wx1234567890ox") + private String appId; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/menu/vo/MpMenuSaveReqVO.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/menu/vo/MpMenuSaveReqVO.java new file mode 100644 index 0000000..9a4de1e --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/menu/vo/MpMenuSaveReqVO.java @@ -0,0 +1,34 @@ +package cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/message/MpAutoReplyController.http b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/message/MpAutoReplyController.http new file mode 100644 index 0000000..8d17c6b --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/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: {{adminTenantId}} diff --git a/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/message/MpAutoReplyController.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/message/MpAutoReplyController.java new file mode 100644 index 0000000..2bcabc0 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/message/MpAutoReplyController.java @@ -0,0 +1,74 @@ +package cn.aagro.pp.module.mp.controller.admin.message; + +import cn.aagro.pp.framework.common.pojo.CommonResult; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.mp.controller.admin.message.vo.autoreply.MpAutoReplyCreateReqVO; +import cn.aagro.pp.module.mp.controller.admin.message.vo.autoreply.MpAutoReplyRespVO; +import cn.aagro.pp.module.mp.controller.admin.message.vo.autoreply.MpAutoReplyUpdateReqVO; +import cn.aagro.pp.module.mp.controller.admin.message.vo.message.MpMessagePageReqVO; +import cn.aagro.pp.module.mp.convert.message.MpAutoReplyConvert; +import cn.aagro.pp.module.mp.dal.dataobject.message.MpAutoReplyDO; +import cn.aagro.pp.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.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/message/MpMessageController.http b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/message/MpMessageController.http new file mode 100644 index 0000000..16677d4 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/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: {{adminTenantId}} + +### 请求 /mp/message/send 接口 => 成功(文本) +POST {{baseUrl}}/mp/message/send +Content-Type: application/json +Authorization: Bearer {{token}} +tenant-id: {{adminTenantId}} + +{ + "userId": 3, + "type": "text", + "content": "测试消息" +} + +### 请求 /mp/message/send 接口 => 成功(音乐) +POST {{baseUrl}}/mp/message/send +Content-Type: application/json +Authorization: Bearer {{token}} +tenant-id: {{adminTenantId}} + +{ + "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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/message/MpMessageController.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/message/MpMessageController.java new file mode 100644 index 0000000..134fc2c --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/message/MpMessageController.java @@ -0,0 +1,47 @@ +package cn.aagro.pp.module.mp.controller.admin.message; + +import cn.aagro.pp.framework.common.pojo.CommonResult; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.mp.controller.admin.message.vo.message.MpMessagePageReqVO; +import cn.aagro.pp.module.mp.controller.admin.message.vo.message.MpMessageRespVO; +import cn.aagro.pp.module.mp.controller.admin.message.vo.message.MpMessageSendReqVO; +import cn.aagro.pp.module.mp.convert.message.MpMessageConvert; +import cn.aagro.pp.module.mp.dal.dataobject.message.MpMessageDO; +import cn.aagro.pp.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.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyBaseVO.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyBaseVO.java new file mode 100644 index 0000000..36ed7c6 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyBaseVO.java @@ -0,0 +1,109 @@ +package cn.aagro.pp.module.mp.controller.admin.message.vo.autoreply; + +import cn.hutool.core.util.ObjectUtil; +import cn.aagro.pp.module.mp.dal.dataobject.message.MpMessageDO; +import cn.aagro.pp.module.mp.enums.message.MpAutoReplyTypeEnum; +import cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyCreateReqVO.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyCreateReqVO.java new file mode 100644 index 0000000..43822e5 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyCreateReqVO.java @@ -0,0 +1,20 @@ +package cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyPageReqVO.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyPageReqVO.java new file mode 100644 index 0000000..e853fb1 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyPageReqVO.java @@ -0,0 +1,21 @@ +package cn.aagro.pp.module.mp.controller.admin.message.vo.autoreply; + +import cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyRespVO.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyRespVO.java new file mode 100644 index 0000000..0a91ece --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyRespVO.java @@ -0,0 +1,27 @@ +package cn.aagro.pp.module.mp.controller.admin.message.vo.autoreply; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 公众号自动回复 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MpAutoReplyRespVO extends MpAutoReplyBaseVO { + + @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long accountId; + @Schema(description = "公众号 appId", requiredMode = Schema.RequiredMode.REQUIRED, example = "wx1234567890") + private String appId; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyUpdateReqVO.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyUpdateReqVO.java new file mode 100644 index 0000000..515da27 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyUpdateReqVO.java @@ -0,0 +1,20 @@ +package cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/message/vo/message/MpMessagePageReqVO.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/message/vo/message/MpMessagePageReqVO.java new file mode 100644 index 0000000..96359da --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/message/vo/message/MpMessagePageReqVO.java @@ -0,0 +1,38 @@ +package cn.aagro.pp.module.mp.controller.admin.message.vo.message; + +import cn.aagro.pp.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.aagro.pp.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; + + @Schema(description = "公众号粉丝 UserId", example = "1") + private String userId; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "创建时间") + private LocalDateTime[] createTime; + +} diff --git a/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/message/vo/message/MpMessageRespVO.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/message/vo/message/MpMessageRespVO.java new file mode 100644 index 0000000..8436084 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/message/vo/message/MpMessageRespVO.java @@ -0,0 +1,101 @@ +package cn.aagro.pp.module.mp.controller.admin.message.vo.message; + +import cn.aagro.pp.module.mp.dal.dataobject.message.MpMessageDO; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import me.chanjar.weixin.common.api.WxConsts; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - 公众号消息 Response VO") +@Data +public class MpMessageRespVO { + + @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Integer id; + + @Schema(description = "微信公众号消息 id", requiredMode = Schema.RequiredMode.REQUIRED, example = "23953173569869169") + private Long msgId; + + @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long accountId; + @Schema(description = "公众号账号的 appid", requiredMode = Schema.RequiredMode.REQUIRED, example = "wx1234567890") + private String appId; + + @Schema(description = "公众号粉丝编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + private Long userId; + @Schema(description = "公众号粉丝标志", requiredMode = Schema.RequiredMode.REQUIRED, example = "o6_bmjrPTlm6_2sgVt7hMZOPfL2M") + private String openid; + + @Schema(description = "消息类型 参见 WxConsts.XmlMsgType 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "text") + private String type; + @Schema(description = "消息来源 参见 MpMessageSendFromEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer sendFrom; + + // ========= 普通消息内容 https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Receiving_standard_messages.html + + @Schema(description = "消息内容 消息类型为 text 时,才有值", example = "你好呀") + private String content; + + @Schema(description = "媒体素材的编号 消息类型为 image、voice、video 时,才有值", example = "1234567890") + private String mediaId; + @Schema(description = "媒体文件的 URL 消息类型为 image、voice、video 时,才有值", example = "https://www.iocoder.cn/xxx.png") + private String mediaUrl; + + @Schema(description = "语音识别后文本 消息类型为 voice 时,才有值", example = "语音识别后文本") + private String recognition; + @Schema(description = "语音格式 消息类型为 voice 时,才有值", example = "amr") + private String format; + + @Schema(description = "标题 消息类型为 video、music、link 时,才有值", example = "我是标题") + private String title; + + @Schema(description = "描述 消息类型为 video、music 时,才有值", example = "我是描述") + private String description; + + @Schema(description = "缩略图的媒体 id 消息类型为 video、music 时,才有值", example = "1234567890") + private String thumbMediaId; + @Schema(description = "缩略图的媒体 URL 消息类型为 video、music 时,才有值", example = "https://www.iocoder.cn/xxx.png") + private String thumbMediaUrl; + + @Schema(description = "点击图文消息跳转链接 消息类型为 link 时,才有值", example = "https://www.iocoder.cn") + private String url; + + @Schema(description = "地理位置维度 消息类型为 location 时,才有值", example = "23.137466") + private Double locationX; + + @Schema(description = "地理位置经度 消息类型为 location 时,才有值", example = "113.352425") + private Double locationY; + + @Schema(description = "地图缩放大小 消息类型为 location 时,才有值", example = "13") + private Double scale; + + @Schema(description = "详细地址 消息类型为 location 时,才有值", example = "杨浦区黄兴路 221-4 号临") + private String label; + + /** + * 图文消息数组 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 NEWS + */ + private List articles; + + @Schema(description = "音乐链接 消息类型为 music 时,才有值", example = "https://www.iocoder.cn/xxx.mp3") + private String musicUrl; + @Schema(description = "高质量音乐链接 消息类型为 music 时,才有值", example = "https://www.iocoder.cn/xxx.mp3") + private String hqMusicUrl; + + // ========= 事件推送 https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Receiving_event_pushes.html + + @Schema(description = "事件类型 参见 WxConsts.EventType 枚举", example = "subscribe") + private String event; + @Schema(description = "事件 Key 参见 WxConsts.EventType 枚举", example = "qrscene_123456") + private String eventKey; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/message/vo/message/MpMessageSendReqVO.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/message/vo/message/MpMessageSendReqVO.java new file mode 100644 index 0000000..80f70a9 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/message/vo/message/MpMessageSendReqVO.java @@ -0,0 +1,58 @@ +package cn.aagro.pp.module.mp.controller.admin.message.vo.message; + +import cn.aagro.pp.module.mp.dal.dataobject.message.MpMessageDO; +import cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/news/MpDraftController.http b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/news/MpDraftController.http new file mode 100644 index 0000000..ea75429 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/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: {{adminTenantId}} + +### 请求 /mp/draft/create 接口 => 成功 +POST {{baseUrl}}/mp/draft/create?accountId=1 +Content-Type: application/json +Authorization: Bearer {{token}} +tenant-id: {{adminTenantId}} + +{ + "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: {{adminTenantId}} + +[{ + "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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/news/MpDraftController.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/news/MpDraftController.java new file mode 100644 index 0000000..6b1fc9d --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/news/MpDraftController.java @@ -0,0 +1,136 @@ +package cn.aagro.pp.module.mp.controller.admin.news; + +import cn.hutool.core.collection.CollUtil; +import cn.aagro.pp.framework.common.pojo.CommonResult; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.common.util.collection.CollectionUtils; +import cn.aagro.pp.framework.common.util.object.PageUtils; +import cn.aagro.pp.module.mp.controller.admin.news.vo.MpDraftPageReqVO; +import cn.aagro.pp.module.mp.dal.dataobject.material.MpMaterialDO; +import cn.aagro.pp.module.mp.framework.mp.core.MpServiceFactory; +import cn.aagro.pp.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.aagro.pp.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.aagro.pp.framework.common.pojo.CommonResult.success; +import static cn.aagro.pp.framework.common.util.collection.MapUtils.findAndThen; +import static cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/news/MpFreePublishController.http b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/news/MpFreePublishController.http new file mode 100644 index 0000000..246a95c --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/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: {{adminTenantId}} + +### 请求 /mp/free-publish/submit 接口 => 成功 +POST {{baseUrl}}/mp/free-publish/submit?accountId=1&mediaId=r6ryvl6LrxBU0miaST4Y-vilmd7iS51D8IPddxflWrau0hIQ2ovY8YanO5jlgUcM +Content-Type: application/json +Authorization: Bearer {{token}} +tenant-id: {{adminTenantId}} + +{} diff --git a/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/news/MpFreePublishController.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/news/MpFreePublishController.java new file mode 100644 index 0000000..5e8f67e --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/news/MpFreePublishController.java @@ -0,0 +1,119 @@ +package cn.aagro.pp.module.mp.controller.admin.news; + +import cn.hutool.core.collection.CollUtil; +import cn.aagro.pp.framework.common.pojo.CommonResult; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.common.util.collection.CollectionUtils; +import cn.aagro.pp.framework.common.util.object.PageUtils; +import cn.aagro.pp.module.mp.controller.admin.news.vo.MpFreePublishPageReqVO; +import cn.aagro.pp.module.mp.dal.dataobject.material.MpMaterialDO; +import cn.aagro.pp.module.mp.framework.mp.core.MpServiceFactory; +import cn.aagro.pp.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.aagro.pp.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.aagro.pp.framework.common.pojo.CommonResult.success; +import static cn.aagro.pp.framework.common.util.collection.MapUtils.findAndThen; +import static cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/news/vo/MpDraftPageReqVO.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/news/vo/MpDraftPageReqVO.java new file mode 100644 index 0000000..94030a2 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/news/vo/MpDraftPageReqVO.java @@ -0,0 +1,21 @@ +package cn.aagro.pp.module.mp.controller.admin.news.vo; + +import cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/news/vo/MpFreePublishPageReqVO.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/news/vo/MpFreePublishPageReqVO.java new file mode 100644 index 0000000..7ad879f --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/news/vo/MpFreePublishPageReqVO.java @@ -0,0 +1,21 @@ +package cn.aagro.pp.module.mp.controller.admin.news.vo; + +import cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/open/MpOpenController.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/open/MpOpenController.java new file mode 100644 index 0000000..9313eea --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/open/MpOpenController.java @@ -0,0 +1,117 @@ +package cn.aagro.pp.module.mp.controller.admin.open; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import cn.aagro.pp.framework.tenant.core.aop.TenantIgnore; +import cn.aagro.pp.framework.tenant.core.util.TenantUtils; +import cn.aagro.pp.module.mp.controller.admin.open.vo.MpOpenCheckSignatureReqVO; +import cn.aagro.pp.module.mp.controller.admin.open.vo.MpOpenHandleMessageReqVO; +import cn.aagro.pp.module.mp.dal.dataobject.account.MpAccountDO; +import cn.aagro.pp.module.mp.framework.mp.core.MpServiceFactory; +import cn.aagro.pp.module.mp.framework.mp.core.context.MpContextHolder; +import cn.aagro.pp.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 = "处理消息") + @PostMapping(value = "/{appId}", produces = "application/xml; charset=UTF-8") + @TenantIgnore + 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(); + } + } + + /** + * 接收微信公众号的校验签名 + * + * 对应 文档 + */ + @Operation(summary = "校验签名") // 参见 + @GetMapping(value = "/{appId}", produces = "text/plain;charset=utf-8") + @TenantIgnore + 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 "非法请求"; + } + + 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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/open/vo/MpOpenCheckSignatureReqVO.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/open/vo/MpOpenCheckSignatureReqVO.java new file mode 100644 index 0000000..262f2d4 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/open/vo/MpOpenCheckSignatureReqVO.java @@ -0,0 +1,29 @@ +package cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/open/vo/MpOpenHandleMessageReqVO.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/open/vo/MpOpenHandleMessageReqVO.java new file mode 100644 index 0000000..eed5119 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/open/vo/MpOpenHandleMessageReqVO.java @@ -0,0 +1,37 @@ +package cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/statistics/MpStatisticsController.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/statistics/MpStatisticsController.java new file mode 100644 index 0000000..53d2655 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/statistics/MpStatisticsController.java @@ -0,0 +1,68 @@ +package cn.aagro.pp.module.mp.controller.admin.statistics; + +import cn.aagro.pp.framework.common.pojo.CommonResult; +import cn.aagro.pp.module.mp.controller.admin.statistics.vo.*; +import cn.aagro.pp.module.mp.convert.statistics.MpStatisticsConvert; +import cn.aagro.pp.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.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/statistics/vo/MpStatisticsGetReqVO.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/statistics/vo/MpStatisticsGetReqVO.java new file mode 100644 index 0000000..b86d67a --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/statistics/vo/MpStatisticsGetReqVO.java @@ -0,0 +1,25 @@ +package cn.aagro.pp.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.aagro.pp.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 获得统计数据 Request VO") +@Data +public class MpStatisticsGetReqVO { + + @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "公众号账号的编号不能为空") + private Long accountId; + + @Schema(description = "查询时间范围", example = "[2022-07-01 00:00:00, 2022-07-01 23:59:59]") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @NotNull(message = "查询时间范围不能为空") + private LocalDateTime[] date; + +} diff --git a/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/statistics/vo/MpStatisticsInterfaceSummaryRespVO.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/statistics/vo/MpStatisticsInterfaceSummaryRespVO.java new file mode 100644 index 0000000..4c5f9f2 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/statistics/vo/MpStatisticsInterfaceSummaryRespVO.java @@ -0,0 +1,27 @@ +package cn.aagro.pp.module.mp.controller.admin.statistics.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 某一天的接口分析数据 Response VO") +@Data +public class MpStatisticsInterfaceSummaryRespVO { + + @Schema(description = "日期", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime refDate; + + @Schema(description = "通过服务器配置地址获得消息后,被动回复粉丝消息的次数", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Integer callbackCount; + + @Schema(description = "上述动作的失败次数", requiredMode = Schema.RequiredMode.REQUIRED, example = "20") + private Integer failCount; + + @Schema(description = "总耗时,除以 callback_count 即为平均耗时", requiredMode = Schema.RequiredMode.REQUIRED, example = "30") + private Integer totalTimeCost; + + @Schema(description = "最大耗时", requiredMode = Schema.RequiredMode.REQUIRED, example = "40") + private Integer maxTimeCost; + +} diff --git a/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/statistics/vo/MpStatisticsUpstreamMessageRespVO.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/statistics/vo/MpStatisticsUpstreamMessageRespVO.java new file mode 100644 index 0000000..d526c6c --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/statistics/vo/MpStatisticsUpstreamMessageRespVO.java @@ -0,0 +1,21 @@ +package cn.aagro.pp.module.mp.controller.admin.statistics.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 某一天的粉丝增减数据 Response VO") +@Data +public class MpStatisticsUpstreamMessageRespVO { + + @Schema(description = "日期", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime refDate; + + @Schema(description = "上行发送了(向公众号发送了)消息的粉丝数", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Integer messageUser; + + @Schema(description = "上行发送了消息的消息总数", requiredMode = Schema.RequiredMode.REQUIRED, example = "20") + private Integer messageCount; + +} diff --git a/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/statistics/vo/MpStatisticsUserCumulateRespVO.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/statistics/vo/MpStatisticsUserCumulateRespVO.java new file mode 100644 index 0000000..cde60b8 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/statistics/vo/MpStatisticsUserCumulateRespVO.java @@ -0,0 +1,18 @@ +package cn.aagro.pp.module.mp.controller.admin.statistics.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 某一天的消息发送概况数据 Response VO") +@Data +public class MpStatisticsUserCumulateRespVO { + + @Schema(description = "日期", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime refDate; + + @Schema(description = "累计粉丝量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Integer cumulateUser; + +} diff --git a/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/statistics/vo/MpStatisticsUserSummaryRespVO.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/statistics/vo/MpStatisticsUserSummaryRespVO.java new file mode 100644 index 0000000..ed26b84 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/statistics/vo/MpStatisticsUserSummaryRespVO.java @@ -0,0 +1,24 @@ +package cn.aagro.pp.module.mp.controller.admin.statistics.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 某一天的粉丝增减数据 Response VO") +@Data +public class MpStatisticsUserSummaryRespVO { + + @Schema(description = "日期", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime refDate; + + @Schema(description = "粉丝来源", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + private Integer userSource; + + @Schema(description = "新关注的粉丝数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Integer newUser; + + @Schema(description = "取消关注的粉丝数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "20") + private Integer cancelUser; + +} diff --git a/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/tag/MpTagController.http b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/tag/MpTagController.http new file mode 100644 index 0000000..a888145 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/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: {{adminTenantId}} + +{ + "accountId": "1", + "name": "测试" +} + +### 请求 /mp/tag/update 接口 => 成功 +PUT {{baseUrl}}/mp/tag/update +Content-Type: application/json +Authorization: Bearer {{token}} +tenant-id: {{adminTenantId}} + +{ + "id": "3", + "name": "测试标签啦" +} + +### 请求 /mp/tag/delete 接口 => 成功 +DELETE {{baseUrl}}/mp/tag/delete?id=3 +Content-Type: application/json +Authorization: Bearer {{token}} +tenant-id: {{adminTenantId}} + +### 请求 /mp/tag/page 接口 => 成功 +GET {{baseUrl}}/mp/tag/page?accountId=1&pageNo=1&pageSize=10 +Content-Type: application/json +Authorization: Bearer {{token}} +tenant-id: {{adminTenantId}} + +### 请求 /mp/tag/sync 接口 => 成功 +POST {{baseUrl}}/mp/tag/sync?accountId=1 +Content-Type: application/json +Authorization: Bearer {{token}} +tenant-id: {{adminTenantId}} diff --git a/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/tag/MpTagController.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/tag/MpTagController.java new file mode 100644 index 0000000..6cfdeda --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/tag/MpTagController.java @@ -0,0 +1,88 @@ +package cn.aagro.pp.module.mp.controller.admin.tag; + +import cn.aagro.pp.framework.common.pojo.CommonResult; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.mp.controller.admin.tag.vo.*; +import cn.aagro.pp.module.mp.convert.tag.MpTagConvert; +import cn.aagro.pp.module.mp.dal.dataobject.tag.MpTagDO; +import cn.aagro.pp.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.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/tag/vo/MpTagBaseVO.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/tag/vo/MpTagBaseVO.java new file mode 100644 index 0000000..c56c0a5 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/tag/vo/MpTagBaseVO.java @@ -0,0 +1,21 @@ +package cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/tag/vo/MpTagCreateReqVO.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/tag/vo/MpTagCreateReqVO.java new file mode 100644 index 0000000..3baedd4 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/tag/vo/MpTagCreateReqVO.java @@ -0,0 +1,20 @@ +package cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/tag/vo/MpTagPageReqVO.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/tag/vo/MpTagPageReqVO.java new file mode 100644 index 0000000..3f0f8d6 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/tag/vo/MpTagPageReqVO.java @@ -0,0 +1,24 @@ +package cn.aagro.pp.module.mp.controller.admin.tag.vo; + +import cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/tag/vo/MpTagRespVO.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/tag/vo/MpTagRespVO.java new file mode 100644 index 0000000..b17ad9f --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/tag/vo/MpTagRespVO.java @@ -0,0 +1,25 @@ +package cn.aagro.pp.module.mp.controller.admin.tag.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 公众号标签 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MpTagRespVO extends MpTagBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "此标签下粉丝数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + private Integer count; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/tag/vo/MpTagSimpleRespVO.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/tag/vo/MpTagSimpleRespVO.java new file mode 100644 index 0000000..020ed24 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/tag/vo/MpTagSimpleRespVO.java @@ -0,0 +1,19 @@ +package cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/tag/vo/MpTagUpdateReqVO.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/tag/vo/MpTagUpdateReqVO.java new file mode 100644 index 0000000..2e0b580 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/tag/vo/MpTagUpdateReqVO.java @@ -0,0 +1,20 @@ +package cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/user/MpUserController.http b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/user/MpUserController.http new file mode 100644 index 0000000..c78356a --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/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: {{adminTenantId}} + +### 请求 /mp/user/update 接口 => 成功 +PUT {{baseUrl}}/mp/user/update +Content-Type: application/json +Authorization: Bearer {{token}} +tenant-id: {{adminTenantId}} + +{ + "id": "3", + "nickname": "test", + "remark": "测试备注", + "tagIds": [103, 104] +} diff --git a/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/user/MpUserController.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/user/MpUserController.java new file mode 100644 index 0000000..1cf3335 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/user/MpUserController.java @@ -0,0 +1,65 @@ +package cn.aagro.pp.module.mp.controller.admin.user; + +import cn.aagro.pp.framework.common.pojo.CommonResult; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.mp.controller.admin.user.vo.MpUserPageReqVO; +import cn.aagro.pp.module.mp.controller.admin.user.vo.MpUserRespVO; +import cn.aagro.pp.module.mp.controller.admin.user.vo.MpUserUpdateReqVO; +import cn.aagro.pp.module.mp.convert.user.MpUserConvert; +import cn.aagro.pp.module.mp.dal.dataobject.user.MpUserDO; +import cn.aagro.pp.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.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/user/vo/MpUserPageReqVO.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/user/vo/MpUserPageReqVO.java new file mode 100644 index 0000000..c520590 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/user/vo/MpUserPageReqVO.java @@ -0,0 +1,30 @@ +package cn.aagro.pp.module.mp.controller.admin.user.vo; + +import cn.aagro.pp.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 公众号粉丝分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MpUserPageReqVO extends PageParam { + + @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + @NotNull(message = "公众号账号的编号不能为空") + private Long accountId; + + @Schema(description = "公众号粉丝标识,模糊匹配", example = "o6_bmjrPTlm6_2sgVt7hMZOPfL2M") + private String openid; + + @Schema(description = "微信生态唯一标识,模糊匹配", example = "o6_bmjrPTlm6_2sgVt7hMZOPfL2M") + private String unionId; + + @Schema(description = "公众号粉丝昵称,模糊匹配", example = "芋艿") + private String nickname; + +} diff --git a/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/user/vo/MpUserRespVO.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/user/vo/MpUserRespVO.java new file mode 100644 index 0000000..0904130 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/user/vo/MpUserRespVO.java @@ -0,0 +1,55 @@ +package cn.aagro.pp.module.mp.controller.admin.user.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - 公众号粉丝 Response VO") +@Data +public class MpUserRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "公众号粉丝标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "o6_bmjrPTlm6_2sgVt7hMZOPfL2M") + private String openid; + + @Schema(description = "微信生态唯一标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "o6_bmjrPTlm6_2sgVt7hMZOPfL2M") + private String unionId; + + @Schema(description = "关注状态 参见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer subscribeStatus; + @Schema(description = "关注时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime subscribeTime; + @Schema(description = "取消关注时间") + private LocalDateTime unsubscribeTime; + + @Schema(description = "昵称", example = "芋道") + private String nickname; + @Schema(description = "头像地址", example = "https://www.iocoder.cn/1.png") + private String headImageUrl; + @Schema(description = "语言", example = "zh_CN") + private String language; + @Schema(description = "国家", example = "中国") + private String country; + @Schema(description = "省份", example = "广东省") + private String province; + @Schema(description = "城市", example = "广州市") + private String city; + @Schema(description = "备注", example = "你是一个芋头嘛") + private String remark; + + @Schema(description = "标签编号数组", example = "1,2,3") + private List tagIds; + + @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long accountId; + @Schema(description = "公众号账号的 appId", requiredMode = Schema.RequiredMode.REQUIRED, example = "wx1234567890") + private String appId; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/user/vo/MpUserUpdateReqVO.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/user/vo/MpUserUpdateReqVO.java new file mode 100644 index 0000000..660c4f6 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/admin/user/vo/MpUserUpdateReqVO.java @@ -0,0 +1,26 @@ +package cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/package-info.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/package-info.java new file mode 100644 index 0000000..454d78b --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/controller/package-info.java @@ -0,0 +1,6 @@ +/** + * 提供 RESTful API 给前端: + * 1. admin 包:提供给管理后台 aagro-ui-admin 前端项目 + * 2. app 包:提供给用户 APP aagro-ui-app 前端项目,它的 Controller 和 VO 都要添加 App 前缀,用于和管理后台进行区分 + */ +package cn.aagro.pp.module.mp.controller; diff --git a/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/convert/account/MpAccountConvert.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/convert/account/MpAccountConvert.java new file mode 100644 index 0000000..b844b5c --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/convert/account/MpAccountConvert.java @@ -0,0 +1,31 @@ +package cn.aagro.pp.module.mp.convert.account; + +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.mp.controller.admin.account.vo.MpAccountCreateReqVO; +import cn.aagro.pp.module.mp.controller.admin.account.vo.MpAccountRespVO; +import cn.aagro.pp.module.mp.controller.admin.account.vo.MpAccountSimpleRespVO; +import cn.aagro.pp.module.mp.controller.admin.account.vo.MpAccountUpdateReqVO; +import cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/convert/material/MpMaterialConvert.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/convert/material/MpMaterialConvert.java new file mode 100644 index 0000000..c817703 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/convert/material/MpMaterialConvert.java @@ -0,0 +1,47 @@ +package cn.aagro.pp.module.mp.convert.material; + +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.mp.controller.admin.material.vo.MpMaterialRespVO; +import cn.aagro.pp.module.mp.controller.admin.material.vo.MpMaterialUploadRespVO; +import cn.aagro.pp.module.mp.dal.dataobject.account.MpAccountDO; +import cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/convert/menu/MpMenuConvert.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/convert/menu/MpMenuConvert.java new file mode 100644 index 0000000..d72aac6 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/convert/menu/MpMenuConvert.java @@ -0,0 +1,50 @@ +package cn.aagro.pp.module.mp.convert.menu; + +import cn.aagro.pp.module.mp.controller.admin.menu.vo.MpMenuRespVO; +import cn.aagro.pp.module.mp.controller.admin.menu.vo.MpMenuSaveReqVO; +import cn.aagro.pp.module.mp.dal.dataobject.menu.MpMenuDO; +import cn.aagro.pp.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"), + @Mapping(source = "miniProgramAppId", target = "appId"), + @Mapping(source = "miniProgramPagePath", target = "pagePath"), + }) + WxMenuButton convert(MpMenuSaveReqVO.Menu bean); + + MpMenuDO convert02(MpMenuSaveReqVO.Menu menu); + +} diff --git a/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/convert/message/MpAutoReplyConvert.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/convert/message/MpAutoReplyConvert.java new file mode 100644 index 0000000..64d599a --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/convert/message/MpAutoReplyConvert.java @@ -0,0 +1,37 @@ +package cn.aagro.pp.module.mp.convert.message; + +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.mp.controller.admin.message.vo.autoreply.MpAutoReplyCreateReqVO; +import cn.aagro.pp.module.mp.controller.admin.message.vo.autoreply.MpAutoReplyRespVO; +import cn.aagro.pp.module.mp.controller.admin.message.vo.autoreply.MpAutoReplyUpdateReqVO; +import cn.aagro.pp.module.mp.dal.dataobject.message.MpAutoReplyDO; +import cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/convert/message/MpMessageConvert.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/convert/message/MpMessageConvert.java new file mode 100644 index 0000000..4d0969d --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/convert/message/MpMessageConvert.java @@ -0,0 +1,172 @@ +package cn.aagro.pp.module.mp.convert.message; + +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.mp.controller.admin.message.vo.message.MpMessageRespVO; +import cn.aagro.pp.module.mp.controller.admin.message.vo.message.MpMessageSendReqVO; +import cn.aagro.pp.module.mp.dal.dataobject.account.MpAccountDO; +import cn.aagro.pp.module.mp.dal.dataobject.message.MpMessageDO; +import cn.aagro.pp.module.mp.dal.dataobject.user.MpUserDO; +import cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/convert/statistics/MpStatisticsConvert.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/convert/statistics/MpStatisticsConvert.java new file mode 100644 index 0000000..ab1391b --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/convert/statistics/MpStatisticsConvert.java @@ -0,0 +1,52 @@ +package cn.aagro.pp.module.mp.convert.statistics; + +import cn.aagro.pp.module.mp.controller.admin.statistics.vo.MpStatisticsInterfaceSummaryRespVO; +import cn.aagro.pp.module.mp.controller.admin.statistics.vo.MpStatisticsUpstreamMessageRespVO; +import cn.aagro.pp.module.mp.controller.admin.statistics.vo.MpStatisticsUserCumulateRespVO; +import cn.aagro.pp.module.mp.controller.admin.statistics.vo.MpStatisticsUserSummaryRespVO; +import me.chanjar.weixin.mp.bean.datacube.WxDataCubeInterfaceResult; +import me.chanjar.weixin.mp.bean.datacube.WxDataCubeMsgResult; +import me.chanjar.weixin.mp.bean.datacube.WxDataCubeUserCumulate; +import me.chanjar.weixin.mp.bean.datacube.WxDataCubeUserSummary; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Mappings; +import org.mapstruct.Named; +import org.mapstruct.factory.Mappers; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; + +import static cn.aagro.pp.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY; + +@Mapper +public interface MpStatisticsConvert { + + MpStatisticsConvert INSTANCE = Mappers.getMapper(MpStatisticsConvert.class); + + List convertList01(List list); + + List convertList02(List list); + + List convertList03(List list); + + @Mappings({ + @Mapping(target = "refDate", expression = "java(dateFormat0(bean.getRefDate()))"), + @Mapping(source = "msgUser", target = "messageUser"), + @Mapping(source = "msgCount", target = "messageCount"), + }) + MpStatisticsUpstreamMessageRespVO convert(WxDataCubeMsgResult bean); + + List convertList04(List list); + + @Mapping(target = "refDate", expression = "java(dateFormat0(bean.getRefDate()))") + MpStatisticsInterfaceSummaryRespVO convert(WxDataCubeInterfaceResult bean); + + @Named("dateFormat0") + default LocalDateTime dateFormat0(String date) { + return LocalDate.parse(date, DateTimeFormatter.ofPattern(FORMAT_YEAR_MONTH_DAY)).atStartOfDay(); + } + +} diff --git a/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/convert/tag/MpTagConvert.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/convert/tag/MpTagConvert.java new file mode 100644 index 0000000..88b2e0b --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/convert/tag/MpTagConvert.java @@ -0,0 +1,44 @@ +package cn.aagro.pp.module.mp.convert.tag; + +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.mp.controller.admin.tag.vo.MpTagRespVO; +import cn.aagro.pp.module.mp.controller.admin.tag.vo.MpTagSimpleRespVO; +import cn.aagro.pp.module.mp.controller.admin.tag.vo.MpTagUpdateReqVO; +import cn.aagro.pp.module.mp.dal.dataobject.account.MpAccountDO; +import cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/convert/user/MpUserConvert.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/convert/user/MpUserConvert.java new file mode 100644 index 0000000..8f2c8c1 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/convert/user/MpUserConvert.java @@ -0,0 +1,56 @@ +package cn.aagro.pp.module.mp.convert.user; + +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.aagro.pp.framework.common.enums.CommonStatusEnum; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.common.util.collection.CollectionUtils; +import cn.aagro.pp.module.mp.controller.admin.user.vo.MpUserRespVO; +import cn.aagro.pp.module.mp.controller.admin.user.vo.MpUserUpdateReqVO; +import cn.aagro.pp.module.mp.dal.dataobject.account.MpAccountDO; +import cn.aagro.pp.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 = "unionId", target = "unionId"), + @Mapping(source = "headImgUrl", target = "headImageUrl"), + @Mapping(target = "subscribeTime", ignore = true), // 单独转换 + }) + MpUserDO convert(WxMpUser wxMpUser); + + default MpUserDO convert(MpAccountDO account, WxMpUser wxMpUser) { + MpUserDO user = convert(wxMpUser); + user.setSubscribeStatus(wxMpUser.getSubscribe() ? CommonStatusEnum.ENABLE.getStatus() + : CommonStatusEnum.DISABLE.getStatus()); + user.setSubscribeTime(LocalDateTimeUtil.of(wxMpUser.getSubscribeTime() * 1000L)); + if (account != null) { + user.setAccountId(account.getId()); + user.setAppId(account.getAppId()); + } + return user; + } + + default List convertList(MpAccountDO account, List wxUsers) { + return CollectionUtils.convertList(wxUsers, wxUser -> convert(account, wxUser)); + } + + MpUserDO convert(MpUserUpdateReqVO bean); + +} diff --git a/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/dal/dataobject/account/MpAccountDO.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/dal/dataobject/account/MpAccountDO.java new file mode 100644 index 0000000..6af19b5 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/dal/dataobject/account/MpAccountDO.java @@ -0,0 +1,62 @@ +package cn.aagro.pp.module.mp.dal.dataobject.account; + +import cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/dal/dataobject/material/MpMaterialDO.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/dal/dataobject/material/MpMaterialDO.java new file mode 100644 index 0000000..cbc4373 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/dal/dataobject/material/MpMaterialDO.java @@ -0,0 +1,99 @@ +package cn.aagro.pp.module.mp.dal.dataobject.material; + +import cn.aagro.pp.framework.mybatis.core.dataobject.BaseDO; +import cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/dal/dataobject/menu/MpMenuDO.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/dal/dataobject/menu/MpMenuDO.java new file mode 100644 index 0000000..bbb43b4 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/dal/dataobject/menu/MpMenuDO.java @@ -0,0 +1,185 @@ +package cn.aagro.pp.module.mp.dal.dataobject.menu; + +import cn.aagro.pp.framework.mybatis.core.dataobject.BaseDO; +import cn.aagro.pp.module.mp.dal.dataobject.account.MpAccountDO; +import cn.aagro.pp.module.mp.dal.dataobject.message.MpMessageDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import me.chanjar.weixin.common.api.WxConsts; +import me.chanjar.weixin.common.api.WxConsts.MenuButtonType; + +import java.util.List; + +/** + * 公众号菜单 DO + * + * @author 芋道源码 + */ +@TableName(value = "mp_menu", autoResultMap = true) +@KeySequence("mp_menu_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MpMenuDO extends BaseDO { + + /** + * 编号 - 顶级菜单 + */ + public static final Long ID_ROOT = 0L; + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 公众号账号的编号 + * + * 关联 {@link MpAccountDO#getId()} + */ + private Long accountId; + /** + * 公众号 appId + * + * 冗余 {@link MpAccountDO#getAppId()} + */ + private String appId; + + /** + * 菜单名称 + */ + private String name; + /** + * 菜单标识 + * + * 支持多 DB 类型时,无法直接使用 key + @TableField("menuKey") 来实现转换,原因是 "menuKey" AS key 而存在报错 + */ + private String menuKey; + /** + * 父菜单编号 + */ + private Long parentId; + + // ========== 按钮操作 ========== + + /** + * 按钮类型 + * + * 枚举 {@link MenuButtonType} + */ + private String type; + + /** + * 网页链接 + * + * 粉丝点击菜单可打开链接,不超过 1024 字节 + * + * 类型为 {@link WxConsts.XmlMsgType} 的 VIEW、MINIPROGRAM + */ + private String url; + + /** + * 小程序的 appId + * + * 类型为 {@link MenuButtonType} 的 MINIPROGRAM + */ + private String miniProgramAppId; + /** + * 小程序的页面路径 + * + * 类型为 {@link MenuButtonType} 的 MINIPROGRAM + */ + private String miniProgramPagePath; + + /** + * 跳转图文的媒体编号 + */ + private String articleId; + + // ========== 消息内容 ========== + + /** + * 消息类型 + * + * 当 {@link #type} 为 CLICK、SCANCODE_WAITMSG + * + * 枚举 {@link WxConsts.XmlMsgType} 中的 TEXT、IMAGE、VOICE、VIDEO、NEWS、MUSIC + */ + private String replyMessageType; + + /** + * 回复的消息内容 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 TEXT + */ + private String replyContent; + + /** + * 回复的媒体 id + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 IMAGE、VOICE、VIDEO + */ + private String replyMediaId; + /** + * 回复的媒体 URL + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 IMAGE、VOICE、VIDEO + */ + private String replyMediaUrl; + + /** + * 回复的标题 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO + */ + private String replyTitle; + /** + * 回复的描述 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO + */ + private String replyDescription; + + /** + * 回复的缩略图的媒体 id,通过素材管理中的接口上传多媒体文件,得到的 id + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC、VIDEO + */ + private String replyThumbMediaId; + /** + * 回复的缩略图的媒体 URL + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC、VIDEO + */ + private String replyThumbMediaUrl; + + /** + * 回复的图文消息数组 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 NEWS + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List replyArticles; + + /** + * 回复的音乐链接 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC + */ + private String replyMusicUrl; + /** + * 回复的高质量音乐链接 + * + * WIFI 环境优先使用该链接播放音乐 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC + */ + private String replyHqMusicUrl; + +} diff --git a/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/dal/dataobject/message/MpAutoReplyDO.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/dal/dataobject/message/MpAutoReplyDO.java new file mode 100644 index 0000000..898b3e2 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/dal/dataobject/message/MpAutoReplyDO.java @@ -0,0 +1,165 @@ +package cn.aagro.pp.module.mp.dal.dataobject.message; + +import cn.aagro.pp.framework.common.util.collection.SetUtils; +import cn.aagro.pp.framework.mybatis.core.dataobject.BaseDO; +import cn.aagro.pp.module.mp.dal.dataobject.account.MpAccountDO; +import cn.aagro.pp.module.mp.enums.message.MpAutoReplyMatchEnum; +import cn.aagro.pp.module.mp.enums.message.MpAutoReplyTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import me.chanjar.weixin.common.api.WxConsts; +import me.chanjar.weixin.common.api.WxConsts.XmlMsgType; + +import java.util.List; +import java.util.Set; + +/** + * 公众号消息自动回复 DO + * + * @author 芋道源码 + */ +@TableName(value = "mp_auto_reply", autoResultMap = true) +@KeySequence("mp_auto_reply_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MpAutoReplyDO extends BaseDO { + + public static Set REQUEST_MESSAGE_TYPE = SetUtils.asSet(WxConsts.XmlMsgType.TEXT, WxConsts.XmlMsgType.IMAGE, + WxConsts.XmlMsgType.VOICE, WxConsts.XmlMsgType.VIDEO, WxConsts.XmlMsgType.SHORTVIDEO, + WxConsts.XmlMsgType.LOCATION, WxConsts.XmlMsgType.LINK); + + /** + * 主键 + */ + @TableId + private Long id; + /** + * 公众号账号的编号 + * + * 关联 {@link MpAccountDO#getId()} + */ + private Long accountId; + /** + * 公众号 appId + * + * 冗余 {@link MpAccountDO#getAppId()} + */ + private String appId; + + /** + * 回复类型 + * + * 枚举 {@link MpAutoReplyTypeEnum} + */ + private Integer type; + + // ==================== 请求消息 ==================== + + /** + * 请求的关键字 + * + * 当 {@link #type} 为 {@link MpAutoReplyTypeEnum#KEYWORD} + */ + private String requestKeyword; + /** + * 请求的关键字的匹配 + * + * 当 {@link #type} 为 {@link MpAutoReplyTypeEnum#KEYWORD} + * + * 枚举 {@link MpAutoReplyMatchEnum} + */ + private Integer requestMatch; + + /** + * 请求的消息类型 + * + * 当 {@link #type} 为 {@link MpAutoReplyTypeEnum#MESSAGE} + * + * 枚举 {@link XmlMsgType} 中的 {@link #REQUEST_MESSAGE_TYPE} + */ + private String requestMessageType; + + // ==================== 响应消息 ==================== + + /** + * 回复的消息类型 + * + * 枚举 {@link XmlMsgType} 中的 TEXT、IMAGE、VOICE、VIDEO、NEWS + */ + private String responseMessageType; + + /** + * 回复的消息内容 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 TEXT + */ + private String responseContent; + + /** + * 回复的媒体 id + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 IMAGE、VOICE、VIDEO + */ + private String responseMediaId; + /** + * 回复的媒体 URL + */ + private String responseMediaUrl; + + /** + * 回复的标题 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO + */ + private String responseTitle; + /** + * 回复的描述 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO + */ + private String responseDescription; + + /** + * 回复的缩略图的媒体 id,通过素材管理中的接口上传多媒体文件,得到的 id + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC、VIDEO + */ + private String responseThumbMediaId; + /** + * 回复的缩略图的媒体 URL + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC、VIDEO + */ + private String responseThumbMediaUrl; + + /** + * 回复的图文消息 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 NEWS + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List responseArticles; + + /** + * 回复的音乐链接 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC + */ + private String responseMusicUrl; + /** + * 回复的高质量音乐链接 + * + * WIFI 环境优先使用该链接播放音乐 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC + */ + private String responseHqMusicUrl; + +} diff --git a/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/dal/dataobject/message/MpMessageDO.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/dal/dataobject/message/MpMessageDO.java new file mode 100644 index 0000000..8891e9d --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/dal/dataobject/message/MpMessageDO.java @@ -0,0 +1,242 @@ +package cn.aagro.pp.module.mp.dal.dataobject.message; + +import cn.aagro.pp.framework.mybatis.core.dataobject.BaseDO; +import cn.aagro.pp.module.mp.dal.dataobject.account.MpAccountDO; +import cn.aagro.pp.module.mp.dal.dataobject.user.MpUserDO; +import cn.aagro.pp.module.mp.enums.message.MpMessageSendFromEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import me.chanjar.weixin.common.api.WxConsts; +import me.chanjar.weixin.mp.builder.kefu.NewsBuilder; + +import javax.validation.constraints.NotEmpty; +import java.io.Serializable; +import java.util.List; + +/** + * 公众号消息 DO + * + * @author 芋道源码 + */ +@TableName(value = "mp_message", autoResultMap = true) +@KeySequence("mp_message_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MpMessageDO extends BaseDO { + + /** + * 主键 + */ + @TableId + private Long id; + /** + * 微信公众号消息 id + */ + private Long msgId; + /** + * 公众号账号的 ID + * + * 关联 {@link MpAccountDO#getId()} + */ + private Long accountId; + /** + * 公众号 appid + * + * 冗余 {@link MpAccountDO#getAppId()} + */ + private String appId; + /** + * 公众号粉丝的编号 + * + * 关联 {@link MpUserDO#getId()} + */ + private Long userId; + /** + * 公众号粉丝标志 + * + * 冗余 {@link MpUserDO#getOpenid()} + */ + private String openid; + + /** + * 消息类型 + * + * 枚举 {@link WxConsts.XmlMsgType} + */ + private String type; + /** + * 消息来源 + * + * 枚举 {@link MpMessageSendFromEnum} + */ + private Integer sendFrom; + + // ========= 普通消息内容 https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Receiving_standard_messages.html + + /** + * 消息内容 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 TEXT + */ + private String content; + + /** + * 媒体文件的编号 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 IMAGE、VOICE、VIDEO + */ + private String mediaId; + /** + * 媒体文件的 URL + */ + private String mediaUrl; + /** + * 语音识别后文本 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 VOICE + */ + private String recognition; + /** + * 语音格式,如 amr,speex 等 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 VOICE + */ + private String format; + /** + * 标题 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO、MUSIC、LINK + */ + private String title; + /** + * 描述 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO、MUSIC + */ + private String description; + + /** + * 缩略图的媒体 id,通过素材管理中的接口上传多媒体文件,得到的 id + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC、VIDEO + */ + private String thumbMediaId; + /** + * 缩略图的媒体 URL + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC、VIDEO + */ + private String thumbMediaUrl; + + /** + * 点击图文消息跳转链接 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 LINK + */ + private String url; + + /** + * 地理位置维度 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 LOCATION + */ + private Double locationX; + /** + * 地理位置经度 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 LOCATION + */ + private Double locationY; + /** + * 地图缩放大小 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 LOCATION + */ + private Double scale; + /** + * 详细地址 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 LOCATION + * + * 例如说杨浦区黄兴路 221-4 号临 + */ + private String label; + + /** + * 图文消息数组 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 NEWS + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List
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; + + } + +} diff --git a/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/dal/dataobject/tag/MpTagDO.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/dal/dataobject/tag/MpTagDO.java new file mode 100644 index 0000000..a60af49 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/dal/dataobject/tag/MpTagDO.java @@ -0,0 +1,58 @@ +package cn.aagro.pp.module.mp.dal.dataobject.tag; + +import cn.aagro.pp.module.mp.dal.dataobject.account.MpAccountDO; +import lombok.*; + +import com.baomidou.mybatisplus.annotation.*; +import cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/dal/dataobject/user/MpUserDO.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/dal/dataobject/user/MpUserDO.java new file mode 100644 index 0000000..ba34274 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/dal/dataobject/user/MpUserDO.java @@ -0,0 +1,114 @@ +package cn.aagro.pp.module.mp.dal.dataobject.user; + +import cn.aagro.pp.framework.common.enums.CommonStatusEnum; +import cn.aagro.pp.framework.mybatis.core.dataobject.BaseDO; +import cn.aagro.pp.framework.mybatis.core.type.LongListTypeHandler; +import cn.aagro.pp.module.mp.dal.dataobject.account.MpAccountDO; +import cn.aagro.pp.module.mp.dal.dataobject.tag.MpTagDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 微信公众号粉丝 DO + * + * @author 芋道源码 + */ +@TableName(value = "mp_user", autoResultMap = true) +@KeySequence("mp_user_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MpUserDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 粉丝标识 + */ + private String openid; + /** + * 微信生态唯一标识 + */ + private String unionId; + /** + * 关注状态 + * + * 枚举 {@link CommonStatusEnum} + * 1. 开启 - 已关注 + * 2. 禁用 - 取消关注 + */ + private Integer subscribeStatus; + /** + * 关注时间 + */ + private LocalDateTime subscribeTime; + /** + * 取消关注时间 + */ + private LocalDateTime unsubscribeTime; + /** + * 昵称 + * + * 注意,2021-12-27 公众号接口不再返回头像和昵称,只能通过微信公众号的网页登录获取 + */ + private String nickname; + /** + * 头像地址 + * + * 注意,2021-12-27 公众号接口不再返回头像和昵称,只能通过微信公众号的网页登录获取 + */ + private String headImageUrl; + /** + * 语言 + */ + private String language; + /** + * 国家 + */ + private String country; + /** + * 省份 + */ + private String province; + /** + * 城市 + */ + private String city; + /** + * 备注 + */ + private String remark; + /** + * 标签编号数组 + * + * 注意,对应的是 {@link MpTagDO#getTagId()} 字段 + */ + @TableField(typeHandler = LongListTypeHandler.class) + private List tagIds; + + /** + * 公众号账号的编号 + * + * 关联 {@link MpAccountDO#getId()} + */ + private Long accountId; + /** + * 公众号 appId + * + * 冗余 {@link MpAccountDO#getAppId()} + */ + private String appId; + +} diff --git a/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/dal/mysql/account/MpAccountMapper.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/dal/mysql/account/MpAccountMapper.java new file mode 100644 index 0000000..65a641f --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/dal/mysql/account/MpAccountMapper.java @@ -0,0 +1,31 @@ +package cn.aagro.pp.module.mp.dal.mysql.account; + +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.mybatis.core.mapper.BaseMapperX; +import cn.aagro.pp.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.aagro.pp.module.mp.controller.admin.account.vo.MpAccountPageReqVO; +import cn.aagro.pp.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 mp_account WHERE update_time > #{maxUpdateTime}") + Long selectCountByUpdateTimeGt(LocalDateTime maxUpdateTime); + +} diff --git a/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/dal/mysql/material/MpMaterialMapper.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/dal/mysql/material/MpMaterialMapper.java new file mode 100644 index 0000000..988bfb5 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/dal/mysql/material/MpMaterialMapper.java @@ -0,0 +1,33 @@ +package cn.aagro.pp.module.mp.dal.mysql.material; + +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.mybatis.core.mapper.BaseMapperX; +import cn.aagro.pp.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.aagro.pp.module.mp.controller.admin.material.vo.MpMaterialPageReqVO; +import cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/dal/mysql/menu/MpMenuMapper.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/dal/mysql/menu/MpMenuMapper.java new file mode 100644 index 0000000..74833f6 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/dal/mysql/menu/MpMenuMapper.java @@ -0,0 +1,25 @@ +package cn.aagro.pp.module.mp.dal.mysql.menu; + +import cn.aagro.pp.framework.mybatis.core.mapper.BaseMapperX; +import cn.aagro.pp.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/dal/mysql/message/MpAutoReplyMapper.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/dal/mysql/message/MpAutoReplyMapper.java new file mode 100644 index 0000000..3bb6cb8 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/dal/mysql/message/MpAutoReplyMapper.java @@ -0,0 +1,70 @@ +package cn.aagro.pp.module.mp.dal.mysql.message; + +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.mybatis.core.mapper.BaseMapperX; +import cn.aagro.pp.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.aagro.pp.module.mp.controller.admin.message.vo.message.MpMessagePageReqVO; +import cn.aagro.pp.module.mp.dal.dataobject.message.MpAutoReplyDO; +import cn.aagro.pp.module.mp.enums.message.MpAutoReplyMatchEnum; +import cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/dal/mysql/message/MpMessageMapper.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/dal/mysql/message/MpMessageMapper.java new file mode 100644 index 0000000..c197d5f --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/dal/mysql/message/MpMessageMapper.java @@ -0,0 +1,23 @@ +package cn.aagro.pp.module.mp.dal.mysql.message; + +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.mybatis.core.mapper.BaseMapperX; +import cn.aagro.pp.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.aagro.pp.module.mp.controller.admin.message.vo.message.MpMessagePageReqVO; +import cn.aagro.pp.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()) + .eqIfPresent(MpMessageDO::getUserId, reqVO.getUserId()) + .betweenIfPresent(MpMessageDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(MpMessageDO::getId)); + } + +} diff --git a/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/dal/mysql/tag/MpTagMapper.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/dal/mysql/tag/MpTagMapper.java new file mode 100644 index 0000000..2baa953 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/dal/mysql/tag/MpTagMapper.java @@ -0,0 +1,26 @@ +package cn.aagro.pp.module.mp.dal.mysql.tag; + +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.mybatis.core.mapper.BaseMapperX; +import cn.aagro.pp.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.aagro.pp.module.mp.controller.admin.tag.vo.MpTagPageReqVO; +import cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/dal/mysql/user/MpUserMapper.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/dal/mysql/user/MpUserMapper.java new file mode 100644 index 0000000..d7e223c --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/dal/mysql/user/MpUserMapper.java @@ -0,0 +1,35 @@ +package cn.aagro.pp.module.mp.dal.mysql.user; + +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.mybatis.core.mapper.BaseMapperX; +import cn.aagro.pp.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.aagro.pp.module.mp.controller.admin.user.vo.MpUserPageReqVO; +import cn.aagro.pp.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 selectFirstOne(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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/enums/ErrorCodeConstants.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/enums/ErrorCodeConstants.java new file mode 100644 index 0000000..3373722 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/enums/ErrorCodeConstants.java @@ -0,0 +1,64 @@ +package cn.aagro.pp.module.mp.enums; + +import cn.aagro.pp.framework.common.exception.ErrorCode; + +/** + * Mp 错误码枚举类 + * + * mp 系统,使用 1-006-000-000 段 + */ +public interface ErrorCodeConstants { + + // ========== 公众号账号 1-006-000-000 ============ + ErrorCode ACCOUNT_NOT_EXISTS = new ErrorCode(1_006_000_000, "公众号账号不存在"); + ErrorCode ACCOUNT_GENERATE_QR_CODE_FAIL = new ErrorCode(1_006_000_001, "生成公众号二维码失败,原因:{}"); + ErrorCode ACCOUNT_CLEAR_QUOTA_FAIL = new ErrorCode(1_006_000_002, "清空公众号的 API 配额失败,原因:{}"); + + // ========== 公众号统计 1-006-001-000 ============ + ErrorCode STATISTICS_GET_USER_SUMMARY_FAIL = new ErrorCode(1_006_001_000, "获取粉丝增减数据失败,原因:{}"); + ErrorCode STATISTICS_GET_USER_CUMULATE_FAIL = new ErrorCode(1_006_001_001, "获得粉丝累计数据失败,原因:{}"); + ErrorCode STATISTICS_GET_UPSTREAM_MESSAGE_FAIL = new ErrorCode(1_006_001_002, "获得消息发送概况数据失败,原因:{}"); + ErrorCode STATISTICS_GET_INTERFACE_SUMMARY_FAIL = new ErrorCode(1_006_001_003, "获得接口分析数据失败,原因:{}"); + + // ========== 公众号标签 1-006-002-000 ============ + ErrorCode TAG_NOT_EXISTS = new ErrorCode(1_006_002_000, "标签不存在"); + ErrorCode TAG_CREATE_FAIL = new ErrorCode(1_006_002_001, "创建标签失败,原因:{}"); + ErrorCode TAG_UPDATE_FAIL = new ErrorCode(1_006_002_002, "更新标签失败,原因:{}"); + ErrorCode TAG_DELETE_FAIL = new ErrorCode(1_006_002_003, "删除标签失败,原因:{}"); + ErrorCode TAG_GET_FAIL = new ErrorCode(1_006_002_004, "获得标签失败,原因:{}"); + + // ========== 公众号粉丝 1-006-003-000 ============ + ErrorCode USER_NOT_EXISTS = new ErrorCode(1_006_003_000, "粉丝不存在"); + ErrorCode USER_UPDATE_TAG_FAIL = new ErrorCode(1_006_003_001, "更新粉丝标签失败,原因:{}"); + + // ========== 公众号素材 1-006-004-000 ============ + ErrorCode MATERIAL_NOT_EXISTS = new ErrorCode(1_006_004_000, "素材不存在"); + ErrorCode MATERIAL_UPLOAD_FAIL = new ErrorCode(1_006_004_001, "上传素材失败,原因:{}"); + ErrorCode MATERIAL_IMAGE_UPLOAD_FAIL = new ErrorCode(1_006_004_002, "上传图片失败,原因:{}"); + ErrorCode MATERIAL_DELETE_FAIL = new ErrorCode(1_006_004_003, "删除素材失败,原因:{}"); + + // ========== 公众号消息 1-006-005-000 ============ + ErrorCode MESSAGE_SEND_FAIL = new ErrorCode(1_006_005_000, "发送消息失败,原因:{}"); + + // ========== 公众号发布能力 1-006-006-000 ============ + ErrorCode FREE_PUBLISH_LIST_FAIL = new ErrorCode(1_006_006_000, "获得已成功发布列表失败,原因:{}"); + ErrorCode FREE_PUBLISH_SUBMIT_FAIL = new ErrorCode(1_006_006_001, "提交发布失败,原因:{}"); + ErrorCode FREE_PUBLISH_DELETE_FAIL = new ErrorCode(1_006_006_002, "删除发布失败,原因:{}"); + + // ========== 公众号草稿 1-006-007-000 ============ + ErrorCode DRAFT_LIST_FAIL = new ErrorCode(1_006_007_000, "获得草稿列表失败,原因:{}"); + ErrorCode DRAFT_CREATE_FAIL = new ErrorCode(1_006_007_001, "创建草稿失败,原因:{}"); + ErrorCode DRAFT_UPDATE_FAIL = new ErrorCode(1_006_007_002, "更新草稿失败,原因:{}"); + ErrorCode DRAFT_DELETE_FAIL = new ErrorCode(1_006_007_003, "删除草稿失败,原因:{}"); + + // ========== 公众号菜单 1-006-008-000 ============ + ErrorCode MENU_SAVE_FAIL = new ErrorCode(1_006_008_000, "创建菜单失败,原因:{}"); + ErrorCode MENU_DELETE_FAIL = new ErrorCode(1_006_008_001, "删除菜单失败,原因:{}"); + + // ========== 公众号自动回复 1-006-009-000 ============ + ErrorCode AUTO_REPLY_NOT_EXISTS = new ErrorCode(1_006_009_000, "自动回复不存在"); + ErrorCode AUTO_REPLY_ADD_SUBSCRIBE_FAIL_EXISTS = new ErrorCode(1_006_009_001, "操作失败,原因:已存在关注时的回复"); + ErrorCode AUTO_REPLY_ADD_MESSAGE_FAIL_EXISTS = new ErrorCode(1_006_009_002, "操作失败,原因:已存在该消息类型的回复"); + ErrorCode AUTO_REPLY_ADD_KEYWORD_FAIL_EXISTS = new ErrorCode(1_006_009_003, "操作失败,原因:已关在该关键字的回复"); + +} diff --git a/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/enums/message/MpAutoReplyMatchEnum.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/enums/message/MpAutoReplyMatchEnum.java new file mode 100644 index 0000000..7cfe29b --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/enums/message/MpAutoReplyMatchEnum.java @@ -0,0 +1,28 @@ +package cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/enums/message/MpAutoReplyTypeEnum.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/enums/message/MpAutoReplyTypeEnum.java new file mode 100644 index 0000000..47c5c57 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/enums/message/MpAutoReplyTypeEnum.java @@ -0,0 +1,29 @@ +package cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/enums/message/MpMessageSendFromEnum.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/enums/message/MpMessageSendFromEnum.java new file mode 100644 index 0000000..1e33d6f --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/enums/message/MpMessageSendFromEnum.java @@ -0,0 +1,28 @@ +package cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/framework/mp/config/MpConfiguration.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/framework/mp/config/MpConfiguration.java new file mode 100644 index 0000000..c1ce9ca --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/framework/mp/config/MpConfiguration.java @@ -0,0 +1,54 @@ +package cn.aagro.pp.module.mp.framework.mp.config; + +import cn.aagro.pp.module.mp.framework.mp.core.DefaultMpServiceFactory; +import cn.aagro.pp.module.mp.framework.mp.core.MpServiceFactory; +import cn.aagro.pp.module.mp.service.handler.menu.MenuHandler; +import cn.aagro.pp.module.mp.service.handler.message.MessageReceiveHandler; +import cn.aagro.pp.module.mp.service.handler.message.MessageAutoReplyHandler; +import cn.aagro.pp.module.mp.service.handler.other.KfSessionHandler; +import cn.aagro.pp.module.mp.service.handler.other.NullHandler; +import cn.aagro.pp.module.mp.service.handler.other.ScanHandler; +import cn.aagro.pp.module.mp.service.handler.other.StoreCheckNotifyHandler; +import cn.aagro.pp.module.mp.service.handler.user.LocationHandler; +import cn.aagro.pp.module.mp.service.handler.user.SubscribeHandler; +import cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/framework/mp/core/DefaultMpServiceFactory.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/framework/mp/core/DefaultMpServiceFactory.java new file mode 100644 index 0000000..92352ca --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/framework/mp/core/DefaultMpServiceFactory.java @@ -0,0 +1,177 @@ +package cn.aagro.pp.module.mp.framework.mp.core; + +import cn.aagro.pp.module.mp.dal.dataobject.account.MpAccountDO; +import cn.aagro.pp.module.mp.service.handler.menu.MenuHandler; +import cn.aagro.pp.module.mp.service.handler.message.MessageReceiveHandler; +import cn.aagro.pp.module.mp.service.handler.message.MessageAutoReplyHandler; +import cn.aagro.pp.module.mp.service.handler.other.KfSessionHandler; +import cn.aagro.pp.module.mp.service.handler.other.NullHandler; +import cn.aagro.pp.module.mp.service.handler.other.ScanHandler; +import cn.aagro.pp.module.mp.service.handler.other.StoreCheckNotifyHandler; +import cn.aagro.pp.module.mp.service.handler.user.LocationHandler; +import cn.aagro.pp.module.mp.service.handler.user.SubscribeHandler; +import cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/framework/mp/core/MpServiceFactory.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/framework/mp/core/MpServiceFactory.java new file mode 100644 index 0000000..3c3af9f --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/framework/mp/core/MpServiceFactory.java @@ -0,0 +1,66 @@ +package cn.aagro.pp.module.mp.framework.mp.core; + +import cn.hutool.core.lang.Assert; +import cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/framework/mp/core/context/MpContextHolder.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/framework/mp/core/context/MpContextHolder.java new file mode 100644 index 0000000..e9201fe --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/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.aagro.pp.module.mp.framework.mp.core.context; + +import cn.aagro.pp.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.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/framework/mp/core/util/MpUtils.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/framework/mp/core/util/MpUtils.java new file mode 100644 index 0000000..495001f --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/framework/mp/core/util/MpUtils.java @@ -0,0 +1,167 @@ +package cn.aagro.pp.module.mp.framework.mp.core.util; + +import cn.hutool.core.util.StrUtil; +import cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/framework/package-info.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/framework/package-info.java new file mode 100644 index 0000000..7c70fcf --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/framework/package-info.java @@ -0,0 +1,6 @@ +/** + * 属于 mp 模块的 framework 封装 + * + * @author 芋道源码 + */ +package cn.aagro.pp.module.mp.framework; diff --git a/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/framework/web/config/MpWebConfiguration.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/framework/web/config/MpWebConfiguration.java new file mode 100644 index 0000000..751dd7b --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/framework/web/config/MpWebConfiguration.java @@ -0,0 +1,24 @@ +package cn.aagro.pp.module.mp.framework.web.config; + +import cn.aagro.pp.framework.swagger.config.AagroSwaggerAutoConfiguration; +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 AagroSwaggerAutoConfiguration.buildGroupedOpenApi("mp"); + } + +} diff --git a/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/framework/web/package-info.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/framework/web/package-info.java new file mode 100644 index 0000000..15011ed --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/framework/web/package-info.java @@ -0,0 +1,4 @@ +/** + * mp 模块的 web 配置 + */ +package cn.aagro.pp.module.mp.framework.web; diff --git a/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/package-info.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/package-info.java new file mode 100644 index 0000000..ac48d7c --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/package-info.java @@ -0,0 +1,8 @@ +/** + * mp 模块,我们放微信微信公众号。 + * 例如说:提供微信公众号的账号、菜单、粉丝、标签、消息、自动回复、素材、模板通知、运营数据等功能 + * + * 1. Controller URL:以 /mp/ 开头,避免和其它 Module 冲突 + * 2. DataObject 表名:以 mp_ 开头,方便在数据库中区分 + */ +package cn.aagro.pp.module.mp; diff --git a/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/account/MpAccountService.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/account/MpAccountService.java new file mode 100644 index 0000000..d6df79a --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/account/MpAccountService.java @@ -0,0 +1,110 @@ +package cn.aagro.pp.module.mp.service.account; + +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.mp.controller.admin.account.vo.MpAccountCreateReqVO; +import cn.aagro.pp.module.mp.controller.admin.account.vo.MpAccountPageReqVO; +import cn.aagro.pp.module.mp.controller.admin.account.vo.MpAccountUpdateReqVO; +import cn.aagro.pp.module.mp.dal.dataobject.account.MpAccountDO; + +import javax.validation.Valid; +import java.util.List; + +import static cn.aagro.pp.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/account/MpAccountServiceImpl.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/account/MpAccountServiceImpl.java new file mode 100644 index 0000000..13db58a --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/account/MpAccountServiceImpl.java @@ -0,0 +1,229 @@ +package cn.aagro.pp.module.mp.service.account; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjUtil; +import cn.aagro.pp.framework.common.exception.util.ServiceExceptionUtil; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.tenant.core.util.TenantUtils; +import cn.aagro.pp.module.mp.controller.admin.account.vo.MpAccountCreateReqVO; +import cn.aagro.pp.module.mp.controller.admin.account.vo.MpAccountPageReqVO; +import cn.aagro.pp.module.mp.controller.admin.account.vo.MpAccountUpdateReqVO; +import cn.aagro.pp.module.mp.convert.account.MpAccountConvert; +import cn.aagro.pp.module.mp.dal.dataobject.account.MpAccountDO; +import cn.aagro.pp.module.mp.dal.mysql.account.MpAccountMapper; +import cn.aagro.pp.module.mp.enums.ErrorCodeConstants; +import cn.aagro.pp.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.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static cn.aagro.pp.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.aagro.pp.framework.common.util.collection.CollectionUtils.convertMap; +import static cn.aagro.pp.framework.common.util.collection.CollectionUtils.getMaxValue; +import static cn.aagro.pp.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 = Collections.emptyList(); + try { + accounts = mpAccountMapper.selectList(); + } catch (Throwable ex) { + if (!ex.getMessage().contains("doesn't exist")) { + throw ex; + } + log.error("[微信公众号 aagro-module-mp - 表结构未导入][参考 https://doc.iocoder.cn/mp/build/ 开启]"); + } + 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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/handler/menu/MenuHandler.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/handler/menu/MenuHandler.java new file mode 100644 index 0000000..925aaf3 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/handler/menu/MenuHandler.java @@ -0,0 +1,34 @@ +package cn.aagro.pp.module.mp.service.handler.menu; + +import cn.aagro.pp.module.mp.framework.mp.core.context.MpContextHolder; +import cn.aagro.pp.module.mp.service.menu.MpMenuService; +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 +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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/handler/message/MessageAutoReplyHandler.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/handler/message/MessageAutoReplyHandler.java new file mode 100644 index 0000000..8fe6f8f --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/handler/message/MessageAutoReplyHandler.java @@ -0,0 +1,41 @@ +package cn.aagro.pp.module.mp.service.handler.message; + +import cn.aagro.pp.module.mp.dal.dataobject.message.MpAutoReplyDO; +import cn.aagro.pp.module.mp.framework.mp.core.context.MpContextHolder; +import cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/handler/message/MessageReceiveHandler.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/handler/message/MessageReceiveHandler.java new file mode 100644 index 0000000..9f68b35 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/handler/message/MessageReceiveHandler.java @@ -0,0 +1,36 @@ +package cn.aagro.pp.module.mp.service.handler.message; + +import cn.aagro.pp.module.mp.framework.mp.core.context.MpContextHolder; +import cn.aagro.pp.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(wxMpService, MpContextHolder.getAppId(), wxMessage); + return null; + } + +} diff --git a/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/handler/other/KfSessionHandler.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/handler/other/KfSessionHandler.java new file mode 100644 index 0000000..074bc85 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/handler/other/KfSessionHandler.java @@ -0,0 +1,26 @@ +package cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/handler/other/NullHandler.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/handler/other/NullHandler.java new file mode 100644 index 0000000..f7f3578 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/handler/other/NullHandler.java @@ -0,0 +1,24 @@ +package cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/handler/other/ScanHandler.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/handler/other/ScanHandler.java new file mode 100644 index 0000000..4817031 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/handler/other/ScanHandler.java @@ -0,0 +1,25 @@ +package cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/handler/other/StoreCheckNotifyHandler.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/handler/other/StoreCheckNotifyHandler.java new file mode 100644 index 0000000..bb1ed9e --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/handler/other/StoreCheckNotifyHandler.java @@ -0,0 +1,24 @@ +package cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/handler/other/package-info.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/handler/other/package-info.java new file mode 100644 index 0000000..f6c522e --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/handler/other/package-info.java @@ -0,0 +1,4 @@ +/** + * 本包内的 handler 都是一些不重要的,所以放在 other 其它里 + */ +package cn.aagro.pp.module.mp.service.handler.other; diff --git a/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/handler/user/LocationHandler.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/handler/user/LocationHandler.java new file mode 100644 index 0000000..740be84 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/handler/user/LocationHandler.java @@ -0,0 +1,49 @@ +package cn.aagro.pp.module.mp.service.handler.user; + +import cn.hutool.core.util.ObjectUtil; +import cn.aagro.pp.framework.common.util.object.ObjectUtils; +import cn.aagro.pp.module.mp.framework.mp.core.context.MpContextHolder; +import cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/handler/user/SubscribeHandler.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/handler/user/SubscribeHandler.java new file mode 100644 index 0000000..7102668 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/handler/user/SubscribeHandler.java @@ -0,0 +1,61 @@ +package cn.aagro.pp.module.mp.service.handler.user; + +import cn.hutool.core.util.ObjUtil; +import cn.aagro.pp.module.mp.framework.mp.core.context.MpContextHolder; +import cn.aagro.pp.module.mp.service.message.MpAutoReplyService; +import cn.aagro.pp.module.mp.service.user.MpUserService; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.error.WxMpErrorMsgEnum; +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); + // 特殊情况(个人账号,无接口权限):https://t.zsxq.com/cLFq5 + if (ObjUtil.equal(e.getError().getErrorCode(), WxMpErrorMsgEnum.CODE_48001)) { + wxMpUser = new WxMpUser(); + wxMpUser.setOpenId(wxMessage.getFromUser()); + wxMpUser.setSubscribe(true); + wxMpUser.setSubscribeTime(System.currentTimeMillis() / 1000L); + } + } + + // 第二步,保存粉丝信息 + mpUserService.saveUser(MpContextHolder.getAppId(), wxMpUser); + + // 第三步,回复关注的欢迎语 + return mpAutoReplyService.replyForSubscribe(MpContextHolder.getAppId(), wxMessage); + } + +} diff --git a/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/handler/user/UnsubscribeHandler.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/handler/user/UnsubscribeHandler.java new file mode 100644 index 0000000..6b68fdd --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/handler/user/UnsubscribeHandler.java @@ -0,0 +1,39 @@ +package cn.aagro.pp.module.mp.service.handler.user; + +import cn.aagro.pp.module.mp.framework.mp.core.context.MpContextHolder; +import cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/material/MpMaterialService.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/material/MpMaterialService.java new file mode 100644 index 0000000..75fa03f --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/material/MpMaterialService.java @@ -0,0 +1,84 @@ +package cn.aagro.pp.module.mp.service.material; + +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.mp.controller.admin.material.vo.MpMaterialPageReqVO; +import cn.aagro.pp.module.mp.controller.admin.material.vo.MpMaterialUploadNewsImageReqVO; +import cn.aagro.pp.module.mp.controller.admin.material.vo.MpMaterialUploadPermanentReqVO; +import cn.aagro.pp.module.mp.controller.admin.material.vo.MpMaterialUploadTemporaryReqVO; +import cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/material/MpMaterialServiceImpl.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/material/MpMaterialServiceImpl.java new file mode 100644 index 0000000..6f8088b --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/material/MpMaterialServiceImpl.java @@ -0,0 +1,224 @@ +package cn.aagro.pp.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.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.infra.api.file.FileApi; +import cn.aagro.pp.module.mp.controller.admin.material.vo.MpMaterialPageReqVO; +import cn.aagro.pp.module.mp.controller.admin.material.vo.MpMaterialUploadNewsImageReqVO; +import cn.aagro.pp.module.mp.controller.admin.material.vo.MpMaterialUploadPermanentReqVO; +import cn.aagro.pp.module.mp.controller.admin.material.vo.MpMaterialUploadTemporaryReqVO; +import cn.aagro.pp.module.mp.convert.material.MpMaterialConvert; +import cn.aagro.pp.module.mp.dal.dataobject.account.MpAccountDO; +import cn.aagro.pp.module.mp.dal.dataobject.material.MpMaterialDO; +import cn.aagro.pp.module.mp.dal.mysql.material.MpMaterialMapper; +import cn.aagro.pp.module.mp.framework.mp.core.MpServiceFactory; +import cn.aagro.pp.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.aagro.pp.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.aagro.pp.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(FileUtil.readBytes(file), path); + } + +} diff --git a/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/menu/MpMenuService.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/menu/MpMenuService.java new file mode 100644 index 0000000..cd68867 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/menu/MpMenuService.java @@ -0,0 +1,49 @@ +package cn.aagro.pp.module.mp.service.menu; + +import cn.aagro.pp.module.mp.controller.admin.menu.vo.MpMenuSaveReqVO; +import cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/menu/MpMenuServiceImpl.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/menu/MpMenuServiceImpl.java new file mode 100644 index 0000000..934ca38 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/menu/MpMenuServiceImpl.java @@ -0,0 +1,171 @@ +package cn.aagro.pp.module.mp.service.menu; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import cn.aagro.pp.module.mp.controller.admin.menu.vo.MpMenuSaveReqVO; +import cn.aagro.pp.module.mp.convert.menu.MpMenuConvert; +import cn.aagro.pp.module.mp.dal.dataobject.account.MpAccountDO; +import cn.aagro.pp.module.mp.dal.dataobject.menu.MpMenuDO; +import cn.aagro.pp.module.mp.dal.mysql.menu.MpMenuMapper; +import cn.aagro.pp.module.mp.framework.mp.core.MpServiceFactory; +import cn.aagro.pp.module.mp.framework.mp.core.util.MpUtils; +import cn.aagro.pp.module.mp.service.account.MpAccountService; +import cn.aagro.pp.module.mp.service.message.MpMessageService; +import cn.aagro.pp.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.aagro.pp.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.aagro.pp.module.mp.enums.ErrorCodeConstants.MENU_DELETE_FAIL; +import static cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/message/MpAutoReplyService.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/message/MpAutoReplyService.java new file mode 100644 index 0000000..2732509 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/message/MpAutoReplyService.java @@ -0,0 +1,75 @@ +package cn.aagro.pp.module.mp.service.message; + +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.mp.controller.admin.message.vo.autoreply.MpAutoReplyCreateReqVO; +import cn.aagro.pp.module.mp.controller.admin.message.vo.autoreply.MpAutoReplyUpdateReqVO; +import cn.aagro.pp.module.mp.controller.admin.message.vo.message.MpMessagePageReqVO; +import cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/message/MpAutoReplyServiceImpl.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/message/MpAutoReplyServiceImpl.java new file mode 100644 index 0000000..d852fdc --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/message/MpAutoReplyServiceImpl.java @@ -0,0 +1,202 @@ +package cn.aagro.pp.module.mp.service.message; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ObjUtil; +import cn.aagro.pp.framework.common.exception.ErrorCode; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.mp.controller.admin.message.vo.autoreply.MpAutoReplyCreateReqVO; +import cn.aagro.pp.module.mp.controller.admin.message.vo.autoreply.MpAutoReplyUpdateReqVO; +import cn.aagro.pp.module.mp.controller.admin.message.vo.message.MpMessagePageReqVO; +import cn.aagro.pp.module.mp.convert.message.MpAutoReplyConvert; +import cn.aagro.pp.module.mp.dal.dataobject.account.MpAccountDO; +import cn.aagro.pp.module.mp.dal.dataobject.message.MpAutoReplyDO; +import cn.aagro.pp.module.mp.dal.mysql.message.MpAutoReplyMapper; +import cn.aagro.pp.module.mp.enums.message.MpAutoReplyTypeEnum; +import cn.aagro.pp.module.mp.framework.mp.core.util.MpUtils; +import cn.aagro.pp.module.mp.service.account.MpAccountService; +import cn.aagro.pp.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.aagro.pp.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/message/MpMessageService.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/message/MpMessageService.java new file mode 100644 index 0000000..d08f615 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/message/MpMessageService.java @@ -0,0 +1,59 @@ +package cn.aagro.pp.module.mp.service.message; + +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.mp.controller.admin.message.vo.message.MpMessagePageReqVO; +import cn.aagro.pp.module.mp.controller.admin.message.vo.message.MpMessageSendReqVO; +import cn.aagro.pp.module.mp.dal.dataobject.message.MpMessageDO; +import cn.aagro.pp.module.mp.service.message.bo.MpMessageSendOutReqBO; +import me.chanjar.weixin.mp.api.WxMpService; +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(WxMpService weixinService, String appId, WxMpXmlMessage wxMessage); + + /** + * 使用公众号,给粉丝回复消息 + * + * 例如说:自动回复、客服消息、菜单回复消息等场景 + * + * 注意,该方法只是返回 WxMpXmlOutMessage 对象,不会真的发送消息 + * + * @param sendReqBO 消息内容 + * @return 微信回复消息 XML + */ + WxMpXmlOutMessage sendOutMessage(@Valid MpMessageSendOutReqBO sendReqBO); + + /** + * 使用公众号,给粉丝发送【客服】消息 + * + * 注意,该方法会真实发送消息 + * + * @param sendReqVO 消息内容 + * @return 消息 + */ + MpMessageDO sendKefuMessage(MpMessageSendReqVO sendReqVO); + +} diff --git a/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/message/MpMessageServiceImpl.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/message/MpMessageServiceImpl.java new file mode 100644 index 0000000..f4b7a04 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/message/MpMessageServiceImpl.java @@ -0,0 +1,172 @@ +package cn.aagro.pp.module.mp.service.message; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.thread.ThreadUtil; +import cn.hutool.core.util.StrUtil; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.mp.controller.admin.message.vo.message.MpMessagePageReqVO; +import cn.aagro.pp.module.mp.controller.admin.message.vo.message.MpMessageSendReqVO; +import cn.aagro.pp.module.mp.convert.message.MpMessageConvert; +import cn.aagro.pp.module.mp.dal.dataobject.account.MpAccountDO; +import cn.aagro.pp.module.mp.dal.dataobject.message.MpMessageDO; +import cn.aagro.pp.module.mp.dal.dataobject.user.MpUserDO; +import cn.aagro.pp.module.mp.dal.mysql.message.MpMessageMapper; +import cn.aagro.pp.module.mp.enums.message.MpMessageSendFromEnum; +import cn.aagro.pp.module.mp.framework.mp.core.MpServiceFactory; +import cn.aagro.pp.module.mp.framework.mp.core.util.MpUtils; +import cn.aagro.pp.module.mp.service.account.MpAccountService; +import cn.aagro.pp.module.mp.service.material.MpMaterialService; +import cn.aagro.pp.module.mp.service.message.bo.MpMessageSendOutReqBO; +import cn.aagro.pp.module.mp.service.user.MpUserService; +import lombok.SneakyThrows; +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 me.chanjar.weixin.mp.bean.result.WxMpUser; +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.concurrent.TimeUnit; + +import static cn.aagro.pp.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.aagro.pp.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 + @SneakyThrows + public void receiveMessage(WxMpService weixinService, String appId, WxMpXmlMessage wxMessage) { + // 获得关联信息 + MpAccountDO account = mpAccountService.getAccountFromCache(appId); + Assert.notNull(account, "公众号账号({}) 不存在", appId); + + // 获取用户 + MpUserDO user = mpUserService.getUser(appId, wxMessage.getFromUser()); + if (user == null) { + // 特殊情况:因为 receiveMessage 是异步记录,可能 SubscribeHandler 还没存储好 User,此时 sleep 轮询 + for (int i = 0; i < 3; i++) { + ThreadUtil.sleep(5, TimeUnit.SECONDS); + user = mpUserService.getUser(appId, wxMessage.getFromUser()); + if (user != null) { + break; + } + log.warn("[receiveMessage][粉丝({}/{}) 不存在,第 {} 次重试失败]", appId, wxMessage.getFromUser(), i + 1); + } + } + // 特殊情况:可能 SubscribeHandler 没处理正确(例如说发生异常),则主动创建 + if (user == null) { + log.warn("[receiveMessage][粉丝({}/{}) 不存在,主动创建]", appId, wxMessage.getFromUser()); + WxMpUser wxMpUser = weixinService.getUserService().userInfo(wxMessage.getFromUser()); + user = mpUserService.saveUser(appId, wxMpUser); + } + 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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/message/bo/MpMessageSendOutReqBO.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/message/bo/MpMessageSendOutReqBO.java new file mode 100644 index 0000000..89a0f71 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/message/bo/MpMessageSendOutReqBO.java @@ -0,0 +1,110 @@ +package cn.aagro.pp.module.mp.service.message.bo; + +import cn.aagro.pp.module.mp.dal.dataobject.message.MpMessageDO; +import cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/statistics/MpStatisticsService.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/statistics/MpStatisticsService.java new file mode 100644 index 0000000..9402974 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/statistics/MpStatisticsService.java @@ -0,0 +1,54 @@ +package cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/statistics/MpStatisticsServiceImpl.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/statistics/MpStatisticsServiceImpl.java new file mode 100644 index 0000000..bff581e --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/statistics/MpStatisticsServiceImpl.java @@ -0,0 +1,77 @@ +package cn.aagro.pp.module.mp.service.statistics; + +import cn.hutool.core.date.DateUtil; +import cn.aagro.pp.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.aagro.pp.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/tag/MpTagService.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/tag/MpTagService.java new file mode 100644 index 0000000..442796b --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/tag/MpTagService.java @@ -0,0 +1,65 @@ +package cn.aagro.pp.module.mp.service.tag; + +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.mp.controller.admin.tag.vo.MpTagCreateReqVO; +import cn.aagro.pp.module.mp.controller.admin.tag.vo.MpTagPageReqVO; +import cn.aagro.pp.module.mp.controller.admin.tag.vo.MpTagUpdateReqVO; +import cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/tag/MpTagServiceImpl.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/tag/MpTagServiceImpl.java new file mode 100644 index 0000000..44d1c74 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/tag/MpTagServiceImpl.java @@ -0,0 +1,164 @@ +package cn.aagro.pp.module.mp.service.tag; + +import cn.hutool.core.collection.CollUtil; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.mp.controller.admin.tag.vo.MpTagCreateReqVO; +import cn.aagro.pp.module.mp.controller.admin.tag.vo.MpTagPageReqVO; +import cn.aagro.pp.module.mp.controller.admin.tag.vo.MpTagUpdateReqVO; +import cn.aagro.pp.module.mp.convert.tag.MpTagConvert; +import cn.aagro.pp.module.mp.dal.dataobject.account.MpAccountDO; +import cn.aagro.pp.module.mp.dal.dataobject.tag.MpTagDO; +import cn.aagro.pp.module.mp.dal.mysql.tag.MpTagMapper; +import cn.aagro.pp.module.mp.framework.mp.core.MpServiceFactory; +import cn.aagro.pp.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.aagro.pp.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.aagro.pp.framework.common.util.collection.CollectionUtils.convertList; +import static cn.aagro.pp.framework.common.util.collection.CollectionUtils.convertMap; +import static cn.aagro.pp.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.deleteByIds(convertList(tagMap.values(), MpTagDO::getId)); + } + } + +} diff --git a/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/user/MpUserService.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/user/MpUserService.java new file mode 100644 index 0000000..5143f95 --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/user/MpUserService.java @@ -0,0 +1,102 @@ +package cn.aagro.pp.module.mp.service.user; + +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.mp.controller.admin.user.vo.MpUserPageReqVO; +import cn.aagro.pp.module.mp.controller.admin.user.vo.MpUserUpdateReqVO; +import cn.aagro.pp.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.aagro.pp.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.aagro.pp.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/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/user/MpUserServiceImpl.java b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/user/MpUserServiceImpl.java new file mode 100644 index 0000000..b23d12c --- /dev/null +++ b/aagro-module-mp/src/main/java/cn/aagro/pp/module/mp/service/user/MpUserServiceImpl.java @@ -0,0 +1,215 @@ +package cn.aagro.pp.module.mp.service.user; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import cn.aagro.pp.framework.common.enums.CommonStatusEnum; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.common.util.collection.CollectionUtils; +import cn.aagro.pp.module.mp.controller.admin.user.vo.MpUserPageReqVO; +import cn.aagro.pp.module.mp.controller.admin.user.vo.MpUserUpdateReqVO; +import cn.aagro.pp.module.mp.convert.user.MpUserConvert; +import cn.aagro.pp.module.mp.dal.dataobject.account.MpAccountDO; +import cn.aagro.pp.module.mp.dal.dataobject.user.MpUserDO; +import cn.aagro.pp.module.mp.dal.mysql.user.MpUserMapper; +import cn.aagro.pp.module.mp.framework.mp.core.MpServiceFactory; +import cn.aagro.pp.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.aagro.pp.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.aagro.pp.module.mp.enums.ErrorCodeConstants.USER_NOT_EXISTS; +import static cn.aagro.pp.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.selectByIds(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()); + } + } + +} diff --git a/aagro-module-pay/pom.xml b/aagro-module-pay/pom.xml new file mode 100644 index 0000000..7478c8a --- /dev/null +++ b/aagro-module-pay/pom.xml @@ -0,0 +1,91 @@ + + + + cn.aagro.gg + aiot + ${revision} + ../pom.xml + + 4.0.0 + aagro-module-pay + jar + + ${project.artifactId} + + pay 模块,我们放支付业务,提供业务的支付能力。 + 例如说:商户、应用、支付、退款等等 + + + + + + cn.aagro.gg + aagro-module-system + ${revision} + + + + + cn.aagro.gg + aagro-spring-boot-starter-biz-tenant + + + + + cn.aagro.gg + aagro-spring-boot-starter-security + + + + + cn.aagro.gg + aagro-spring-boot-starter-mybatis + + + + cn.aagro.gg + aagro-spring-boot-starter-redis + + + + + cn.aagro.gg + aagro-spring-boot-starter-job + + + + + cn.aagro.gg + aagro-spring-boot-starter-test + ${revision} + test + + + + + cn.aagro.gg + aagro-spring-boot-starter-excel + + + + + com.alipay.sdk + alipay-sdk-java + 4.35.79.ALL + + + org.bouncycastle + bcprov-jdk15on + + + + + com.github.binarywang + weixin-java-pay + + + + + diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/notify/dto/PayOrderNotifyReqDTO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/notify/dto/PayOrderNotifyReqDTO.java new file mode 100644 index 0000000..b1a002b --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/notify/dto/PayOrderNotifyReqDTO.java @@ -0,0 +1,34 @@ +package cn.aagro.pp.module.pay.api.notify.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + * 支付单的通知 Request DTO + * + * @author 芋道源码 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PayOrderNotifyReqDTO { + + /** + * 商户订单编号 + */ + @NotEmpty(message = "商户订单号不能为空") + private String merchantOrderId; + + /** + * 支付订单编号 + */ + @NotNull(message = "支付订单编号不能为空") + private Long payOrderId; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/notify/dto/PayRefundNotifyReqDTO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/notify/dto/PayRefundNotifyReqDTO.java new file mode 100644 index 0000000..947a8c0 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/notify/dto/PayRefundNotifyReqDTO.java @@ -0,0 +1,40 @@ +package cn.aagro.pp.module.pay.api.notify.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + * 退款单的通知 Request DTO + * + * @author 芋道源码 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PayRefundNotifyReqDTO { + + /** + * 商户退款单编号 + */ + @NotEmpty(message = "商户退款单编号不能为空") + private String merchantOrderId; + + /** + * 商户退款编号 + */ + @NotEmpty(message = "商户退款编号不能为空") + private String merchantRefundId; + + /** + * 支付退款编号 + */ + @NotNull(message = "支付退款编号不能为空") + private Long payRefundId; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/notify/dto/PayTransferNotifyReqDTO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/notify/dto/PayTransferNotifyReqDTO.java new file mode 100644 index 0000000..e7c69b8 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/notify/dto/PayTransferNotifyReqDTO.java @@ -0,0 +1,34 @@ +package cn.aagro.pp.module.pay.api.notify.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + * 转账单的通知 Request DTO + * + * @author jason + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PayTransferNotifyReqDTO { + + /** + * 商户转账单号 + */ + @NotEmpty(message = "商户转账单号不能为空") + private String merchantTransferId; + + /** + * 转账订单编号 + */ + @NotNull(message = "转账订单编号不能为空") + private Long payTransferId; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/notify/package-info.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/notify/package-info.java new file mode 100644 index 0000000..12ba966 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/notify/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位符,无特殊作用 + */ +package cn.aagro.pp.module.pay.api.notify; diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/order/PayOrderApi.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/order/PayOrderApi.java new file mode 100644 index 0000000..2bad987 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/order/PayOrderApi.java @@ -0,0 +1,40 @@ +package cn.aagro.pp.module.pay.api.order; + +import cn.aagro.pp.module.pay.api.order.dto.PayOrderCreateReqDTO; +import cn.aagro.pp.module.pay.api.order.dto.PayOrderRespDTO; + +import javax.validation.Valid; + +/** + * 支付单 API 接口 + * + * @author LeeYan9 + * @since 2022-08-26 + */ +public interface PayOrderApi { + + /** + * 创建支付单 + * + * @param reqDTO 创建请求 + * @return 支付单编号 + */ + Long createOrder(@Valid PayOrderCreateReqDTO reqDTO); + + /** + * 获得支付单 + * + * @param id 支付单编号 + * @return 支付单 + */ + PayOrderRespDTO getOrder(Long id); + + /** + * 更新支付订单价格 + * + * @param id 支付单编号 + * @param payPrice 支付单价格 + */ + void updatePayOrderPrice(Long id, Integer payPrice); + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/order/PayOrderApiImpl.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/order/PayOrderApiImpl.java new file mode 100644 index 0000000..37c5bc1 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/order/PayOrderApiImpl.java @@ -0,0 +1,39 @@ +package cn.aagro.pp.module.pay.api.order; + +import cn.aagro.pp.module.pay.api.order.dto.PayOrderCreateReqDTO; +import cn.aagro.pp.module.pay.api.order.dto.PayOrderRespDTO; +import cn.aagro.pp.module.pay.convert.order.PayOrderConvert; +import cn.aagro.pp.module.pay.dal.dataobject.order.PayOrderDO; +import cn.aagro.pp.module.pay.service.order.PayOrderService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; + +/** + * 支付单 API 实现类 + * + * @author 芋道源码 + */ +@Service +public class PayOrderApiImpl implements PayOrderApi { + + @Resource + private PayOrderService payOrderService; + + @Override + public Long createOrder(PayOrderCreateReqDTO reqDTO) { + return payOrderService.createOrder(reqDTO); + } + + @Override + public PayOrderRespDTO getOrder(Long id) { + PayOrderDO order = payOrderService.getOrder(id); + return PayOrderConvert.INSTANCE.convert2(order); + } + + @Override + public void updatePayOrderPrice(Long id, Integer payPrice) { + payOrderService.updatePayOrderPrice(id, payPrice); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/order/dto/PayOrderCreateReqDTO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/order/dto/PayOrderCreateReqDTO.java new file mode 100644 index 0000000..b2175a0 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/order/dto/PayOrderCreateReqDTO.java @@ -0,0 +1,78 @@ +package cn.aagro.pp.module.pay.api.order.dto; + +import cn.aagro.pp.framework.common.enums.UserTypeEnum; +import cn.aagro.pp.framework.common.validation.InEnum; +import lombok.Data; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.DecimalMin; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 支付单创建 Request DTO + */ +@Data +public class PayOrderCreateReqDTO implements Serializable { + + public static final int SUBJECT_MAX_LENGTH = 32; + + /** + * 应用标识 + */ + @NotNull(message = "应用标识不能为空") + private String appKey; + + /** + * 用户 IP + */ + @NotEmpty(message = "用户 IP 不能为空") + private String userIp; + + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + */ + @InEnum(UserTypeEnum.class) + private Integer userType; + + // ========== 商户相关字段 ========== + + /** + * 商户订单编号 + */ + @NotEmpty(message = "商户订单编号不能为空") + private String merchantOrderId; + /** + * 商品标题 + */ + @NotEmpty(message = "商品标题不能为空") + @Length(max = SUBJECT_MAX_LENGTH, message = "商品标题不能超过 32") + private String subject; + /** + * 商品描述 + */ + @Length(max = 128, message = "商品描述信息长度不能超过128") + private String body; + + // ========== 订单相关字段 ========== + + /** + * 支付金额,单位:分 + */ + @NotNull(message = "支付金额不能为空") + @DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零") + private Integer price; + + /** + * 支付过期时间 + */ + @NotNull(message = "支付过期时间不能为空") + private LocalDateTime expireTime; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/order/dto/PayOrderRespDTO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/order/dto/PayOrderRespDTO.java new file mode 100644 index 0000000..756b709 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/order/dto/PayOrderRespDTO.java @@ -0,0 +1,68 @@ +package cn.aagro.pp.module.pay.api.order.dto; + +import cn.aagro.pp.module.pay.enums.order.PayOrderStatusEnum; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 支付单信息 Response DTO + * + * @author 芋道源码 + */ +@Data +public class PayOrderRespDTO { + + /** + * 订单编号,数据库自增 + */ + private Long id; + /** + * 渠道编码 + * + * 枚举 PayChannelEnum + */ + private String channelCode; + + // ========== 商户相关字段 ========== + /** + * 商户订单编号 + * 例如说,内部系统 A 的订单号。需要保证每个 PayMerchantDO 唯一 + */ + private String merchantOrderId; + + // ========== 订单相关字段 ========== + /** + * 商品标题 + */ + private String subject; + /** + * 支付金额,单位:分 + */ + private Integer price; + /** + * 支付状态 + * + * 枚举 {@link PayOrderStatusEnum} + */ + private Integer status; + + /** + * 订单支付成功时间 + */ + private LocalDateTime successTime; + + // ========== 渠道相关字段 ========== + + /** + * 渠道用户编号 + * + * 例如说,微信 openid、支付宝账号 + */ + private String channelUserId; + /** + * 渠道订单号 + */ + private String channelOrderNo; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/package-info.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/package-info.java new file mode 100644 index 0000000..4c48e8f --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/package-info.java @@ -0,0 +1,4 @@ +/** + * pay API 包,定义并实现提供给其它模块的 API + */ +package cn.aagro.pp.module.pay.api; diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/refund/PayRefundApi.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/refund/PayRefundApi.java new file mode 100644 index 0000000..446640a --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/refund/PayRefundApi.java @@ -0,0 +1,31 @@ +package cn.aagro.pp.module.pay.api.refund; + +import cn.aagro.pp.module.pay.api.refund.dto.PayRefundCreateReqDTO; +import cn.aagro.pp.module.pay.api.refund.dto.PayRefundRespDTO; + +import javax.validation.Valid; + +/** + * 退款单 API 接口 + * + * @author 芋道源码 + */ +public interface PayRefundApi { + + /** + * 创建退款单 + * + * @param reqDTO 创建请求 + * @return 退款单编号 + */ + Long createRefund(@Valid PayRefundCreateReqDTO reqDTO); + + /** + * 获得退款单 + * + * @param id 退款单编号 + * @return 退款单 + */ + PayRefundRespDTO getRefund(Long id); + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/refund/PayRefundApiImpl.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/refund/PayRefundApiImpl.java new file mode 100644 index 0000000..7e5b85e --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/refund/PayRefundApiImpl.java @@ -0,0 +1,36 @@ +package cn.aagro.pp.module.pay.api.refund; + +import cn.aagro.pp.framework.common.util.object.BeanUtils; +import cn.aagro.pp.module.pay.api.refund.dto.PayRefundCreateReqDTO; +import cn.aagro.pp.module.pay.api.refund.dto.PayRefundRespDTO; +import cn.aagro.pp.module.pay.dal.dataobject.refund.PayRefundDO; +import cn.aagro.pp.module.pay.service.refund.PayRefundService; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; + +/** + * 退款单 API 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class PayRefundApiImpl implements PayRefundApi { + + @Resource + private PayRefundService payRefundService; + + @Override + public Long createRefund(PayRefundCreateReqDTO reqDTO) { + return payRefundService.createRefund(reqDTO); + } + + @Override + public PayRefundRespDTO getRefund(Long id) { + PayRefundDO refund = payRefundService.getRefund(id); + return BeanUtils.toBean(refund, PayRefundRespDTO.class); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/refund/dto/PayRefundCreateReqDTO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/refund/dto/PayRefundCreateReqDTO.java new file mode 100644 index 0000000..c80ccda --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/refund/dto/PayRefundCreateReqDTO.java @@ -0,0 +1,71 @@ +package cn.aagro.pp.module.pay.api.refund.dto; + +import cn.aagro.pp.framework.common.enums.UserTypeEnum; +import cn.aagro.pp.framework.common.validation.InEnum; +import lombok.Data; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.Min; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + * 退款单创建 Request DTO + * + * @author 芋道源码 + */ +@Data +public class PayRefundCreateReqDTO { + + /** + * 应用标识 + */ + @NotNull(message = "应用标识不能为空") + private String appKey; + + /** + * 用户 IP + */ + @NotEmpty(message = "用户 IP 不能为空") + private String userIp; + + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + */ + @InEnum(UserTypeEnum.class) + private Integer userType; + + // ========== 商户相关字段 ========== + /** + * 商户订单编号 + */ + @NotEmpty(message = "商户订单编号不能为空") + private String merchantOrderId; + + /** + * 商户退款编号 + */ + @NotEmpty(message = "商户退款编号不能为空") + private String merchantRefundId; + + /** + * 退款描述 + */ + @NotEmpty(message = "退款描述不能为空") + @Length(max = 128, message = "退款描述长度不能超过 128") + private String reason; + + // ========== 订单相关字段 ========== + + /** + * 退款金额,单位:分 + */ + @NotNull(message = "退款金额不能为空") + @Min(value = 1, message = "退款金额必须大于零") + private Integer price; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/refund/dto/PayRefundRespDTO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/refund/dto/PayRefundRespDTO.java new file mode 100644 index 0000000..20abbfb --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/refund/dto/PayRefundRespDTO.java @@ -0,0 +1,65 @@ +package cn.aagro.pp.module.pay.api.refund.dto; + +import cn.aagro.pp.module.pay.enums.refund.PayRefundStatusEnum; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 退款单信息 Response DTO + * + * @author 芋道源码 + */ +@Data +public class PayRefundRespDTO { + + /** + * 退款单编号 + */ + private Long id; + + /** + * 渠道编码 + * + * 枚举 PayChannelEnum + */ + private String channelCode; + + // ========== 退款相关字段 ========== + /** + * 退款状态 + * + * 枚举 {@link PayRefundStatusEnum} + */ + private Integer status; + /** + * 退款金额,单位:分 + */ + private Integer refundPrice; + + // ========== 商户相关字段 ========== + /** + * 商户订单编号 + */ + private String merchantOrderId; + /** + * 商户退款编号 + */ + private String merchantRefundId; + /** + * 退款成功时间 + */ + private LocalDateTime successTime; + + // ========== 渠道相关字段 ========== + + /** + * 调用渠道的错误码 + */ + private String channelErrorCode; + /** + * 调用渠道的错误提示 + */ + private String channelErrorMsg; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/transfer/PayTransferApi.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/transfer/PayTransferApi.java new file mode 100644 index 0000000..1c32c39 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/transfer/PayTransferApi.java @@ -0,0 +1,31 @@ +package cn.aagro.pp.module.pay.api.transfer; + +import cn.aagro.pp.module.pay.api.transfer.dto.PayTransferCreateReqDTO; +import cn.aagro.pp.module.pay.api.transfer.dto.PayTransferCreateRespDTO; +import cn.aagro.pp.module.pay.api.transfer.dto.PayTransferRespDTO; +import javax.validation.Valid; + +/** + * 转账单 API 接口 + * + * @author jason + */ +public interface PayTransferApi { + + /** + * 创建转账单 + * + * @param reqDTO 创建请求 + * @return 创建结果 + */ + PayTransferCreateRespDTO createTransfer(@Valid PayTransferCreateReqDTO reqDTO); + + /** + * 获得转账单 + * + * @param id 转账单编号 + * @return 转账单 + */ + PayTransferRespDTO getTransfer(Long id); + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/transfer/PayTransferApiImpl.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/transfer/PayTransferApiImpl.java new file mode 100644 index 0000000..e1f872c --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/transfer/PayTransferApiImpl.java @@ -0,0 +1,50 @@ +package cn.aagro.pp.module.pay.api.transfer; + +import cn.aagro.pp.framework.common.util.object.BeanUtils; +import cn.aagro.pp.module.pay.framework.pay.core.client.impl.weixin.WxPayClientConfig; +import cn.aagro.pp.module.pay.api.transfer.dto.PayTransferCreateReqDTO; +import cn.aagro.pp.module.pay.api.transfer.dto.PayTransferCreateRespDTO; +import cn.aagro.pp.module.pay.api.transfer.dto.PayTransferRespDTO; +import cn.aagro.pp.module.pay.dal.dataobject.channel.PayChannelDO; +import cn.aagro.pp.module.pay.dal.dataobject.transfer.PayTransferDO; +import cn.aagro.pp.module.pay.service.channel.PayChannelService; +import cn.aagro.pp.module.pay.service.transfer.PayTransferService; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; + +/** + * 转账单 API 实现类 + * + * @author jason + */ +@Service +@Validated +public class PayTransferApiImpl implements PayTransferApi { + + @Resource + private PayTransferService payTransferService; + @Resource + private PayChannelService payChannelService; + + @Override + public PayTransferCreateRespDTO createTransfer(PayTransferCreateReqDTO reqDTO) { + return payTransferService.createTransfer(reqDTO); + } + + @Override + public PayTransferRespDTO getTransfer(Long id) { + PayTransferDO transfer = payTransferService.getTransfer(id); + if (transfer == null) { + return null; + } + PayChannelDO channel = payChannelService.getChannel(transfer.getChannelId()); + String mchId = null; + if (channel != null && channel.getConfig() instanceof WxPayClientConfig) { + mchId = ((WxPayClientConfig) channel.getConfig()).getMchId(); + } + return BeanUtils.toBean(transfer, PayTransferRespDTO.class).setChannelMchId(mchId); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/transfer/dto/PayTransferCreateReqDTO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/transfer/dto/PayTransferCreateReqDTO.java new file mode 100644 index 0000000..35bfb50 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/transfer/dto/PayTransferCreateReqDTO.java @@ -0,0 +1,133 @@ +package cn.aagro.pp.module.pay.api.transfer.dto; + +import cn.hutool.core.util.StrUtil; +import cn.aagro.pp.framework.common.enums.UserTypeEnum; +import cn.aagro.pp.framework.common.util.json.JsonUtils; +import cn.aagro.pp.framework.common.validation.InEnum; +import lombok.Data; + +import javax.validation.constraints.Min; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 转账单创建 Request DTO + * + * @author jason + */ +@Data +public class PayTransferCreateReqDTO { + + /** + * 应用标识 + */ + @NotNull(message = "应用标识不能为空") + private String appKey; + + /** + * 用户 IP + */ + @NotEmpty(message = "用户 IP 不能为空") + private String userIp; + + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + */ + @InEnum(UserTypeEnum.class) + private Integer userType; + + // ========== 商户相关字段 ========== + /** + * 商户转账单编号 + */ + @NotEmpty(message = "商户转账单编号能为空") + private String merchantTransferId; + + /** + * 转账金额,单位:分 + */ + @Min(value = 1, message = "转账金额必须大于零") + @NotNull(message = "转账金额不能为空") + private Integer price; + + /** + * 转账标题 + */ + @NotEmpty(message = "转账标题不能为空") + private String subject; + + /** + * 收款人账号 + * + * 微信场景下:openid + * 支付宝场景下:支付宝账号 + */ + @NotEmpty(message = "收款人账号不能为空") + private String userAccount; + /** + * 收款人姓名 + */ + private String userName; + + /** + * 转账渠道 + */ + @NotEmpty(message = "转账渠道不能为空") + private String channelCode; + + /** + * 转账渠道的额外参数 + */ + private Map channelExtras; + + /** + * 【微信】现金营销场景 + * + * @param activityName 活动名称 + * @param rewardDescription 奖励说明 + * @return channelExtras + */ + public static Map buildWeiXinChannelExtra1000(String activityName, String rewardDescription) { + return buildWeiXinChannelExtra(1000, + "活动名称", activityName, + "奖励说明", rewardDescription); + } + + /** + * 【微信】企业报销场景 + * + * @param expenseType 报销类型 + * @param expenseDescription 报销说明 + * @return channelExtras + */ + public static Map buildWeiXinChannelExtra1006(String expenseType, String expenseDescription) { + return buildWeiXinChannelExtra(1006, + "报销类型", expenseType, + "报销说明", expenseDescription); + } + + private static Map buildWeiXinChannelExtra(Integer sceneId, String... values) { + Map channelExtras = new HashMap<>(); + // 构建场景报备信息列表 + List> sceneReportInfos = new ArrayList<>(); + for (int i = 0; i < values.length; i += 2) { + Map info = new HashMap<>(); + info.put("infoType", values[i]); + info.put("infoContent", values[i + 1]); + sceneReportInfos.add(info); + } + // 设置场景ID和场景报备信息 + channelExtras.put("sceneId", StrUtil.toString(sceneId)); + channelExtras.put("sceneReportInfos", JsonUtils.toJsonString(sceneReportInfos)); + return channelExtras; + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/transfer/dto/PayTransferCreateRespDTO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/transfer/dto/PayTransferCreateRespDTO.java new file mode 100644 index 0000000..6948892 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/transfer/dto/PayTransferCreateRespDTO.java @@ -0,0 +1,28 @@ +package cn.aagro.pp.module.pay.api.transfer.dto; + +import lombok.Data; + +/** + * 转账单创建 Response DTO + * + * @author 芋道源码 + */ +@Data +public class PayTransferCreateRespDTO { + + /** + * 编号 + */ + private Long id; + + // ========== 其它字段 ========== + + /** + * 渠道 package 信息 + * + * 特殊:目前只有微信转账有这个东西!!! + * @see JSAPI 调起用户确认收款 + */ + private String channelPackageInfo; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/transfer/dto/PayTransferRespDTO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/transfer/dto/PayTransferRespDTO.java new file mode 100644 index 0000000..5e8a203 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/transfer/dto/PayTransferRespDTO.java @@ -0,0 +1,81 @@ +package cn.aagro.pp.module.pay.api.transfer.dto; + +import cn.aagro.pp.module.pay.enums.PayChannelEnum; +import cn.aagro.pp.module.pay.enums.transfer.PayTransferStatusEnum; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +public class PayTransferRespDTO { + + /** + * 编号 + */ + private Long id; + + /** + * 转账单号 + */ + private String no; + + /** + * 转账渠道编码 + * + * 枚举 {@link PayChannelEnum} + */ + private String channelCode; + + // ========== 商户相关字段 ========== + + /** + * 商户转账单编号 + */ + private String merchantTransferId; + + // ========== 转账相关字段 ========== + + /** + * 转账金额,单位:分 + */ + private Integer price; + + /** + * 转账状态 + * + * 枚举 {@link PayTransferStatusEnum} + */ + private Integer status; + + /** + * 订单转账成功时间 + */ + private LocalDateTime successTime; + + // ========== 其它字段 ========== + + /** + * 调用渠道的错误码 + */ + private String channelErrorCode; + /** + * 调用渠道的错误提示 + */ + private String channelErrorMsg; + + /** + * 渠道 package 信息 + * + * 特殊:目前只有微信转账有这个东西!!! + * @see JSAPI 调起用户确认收款 + */ + private String channelPackageInfo; + /** + * 渠道商户号 + * + * 特殊:目前只有微信转账有这个东西!!! + * @see JSAPI 调起用户确认收款 + */ + private String channelMchId; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/wallet/PayWalletApi.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/wallet/PayWalletApi.java new file mode 100644 index 0000000..e7078d9 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/wallet/PayWalletApi.java @@ -0,0 +1,29 @@ +package cn.aagro.pp.module.pay.api.wallet; + +import cn.aagro.pp.module.pay.api.wallet.dto.PayWalletAddBalanceReqDTO; +import cn.aagro.pp.module.pay.api.wallet.dto.PayWalletRespDTO; + +/** + * 钱包 API 接口 + * + * @author liurulin + */ +public interface PayWalletApi { + + /** + * 添加钱包余额 + * + * @param reqDTO 增加余额请求 + */ + void addWalletBalance(PayWalletAddBalanceReqDTO reqDTO); + + /** + * 获取钱包信息 + * + * @param userId 用户编号 + * @param userType 用户类型 + * @return 钱包信息 + */ + PayWalletRespDTO getOrCreateWallet(Long userId, Integer userType); + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/wallet/PayWalletApiImpl.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/wallet/PayWalletApiImpl.java new file mode 100644 index 0000000..f4c534f --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/wallet/PayWalletApiImpl.java @@ -0,0 +1,42 @@ +package cn.aagro.pp.module.pay.api.wallet; + +import cn.hutool.core.lang.Assert; +import cn.aagro.pp.framework.common.util.object.BeanUtils; +import cn.aagro.pp.module.pay.api.wallet.dto.PayWalletAddBalanceReqDTO; +import cn.aagro.pp.module.pay.api.wallet.dto.PayWalletRespDTO; +import cn.aagro.pp.module.pay.dal.dataobject.wallet.PayWalletDO; +import cn.aagro.pp.module.pay.enums.wallet.PayWalletBizTypeEnum; +import cn.aagro.pp.module.pay.service.wallet.PayWalletService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; + +/** + * 钱包 API 实现类 + * + * @author 芋道源码 + */ +@Service +public class PayWalletApiImpl implements PayWalletApi { + + @Resource + private PayWalletService payWalletService; + + @Override + public void addWalletBalance(PayWalletAddBalanceReqDTO reqDTO) { + // 创建或获取钱包 + PayWalletDO wallet = payWalletService.getOrCreateWallet(reqDTO.getUserId(), reqDTO.getUserType()); + Assert.notNull(wallet, "钱包({}/{})不存在", reqDTO.getUserId(), reqDTO.getUserType()); + + // 增加余额 + PayWalletBizTypeEnum bizType = PayWalletBizTypeEnum.valueOf(reqDTO.getBizType()); + payWalletService.addWalletBalance(wallet.getId(), reqDTO.getBizId(), bizType, reqDTO.getPrice()); + } + + @Override + public PayWalletRespDTO getOrCreateWallet(Long userId, Integer userType) { + PayWalletDO wallet = payWalletService.getOrCreateWallet(userId, userType); + return BeanUtils.toBean(wallet, PayWalletRespDTO.class); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/wallet/dto/PayWalletAddBalanceReqDTO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/wallet/dto/PayWalletAddBalanceReqDTO.java new file mode 100644 index 0000000..40b336f --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/wallet/dto/PayWalletAddBalanceReqDTO.java @@ -0,0 +1,50 @@ +package cn.aagro.pp.module.pay.api.wallet.dto; + +import cn.aagro.pp.framework.common.enums.UserTypeEnum; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +/** + * 钱包余额增加 Request DTO + * + * @author 芋道源码 + */ +@Data +public class PayWalletAddBalanceReqDTO { + + /** + * 用户编号 + * + * 关联 MemberUserDO 的 id 属性,或者 AdminUserDO 的 id 属性 + */ + @NotNull(message = "用户编号不能为空") + private Long userId; + /** + * 用户类型 + * + * 关联 {@link UserTypeEnum} + */ + @NotNull(message = "用户类型不能为空") + private Integer userType; + + /** + * 关联业务分类 + */ + @NotNull(message = "关联业务分类不能为空") + private Integer bizType; + /** + * 关联业务编号 + */ + @NotNull(message = "关联业务编号不能为空") + private String bizId; + + /** + * 交易金额,单位分 + * + * 正值表示余额增加,负值表示余额减少 + */ + @NotNull(message = "交易金额不能为空") + private Integer price; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/wallet/dto/PayWalletRespDTO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/wallet/dto/PayWalletRespDTO.java new file mode 100644 index 0000000..a684e43 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/api/wallet/dto/PayWalletRespDTO.java @@ -0,0 +1,52 @@ +package cn.aagro.pp.module.pay.api.wallet.dto; + +import cn.aagro.pp.framework.common.enums.UserTypeEnum; +import lombok.Data; + +/** + * 钱包 Response DTO + * + * @author jason + */ +@Data +public class PayWalletRespDTO { + + /** + * 编号 + */ + private Long id; + + /** + * 用户 id + * + * 关联 MemberUserDO 的 id 编号 + * 关联 AdminUserDO 的 id 编号 + */ + private Long userId; + /** + * 用户类型, 预留 多商户转帐可能需要用到 + * + * 关联 {@link UserTypeEnum} + */ + private Integer userType; + + /** + * 余额,单位分 + */ + private Integer balance; + + /** + * 冻结金额,单位分 + */ + private Integer freezePrice; + + /** + * 累计支出,单位分 + */ + private Integer totalExpense; + /** + * 累计充值,单位分 + */ + private Integer totalRecharge; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/app/PayAppController.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/app/PayAppController.java new file mode 100644 index 0000000..0b9bc13 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/app/PayAppController.java @@ -0,0 +1,108 @@ +package cn.aagro.pp.module.pay.controller.admin.app; + +import cn.hutool.core.collection.CollUtil; +import cn.aagro.pp.framework.common.enums.CommonStatusEnum; +import cn.aagro.pp.framework.common.pojo.CommonResult; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.pay.controller.admin.app.vo.*; +import cn.aagro.pp.module.pay.convert.app.PayAppConvert; +import cn.aagro.pp.module.pay.dal.dataobject.app.PayAppDO; +import cn.aagro.pp.module.pay.dal.dataobject.channel.PayChannelDO; +import cn.aagro.pp.module.pay.service.app.PayAppService; +import cn.aagro.pp.module.pay.service.channel.PayChannelService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +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.aagro.pp.framework.common.pojo.CommonResult.success; +import static cn.aagro.pp.framework.common.util.collection.CollectionUtils.convertList; + +@Slf4j +@Tag(name = "管理后台 - 支付应用信息") +@RestController +@RequestMapping("/pay/app") +@Validated +public class PayAppController { + + @Resource + private PayAppService appService; + @Resource + private PayChannelService channelService; + + @PostMapping("/create") + @Operation(summary = "创建支付应用信息") + @PreAuthorize("@ss.hasPermission('pay:app:create')") + public CommonResult createApp(@Valid @RequestBody PayAppCreateReqVO createReqVO) { + return success(appService.createApp(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新支付应用信息") + @PreAuthorize("@ss.hasPermission('pay:app:update')") + public CommonResult updateApp(@Valid @RequestBody PayAppUpdateReqVO updateReqVO) { + appService.updateApp(updateReqVO); + return success(true); + } + + @PutMapping("/update-status") + @Operation(summary = "更新支付应用状态") + @PreAuthorize("@ss.hasPermission('pay:app:update')") + public CommonResult updateAppStatus(@Valid @RequestBody PayAppUpdateStatusReqVO updateReqVO) { + appService.updateAppStatus(updateReqVO.getId(), updateReqVO.getStatus()); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除支付应用信息") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('pay:app:delete')") + public CommonResult deleteApp(@RequestParam("id") Long id) { + appService.deleteApp(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得支付应用信息") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pay:app:query')") + public CommonResult getApp(@RequestParam("id") Long id) { + PayAppDO app = appService.getApp(id); + return success(PayAppConvert.INSTANCE.convert(app)); + } + + @GetMapping("/page") + @Operation(summary = "获得支付应用信息分页") + @PreAuthorize("@ss.hasPermission('pay:app:query')") + public CommonResult> getAppPage(@Valid PayAppPageReqVO pageVO) { + // 得到应用分页列表 + PageResult pageResult = appService.getAppPage(pageVO); + if (CollUtil.isEmpty(pageResult.getList())) { + return success(PageResult.empty()); + } + + // 得到所有的应用编号,查出所有的渠道,并移除未启用的渠道 + List channels = channelService.getChannelListByAppIds( + convertList(pageResult.getList(), PayAppDO::getId)); + channels.removeIf(channel -> !CommonStatusEnum.ENABLE.getStatus().equals(channel.getStatus())); + + // 拼接后返回 + return success(PayAppConvert.INSTANCE.convertPage(pageResult, channels)); + } + + @GetMapping("/list") + @Operation(summary = "获得应用列表") + @PreAuthorize("@ss.hasPermission('pay:merchant:query')") + public CommonResult> getAppList() { + List appListDO = appService.getAppList(); + return success(PayAppConvert.INSTANCE.convertList(appListDO)); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/app/vo/PayAppBaseVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/app/vo/PayAppBaseVO.java new file mode 100644 index 0000000..7a5f4bd --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/app/vo/PayAppBaseVO.java @@ -0,0 +1,47 @@ +package cn.aagro.pp.module.pay.controller.admin.app.vo; +import cn.aagro.pp.framework.common.enums.CommonStatusEnum; +import cn.aagro.pp.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import org.hibernate.validator.constraints.URL; + +import javax.validation.constraints.*; + +/** +* 支付应用信息 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class PayAppBaseVO { + + @Schema(description = "应用标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "aagro") + @NotEmpty(message = "应用标识不能为空") + private String appKey; + + @Schema(description = "应用名", requiredMode = Schema.RequiredMode.REQUIRED, example = "小豆") + @NotNull(message = "应用名不能为空") + private String name; + + @Schema(description = "开启状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @NotNull(message = "开启状态不能为空") + @InEnum(CommonStatusEnum.class) + private Integer status; + + @Schema(description = "备注", example = "我是一个测试应用") + private String remark; + + @Schema(description = "支付结果的回调地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "http://127.0.0.1:48080/pay-callback") + @NotNull(message = "支付结果的回调地址不能为空") + @URL(message = "支付结果的回调地址必须为 URL 格式") + private String orderNotifyUrl; + + @Schema(description = "退款结果的回调地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "http://127.0.0.1:48080/refund-callback") + @NotNull(message = "退款结果的回调地址不能为空") + @URL(message = "退款结果的回调地址必须为 URL 格式") + private String refundNotifyUrl; + + @Schema(description = "转账结果的回调地址", example = "http://127.0.0.1:48080/transfer-callback") + @URL(message = "转账结果的回调地址必须为 URL 格式") + private String transferNotifyUrl; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/app/vo/PayAppCreateReqVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/app/vo/PayAppCreateReqVO.java new file mode 100644 index 0000000..daf31ff --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/app/vo/PayAppCreateReqVO.java @@ -0,0 +1,14 @@ +package cn.aagro.pp.module.pay.controller.admin.app.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 PayAppCreateReqVO extends PayAppBaseVO { + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/app/vo/PayAppPageItemRespVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/app/vo/PayAppPageItemRespVO.java new file mode 100644 index 0000000..542f3ab --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/app/vo/PayAppPageItemRespVO.java @@ -0,0 +1,26 @@ +package cn.aagro.pp.module.pay.controller.admin.app.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; +import java.util.Set; + +@Schema(description = "管理后台 - 支付应用信息分页查询 Response VO,相比于支付信息,还会多出应用渠道的开关信息") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PayAppPageItemRespVO extends PayAppBaseVO { + + @Schema(description = "应用编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + @Schema(description = "已配置的支付渠道编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "[alipay_pc, alipay_wap]") + private Set channelCodes; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/app/vo/PayAppPageReqVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/app/vo/PayAppPageReqVO.java new file mode 100644 index 0000000..de764f4 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/app/vo/PayAppPageReqVO.java @@ -0,0 +1,33 @@ +package cn.aagro.pp.module.pay.controller.admin.app.vo; + +import cn.aagro.pp.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 java.time.LocalDateTime; + +import static cn.aagro.pp.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 PayAppPageReqVO extends PageParam { + + @Schema(description = "应用名", example = "小豆") + private String name; + + @Schema(description = "应用标识", example = "aagro") + private String appKey; + + @Schema(description = "开启状态", example = "0") + private Integer status; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "创建时间") + private LocalDateTime[] createTime; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/app/vo/PayAppRespVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/app/vo/PayAppRespVO.java new file mode 100644 index 0000000..eb8b5fb --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/app/vo/PayAppRespVO.java @@ -0,0 +1,25 @@ +package cn.aagro.pp.module.pay.controller.admin.app.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 支付应用信息 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PayAppRespVO extends PayAppBaseVO { + + @Schema(description = "应用编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "应用标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "aagro") + private String appKey; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/app/vo/PayAppUpdateReqVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/app/vo/PayAppUpdateReqVO.java new file mode 100644 index 0000000..4f1d74c --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/app/vo/PayAppUpdateReqVO.java @@ -0,0 +1,17 @@ +package cn.aagro.pp.module.pay.controller.admin.app.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import javax.validation.constraints.*; + +@Schema(description = "管理后台 - 支付应用信息更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PayAppUpdateReqVO extends PayAppBaseVO { + + @Schema(description = "应用编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "应用编号不能为空") + private Long id; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/app/vo/PayAppUpdateStatusReqVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/app/vo/PayAppUpdateStatusReqVO.java new file mode 100644 index 0000000..182d2d0 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/app/vo/PayAppUpdateStatusReqVO.java @@ -0,0 +1,20 @@ +package cn.aagro.pp.module.pay.controller.admin.app.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 应用更新状态 Request VO") +@Data +public class PayAppUpdateStatusReqVO { + + @Schema(description = "应用编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "应用编号不能为空") + private Long id; + + @Schema(description = "状态,见 SysCommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "状态不能为空") + private Integer status; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/channel/PayChannelController.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/channel/PayChannelController.java new file mode 100644 index 0000000..65b30e3 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/channel/PayChannelController.java @@ -0,0 +1,82 @@ +package cn.aagro.pp.module.pay.controller.admin.channel; + +import cn.aagro.pp.framework.common.pojo.CommonResult; +import cn.aagro.pp.module.pay.controller.admin.channel.vo.PayChannelCreateReqVO; +import cn.aagro.pp.module.pay.controller.admin.channel.vo.PayChannelRespVO; +import cn.aagro.pp.module.pay.controller.admin.channel.vo.PayChannelUpdateReqVO; +import cn.aagro.pp.module.pay.convert.channel.PayChannelConvert; +import cn.aagro.pp.module.pay.dal.dataobject.channel.PayChannelDO; +import cn.aagro.pp.module.pay.service.channel.PayChannelService; +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 java.util.Set; + +import static cn.aagro.pp.framework.common.pojo.CommonResult.success; +import static cn.aagro.pp.framework.common.util.collection.CollectionUtils.convertSet; + +@Tag(name = "管理后台 - 支付渠道") +@RestController +@RequestMapping("/pay/channel") +@Validated +public class PayChannelController { + + @Resource + private PayChannelService channelService; + + @PostMapping("/create") + @Operation(summary = "创建支付渠道 ") + @PreAuthorize("@ss.hasPermission('pay:channel:create')") + public CommonResult createChannel(@Valid @RequestBody PayChannelCreateReqVO createReqVO) { + return success(channelService.createChannel(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新支付渠道 ") + @PreAuthorize("@ss.hasPermission('pay:channel:update')") + public CommonResult updateChannel(@Valid @RequestBody PayChannelUpdateReqVO updateReqVO) { + channelService.updateChannel(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除支付渠道 ") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('pay:channel:delete')") + public CommonResult deleteChannel(@RequestParam("id") Long id) { + channelService.deleteChannel(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得支付渠道") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pay:channel:query')") + public CommonResult getChannel(@RequestParam(value = "id", required = false) Long id, + @RequestParam(value = "appId", required = false) Long appId, + @RequestParam(value = "code", required = false) String code) { + PayChannelDO channel = null; + if (id != null) { + channel = channelService.getChannel(id); + } else if (appId != null && code != null) { + channel = channelService.getChannelByAppIdAndCode(appId, code); + } + return success(PayChannelConvert.INSTANCE.convert(channel)); + } + + @GetMapping("/get-enable-code-list") + @Operation(summary = "获得指定应用的开启的支付渠道编码列表") + @Parameter(name = "appId", description = "应用编号", required = true, example = "1") + public CommonResult> getEnableChannelCodeList(@RequestParam("appId") Long appId) { + List channels = channelService.getEnableChannelList(appId); + return success(convertSet(channels, PayChannelDO::getCode)); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/channel/vo/PayChannelBaseVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/channel/vo/PayChannelBaseVO.java new file mode 100644 index 0000000..1946e60 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/channel/vo/PayChannelBaseVO.java @@ -0,0 +1,31 @@ +package cn.aagro.pp.module.pay.controller.admin.channel.vo; +import cn.aagro.pp.framework.common.enums.CommonStatusEnum; +import cn.aagro.pp.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import javax.validation.constraints.*; + +/** +* 支付渠道 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class PayChannelBaseVO { + + @Schema(description = "开启状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "开启状态不能为空") + @InEnum(CommonStatusEnum.class) + private Integer status; + + @Schema(description = "备注", example = "我是小备注") + private String remark; + + @Schema(description = "渠道费率,单位:百分比", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + @NotNull(message = "渠道费率,单位:百分比不能为空") + private Double feeRate; + + @Schema(description = "应用编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "应用编号不能为空") + private Long appId; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/channel/vo/PayChannelCreateReqVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/channel/vo/PayChannelCreateReqVO.java new file mode 100644 index 0000000..914b338 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/channel/vo/PayChannelCreateReqVO.java @@ -0,0 +1,25 @@ +package cn.aagro.pp.module.pay.controller.admin.channel.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 支付渠道 创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PayChannelCreateReqVO extends PayChannelBaseVO { + + @Schema(description = "渠道编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "alipay_pc") + @NotNull(message = "渠道编码不能为空") + private String code; + + @Schema(description = "渠道配置的 json 字符串") + @NotBlank(message = "渠道配置不能为空") + private String config; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/channel/vo/PayChannelRespVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/channel/vo/PayChannelRespVO.java new file mode 100644 index 0000000..4d9d058 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/channel/vo/PayChannelRespVO.java @@ -0,0 +1,25 @@ +package cn.aagro.pp.module.pay.controller.admin.channel.vo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 支付渠道 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PayChannelRespVO extends PayChannelBaseVO { + + @Schema(description = "商户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private LocalDateTime createTime; + + @Schema(description = "渠道编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "alipay_pc") + private String code; + + @Schema(description = "配置", requiredMode = Schema.RequiredMode.REQUIRED) + private String config; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/channel/vo/PayChannelUpdateReqVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/channel/vo/PayChannelUpdateReqVO.java new file mode 100644 index 0000000..315c1ce --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/channel/vo/PayChannelUpdateReqVO.java @@ -0,0 +1,20 @@ +package cn.aagro.pp.module.pay.controller.admin.channel.vo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import javax.validation.constraints.*; + +@Schema(description = "管理后台 - 支付渠道 更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PayChannelUpdateReqVO extends PayChannelBaseVO { + + @Schema(description = "商户编号", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "商户编号不能为空") + private Long id; + + @Schema(description = "渠道配置的json字符串") + @NotBlank(message = "渠道配置不能为空") + private String config; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/demo/PayDemoOrderController.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/demo/PayDemoOrderController.java new file mode 100644 index 0000000..bf9ba23 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/demo/PayDemoOrderController.java @@ -0,0 +1,77 @@ +package cn.aagro.pp.module.pay.controller.admin.demo; + +import cn.aagro.pp.framework.common.pojo.CommonResult; +import cn.aagro.pp.framework.common.pojo.PageParam; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.common.util.object.BeanUtils; +import cn.aagro.pp.module.pay.api.notify.dto.PayOrderNotifyReqDTO; +import cn.aagro.pp.module.pay.api.notify.dto.PayRefundNotifyReqDTO; +import cn.aagro.pp.module.pay.controller.admin.demo.vo.order.PayDemoOrderCreateReqVO; +import cn.aagro.pp.module.pay.controller.admin.demo.vo.order.PayDemoOrderRespVO; +import cn.aagro.pp.module.pay.dal.dataobject.demo.PayDemoOrderDO; +import cn.aagro.pp.module.pay.service.demo.PayDemoOrderService; +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.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.annotation.security.PermitAll; +import javax.validation.Valid; + +import static cn.aagro.pp.framework.common.pojo.CommonResult.success; +import static cn.aagro.pp.framework.common.util.servlet.ServletUtils.getClientIP; +import static cn.aagro.pp.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - 示例订单") // 目的:演示支付、退款功能 +@RestController +@RequestMapping("/pay/demo-order") +@Validated +public class PayDemoOrderController { + + @Resource + private PayDemoOrderService payDemoOrderService; + + @PostMapping("/create") + @Operation(summary = "创建示例订单") + public CommonResult createDemoOrder(@Valid @RequestBody PayDemoOrderCreateReqVO createReqVO) { + return success(payDemoOrderService.createDemoOrder(getLoginUserId(), createReqVO)); + } + + @GetMapping("/page") + @Operation(summary = "获得示例订单分页") + public CommonResult> getDemoOrderPage(@Valid PageParam pageVO) { + PageResult pageResult = payDemoOrderService.getDemoOrderPage(pageVO); + return success(BeanUtils.toBean(pageResult, PayDemoOrderRespVO.class)); + } + + @PostMapping("/update-paid") + @Operation(summary = "更新示例订单为已支付") // 由 pay-module 支付服务,进行回调,可见 PayNotifyJob + @PermitAll // 无需登录,安全由 PayDemoOrderService 内部校验实现 + public CommonResult updateDemoOrderPaid(@RequestBody PayOrderNotifyReqDTO notifyReqDTO) { + payDemoOrderService.updateDemoOrderPaid(Long.valueOf(notifyReqDTO.getMerchantOrderId()), + notifyReqDTO.getPayOrderId()); + return success(true); + } + + @PutMapping("/refund") + @Operation(summary = "发起示例订单的退款") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + public CommonResult refundDemoOrder(@RequestParam("id") Long id) { + payDemoOrderService.refundDemoOrder(id, getClientIP()); + return success(true); + } + + @PostMapping("/update-refunded") + @Operation(summary = "更新示例订单为已退款") // 由 pay-module 支付服务,进行回调,可见 PayNotifyJob + @PermitAll // 无需登录,安全由 PayDemoOrderService 内部校验实现 + public CommonResult updateDemoOrderRefunded(@RequestBody PayRefundNotifyReqDTO notifyReqDTO) { + payDemoOrderService.updateDemoOrderRefunded( + Long.valueOf(notifyReqDTO.getMerchantOrderId()), + notifyReqDTO.getMerchantRefundId(), + notifyReqDTO.getPayRefundId()); + return success(true); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/demo/PayDemoWithdrawController.http b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/demo/PayDemoWithdrawController.http new file mode 100644 index 0000000..4162baa --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/demo/PayDemoWithdrawController.http @@ -0,0 +1,50 @@ +### 请求 /pay/demo-withdraw/create 接口(支付宝) +POST {{baseUrl}}/pay/demo-withdraw/create +Authorization: Bearer {{token}} +Content-Type: application/json +tenant-id: {{adminTenantId}} + +{ + "type": 1, + "subject": "测试转账", + "price": 10, + "userAccount": "oespxk7368@sandbox.com", + "userName": "oespxk7368" +} + +### 请求 /pay/demo-withdraw/create 接口(微信余额) +POST {{baseUrl}}/pay/demo-withdraw/create +Authorization: Bearer {{token}} +Content-Type: application/json +tenant-id: {{adminTenantId}} + +{ + "type": 2, + "subject": "测试转账", + "price": 1, + "userAccount": "oiSC85elO_OZogXODC5RoGyXamK4", + "userName": "芋艿" +} + +### 请求 /pay/demo-withdraw/create 接口(钱包余额) +POST {{baseUrl}}/pay/demo-withdraw/create +Authorization: Bearer {{token}} +Content-Type: application/json +tenant-id: {{adminTenantId}} + +{ + "type": 3, + "subject": "测试转账", + "price": 1, + "userAccount": "1" +} + +### 请求 /pay/demo-withdraw/transfer 接口(发起转账) +POST {{baseUrl}}/pay/demo-withdraw/transfer?id=1 +Authorization: Bearer {{token}} +tenant-id: {{adminTenantId}} + +### 请求 /pay/demo-withdraw/page 接口(查询分页) +GET {{baseUrl}}/pay/demo-withdraw/page?pageNo=1&pageSize=10 +Authorization: Bearer {{token}} +tenant-id: {{adminTenantId}} \ No newline at end of file diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/demo/PayDemoWithdrawController.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/demo/PayDemoWithdrawController.java new file mode 100644 index 0000000..761310b --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/demo/PayDemoWithdrawController.java @@ -0,0 +1,65 @@ +package cn.aagro.pp.module.pay.controller.admin.demo; + +import cn.aagro.pp.framework.common.pojo.CommonResult; +import cn.aagro.pp.framework.common.pojo.PageParam; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.common.util.object.BeanUtils; +import cn.aagro.pp.module.pay.api.notify.dto.PayTransferNotifyReqDTO; +import cn.aagro.pp.module.pay.controller.admin.demo.vo.withdraw.PayDemoWithdrawCreateReqVO; +import cn.aagro.pp.module.pay.controller.admin.demo.vo.withdraw.PayDemoWithdrawRespVO; +import cn.aagro.pp.module.pay.dal.dataobject.demo.PayDemoWithdrawDO; +import cn.aagro.pp.module.pay.service.demo.PayDemoWithdrawService; +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.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.annotation.security.PermitAll; +import javax.validation.Valid; + +import static cn.aagro.pp.framework.common.pojo.CommonResult.success; +import static cn.aagro.pp.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - 示例提现订单") // 目的:演示转账功能 +@RestController +@RequestMapping("/pay/demo-withdraw") +@Validated +public class PayDemoWithdrawController { + + @Resource + private PayDemoWithdrawService demoWithdrawService; + + @PostMapping("/create") + @Operation(summary = "创建示例提现单") + public CommonResult createDemoWithdraw(@Valid @RequestBody PayDemoWithdrawCreateReqVO createReqVO) { + Long id = demoWithdrawService.createDemoWithdraw(createReqVO); + return success(id); + } + + @PostMapping("/transfer") + @Operation(summary = "提现单转账") + @Parameter(name = "id", required = true, description = "提现单编号", example = "1024") + public CommonResult transferDemoWithdraw(@RequestParam("id") Long id) { + Long payTransferId = demoWithdrawService.transferDemoWithdraw(id, getLoginUserId()); + return success(payTransferId); + } + + @GetMapping("/page") + @Operation(summary = "获得示例提现单分页") + public CommonResult> getDemoWithdrawPage(@Valid PageParam pageVO) { + PageResult pageResult = demoWithdrawService.getDemoWithdrawPage(pageVO); + return success(BeanUtils.toBean(pageResult, PayDemoWithdrawRespVO.class)); + } + + @PostMapping("/update-transferred") + @Operation(summary = "更新示例提现单的转账状态") // 由 pay-module 转账服务,进行回调 + @PermitAll // 无需登录,安全由 PayDemoTransferService 内部校验实现 + public CommonResult updateDemoWithdrawTransferred(@RequestBody PayTransferNotifyReqDTO notifyReqDTO) { + demoWithdrawService.updateDemoWithdrawTransferred(Long.valueOf(notifyReqDTO.getMerchantTransferId()), + notifyReqDTO.getPayTransferId()); + return success(true); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/demo/vo/order/PayDemoOrderCreateReqVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/demo/vo/order/PayDemoOrderCreateReqVO.java new file mode 100644 index 0000000..6e00d37 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/demo/vo/order/PayDemoOrderCreateReqVO.java @@ -0,0 +1,16 @@ +package cn.aagro.pp.module.pay.controller.admin.demo.vo.order; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 示例订单创建 Request VO") +@Data +public class PayDemoOrderCreateReqVO { + + @Schema(description = "商品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "17682") + @NotNull(message = "商品编号不能为空") + private Long spuId; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/demo/vo/order/PayDemoOrderRespVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/demo/vo/order/PayDemoOrderRespVO.java new file mode 100644 index 0000000..6960c98 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/demo/vo/order/PayDemoOrderRespVO.java @@ -0,0 +1,54 @@ +package cn.aagro.pp.module.pay.controller.admin.demo.vo.order; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +import java.time.LocalDateTime; + +/** +* 示例订单 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class PayDemoOrderRespVO { + + @Schema(description = "订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "23199") + private Long userId; + + @Schema(description = "商品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "17682") + private Long spuId; + + @Schema(description = "商家备注", example = "李四") + private String spuName; + + @Schema(description = "价格,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "30381") + private Integer price; + + @Schema(description = "是否已支付", requiredMode = Schema.RequiredMode.REQUIRED) + private Boolean payStatus; + + @Schema(description = "支付订单编号", example = "16863") + private Long payOrderId; + + @Schema(description = "订单支付时间") + private LocalDateTime payTime; + + @Schema(description = "支付渠道", example = "alipay_qr") + private String payChannelCode; + + @Schema(description = "支付退款编号", example = "23366") + private Long payRefundId; + + @Schema(description = "退款金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "14039") + private Integer refundPrice; + + @Schema(description = "退款时间") + private LocalDateTime refundTime; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/demo/vo/withdraw/PayDemoWithdrawCreateReqVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/demo/vo/withdraw/PayDemoWithdrawCreateReqVO.java new file mode 100644 index 0000000..5d32996 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/demo/vo/withdraw/PayDemoWithdrawCreateReqVO.java @@ -0,0 +1,43 @@ +package cn.aagro.pp.module.pay.controller.admin.demo.vo.withdraw; + +import cn.hutool.core.util.ObjectUtil; +import cn.aagro.pp.framework.common.validation.InEnum; +import cn.aagro.pp.module.pay.enums.demo.PayDemoWithdrawTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.*; + +@Schema(description = "管理后台 - 示例提现单创建 Request VO") +@Data +public class PayDemoWithdrawCreateReqVO { + + @Schema(description = "提现标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿是一种菜") + @NotEmpty(message = "提现标题不能为空") + private String subject; + + @Schema(description = "提现金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + @NotNull(message = "提现金额不能为空") + @Min(value = 1, message = "提现金额必须大于零") + private Integer price; + + @Schema(description = "收款人账号", requiredMode= Schema.RequiredMode.REQUIRED, example = "test1") + @NotBlank(message = "收款人账号不能为空") + private String userAccount; + + @Schema(description = "收款人姓名", example = "test1") + private String userName; + + @Schema(description = "提现方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "提现方式不能为空") + @InEnum(PayDemoWithdrawTypeEnum.class) + private Integer type; + + @AssertTrue(message = "收款人姓名") + public boolean isUserNameValid() { + // 特殊:支付宝必须填写用户名!!! + return ObjectUtil.notEqual(type, PayDemoWithdrawTypeEnum.ALIPAY.getType()) + || ObjectUtil.isNotEmpty(userName); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/demo/vo/withdraw/PayDemoWithdrawRespVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/demo/vo/withdraw/PayDemoWithdrawRespVO.java new file mode 100644 index 0000000..a75fa07 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/demo/vo/withdraw/PayDemoWithdrawRespVO.java @@ -0,0 +1,47 @@ +package cn.aagro.pp.module.pay.controller.admin.demo.vo.withdraw; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 示例转账单创建 Request VO") +@Data +public class PayDemoWithdrawRespVO { + + @Schema(description = "转账单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "提现标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "吃饭报销") + private String subject; + + @Schema(description = "提现金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "22338") + private Integer price; + + @Schema(description = "收款人姓名", example = "test") + private String userName; + + @Schema(description = "收款人账号", example = "32167") + private String userAccount; + + @Schema(description = "提现类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer type; + + @Schema(description = "提现状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + private Integer status; + + // ========== 转账相关字段 ========== + + @Schema(description = "转账单编号", example = "23695") + private Long payTransferId; + + @Schema(description = "转账渠道", example = "wx_lite") + private String transferChannelCode; + + @Schema(description = "转账成功时间") + private LocalDateTime transferTime; + + @Schema(description = "转账失败原因", example = "IP 不正确") + private String transferErrorMsg; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/notify/PayNotifyController.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/notify/PayNotifyController.java new file mode 100644 index 0000000..dd09df1 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/notify/PayNotifyController.java @@ -0,0 +1,169 @@ +package cn.aagro.pp.module.pay.controller.admin.notify; + +import cn.hutool.core.collection.CollUtil; +import cn.aagro.pp.framework.common.pojo.CommonResult; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.common.util.object.BeanUtils; +import cn.aagro.pp.module.pay.framework.pay.core.client.PayClient; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.refund.PayRefundRespDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.transfer.PayTransferRespDTO; +import cn.aagro.pp.framework.tenant.core.aop.TenantIgnore; +import cn.aagro.pp.module.pay.controller.admin.notify.vo.PayNotifyTaskDetailRespVO; +import cn.aagro.pp.module.pay.controller.admin.notify.vo.PayNotifyTaskPageReqVO; +import cn.aagro.pp.module.pay.controller.admin.notify.vo.PayNotifyTaskRespVO; +import cn.aagro.pp.module.pay.dal.dataobject.app.PayAppDO; +import cn.aagro.pp.module.pay.dal.dataobject.notify.PayNotifyLogDO; +import cn.aagro.pp.module.pay.dal.dataobject.notify.PayNotifyTaskDO; +import cn.aagro.pp.module.pay.service.app.PayAppService; +import cn.aagro.pp.module.pay.service.channel.PayChannelService; +import cn.aagro.pp.module.pay.service.notify.PayNotifyService; +import cn.aagro.pp.module.pay.service.order.PayOrderService; +import cn.aagro.pp.module.pay.service.refund.PayRefundService; +import cn.aagro.pp.module.pay.service.transfer.PayTransferService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +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.annotation.security.PermitAll; +import javax.validation.Valid; +import java.util.List; +import java.util.Map; + +import static cn.aagro.pp.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.aagro.pp.framework.common.pojo.CommonResult.success; +import static cn.aagro.pp.framework.common.util.collection.CollectionUtils.convertList; +import static cn.aagro.pp.module.pay.enums.ErrorCodeConstants.CHANNEL_NOT_FOUND; + +@Tag(name = "管理后台 - 回调通知") +@RestController +@RequestMapping("/pay/notify") +@Validated +@Slf4j +public class PayNotifyController { + + @Resource + private PayOrderService orderService; + @Resource + private PayRefundService refundService; + @Resource + private PayTransferService payTransferService; + @Resource + private PayNotifyService notifyService; + @Resource + private PayAppService appService; + @Resource + private PayChannelService channelService; + + @PostMapping(value = "/order/{channelId}") + @Operation(summary = "支付渠道的统一【支付】回调") + @PermitAll + @TenantIgnore + public String notifyOrder(@PathVariable("channelId") Long channelId, + @RequestParam(required = false) Map params, + @RequestBody(required = false) String body, + @RequestHeader Map headers) { + log.info("[notifyOrder][channelId({}) 回调数据({}/{})]", channelId, params, body); + // 1. 校验支付渠道是否存在 + PayClient payClient = channelService.getPayClient(channelId); + if (payClient == null) { + log.error("[notifyOrder][渠道编号({}) 找不到对应的支付客户端]", channelId); + throw exception(CHANNEL_NOT_FOUND); + } + + // 2. 解析通知数据 + PayOrderRespDTO notify = payClient.parseOrderNotify(params, body, headers); + orderService.notifyOrder(channelId, notify); + return "success"; + } + + @PostMapping(value = "/refund/{channelId}") + @Operation(summary = "支付渠道的统一【退款】回调") + @PermitAll + @TenantIgnore + public String notifyRefund(@PathVariable("channelId") Long channelId, + @RequestParam(required = false) Map params, + @RequestBody(required = false) String body, + @RequestHeader Map headers) { + log.info("[notifyRefund][channelId({}) 回调数据({}/{})]", channelId, params, body); + // 1. 校验支付渠道是否存在 + PayClient payClient = channelService.getPayClient(channelId); + if (payClient == null) { + log.error("[notifyRefund][渠道编号({}) 找不到对应的支付客户端]", channelId); + throw exception(CHANNEL_NOT_FOUND); + } + + // 2. 解析通知数据 + PayRefundRespDTO notify = payClient.parseRefundNotify(params, body, headers); + refundService.notifyRefund(channelId, notify); + return "success"; + } + + @PostMapping(value = "/transfer/{channelId}") + @Operation(summary = "支付渠道的统一【转账】回调") + @PermitAll + @TenantIgnore + public String notifyTransfer(@PathVariable("channelId") Long channelId, + @RequestParam(required = false) Map params, + @RequestBody(required = false) String body, + @RequestHeader Map headers) { + log.info("[notifyTransfer][channelId({}) 回调数据({}/{})]", channelId, params, body); + // 1. 校验支付渠道是否存在 + PayClient payClient = channelService.getPayClient(channelId); + if (payClient == null) { + log.error("[notifyTransfer][渠道编号({}) 找不到对应的支付客户端]", channelId); + throw exception(CHANNEL_NOT_FOUND); + } + + // 2. 解析通知数据 + PayTransferRespDTO notify = payClient.parseTransferNotify(params, body, headers); + payTransferService.notifyTransfer(channelId, notify); + return "success"; + } + + @GetMapping("/get-detail") + @Operation(summary = "获得回调通知的明细") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pay:notify:query')") + public CommonResult getNotifyTaskDetail(@RequestParam("id") Long id) { + PayNotifyTaskDO task = notifyService.getNotifyTask(id); + if (task == null) { + return success(null); + } + // 拼接返回 + PayAppDO app = appService.getApp(task.getAppId()); + List logs = notifyService.getNotifyLogList(id); + return success(BeanUtils.toBean(task, PayNotifyTaskDetailRespVO.class, respVO -> { + if (app != null) { + respVO.setAppName(app.getName()); + } + respVO.setLogs(BeanUtils.toBean(logs, PayNotifyTaskDetailRespVO.Log.class)); + })); + } + + @GetMapping("/page") + @Operation(summary = "获得回调通知分页") + @PreAuthorize("@ss.hasPermission('pay:notify:query')") + public CommonResult> getNotifyTaskPage(@Valid PayNotifyTaskPageReqVO pageVO) { + PageResult pageResult = notifyService.getNotifyTaskPage(pageVO); + if (CollUtil.isEmpty(pageResult.getList())) { + return success(PageResult.empty()); + } + // 拼接返回 + Map apps = appService.getAppMap(convertList(pageResult.getList(), PayNotifyTaskDO::getAppId)); + + // 转换对象 + return success(BeanUtils.toBean(pageResult, PayNotifyTaskRespVO.class, order -> { + PayAppDO app = apps.get(order.getAppId()); + if (app != null) { + order.setAppName(app.getName()); + } + })); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/notify/vo/PayNotifyTaskDetailRespVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/notify/vo/PayNotifyTaskDetailRespVO.java new file mode 100644 index 0000000..0533957 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/notify/vo/PayNotifyTaskDetailRespVO.java @@ -0,0 +1,42 @@ + +package cn.aagro.pp.module.pay.controller.admin.notify.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - 回调通知的明细 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PayNotifyTaskDetailRespVO extends PayNotifyTaskRespVO { + + @Schema(description = "回调日志列表") + private List logs; + + @Schema(description = "管理后台 - 回调日志") + @Data + public static class Log { + + @Schema(description = "日志编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "8848") + private Long id; + + @Schema(description = "通知状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Byte status; + + @Schema(description = "当前通知次数", requiredMode = Schema.RequiredMode.REQUIRED) + private Byte notifyTimes; + + @Schema(description = "HTTP 响应结果", requiredMode = Schema.RequiredMode.REQUIRED) + private String response; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/notify/vo/PayNotifyTaskPageReqVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/notify/vo/PayNotifyTaskPageReqVO.java new file mode 100644 index 0000000..ccaecc7 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/notify/vo/PayNotifyTaskPageReqVO.java @@ -0,0 +1,45 @@ +package cn.aagro.pp.module.pay.controller.admin.notify.vo; + +import cn.aagro.pp.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 java.time.LocalDateTime; + +import static cn.aagro.pp.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 PayNotifyTaskPageReqVO extends PageParam { + + @Schema(description = "应用编号", example = "10636") + private Long appId; + + @Schema(description = "通知类型", example = "2") + private Integer type; + + @Schema(description = "数据编号", example = "6722") + private Long dataId; + + @Schema(description = "通知状态", example = "1") + private Integer status; + + @Schema(description = "商户订单编号", example = "26697") + private String merchantOrderId; + + @Schema(description = "商户退款编号", example = "26697") + private String merchantRefundId; + + @Schema(description = "商户转账编号", example = "26697") + private String merchantTransferId; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/notify/vo/PayNotifyTaskRespVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/notify/vo/PayNotifyTaskRespVO.java new file mode 100644 index 0000000..9c84255 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/notify/vo/PayNotifyTaskRespVO.java @@ -0,0 +1,56 @@ +package cn.aagro.pp.module.pay.controller.admin.notify.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 回调通知 Response VO") +@Data +public class PayNotifyTaskRespVO { + + @Schema(description = "任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3380") + private Long id; + + @Schema(description = "应用编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10636") + private Long appId; + + @Schema(description = "应用名称", example = "wx_pay") + private String appName; + + @Schema(description = "通知类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + private Byte type; + + @Schema(description = "数据编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "6722") + private Long dataId; + + @Schema(description = "商户订单编号", example = "26697") + private String merchantOrderId; + + @Schema(description = "商户退款编号", example = "26697") + private String merchantRefundId; + + @Schema(description = "商户转账编号", example = "26697") + private String merchantTransferId; + + @Schema(description = "通知状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Byte status; + + @Schema(description = "下一次通知时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime nextNotifyTime; + + @Schema(description = "最后一次执行时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime lastExecuteTime; + + @Schema(description = "当前通知次数", requiredMode = Schema.RequiredMode.REQUIRED) + private Byte notifyTimes; + + @Schema(description = "最大可通知次数", requiredMode = Schema.RequiredMode.REQUIRED) + private Byte maxNotifyTimes; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + @Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime updateTime; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/order/PayOrderController.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/order/PayOrderController.java new file mode 100644 index 0000000..8aaa2c0 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/order/PayOrderController.java @@ -0,0 +1,145 @@ +package cn.aagro.pp.module.pay.controller.admin.order; + +import cn.hutool.core.collection.CollectionUtil; +import cn.aagro.pp.framework.apilog.core.annotation.ApiAccessLog; +import cn.aagro.pp.framework.common.pojo.CommonResult; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.common.util.object.BeanUtils; +import cn.aagro.pp.framework.excel.core.util.ExcelUtils; +import cn.aagro.pp.module.pay.controller.admin.order.vo.*; +import cn.aagro.pp.module.pay.convert.order.PayOrderConvert; +import cn.aagro.pp.module.pay.dal.dataobject.app.PayAppDO; +import cn.aagro.pp.module.pay.dal.dataobject.order.PayOrderDO; +import cn.aagro.pp.module.pay.dal.dataobject.order.PayOrderExtensionDO; +import cn.aagro.pp.module.pay.dal.dataobject.wallet.PayWalletDO; +import cn.aagro.pp.module.pay.enums.PayChannelEnum; +import cn.aagro.pp.module.pay.enums.order.PayOrderStatusEnum; +import cn.aagro.pp.module.pay.framework.pay.core.client.impl.wallet.WalletPayClient; +import cn.aagro.pp.module.pay.service.app.PayAppService; +import cn.aagro.pp.module.pay.service.order.PayOrderService; +import cn.aagro.pp.module.pay.service.wallet.PayWalletService; +import com.google.common.collect.Maps; +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 org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import static cn.aagro.pp.framework.apilog.core.enums.OperateTypeEnum.EXPORT; +import static cn.aagro.pp.framework.common.pojo.CommonResult.success; +import static cn.aagro.pp.framework.common.util.collection.CollectionUtils.convertList; +import static cn.aagro.pp.framework.common.util.servlet.ServletUtils.getClientIP; +import static cn.aagro.pp.framework.web.core.util.WebFrameworkUtils.getLoginUserId; +import static cn.aagro.pp.framework.web.core.util.WebFrameworkUtils.getLoginUserType; + +@Tag(name = "管理后台 - 支付订单") +@RestController +@RequestMapping("/pay/order") +@Validated +public class PayOrderController { + + @Resource + private PayOrderService orderService; + @Resource + private PayAppService appService; + @Resource + private PayWalletService payWalletService; + + @GetMapping("/get") + @Operation(summary = "获得支付订单") + @Parameters({ + @Parameter(name = "id", description = "编号", required = true, example = "1024"), + @Parameter(name = "sync", description = "是否同步", example = "true") + }) + @PreAuthorize("@ss.hasPermission('pay:order:query')") + public CommonResult getOrder(@RequestParam("id") Long id, + @RequestParam(value = "sync", required = false) Boolean sync) { + PayOrderDO order = orderService.getOrder(id); + // sync 仅在等待支付 + if (Boolean.TRUE.equals(sync) && PayOrderStatusEnum.isWaiting(order.getStatus())) { + orderService.syncOrderQuietly(order.getId()); + // 重新查询,因为同步后,可能会有变化 + order = orderService.getOrder(id); + } + return success(BeanUtils.toBean(order, PayOrderRespVO.class)); + } + + @GetMapping("/get-detail") + @Operation(summary = "获得支付订单详情") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pay:order:query')") + public CommonResult getOrderDetail(@RequestParam("id") Long id) { + PayOrderDO order = orderService.getOrder(id); + if (order == null) { + return success(null); + } + + // 拼接返回 + PayAppDO app = appService.getApp(order.getAppId()); + PayOrderExtensionDO orderExtension = orderService.getOrderExtension(order.getExtensionId()); + return success(PayOrderConvert.INSTANCE.convert(order, orderExtension, app)); + } + + @PostMapping("/submit") + @Operation(summary = "提交支付订单") + public CommonResult submitPayOrder(@RequestBody PayOrderSubmitReqVO reqVO) { + // 1. 钱包支付事,需要额外传 user_id 和 user_type + if (Objects.equals(reqVO.getChannelCode(), PayChannelEnum.WALLET.getCode())) { + if (reqVO.getChannelExtras() == null) { + reqVO.setChannelExtras(Maps.newHashMapWithExpectedSize(1)); + } + PayWalletDO wallet = payWalletService.getOrCreateWallet(getLoginUserId(), getLoginUserType()); + reqVO.getChannelExtras().put(WalletPayClient.WALLET_ID_KEY, String.valueOf(wallet.getId())); + } + + // 2. 提交支付 + PayOrderSubmitRespVO respVO = orderService.submitOrder(reqVO, getClientIP()); + return success(respVO); + } + + @GetMapping("/page") + @Operation(summary = "获得支付订单分页") + @PreAuthorize("@ss.hasPermission('pay:order:query')") + public CommonResult> getOrderPage(@Valid PayOrderPageReqVO pageVO) { + PageResult pageResult = orderService.getOrderPage(pageVO); + if (CollectionUtil.isEmpty(pageResult.getList())) { + return success(new PageResult<>(pageResult.getTotal())); + } + + // 拼接返回 + Map appMap = appService.getAppMap(convertList(pageResult.getList(), PayOrderDO::getAppId)); + return success(PayOrderConvert.INSTANCE.convertPage(pageResult, appMap)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出支付订单 Excel") + @PreAuthorize("@ss.hasPermission('pay:order:export')") + @ApiAccessLog(operateType = EXPORT) + public void exportOrderExcel(@Valid PayOrderExportReqVO exportReqVO, + HttpServletResponse response) throws IOException { + List list = orderService.getOrderList(exportReqVO); + if (CollectionUtil.isEmpty(list)) { + ExcelUtils.write(response, "支付订单.xls", "数据", + PayOrderExcelVO.class, new ArrayList<>()); + return; + } + + // 拼接返回 + Map appMap = appService.getAppMap(convertList(list, PayOrderDO::getAppId)); + List excelList = PayOrderConvert.INSTANCE.convertList(list, appMap); + // 导出 Excel + ExcelUtils.write(response, "支付订单.xls", "数据", PayOrderExcelVO.class, excelList); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/order/vo/PayOrderBaseVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/order/vo/PayOrderBaseVO.java new file mode 100644 index 0000000..e30f1d2 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/order/vo/PayOrderBaseVO.java @@ -0,0 +1,89 @@ +package cn.aagro.pp.module.pay.controller.admin.order.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.aagro.pp.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +/** + * 支付订单 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + * + * @author aquan + */ +@Data +public class PayOrderBaseVO { + + @Schema(description = "应用编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "应用编号不能为空") + private Long appId; + + @Schema(description = "渠道编号", example = "2048") + private Long channelId; + + @Schema(description = "渠道编码", example = "wx_app") + private String channelCode; + + @Schema(description = "商户订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "888") + @NotNull(message = "商户订单编号不能为空") + private String merchantOrderId; + + @Schema(description = "商品标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "土豆") + @NotNull(message = "商品标题不能为空") + private String subject; + + @Schema(description = "商品描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是土豆") + @NotNull(message = "商品描述不能为空") + private String body; + + @Schema(description = "异步通知地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "http://127.0.0.1:48080/pay/notify") + @NotNull(message = "异步通知地址不能为空") + private String notifyUrl; + + @Schema(description = "支付金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + @NotNull(message = "支付金额,单位:分不能为空") + private Long price; + + @Schema(description = "渠道手续费,单位:百分比", example = "10") + private Double channelFeeRate; + + @Schema(description = "渠道手续金额,单位:分", example = "100") + private Integer channelFeePrice; + + @Schema(description = "支付状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "支付状态不能为空") + private Integer status; + + @Schema(description = "用户 IP", requiredMode = Schema.RequiredMode.REQUIRED, example = "127.0.0.1") + @NotNull(message = "用户 IP不能为空") + private String userIp; + + @Schema(description = "订单失效时间", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "订单失效时间不能为空") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime expireTime; + + @Schema(description = "订单支付成功时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime successTime; + + @Schema(description = "支付成功的订单拓展单编号", example = "50") + private Long extensionId; + + @Schema(description = "支付订单号", example = "2048888") + private String no; + + @Schema(description = "退款总金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + @NotNull(message = "退款总金额,单位:分不能为空") + private Long refundPrice; + + @Schema(description = "渠道用户编号", example = "2048") + private String channelUserId; + + @Schema(description = "渠道订单号", example = "4096") + private String channelOrderNo; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/order/vo/PayOrderDetailsRespVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/order/vo/PayOrderDetailsRespVO.java new file mode 100644 index 0000000..ec8000e --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/order/vo/PayOrderDetailsRespVO.java @@ -0,0 +1,45 @@ +package cn.aagro.pp.module.pay.controller.admin.order.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 支付订单详细信息 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PayOrderDetailsRespVO extends PayOrderBaseVO { + + @Schema(description = "支付订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "应用名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道源码") + private String appName; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + @Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime updateTime; + + /** + * 支付订单扩展 + */ + private PayOrderExtension extension; + + @Data + @Schema(description = "支付订单扩展") + public static class PayOrderExtension { + + @Schema(description = "支付订单号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private String no; + + @Schema(description = "支付异步通知的内容") + private String channelNotifyData; + + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/order/vo/PayOrderExcelVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/order/vo/PayOrderExcelVO.java new file mode 100644 index 0000000..af06478 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/order/vo/PayOrderExcelVO.java @@ -0,0 +1,67 @@ +package cn.aagro.pp.module.pay.controller.admin.order.vo; + +import cn.aagro.pp.framework.excel.core.annotations.DictFormat; +import cn.aagro.pp.framework.excel.core.convert.DictConvert; +import cn.aagro.pp.framework.excel.core.convert.MoneyConvert; +import cn.aagro.pp.module.pay.enums.DictTypeConstants; +import cn.idev.excel.annotation.ExcelProperty; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 支付订单 Excel VO + * + * @author aquan + */ +@Data +public class PayOrderExcelVO { + + @ExcelProperty("编号") + private Long id; + + @ExcelProperty("创建时间") + private LocalDateTime createTime; + + @ExcelProperty(value = "支付金额", converter = MoneyConvert.class) + private Integer price; + + @ExcelProperty(value = "退款金额", converter = MoneyConvert.class) + private Integer refundPrice; + + @ExcelProperty(value = "手续金额", converter = MoneyConvert.class) + private Integer channelFeePrice; + + @ExcelProperty("商户单号") + private String merchantOrderId; + + @ExcelProperty(value = "支付单号") + private String no; + + @ExcelProperty("渠道单号") + private String channelOrderNo; + + @ExcelProperty(value = "支付状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.ORDER_STATUS) + private Integer status; + + @ExcelProperty(value = "渠道编号名称", converter = DictConvert.class) + @DictFormat(DictTypeConstants.CHANNEL_CODE) + private String channelCode; + + @ExcelProperty("订单支付成功时间") + private LocalDateTime successTime; + + @ExcelProperty("订单失效时间") + private LocalDateTime expireTime; + + @ExcelProperty(value = "应用名称") + private String appName; + + @ExcelProperty("商品标题") + private String subject; + + @ExcelProperty("商品描述") + private String body; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/order/vo/PayOrderExportReqVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/order/vo/PayOrderExportReqVO.java new file mode 100644 index 0000000..48da6af --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/order/vo/PayOrderExportReqVO.java @@ -0,0 +1,37 @@ +package cn.aagro.pp.module.pay.controller.admin.order.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.aagro.pp.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 支付订单 Excel 导出 Request VO,参数和 PayOrderPageReqVO 是一致的") +@Data +public class PayOrderExportReqVO { + + @Schema(description = "应用编号", example = "1024") + private Long appId; + + @Schema(description = "渠道编码", example = "wx_app") + private String channelCode; + + @Schema(description = "商户订单编号", example = "4096") + private String merchantOrderId; + + @Schema(description = "渠道编号", example = "1888") + private String channelOrderNo; + + @Schema(description = "支付单号", example = "2014888") + private String no; + + @Schema(description = "支付状态", example = "0") + private Integer status; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "创建时间") + private LocalDateTime[] createTime; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/order/vo/PayOrderPageItemRespVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/order/vo/PayOrderPageItemRespVO.java new file mode 100644 index 0000000..aecf1cd --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/order/vo/PayOrderPageItemRespVO.java @@ -0,0 +1,25 @@ +package cn.aagro.pp.module.pay.controller.admin.order.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 支付订单分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PayOrderPageItemRespVO extends PayOrderBaseVO { + + @Schema(description = "支付订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + @Schema(description = "应用名称", example = "wx_pay") + private String appName; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/order/vo/PayOrderPageReqVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/order/vo/PayOrderPageReqVO.java new file mode 100644 index 0000000..5f1439b --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/order/vo/PayOrderPageReqVO.java @@ -0,0 +1,42 @@ +package cn.aagro.pp.module.pay.controller.admin.order.vo; + +import cn.aagro.pp.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 java.time.LocalDateTime; + +import static cn.aagro.pp.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 PayOrderPageReqVO extends PageParam { + + @Schema(description = "应用编号", example = "1024") + private Long appId; + + @Schema(description = "渠道编码", example = "wx_app") + private String channelCode; + + @Schema(description = "商户订单编号", example = "4096") + private String merchantOrderId; + + @Schema(description = "渠道编号", example = "1888") + private String channelOrderNo; + + @Schema(description = "支付单号", example = "2014888") + private String no; + + @Schema(description = "支付状态", example = "0") + private Integer status; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "创建时间") + private LocalDateTime[] createTime; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/order/vo/PayOrderRespVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/order/vo/PayOrderRespVO.java new file mode 100644 index 0000000..702744b --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/order/vo/PayOrderRespVO.java @@ -0,0 +1,22 @@ +package cn.aagro.pp.module.pay.controller.admin.order.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 支付订单 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PayOrderRespVO extends PayOrderBaseVO { + + @Schema(description = "支付订单编号", requiredMode = Schema.RequiredMode.REQUIRED) + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/order/vo/PayOrderSubmitReqVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/order/vo/PayOrderSubmitReqVO.java new file mode 100644 index 0000000..bc520f6 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/order/vo/PayOrderSubmitReqVO.java @@ -0,0 +1,33 @@ +package cn.aagro.pp.module.pay.controller.admin.order.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.hibernate.validator.constraints.URL; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.Map; + +@Schema(description = "管理后台 - 支付订单提交 Request VO") +@Data +public class PayOrderSubmitReqVO { + + @Schema(description = "支付单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "支付单编号不能为空") + private Long id; + + @Schema(description = "支付渠道", requiredMode = Schema.RequiredMode.REQUIRED, example = "wx_pub") + @NotEmpty(message = "支付渠道不能为空") + private String channelCode; + + @Schema(description = "支付渠道的额外参数,例如说,微信公众号需要传递 openid 参数") + private Map channelExtras; + + @Schema(description = "展示模式", example = "url") // 参见 {@link PayDisplayModeEnum} 枚举。如果不传递,则每个支付渠道使用默认的方式 + private String displayMode; + + @Schema(description = "回跳地址") + @URL(message = "回跳地址的格式必须是 URL") + private String returnUrl; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/order/vo/PayOrderSubmitRespVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/order/vo/PayOrderSubmitRespVO.java new file mode 100644 index 0000000..8fcc949 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/order/vo/PayOrderSubmitRespVO.java @@ -0,0 +1,18 @@ +package cn.aagro.pp.module.pay.controller.admin.order.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - 支付订单提交 Response VO") +@Data +public class PayOrderSubmitRespVO { + + @Schema(description = "支付状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") // 参见 PayOrderStatusEnum 枚举 + private Integer status; + + @Schema(description = "展示模式", requiredMode = Schema.RequiredMode.REQUIRED, example = "url") // 参见 PayDisplayModeEnum 枚举 + private String displayMode; + @Schema(description = "展示内容", requiredMode = Schema.RequiredMode.REQUIRED) + private String displayContent; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/refund/PayRefundController.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/refund/PayRefundController.java new file mode 100644 index 0000000..ea68f5f --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/refund/PayRefundController.java @@ -0,0 +1,90 @@ +package cn.aagro.pp.module.pay.controller.admin.refund; + +import cn.hutool.core.collection.CollectionUtil; +import cn.aagro.pp.framework.apilog.core.annotation.ApiAccessLog; +import cn.aagro.pp.framework.common.pojo.CommonResult; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.excel.core.util.ExcelUtils; +import cn.aagro.pp.module.pay.controller.admin.refund.vo.*; +import cn.aagro.pp.module.pay.convert.refund.PayRefundConvert; +import cn.aagro.pp.module.pay.dal.dataobject.app.PayAppDO; +import cn.aagro.pp.module.pay.dal.dataobject.refund.PayRefundDO; +import cn.aagro.pp.module.pay.service.app.PayAppService; +import cn.aagro.pp.module.pay.service.refund.PayRefundService; +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.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import static cn.aagro.pp.framework.apilog.core.enums.OperateTypeEnum.EXPORT; +import static cn.aagro.pp.framework.common.pojo.CommonResult.success; +import static cn.aagro.pp.framework.common.util.collection.CollectionUtils.convertList; + +@Tag(name = "管理后台 - 退款订单") +@RestController +@RequestMapping("/pay/refund") +@Validated +public class PayRefundController { + + @Resource + private PayRefundService refundService; + @Resource + private PayAppService appService; + + @GetMapping("/get") + @Operation(summary = "获得退款订单") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pay:refund:query')") + public CommonResult getRefund(@RequestParam("id") Long id) { + PayRefundDO refund = refundService.getRefund(id); + if (refund == null) { + return success(new PayRefundDetailsRespVO()); + } + + // 拼接数据 + PayAppDO app = appService.getApp(refund.getAppId()); + return success(PayRefundConvert.INSTANCE.convert(refund, app)); + } + + @GetMapping("/page") + @Operation(summary = "获得退款订单分页") + @PreAuthorize("@ss.hasPermission('pay:refund:query')") + public CommonResult> getRefundPage(@Valid PayRefundPageReqVO pageVO) { + PageResult pageResult = refundService.getRefundPage(pageVO); + if (CollectionUtil.isEmpty(pageResult.getList())) { + return success(new PageResult<>(pageResult.getTotal())); + } + + // 处理应用ID数据 + Map appMap = appService.getAppMap(convertList(pageResult.getList(), PayRefundDO::getAppId)); + return success(PayRefundConvert.INSTANCE.convertPage(pageResult, appMap)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出退款订单 Excel") + @PreAuthorize("@ss.hasPermission('pay:refund:export')") + @ApiAccessLog(operateType = EXPORT) + public void exportRefundExcel(@Valid PayRefundExportReqVO exportReqVO, + HttpServletResponse response) throws IOException { + List list = refundService.getRefundList(exportReqVO); + + // 拼接返回 + Map appMap = appService.getAppMap(convertList(list, PayRefundDO::getAppId)); + List excelList = PayRefundConvert.INSTANCE.convertList(list, appMap); + // 导出 Excel + ExcelUtils.write(response, "退款订单.xls", "数据", PayRefundExcelVO.class, excelList); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/refund/vo/PayRefundBaseVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/refund/vo/PayRefundBaseVO.java new file mode 100644 index 0000000..62263c7 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/refund/vo/PayRefundBaseVO.java @@ -0,0 +1,78 @@ +package cn.aagro.pp.module.pay.controller.admin.refund.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +/** +* 退款订单 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class PayRefundBaseVO { + + @Schema(description = "外部退款号", requiredMode = Schema.RequiredMode.REQUIRED, example = "110") + private String no; + + @Schema(description = "应用编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long appId; + + @Schema(description = "渠道编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + private Long channelId; + + @Schema(description = "渠道编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "wx_app") + private String channelCode; + + @Schema(description = "订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long orderId; + + // ========== 商户相关字段 ========== + + @Schema(description = "商户订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "225") + private String merchantOrderId; + + @Schema(description = "商户退款订单号", requiredMode = Schema.RequiredMode.REQUIRED, example = "512") + private String merchantRefundId; + + @Schema(description = "异步通知地址", requiredMode = Schema.RequiredMode.REQUIRED) + private String notifyUrl; + + // ========== 退款相关字段 ========== + + @Schema(description = "退款状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + private Integer status; + + @Schema(description = "支付金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Long payPrice; + + @Schema(description = "退款金额,单位分", requiredMode = Schema.RequiredMode.REQUIRED, example = "200") + private Long refundPrice; + + @Schema(description = "退款原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "我要退了") + private String reason; + + @Schema(description = "用户 IP", requiredMode = Schema.RequiredMode.REQUIRED, example = "127.0.0.1") + private String userIp; + + // ========== 渠道相关字段 ========== + + @Schema(description = "渠道订单号", requiredMode = Schema.RequiredMode.REQUIRED, example = "233") + private String channelOrderNo; + + @Schema(description = "渠道退款单号", example = "2022") + private String channelRefundNo; + + @Schema(description = "退款成功时间") + private LocalDateTime successTime; + + @Schema(description = "调用渠道的错误码") + private String channelErrorCode; + + @Schema(description = "调用渠道的错误提示") + private String channelErrorMsg; + + @Schema(description = "支付渠道的额外参数") + private String channelNotifyData; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/refund/vo/PayRefundDetailsRespVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/refund/vo/PayRefundDetailsRespVO.java new file mode 100644 index 0000000..1345141 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/refund/vo/PayRefundDetailsRespVO.java @@ -0,0 +1,40 @@ +package cn.aagro.pp.module.pay.controller.admin.refund.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 退款订单详情 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PayRefundDetailsRespVO extends PayRefundBaseVO { + + @Schema(description = "支付退款编号", requiredMode = Schema.RequiredMode.REQUIRED) + private Long id; + + @Schema(description = "应用名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是芋艿") + private String appName; + + @Schema(description = "支付订单", requiredMode = Schema.RequiredMode.REQUIRED) + private Order order; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + + @Schema(description = "更新时间") + private LocalDateTime updateTime; + + @Schema(description = "管理后台 - 支付订单") + @Data + public static class Order { + + @Schema(description = "商品标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "土豆") + private String subject; + + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/refund/vo/PayRefundExcelVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/refund/vo/PayRefundExcelVO.java new file mode 100644 index 0000000..67ff768 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/refund/vo/PayRefundExcelVO.java @@ -0,0 +1,61 @@ +package cn.aagro.pp.module.pay.controller.admin.refund.vo; + +import cn.aagro.pp.framework.excel.core.annotations.DictFormat; +import cn.aagro.pp.framework.excel.core.convert.DictConvert; +import cn.aagro.pp.framework.excel.core.convert.MoneyConvert; +import cn.aagro.pp.module.pay.enums.DictTypeConstants; +import cn.idev.excel.annotation.ExcelProperty; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 退款订单 Excel VO + * + * @author aquan + */ +@Data +public class PayRefundExcelVO { + + @ExcelProperty("支付退款编号") + private Long id; + + @ExcelProperty("创建时间") + private LocalDateTime createTime; + + @ExcelProperty(value = "支付金额", converter = MoneyConvert.class) + private Integer payPrice; + + @ExcelProperty(value = "退款金额", converter = MoneyConvert.class) + private Integer refundPrice; + + @ExcelProperty("商户退款单号") + private String merchantRefundId; + @ExcelProperty("退款单号") + private String no; + @ExcelProperty("渠道退款单号") + private String channelRefundNo; + + @ExcelProperty("商户支付单号") + private String merchantOrderId; + @ExcelProperty("渠道支付单号") + private String channelOrderNo; + + @ExcelProperty(value = "退款状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.REFUND_STATUS) + private Integer status; + + @ExcelProperty(value = "退款渠道", converter = DictConvert.class) + @DictFormat(DictTypeConstants.CHANNEL_CODE) + private String channelCode; + + @ExcelProperty("成功时间") + private LocalDateTime successTime; + + @ExcelProperty(value = "支付应用") + private String appName; + + @ExcelProperty("退款原因") + private String reason; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/refund/vo/PayRefundExportReqVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/refund/vo/PayRefundExportReqVO.java new file mode 100644 index 0000000..f37dea0 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/refund/vo/PayRefundExportReqVO.java @@ -0,0 +1,40 @@ +package cn.aagro.pp.module.pay.controller.admin.refund.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.aagro.pp.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 退款订单 Excel 导出 Request VO,参数和 PayRefundPageReqVO 是一致的") +@Data +public class PayRefundExportReqVO { + + @Schema(description = "应用编号", example = "1024") + private Long appId; + + @Schema(description = "渠道编码", example = "wx_app") + private String channelCode; + + @Schema(description = "商户支付单号", example = "10") + private String merchantOrderId; + + @Schema(description = "商户退款单号", example = "20") + private String merchantRefundId; + + @Schema(description = "渠道支付单号", example = "30") + private String channelOrderNo; + + @Schema(description = "渠道退款单号", example = "40") + private String channelRefundNo; + + @Schema(description = "退款状态", example = "0") + private Integer status; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "创建时间") + private LocalDateTime[] createTime; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/refund/vo/PayRefundPageItemRespVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/refund/vo/PayRefundPageItemRespVO.java new file mode 100644 index 0000000..ac269a0 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/refund/vo/PayRefundPageItemRespVO.java @@ -0,0 +1,25 @@ +package cn.aagro.pp.module.pay.controller.admin.refund.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 退款订单分页查询 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PayRefundPageItemRespVO extends PayRefundBaseVO { + + @Schema(description = "支付订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "应用名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是芋艿") + private String appName; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/refund/vo/PayRefundPageReqVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/refund/vo/PayRefundPageReqVO.java new file mode 100644 index 0000000..f738243 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/refund/vo/PayRefundPageReqVO.java @@ -0,0 +1,45 @@ +package cn.aagro.pp.module.pay.controller.admin.refund.vo; + +import cn.aagro.pp.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 java.time.LocalDateTime; + +import static cn.aagro.pp.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 PayRefundPageReqVO extends PageParam { + + @Schema(description = "应用编号", example = "1024") + private Long appId; + + @Schema(description = "渠道编码", example = "wx_app") + private String channelCode; + + @Schema(description = "商户支付单号", example = "10") + private String merchantOrderId; + + @Schema(description = "商户退款单号", example = "20") + private String merchantRefundId; + + @Schema(description = "渠道支付单号", example = "30") + private String channelOrderNo; + + @Schema(description = "渠道退款单号", example = "40") + private String channelRefundNo; + + @Schema(description = "退款状态", example = "0") + private Integer status; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "创建时间") + private LocalDateTime[] createTime; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/transfer/PayTransferController.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/transfer/PayTransferController.java new file mode 100644 index 0000000..13f2cc9 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/transfer/PayTransferController.java @@ -0,0 +1,91 @@ +package cn.aagro.pp.module.pay.controller.admin.transfer; + +import cn.aagro.pp.framework.apilog.core.annotation.ApiAccessLog; +import cn.aagro.pp.framework.common.pojo.CommonResult; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.common.util.object.BeanUtils; +import cn.aagro.pp.framework.excel.core.util.ExcelUtils; +import cn.aagro.pp.module.pay.controller.admin.transfer.vo.PayTransferPageReqVO; +import cn.aagro.pp.module.pay.controller.admin.transfer.vo.PayTransferRespVO; +import cn.aagro.pp.module.pay.dal.dataobject.app.PayAppDO; +import cn.aagro.pp.module.pay.dal.dataobject.transfer.PayTransferDO; +import cn.aagro.pp.module.pay.service.app.PayAppService; +import cn.aagro.pp.module.pay.service.transfer.PayTransferService; +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.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.Map; + +import static cn.aagro.pp.framework.apilog.core.enums.OperateTypeEnum.EXPORT; +import static cn.aagro.pp.framework.common.pojo.CommonResult.success; +import static cn.aagro.pp.framework.common.pojo.PageParam.PAGE_SIZE_NONE; +import static cn.aagro.pp.framework.common.util.collection.CollectionUtils.convertList; + +@Tag(name = "管理后台 - 转账单") +@RestController +@RequestMapping("/pay/transfer") +@Validated +public class PayTransferController { + + @Resource + private PayTransferService payTransferService; + @Resource + private PayAppService payAppService; + + @GetMapping("/get") + @Operation(summary = "获得转账订单") + @PreAuthorize("@ss.hasPermission('pay:transfer:query')") + public CommonResult getTransfer(@RequestParam("id") Long id) { + PayTransferDO transfer = payTransferService.getTransfer(id); + if (transfer == null) { + return success(new PayTransferRespVO()); + } + + // 拼接数据 + PayAppDO app = payAppService.getApp(transfer.getAppId()); + return success(BeanUtils.toBean(transfer, PayTransferRespVO.class, transferVO -> { + if (app != null) { + transferVO.setAppName(app.getName()); + } + })); + } + + @GetMapping("/page") + @Operation(summary = "获得转账订单分页") + @PreAuthorize("@ss.hasPermission('pay:transfer:query')") + public CommonResult> getTransferPage(@Valid PayTransferPageReqVO pageVO) { + PageResult pageResult = payTransferService.getTransferPage(pageVO); + + // 拼接数据 + Map apps = payAppService.getAppMap(convertList(pageResult.getList(), PayTransferDO::getAppId)); + return success(BeanUtils.toBean(pageResult, PayTransferRespVO.class, transferVO -> { + if (apps.containsKey(transferVO.getAppId())) { + transferVO.setAppName(apps.get(transferVO.getAppId()).getName()); + } + })); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出转账订单 Excel") + @PreAuthorize("@ss.hasPermission('pay:transfer:export')") + @ApiAccessLog(operateType = EXPORT) + public void exportTransfer(PayTransferPageReqVO pageReqVO, + HttpServletResponse response) throws IOException { + pageReqVO.setPageSize(PAGE_SIZE_NONE); + PageResult pageResult = getTransferPage(pageReqVO).getData(); + + // 导出 Excel + ExcelUtils.write(response, "转账订单.xls", "数据", PayTransferRespVO.class, pageResult.getList()); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/transfer/vo/PayTransferCreateRespVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/transfer/vo/PayTransferCreateRespVO.java new file mode 100644 index 0000000..5d954cd --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/transfer/vo/PayTransferCreateRespVO.java @@ -0,0 +1,16 @@ +package cn.aagro.pp.module.pay.controller.admin.transfer.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - 发起转账 Response VO") +@Data +public class PayTransferCreateRespVO { + + @Schema(description = "转账单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "转账状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") // 参见 PayTransferStatusEnum 枚举 + private Integer status; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/transfer/vo/PayTransferPageReqVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/transfer/vo/PayTransferPageReqVO.java new file mode 100644 index 0000000..f698412 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/transfer/vo/PayTransferPageReqVO.java @@ -0,0 +1,45 @@ +package cn.aagro.pp.module.pay.controller.admin.transfer.vo; + +import lombok.*; +import io.swagger.v3.oas.annotations.media.Schema; +import cn.aagro.pp.framework.common.pojo.PageParam; +import org.springframework.format.annotation.DateTimeFormat; +import java.time.LocalDateTime; + +import static cn.aagro.pp.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 PayTransferPageReqVO extends PageParam { + + @Schema(description = "转账单号") + private String no; + + @Schema(description = "应用编号", example = "12831") + private Long appId; + + @Schema(description = "渠道编码", example = "wx_app") + private String channelCode; + + @Schema(description = "商户转账单编号", example = "17481") + private String merchantOrderId; + + @Schema(description = "转账状态", example = "2") + private Integer status; + + @Schema(description = "收款人姓名", example = "王五") + private String userName; + + @Schema(description = "收款人账号", example = "26589") + private String userAccount; + + @Schema(description = "渠道转账单号") + private String channelTransferNo; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/transfer/vo/PayTransferRespVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/transfer/vo/PayTransferRespVO.java new file mode 100644 index 0000000..41bf829 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/transfer/vo/PayTransferRespVO.java @@ -0,0 +1,100 @@ +package cn.aagro.pp.module.pay.controller.admin.transfer.vo; + +import cn.aagro.pp.framework.excel.core.annotations.DictFormat; +import cn.aagro.pp.framework.excel.core.convert.DictConvert; +import cn.aagro.pp.framework.excel.core.convert.MoneyConvert; +import cn.aagro.pp.module.pay.enums.DictTypeConstants; + +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import java.time.LocalDateTime; +import java.util.Map; + +@Schema(description = "管理后台 - 转账单 Response VO") +@Data +@ExcelIgnoreUnannotated +public class PayTransferRespVO { + + @ExcelProperty("转账单编号") + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2931") + private Long id; + + @ExcelProperty("转账单号") + @Schema(description = "转账单号", requiredMode = Schema.RequiredMode.REQUIRED) + private String no; + + @Schema(description = "应用编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "12831") + private Long appId; + + @ExcelProperty("应用名称") + @Schema(description = "应用名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿") + private String appName; + + @Schema(description = "转账渠道编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24833") + private Long channelId; + + @ExcelProperty(value = "转账渠道", converter = DictConvert.class) + @DictFormat(DictTypeConstants.CHANNEL_CODE) + @Schema(description = "转账渠道编码", requiredMode = Schema.RequiredMode.REQUIRED) + private String channelCode; + + @ExcelProperty("商户转账单编号") + @Schema(description = "商户转账单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "17481") + private String merchantTransferId; + + @ExcelProperty(value = "转账状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.TRANSFER_STATUS) + @Schema(description = "转账状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + private Integer status; + + @ExcelProperty("转账成功时间") + @Schema(description = "转账成功时间") + private LocalDateTime successTime; + + @Schema(description = "转账金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "964") + @ExcelProperty(value = "转账金额", converter = MoneyConvert.class) + private Integer price; + + @ExcelProperty("转账标题") + @Schema(description = "转账标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "冲冲冲!") + private String subject; + + @Schema(description = "收款人姓名", example = "王五") + @ExcelProperty("收款人姓名") + private String userName; + + @Schema(description = "收款人账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "26589") + @ExcelProperty("收款人账号") + private String userAccount; + + @Schema(description = "异步通知商户地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn") + private String notifyUrl; + + @ExcelProperty("用户 IP") + @Schema(description = "用户 IP", requiredMode = Schema.RequiredMode.REQUIRED) + private String userIp; + + @Schema(description = "渠道的额外参数") + private Map channelExtras; + + @Schema(description = "渠道转账单号") + @ExcelProperty("渠道转账单号") + private String channelTransferNo; + + @Schema(description = "调用渠道的错误码") + private String channelErrorCode; + + @ExcelProperty("渠道错误提示") + @Schema(description = "调用渠道的错误提示") + private String channelErrorMsg; + + @Schema(description = "渠道的同步/异步通知的内容") + private String channelNotifyData; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("创建时间") + private LocalDateTime createTime; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/PayWalletController.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/PayWalletController.java new file mode 100644 index 0000000..41e4af3 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/PayWalletController.java @@ -0,0 +1,71 @@ +package cn.aagro.pp.module.pay.controller.admin.wallet; + +import cn.aagro.pp.framework.common.pojo.CommonResult; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.pay.controller.admin.wallet.vo.wallet.PayWalletPageReqVO; +import cn.aagro.pp.module.pay.controller.admin.wallet.vo.wallet.PayWalletRespVO; +import cn.aagro.pp.module.pay.controller.admin.wallet.vo.wallet.PayWalletUpdateBalanceReqVO; +import cn.aagro.pp.module.pay.controller.admin.wallet.vo.wallet.PayWalletUserReqVO; +import cn.aagro.pp.module.pay.convert.wallet.PayWalletConvert; +import cn.aagro.pp.module.pay.dal.dataobject.wallet.PayWalletDO; +import cn.aagro.pp.module.pay.enums.wallet.PayWalletBizTypeEnum; +import cn.aagro.pp.module.pay.service.wallet.PayWalletService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +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.aagro.pp.framework.common.enums.UserTypeEnum.MEMBER; +import static cn.aagro.pp.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.aagro.pp.framework.common.pojo.CommonResult.success; +import static cn.aagro.pp.module.pay.enums.ErrorCodeConstants.WALLET_NOT_FOUND; + +@Tag(name = "管理后台 - 用户钱包") +@RestController +@RequestMapping("/pay/wallet") +@Validated +@Slf4j +public class PayWalletController { + + @Resource + private PayWalletService payWalletService; + + @GetMapping("/get") + @PreAuthorize("@ss.hasPermission('pay:wallet:query')") + @Operation(summary = "获得用户钱包明细") + public CommonResult getWallet(PayWalletUserReqVO reqVO) { + PayWalletDO wallet = payWalletService.getOrCreateWallet(reqVO.getUserId(), MEMBER.getValue()); + return success(PayWalletConvert.INSTANCE.convert02(wallet)); + } + + @GetMapping("/page") + @Operation(summary = "获得会员钱包分页") + @PreAuthorize("@ss.hasPermission('pay:wallet:query')") + public CommonResult> getWalletPage(@Valid PayWalletPageReqVO pageVO) { + PageResult pageResult = payWalletService.getWalletPage(pageVO); + return success(PayWalletConvert.INSTANCE.convertPage(pageResult)); + } + + @PutMapping("/update-balance") + @Operation(summary = "更新会员用户余额") + @PreAuthorize("@ss.hasPermission('pay:wallet:update-balance')") + public CommonResult updateWalletBalance(@Valid @RequestBody PayWalletUpdateBalanceReqVO updateReqVO) { + // 获得用户钱包 + PayWalletDO wallet = payWalletService.getOrCreateWallet(updateReqVO.getUserId(), MEMBER.getValue()); + if (wallet == null) { + log.error("[updateWalletBalance],updateReqVO({}) 用户钱包不存在.", updateReqVO); + throw exception(WALLET_NOT_FOUND); + } + + // 更新钱包余额 + payWalletService.addWalletBalance(wallet.getId(), String.valueOf(updateReqVO.getUserId()), + PayWalletBizTypeEnum.UPDATE_BALANCE, updateReqVO.getBalance()); + return success(true); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/PayWalletRechargeController.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/PayWalletRechargeController.java new file mode 100644 index 0000000..a9cd5dd --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/PayWalletRechargeController.java @@ -0,0 +1,59 @@ +package cn.aagro.pp.module.pay.controller.admin.wallet; + +import cn.aagro.pp.framework.common.pojo.CommonResult; +import cn.aagro.pp.module.pay.api.notify.dto.PayOrderNotifyReqDTO; +import cn.aagro.pp.module.pay.api.notify.dto.PayRefundNotifyReqDTO; +import cn.aagro.pp.module.pay.service.wallet.PayWalletRechargeService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.annotation.security.PermitAll; +import javax.validation.Valid; + +import static cn.aagro.pp.framework.common.pojo.CommonResult.success; +import static cn.aagro.pp.framework.common.util.servlet.ServletUtils.getClientIP; + +@Tag(name = "管理后台 - 钱包充值") +@RestController +@RequestMapping("/pay/wallet-recharge") +@Validated +@Slf4j +public class PayWalletRechargeController { + + @Resource + private PayWalletRechargeService walletRechargeService; + + @PostMapping("/update-paid") + @Operation(summary = "更新钱包充值为已充值") // 由 pay-module 支付服务,进行回调,可见 PayNotifyJob + @PermitAll // 无需登录, 内部校验实现 + public CommonResult updateWalletRechargerPaid(@Valid @RequestBody PayOrderNotifyReqDTO notifyReqDTO) { + walletRechargeService.updateWalletRechargerPaid(Long.valueOf(notifyReqDTO.getMerchantOrderId()), + notifyReqDTO.getPayOrderId()); + return success(true); + } + + @PostMapping("/refund") + @Operation(summary = "发起钱包充值退款") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + public CommonResult refundWalletRecharge(@RequestParam("id") Long id) { + walletRechargeService.refundWalletRecharge(id, getClientIP()); + return success(true); + } + + @PostMapping("/update-refunded") + @Operation(summary = "更新钱包充值为已退款") // 由 pay-module 支付服务,进行回调,可见 PayNotifyJob + @PermitAll // 无需登录, 内部校验实现 + public CommonResult updateWalletRechargeRefunded(@RequestBody PayRefundNotifyReqDTO notifyReqDTO) { + walletRechargeService.updateWalletRechargeRefunded( + Long.valueOf(notifyReqDTO.getMerchantOrderId()), + Long.valueOf(notifyReqDTO.getMerchantRefundId()), + notifyReqDTO.getPayRefundId()); + return success(true); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/PayWalletRechargePackageController.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/PayWalletRechargePackageController.java new file mode 100644 index 0000000..408198c --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/PayWalletRechargePackageController.java @@ -0,0 +1,75 @@ +package cn.aagro.pp.module.pay.controller.admin.wallet; + +import cn.aagro.pp.framework.common.pojo.CommonResult; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.pay.controller.admin.wallet.vo.rechargepackage.WalletRechargePackageCreateReqVO; +import cn.aagro.pp.module.pay.controller.admin.wallet.vo.rechargepackage.WalletRechargePackagePageReqVO; +import cn.aagro.pp.module.pay.controller.admin.wallet.vo.rechargepackage.WalletRechargePackageRespVO; +import cn.aagro.pp.module.pay.controller.admin.wallet.vo.rechargepackage.WalletRechargePackageUpdateReqVO; +import cn.aagro.pp.module.pay.convert.wallet.PayWalletRechargePackageConvert; +import cn.aagro.pp.module.pay.dal.dataobject.wallet.PayWalletRechargePackageDO; +import cn.aagro.pp.module.pay.service.wallet.PayWalletRechargePackageService; +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.aagro.pp.framework.common.pojo.CommonResult.success; + + +@Tag(name = "管理后台 - 钱包充值套餐") +@RestController +@RequestMapping("/pay/wallet-recharge-package") +@Validated +public class PayWalletRechargePackageController { + + @Resource + private PayWalletRechargePackageService walletRechargePackageService; + + @PostMapping("/create") + @Operation(summary = "创建钱包充值套餐") + @PreAuthorize("@ss.hasPermission('pay:wallet-recharge-package:create')") + public CommonResult createWalletRechargePackage(@Valid @RequestBody WalletRechargePackageCreateReqVO createReqVO) { + return success(walletRechargePackageService.createWalletRechargePackage(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新钱包充值套餐") + @PreAuthorize("@ss.hasPermission('pay:wallet-recharge-package:update')") + public CommonResult updateWalletRechargePackage(@Valid @RequestBody WalletRechargePackageUpdateReqVO updateReqVO) { + walletRechargePackageService.updateWalletRechargePackage(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除钱包充值套餐") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('pay:wallet-recharge-package:delete')") + public CommonResult deleteWalletRechargePackage(@RequestParam("id") Long id) { + walletRechargePackageService.deleteWalletRechargePackage(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得钱包充值套餐") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pay:wallet-recharge-package:query')") + public CommonResult getWalletRechargePackage(@RequestParam("id") Long id) { + PayWalletRechargePackageDO walletRechargePackage = walletRechargePackageService.getWalletRechargePackage(id); + return success(PayWalletRechargePackageConvert.INSTANCE.convert(walletRechargePackage)); + } + + @GetMapping("/page") + @Operation(summary = "获得钱包充值套餐分页") + @PreAuthorize("@ss.hasPermission('pay:wallet-recharge-package:query')") + public CommonResult> getWalletRechargePackagePage(@Valid WalletRechargePackagePageReqVO pageVO) { + PageResult pageResult = walletRechargePackageService.getWalletRechargePackagePage(pageVO); + return success(PayWalletRechargePackageConvert.INSTANCE.convertPage(pageResult)); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/PayWalletTransactionController.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/PayWalletTransactionController.java new file mode 100644 index 0000000..2a26b8e --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/PayWalletTransactionController.java @@ -0,0 +1,43 @@ +package cn.aagro.pp.module.pay.controller.admin.wallet; + +import cn.aagro.pp.framework.common.pojo.CommonResult; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.pay.controller.admin.wallet.vo.transaction.PayWalletTransactionPageReqVO; +import cn.aagro.pp.module.pay.controller.admin.wallet.vo.transaction.PayWalletTransactionRespVO; +import cn.aagro.pp.module.pay.convert.wallet.PayWalletTransactionConvert; +import cn.aagro.pp.module.pay.dal.dataobject.wallet.PayWalletTransactionDO; +import cn.aagro.pp.module.pay.service.wallet.PayWalletTransactionService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +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 javax.validation.Valid; + +import static cn.aagro.pp.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 钱包余额明细") +@RestController +@RequestMapping("/pay/wallet-transaction") +@Validated +@Slf4j +public class PayWalletTransactionController { + + @Resource + private PayWalletTransactionService payWalletTransactionService; + + @GetMapping("/page") + @Operation(summary = "获得钱包流水分页") + @PreAuthorize("@ss.hasPermission('pay:wallet:query')") + public CommonResult> getWalletTransactionPage( + @Valid PayWalletTransactionPageReqVO pageReqVO) { + PageResult result = payWalletTransactionService.getWalletTransactionPage(pageReqVO); + return success(PayWalletTransactionConvert.INSTANCE.convertPage2(result)); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/vo/rechargepackage/WalletRechargePackageBaseVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/vo/rechargepackage/WalletRechargePackageBaseVO.java new file mode 100644 index 0000000..9d61ec3 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/vo/rechargepackage/WalletRechargePackageBaseVO.java @@ -0,0 +1,31 @@ +package cn.aagro.pp.module.pay.controller.admin.wallet.vo.rechargepackage; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +/** + * 充值套餐 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class WalletRechargePackageBaseVO { + + @Schema(description = "套餐名", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四") + @NotNull(message = "套餐名不能为空") + private String name; + + @Schema(description = "支付金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "16454") + @NotNull(message = "支付金额不能为空") + private Integer payPrice; + + @Schema(description = "赠送金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "20887") + @NotNull(message = "赠送金额不能为空") + private Integer bonusPrice; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + @NotNull(message = "状态不能为空") + private Byte status; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/vo/rechargepackage/WalletRechargePackageCreateReqVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/vo/rechargepackage/WalletRechargePackageCreateReqVO.java new file mode 100644 index 0000000..7187518 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/vo/rechargepackage/WalletRechargePackageCreateReqVO.java @@ -0,0 +1,14 @@ +package cn.aagro.pp.module.pay.controller.admin.wallet.vo.rechargepackage; + +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 WalletRechargePackageCreateReqVO extends WalletRechargePackageBaseVO { + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/vo/rechargepackage/WalletRechargePackagePageReqVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/vo/rechargepackage/WalletRechargePackagePageReqVO.java new file mode 100644 index 0000000..eb9d898 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/vo/rechargepackage/WalletRechargePackagePageReqVO.java @@ -0,0 +1,30 @@ +package cn.aagro.pp.module.pay.controller.admin.wallet.vo.rechargepackage; + +import cn.aagro.pp.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 java.time.LocalDateTime; + +import static cn.aagro.pp.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 WalletRechargePackagePageReqVO extends PageParam { + + @Schema(description = "套餐名", example = "李四") + private String name; + + @Schema(description = "状态", example = "2") + private Integer status; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/vo/rechargepackage/WalletRechargePackageRespVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/vo/rechargepackage/WalletRechargePackageRespVO.java new file mode 100644 index 0000000..2d03af2 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/vo/rechargepackage/WalletRechargePackageRespVO.java @@ -0,0 +1,19 @@ +package cn.aagro.pp.module.pay.controller.admin.wallet.vo.rechargepackage; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 充值套餐 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class WalletRechargePackageRespVO extends WalletRechargePackageBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "9032") + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/vo/rechargepackage/WalletRechargePackageUpdateReqVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/vo/rechargepackage/WalletRechargePackageUpdateReqVO.java new file mode 100644 index 0000000..34c4286 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/vo/rechargepackage/WalletRechargePackageUpdateReqVO.java @@ -0,0 +1,20 @@ +package cn.aagro.pp.module.pay.controller.admin.wallet.vo.rechargepackage; + +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 WalletRechargePackageUpdateReqVO extends WalletRechargePackageBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "9032") + @NotNull(message = "编号不能为空") + private Long id; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/vo/transaction/PayWalletTransactionPageReqVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/vo/transaction/PayWalletTransactionPageReqVO.java new file mode 100644 index 0000000..fd28d63 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/vo/transaction/PayWalletTransactionPageReqVO.java @@ -0,0 +1,23 @@ +package cn.aagro.pp.module.pay.controller.admin.wallet.vo.transaction; + +import cn.aagro.pp.framework.common.enums.UserTypeEnum; +import cn.aagro.pp.framework.common.pojo.PageParam; +import cn.aagro.pp.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - 钱包流水分页 Request VO") +@Data +public class PayWalletTransactionPageReqVO extends PageParam { + + @Schema(description = "钱包编号", example = "888") + private Long walletId; + + @Schema(description = "用户编号", example = "1024") + private Long userId; + + @Schema(description = "用户类型", example = "1") + @InEnum(UserTypeEnum.class) + private Integer userType; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/vo/transaction/PayWalletTransactionRespVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/vo/transaction/PayWalletTransactionRespVO.java new file mode 100644 index 0000000..61abecc --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/vo/transaction/PayWalletTransactionRespVO.java @@ -0,0 +1,35 @@ +package cn.aagro.pp.module.pay.controller.admin.wallet.vo.transaction; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "用户 APP - 钱包流水分页 Response VO") +@Data +public class PayWalletTransactionRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "钱包编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "5") + private Long walletId; + + @Schema(description = "业务分类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer bizType; + + @Schema(description = "交易金额,单位分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Long price; + + @Schema(description = "流水标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "土豆土豆") + private String title; + + @Schema(description = "交易后的余额,单位分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Long balance; + + @Schema(description = "交易时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + // TODO @jason:merchantOrderId 字段,需要在 PayWalletTransaction 存储下;然后,前端也返回下这个字段,界面也展示下商户名 + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/vo/wallet/PayWalletBaseVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/vo/wallet/PayWalletBaseVO.java new file mode 100644 index 0000000..29438ff --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/vo/wallet/PayWalletBaseVO.java @@ -0,0 +1,39 @@ +package cn.aagro.pp.module.pay.controller.admin.wallet.vo.wallet; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +/** + * 用户钱包 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class PayWalletBaseVO { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "20020") + @NotNull(message = "用户编号不能为空") + private Long userId; + + @Schema(description = "用户类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "用户类型不能为空") + private Integer userType; + + @Schema(description = "余额,单位分", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "余额,单位分不能为空") + private Integer balance; + + @Schema(description = "累计支出,单位分", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "累计支出,单位分不能为空") + private Integer totalExpense; + + @Schema(description = "累计充值,单位分", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "累计充值,单位分不能为空") + private Integer totalRecharge; + + @Schema(description = "冻结金额,单位分", requiredMode = Schema.RequiredMode.REQUIRED, example = "20737") + @NotNull(message = "冻结金额,单位分不能为空") + private Integer freezePrice; + +} \ No newline at end of file diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/vo/wallet/PayWalletPageReqVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/vo/wallet/PayWalletPageReqVO.java new file mode 100644 index 0000000..7c0d7ec --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/vo/wallet/PayWalletPageReqVO.java @@ -0,0 +1,33 @@ +package cn.aagro.pp.module.pay.controller.admin.wallet.vo.wallet; + +import cn.aagro.pp.framework.common.enums.UserTypeEnum; +import cn.aagro.pp.framework.common.pojo.PageParam; +import cn.aagro.pp.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.aagro.pp.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 PayWalletPageReqVO extends PageParam { + + @Schema(description = "用户编号", example = "1024") + private Long userId; + + @Schema(description = "用户类型", example = "1") + @InEnum(value = UserTypeEnum.class) + private Integer userType; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/vo/wallet/PayWalletRespVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/vo/wallet/PayWalletRespVO.java new file mode 100644 index 0000000..3d41988 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/vo/wallet/PayWalletRespVO.java @@ -0,0 +1,22 @@ +package cn.aagro.pp.module.pay.controller.admin.wallet.vo.wallet; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 用户钱包 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PayWalletRespVO extends PayWalletBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "29528") + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/vo/wallet/PayWalletUpdateBalanceReqVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/vo/wallet/PayWalletUpdateBalanceReqVO.java new file mode 100644 index 0000000..441c371 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/vo/wallet/PayWalletUpdateBalanceReqVO.java @@ -0,0 +1,20 @@ +package cn.aagro.pp.module.pay.controller.admin.wallet.vo.wallet; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 修改钱包余额 Request VO") +@Data +public class PayWalletUpdateBalanceReqVO { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "23788") + @NotNull(message = "用户编号不能为空") + private Long userId; + + @Schema(description = "变动余额,正数为增加,负数为减少", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + @NotNull(message = "变动余额不能为空") + private Integer balance; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/vo/wallet/PayWalletUserReqVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/vo/wallet/PayWalletUserReqVO.java new file mode 100644 index 0000000..b3acdb9 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/admin/wallet/vo/wallet/PayWalletUserReqVO.java @@ -0,0 +1,16 @@ +package cn.aagro.pp.module.pay.controller.admin.wallet.vo.wallet; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 用户钱包明细 Request VO") +@Data +public class PayWalletUserReqVO { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "用户编号不能为空") + private Long userId; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/channel/AppPayChannelController.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/channel/AppPayChannelController.java new file mode 100644 index 0000000..c24b502 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/channel/AppPayChannelController.java @@ -0,0 +1,39 @@ +package cn.aagro.pp.module.pay.controller.app.channel; + +import cn.aagro.pp.framework.common.pojo.CommonResult; +import cn.aagro.pp.module.pay.dal.dataobject.channel.PayChannelDO; +import cn.aagro.pp.module.pay.service.channel.PayChannelService; +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.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Set; + +import static cn.aagro.pp.framework.common.pojo.CommonResult.success; +import static cn.aagro.pp.framework.common.util.collection.CollectionUtils.convertSet; + +@Tag(name = "用户 App - 支付渠道") +@RestController +@RequestMapping("/pay/channel") +@Validated +public class AppPayChannelController { + + @Resource + private PayChannelService channelService; + + @GetMapping("/get-enable-code-list") + @Operation(summary = "获得指定应用的开启的支付渠道编码列表") + @Parameter(name = "appId", description = "应用编号", required = true, example = "1") + public CommonResult> getEnableChannelCodeList(@RequestParam("appId") Long appId) { + List channels = channelService.getEnableChannelList(appId); + return success(convertSet(channels, PayChannelDO::getCode)); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/order/AppPayOrderController.http b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/order/AppPayOrderController.http new file mode 100644 index 0000000..4fe1898 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/order/AppPayOrderController.http @@ -0,0 +1,63 @@ +### /pay/create 提交支付订单【alipay_pc】 +POST {{appApi}}/pay/order/submit +Content-Type: application/json +Authorization: Bearer {{appToken}} +tenant-id: {{appTenantId}} + +{ + "id": 174, + "channelCode": "alipay_pc" +} + +### /pay/create 提交支付订单【wx_bar】 +POST {{appApi}}/pay/order/submit +Content-Type: application/json +Authorization: Bearer {{appToken}} +tenant-id: {{appTenantId}} + +{ + "id": 202, + "channelCode": "wx_bar", + "channelExtras": { + "authCode": "134042110834344848" + } +} + +### /pay/create 提交支付订单【wx_pub】 +POST {{appApi}}/pay/order/submit +Content-Type: application/json +Authorization: Bearer {{appToken}} +tenant-id: {{appTenantId}} + +{ + "id": 202, + "channelCode": "wx_pub", + "channelExtras": { + "openid": "ockUAwIZ-0OeMZl9ogcZ4ILrGba0" + } +} + +### /pay/create 提交支付订单【wx_lite】 +POST {{appApi}}/pay/order/submit +Content-Type: application/json +Authorization: Bearer {{appToken}} +tenant-id: {{appTenantId}} + +{ + "id": 202, + "channelCode": "wx_lite", + "channelExtras": { + "openid": "oLefc4g5GjKWHJjLjMSXB3wX0fD0" + } +} + +### /pay/create 提交支付订单【wx_native】 +POST {{appApi}}/pay/order/submit +Content-Type: application/json +Authorization: Bearer {{appToken}} +tenant-id: {{appTenantId}} + +{ + "id": 202, + "channelCode": "wx_native" +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/order/AppPayOrderController.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/order/AppPayOrderController.java new file mode 100644 index 0000000..86c87f3 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/order/AppPayOrderController.java @@ -0,0 +1,90 @@ +package cn.aagro.pp.module.pay.controller.app.order; + +import cn.hutool.core.util.ObjUtil; +import cn.aagro.pp.framework.common.pojo.CommonResult; +import cn.aagro.pp.framework.common.util.object.BeanUtils; +import cn.aagro.pp.module.pay.controller.admin.order.vo.PayOrderRespVO; +import cn.aagro.pp.module.pay.controller.admin.order.vo.PayOrderSubmitRespVO; +import cn.aagro.pp.module.pay.controller.app.order.vo.AppPayOrderSubmitReqVO; +import cn.aagro.pp.module.pay.controller.app.order.vo.AppPayOrderSubmitRespVO; +import cn.aagro.pp.module.pay.dal.dataobject.order.PayOrderDO; +import cn.aagro.pp.module.pay.dal.dataobject.wallet.PayWalletDO; +import cn.aagro.pp.module.pay.enums.PayChannelEnum; +import cn.aagro.pp.module.pay.enums.order.PayOrderStatusEnum; +import cn.aagro.pp.module.pay.framework.pay.core.client.impl.wallet.WalletPayClient; +import cn.aagro.pp.module.pay.service.order.PayOrderService; +import cn.aagro.pp.module.pay.service.wallet.PayWalletService; +import com.google.common.collect.Maps; +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 lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.Objects; + +import static cn.aagro.pp.framework.common.pojo.CommonResult.success; +import static cn.aagro.pp.framework.common.util.servlet.ServletUtils.getClientIP; +import static cn.aagro.pp.framework.web.core.util.WebFrameworkUtils.getLoginUserId; +import static cn.aagro.pp.framework.web.core.util.WebFrameworkUtils.getLoginUserType; + +@Tag(name = "用户 APP - 支付订单") +@RestController +@RequestMapping("/pay/order") +@Validated +@Slf4j +public class AppPayOrderController { + + @Resource + private PayOrderService payOrderService; + @Resource + private PayWalletService payWalletService; + + @GetMapping("/get") + @Operation(summary = "获得支付订单") + @Parameters({ + @Parameter(name = "id", description = "编号", required = true, example = "1024"), + @Parameter(name = "sync", description = "是否同步", example = "true") + }) + public CommonResult getOrder(@RequestParam("id") Long id, + @RequestParam(value = "sync", required = false) Boolean sync) { + PayOrderDO order = payOrderService.getOrder(id); + if (order== null) { + return success(null); + } + // 重要:校验订单是否是当前用户,避免越权 + if (order.getUserId() != null // 特殊:早期订单未存储 userId,所以忽略 + && ObjUtil.notEqual(order.getUserId(), getLoginUserId())) { + return success(null); + } + + // sync 仅在等待支付 + if (Boolean.TRUE.equals(sync) && PayOrderStatusEnum.isWaiting(order.getStatus())) { + payOrderService.syncOrderQuietly(order.getId()); + // 重新查询,因为同步后,可能会有变化 + order = payOrderService.getOrder(id); + } + return success(BeanUtils.toBean(order, PayOrderRespVO.class)); + } + + @PostMapping("/submit") + @Operation(summary = "提交支付订单") + public CommonResult submitPayOrder(@RequestBody AppPayOrderSubmitReqVO reqVO) { + // 1. 钱包支付事,需要额外传 user_id 和 user_type + if (Objects.equals(reqVO.getChannelCode(), PayChannelEnum.WALLET.getCode())) { + if (reqVO.getChannelExtras() == null) { + reqVO.setChannelExtras(Maps.newHashMapWithExpectedSize(1)); + } + PayWalletDO wallet = payWalletService.getOrCreateWallet(getLoginUserId(), getLoginUserType()); + reqVO.getChannelExtras().put(WalletPayClient.WALLET_ID_KEY, String.valueOf(wallet.getId())); + } + + // 2. 提交支付 + PayOrderSubmitRespVO respVO = payOrderService.submitOrder(reqVO, getClientIP()); + return success(BeanUtils.toBean(respVO, AppPayOrderSubmitRespVO.class)); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/order/vo/AppPayOrderSubmitReqVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/order/vo/AppPayOrderSubmitReqVO.java new file mode 100644 index 0000000..bc90ab8 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/order/vo/AppPayOrderSubmitReqVO.java @@ -0,0 +1,14 @@ +package cn.aagro.pp.module.pay.controller.app.order.vo; + +import cn.aagro.pp.module.pay.controller.admin.order.vo.PayOrderSubmitReqVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.Map; + +@Schema(description = "用户 APP - 支付订单提交 Request VO") +@Data +public class AppPayOrderSubmitReqVO extends PayOrderSubmitReqVO { +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/order/vo/AppPayOrderSubmitRespVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/order/vo/AppPayOrderSubmitRespVO.java new file mode 100644 index 0000000..d63e36b --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/order/vo/AppPayOrderSubmitRespVO.java @@ -0,0 +1,14 @@ +package cn.aagro.pp.module.pay.controller.app.order.vo; + +import cn.aagro.pp.module.pay.controller.admin.order.vo.PayOrderSubmitRespVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Schema(description = "用户 APP - 支付订单提交 Response VO") +@Data +public class AppPayOrderSubmitRespVO extends PayOrderSubmitRespVO { + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/transfer/AppPayTransferController.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/transfer/AppPayTransferController.java new file mode 100644 index 0000000..5fa8155 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/transfer/AppPayTransferController.java @@ -0,0 +1,37 @@ +package cn.aagro.pp.module.pay.controller.app.transfer; + +import cn.aagro.pp.framework.common.pojo.CommonResult; +import cn.aagro.pp.module.pay.service.transfer.PayTransferService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; + +import static cn.aagro.pp.framework.common.pojo.CommonResult.success; + +@Tag(name = "用户 APP - 转账单") +@RestController +@RequestMapping("/pay/transfer") +@Validated +@Slf4j +public class AppPayTransferController { + + @Resource + private PayTransferService transferService; + + @GetMapping("/sync") + @Operation(summary = "同步转账单") // 目的:解决微信转账的异步回调可能有延迟的问题 + @Parameter(name = "id", description = "编号", required = true, example = "1024") + public CommonResult syncTransfer(@RequestParam("id") Long id) { + transferService.syncTransfer(id); + return success(true); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/wallet/AppPayWalletController.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/wallet/AppPayWalletController.java new file mode 100644 index 0000000..b5dfab9 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/wallet/AppPayWalletController.java @@ -0,0 +1,42 @@ +package cn.aagro.pp.module.pay.controller.app.wallet; + +import cn.aagro.pp.framework.common.enums.UserTypeEnum; +import cn.aagro.pp.framework.common.pojo.CommonResult; +import cn.aagro.pp.module.pay.controller.app.wallet.vo.wallet.AppPayWalletRespVO; +import cn.aagro.pp.module.pay.convert.wallet.PayWalletConvert; +import cn.aagro.pp.module.pay.dal.dataobject.wallet.PayWalletDO; +import cn.aagro.pp.module.pay.service.wallet.PayWalletService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +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 static cn.aagro.pp.framework.common.pojo.CommonResult.success; +import static cn.aagro.pp.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +/** + * @author jason + */ +@Tag(name = "用户 APP - 钱包") +@RestController +@RequestMapping("/pay/wallet") +@Validated +@Slf4j +public class AppPayWalletController { + + @Resource + private PayWalletService payWalletService; + + @GetMapping("/get") + @Operation(summary = "获取钱包") + public CommonResult getPayWallet() { + PayWalletDO wallet = payWalletService.getOrCreateWallet(getLoginUserId(), UserTypeEnum.MEMBER.getValue()); + return success(PayWalletConvert.INSTANCE.convert(wallet)); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/wallet/AppPayWalletRechargeController.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/wallet/AppPayWalletRechargeController.java new file mode 100644 index 0000000..cd52338 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/wallet/AppPayWalletRechargeController.java @@ -0,0 +1,72 @@ +package cn.aagro.pp.module.pay.controller.app.wallet; + +import cn.hutool.core.collection.CollUtil; +import cn.aagro.pp.framework.common.enums.UserTypeEnum; +import cn.aagro.pp.framework.common.pojo.CommonResult; +import cn.aagro.pp.framework.common.pojo.PageParam; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.pay.controller.app.wallet.vo.recharge.AppPayWalletRechargeCreateReqVO; +import cn.aagro.pp.module.pay.controller.app.wallet.vo.recharge.AppPayWalletRechargeCreateRespVO; +import cn.aagro.pp.module.pay.controller.app.wallet.vo.recharge.AppPayWalletRechargeRespVO; +import cn.aagro.pp.module.pay.convert.wallet.PayWalletConvert; +import cn.aagro.pp.module.pay.convert.wallet.PayWalletRechargeConvert; +import cn.aagro.pp.module.pay.dal.dataobject.order.PayOrderDO; +import cn.aagro.pp.module.pay.dal.dataobject.wallet.PayWalletRechargeDO; +import cn.aagro.pp.module.pay.service.order.PayOrderService; +import cn.aagro.pp.module.pay.service.wallet.PayWalletRechargeService; +import com.google.common.collect.Lists; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.List; + +import static cn.aagro.pp.framework.common.pojo.CommonResult.success; +import static cn.aagro.pp.framework.common.util.collection.CollectionUtils.convertList; +import static cn.aagro.pp.framework.common.util.servlet.ServletUtils.getClientIP; +import static cn.aagro.pp.framework.web.core.util.WebFrameworkUtils.getLoginUserId; +import static cn.aagro.pp.framework.web.core.util.WebFrameworkUtils.getLoginUserType; + +@Tag(name = "用户 APP - 钱包充值") +@RestController +@RequestMapping("/pay/wallet-recharge") +@Validated +@Slf4j +public class AppPayWalletRechargeController { + + @Resource + private PayWalletRechargeService walletRechargeService; + @Resource + private PayOrderService payOrderService; + + @PostMapping("/create") + @Operation(summary = "创建钱包充值记录(发起充值)") + public CommonResult createWalletRecharge( + @Valid @RequestBody AppPayWalletRechargeCreateReqVO reqVO) { + PayWalletRechargeDO walletRecharge = walletRechargeService.createWalletRecharge( + getLoginUserId(), getLoginUserType(), getClientIP(), reqVO); + return success(PayWalletRechargeConvert.INSTANCE.convert(walletRecharge)); + } + + @GetMapping("/page") + @Operation(summary = "获得钱包充值记录分页") + public CommonResult> getWalletRechargePage(@Valid PageParam pageReqVO) { + PageResult pageResult = walletRechargeService.getWalletRechargePackagePage( + getLoginUserId(), UserTypeEnum.MEMBER.getValue(), pageReqVO, true); + if (CollUtil.isEmpty(pageResult.getList())) { + return success(PageResult.empty(pageResult.getTotal())); + } + // 拼接数据 + List payOrderList = payOrderService.getOrderList( + convertList(pageResult.getList(), PayWalletRechargeDO::getPayOrderId)); + return success(PayWalletRechargeConvert.INSTANCE.convertPage(pageResult, payOrderList)); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/wallet/AppPayWalletRechargePackageController.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/wallet/AppPayWalletRechargePackageController.java new file mode 100644 index 0000000..9b23f12 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/wallet/AppPayWalletRechargePackageController.java @@ -0,0 +1,42 @@ +package cn.aagro.pp.module.pay.controller.app.wallet; + +import cn.aagro.pp.framework.common.enums.CommonStatusEnum; +import cn.aagro.pp.framework.common.pojo.CommonResult; +import cn.aagro.pp.framework.common.util.object.BeanUtils; +import cn.aagro.pp.module.pay.controller.app.wallet.vo.recharge.AppPayWalletPackageRespVO; +import cn.aagro.pp.module.pay.dal.dataobject.wallet.PayWalletRechargePackageDO; +import cn.aagro.pp.module.pay.service.wallet.PayWalletRechargePackageService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +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.Comparator; +import java.util.List; + +import static cn.aagro.pp.framework.common.pojo.CommonResult.success; + +@Tag(name = "用户 APP - 钱包充值套餐") +@RestController +@RequestMapping("/pay/wallet-recharge-package") +@Validated +@Slf4j +public class AppPayWalletRechargePackageController { + + @Resource + private PayWalletRechargePackageService walletRechargePackageService; + + @GetMapping("/list") + @Operation(summary = "获得钱包充值套餐列表") + public CommonResult> getWalletRechargePackageList() { + List list = walletRechargePackageService.getWalletRechargePackageList( + CommonStatusEnum.ENABLE.getStatus()); + list.sort(Comparator.comparingInt(PayWalletRechargePackageDO::getPayPrice)); + return success(BeanUtils.toBean(list, AppPayWalletPackageRespVO.class)); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/wallet/AppPayWalletTransactionController.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/wallet/AppPayWalletTransactionController.java new file mode 100644 index 0000000..8bb0976 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/wallet/AppPayWalletTransactionController.java @@ -0,0 +1,60 @@ +package cn.aagro.pp.module.pay.controller.app.wallet; + +import cn.aagro.pp.framework.common.enums.UserTypeEnum; +import cn.aagro.pp.framework.common.pojo.CommonResult; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.common.util.object.BeanUtils; +import cn.aagro.pp.module.pay.controller.app.wallet.vo.transaction.AppPayWalletTransactionPageReqVO; +import cn.aagro.pp.module.pay.controller.app.wallet.vo.transaction.AppPayWalletTransactionRespVO; +import cn.aagro.pp.module.pay.controller.app.wallet.vo.transaction.AppPayWalletTransactionSummaryRespVO; +import cn.aagro.pp.module.pay.dal.dataobject.wallet.PayWalletTransactionDO; +import cn.aagro.pp.module.pay.service.wallet.PayWalletTransactionService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.format.annotation.DateTimeFormat; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.time.LocalDateTime; + +import static cn.aagro.pp.framework.common.pojo.CommonResult.success; +import static cn.aagro.pp.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; +import static cn.aagro.pp.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "用户 APP - 钱包余额明细") +@RestController +@RequestMapping("/pay/wallet-transaction") +@Validated +@Slf4j +public class AppPayWalletTransactionController { + + @Resource + private PayWalletTransactionService payWalletTransactionService; + + @GetMapping("/page") + @Operation(summary = "获得钱包流水分页") + public CommonResult> getWalletTransactionPage( + @Valid AppPayWalletTransactionPageReqVO pageReqVO) { + PageResult pageResult = payWalletTransactionService.getWalletTransactionPage( + getLoginUserId(), UserTypeEnum.MEMBER.getValue(), pageReqVO); + return success(BeanUtils.toBean(pageResult, AppPayWalletTransactionRespVO.class)); + } + + @GetMapping("/get-summary") + @Operation(summary = "获得钱包流水统计") + @Parameter(name = "times", description = "时间段", required = true) + public CommonResult getWalletTransactionSummary( + @RequestParam("createTime") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) LocalDateTime[] createTime) { + AppPayWalletTransactionSummaryRespVO summary = payWalletTransactionService.getWalletTransactionSummary( + getLoginUserId(), UserTypeEnum.MEMBER.getValue(), createTime); + return success(summary); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/wallet/vo/recharge/AppPayWalletPackageRespVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/wallet/vo/recharge/AppPayWalletPackageRespVO.java new file mode 100644 index 0000000..f161d69 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/wallet/vo/recharge/AppPayWalletPackageRespVO.java @@ -0,0 +1,20 @@ +package cn.aagro.pp.module.pay.controller.app.wallet.vo.recharge; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "用户 APP - 用户充值套餐 Response VO") +@Data +public class AppPayWalletPackageRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + @Schema(description = "套餐名", requiredMode = Schema.RequiredMode.REQUIRED, example = "小套餐") + private String name; + + @Schema(description = "支付金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Integer payPrice; + @Schema(description = "赠送金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "20") + private Integer bonusPrice; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/wallet/vo/recharge/AppPayWalletRechargeCreateReqVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/wallet/vo/recharge/AppPayWalletRechargeCreateReqVO.java new file mode 100644 index 0000000..79a48c3 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/wallet/vo/recharge/AppPayWalletRechargeCreateReqVO.java @@ -0,0 +1,26 @@ +package cn.aagro.pp.module.pay.controller.app.wallet.vo.recharge; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.AssertTrue; +import javax.validation.constraints.Min; +import java.util.Objects; + +@Schema(description = "用户 APP - 创建钱包充值 Request VO") +@Data +public class AppPayWalletRechargeCreateReqVO { + + @Schema(description = "支付金额", example = "1000") + @Min(value = 1, message = "支付金额必须大于零") + private Integer payPrice; + + @Schema(description = "充值套餐编号", example = "1024") + private Long packageId; + + @AssertTrue(message = "充值金额和充钱套餐不能同时为空") + public boolean isValidPayPriceAndPackageId() { + return Objects.nonNull(payPrice) || Objects.nonNull(packageId); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/wallet/vo/recharge/AppPayWalletRechargeCreateRespVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/wallet/vo/recharge/AppPayWalletRechargeCreateRespVO.java new file mode 100644 index 0000000..edf8a9b --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/wallet/vo/recharge/AppPayWalletRechargeCreateRespVO.java @@ -0,0 +1,16 @@ +package cn.aagro.pp.module.pay.controller.app.wallet.vo.recharge; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "用户 APP - 创建钱包充值 Resp VO") +@Data +public class AppPayWalletRechargeCreateRespVO { + + @Schema(description = "钱包充值编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "支付订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Long payOrderId; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/wallet/vo/recharge/AppPayWalletRechargeRespVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/wallet/vo/recharge/AppPayWalletRechargeRespVO.java new file mode 100644 index 0000000..e574e79 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/wallet/vo/recharge/AppPayWalletRechargeRespVO.java @@ -0,0 +1,42 @@ +package cn.aagro.pp.module.pay.controller.app.wallet.vo.recharge; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "用户 APP - 钱包充值记录 Resp VO") +@Data +public class AppPayWalletRechargeRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "用户实际到账余额", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer totalPrice; + + @Schema(description = "实际支付金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "20") + private Integer payPrice; + + @Schema(description = "钱包赠送金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "80") + private Integer bonusPrice; + + @Schema(description = "支付成功的支付渠道", requiredMode = Schema.RequiredMode.REQUIRED) + private String payChannelCode; + + @Schema(description = "支付渠道名", example = "微信小程序支付") + private String payChannelName; + + @Schema(description = "支付订单编号", requiredMode = Schema.RequiredMode.REQUIRED) + private Long payOrderId; + + @Schema(description = "支付成功的外部订单号", requiredMode = Schema.RequiredMode.REQUIRED) + private String payOrderChannelOrderNo; // 从 PayOrderDO 的 channelOrderNo 字段 + + @Schema(description = "订单支付时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime payTime; + + @Schema(description = "退款状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + private Integer refundStatus; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/wallet/vo/transaction/AppPayWalletTransactionPageReqVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/wallet/vo/transaction/AppPayWalletTransactionPageReqVO.java new file mode 100644 index 0000000..f537444 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/wallet/vo/transaction/AppPayWalletTransactionPageReqVO.java @@ -0,0 +1,31 @@ +package cn.aagro.pp.module.pay.controller.app.wallet.vo.transaction; + +import cn.aagro.pp.framework.common.pojo.PageParam; +import cn.aagro.pp.framework.common.util.date.DateUtils; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +@Schema(description = "用户 APP - 钱包流水分页 Request VO") +@Data +public class AppPayWalletTransactionPageReqVO extends PageParam { + + /** + * 类型 - 收入 + */ + public static final Integer TYPE_INCOME = 1; + /** + * 类型 - 支出 + */ + public static final Integer TYPE_EXPENSE = 2; + + @Schema(description = "类型", example = "1") + private Integer type; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/wallet/vo/transaction/AppPayWalletTransactionRespVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/wallet/vo/transaction/AppPayWalletTransactionRespVO.java new file mode 100644 index 0000000..50a7ed1 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/wallet/vo/transaction/AppPayWalletTransactionRespVO.java @@ -0,0 +1,24 @@ +package cn.aagro.pp.module.pay.controller.app.wallet.vo.transaction; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "用户 APP - 钱包流水分页 Response VO") +@Data +public class AppPayWalletTransactionRespVO { + + @Schema(description = "业务分类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer bizType; + + @Schema(description = "交易金额,单位分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Long price; + + @Schema(description = "流水标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "土豆土豆") + private String title; + + @Schema(description = "交易时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/wallet/vo/transaction/AppPayWalletTransactionSummaryRespVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/wallet/vo/transaction/AppPayWalletTransactionSummaryRespVO.java new file mode 100644 index 0000000..9def0ed --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/wallet/vo/transaction/AppPayWalletTransactionSummaryRespVO.java @@ -0,0 +1,16 @@ +package cn.aagro.pp.module.pay.controller.app.wallet.vo.transaction; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "用户 APP - 钱包流水统计 Request VO") +@Data +public class AppPayWalletTransactionSummaryRespVO { + + @Schema(description = "累计支出,单位分", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000") + private Integer totalExpense; + + @Schema(description = "累计收入,单位分", requiredMode = Schema.RequiredMode.REQUIRED, example = "2000") + private Integer totalIncome; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/wallet/vo/wallet/AppPayWalletRespVO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/wallet/vo/wallet/AppPayWalletRespVO.java new file mode 100644 index 0000000..2de67f0 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/app/wallet/vo/wallet/AppPayWalletRespVO.java @@ -0,0 +1,19 @@ +package cn.aagro.pp.module.pay.controller.app.wallet.vo.wallet; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "用户 APP - 用户钱包 Response VO") +@Data +public class AppPayWalletRespVO { + + @Schema(description = "钱包余额,单位分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer balance; + + @Schema(description = "累计支出,单位分", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000") + private Integer totalExpense; + + @Schema(description = "累计充值,单位分", requiredMode = Schema.RequiredMode.REQUIRED, example = "2000") + private Integer totalRecharge; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/package-info.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/package-info.java new file mode 100644 index 0000000..a4ba724 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/controller/package-info.java @@ -0,0 +1,6 @@ +/** + * 提供 RESTful API 给前端: + * 1. admin 包:提供给管理后台 aagro-ui-admin 前端项目 + * 2. app 包:提供给用户 APP aagro-ui-app 前端项目,它的 Controller 和 VO 都要添加 App 前缀,用于和管理后台进行区分 + */ +package cn.aagro.pp.module.pay.controller; diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/convert/app/PayAppConvert.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/convert/app/PayAppConvert.java new file mode 100644 index 0000000..8cb559c --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/convert/app/PayAppConvert.java @@ -0,0 +1,49 @@ +package cn.aagro.pp.module.pay.convert.app; + +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.common.util.collection.CollectionUtils; +import cn.aagro.pp.module.pay.controller.admin.app.vo.PayAppCreateReqVO; +import cn.aagro.pp.module.pay.controller.admin.app.vo.PayAppPageItemRespVO; +import cn.aagro.pp.module.pay.controller.admin.app.vo.PayAppRespVO; +import cn.aagro.pp.module.pay.controller.admin.app.vo.PayAppUpdateReqVO; +import cn.aagro.pp.module.pay.dal.dataobject.app.PayAppDO; +import cn.aagro.pp.module.pay.dal.dataobject.channel.PayChannelDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; + +/** + * 支付应用信息 Convert + * + * @author 芋艿 + */ +@Mapper +public interface PayAppConvert { + + PayAppConvert INSTANCE = Mappers.getMapper(PayAppConvert.class); + + PayAppPageItemRespVO pageConvert (PayAppDO bean); + + PayAppDO convert(PayAppCreateReqVO bean); + + PayAppDO convert(PayAppUpdateReqVO bean); + + PayAppRespVO convert(PayAppDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + + default PageResult convertPage(PageResult pageResult, List channels) { + PageResult voPageResult = convertPage(pageResult); + // 处理 channel 关系 + Map> appIdChannelMap = CollectionUtils.convertMultiMap2(channels, PayChannelDO::getAppId, PayChannelDO::getCode); + voPageResult.getList().forEach(app -> app.setChannelCodes(appIdChannelMap.get(app.getId()))); + return voPageResult; + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/convert/channel/PayChannelConvert.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/convert/channel/PayChannelConvert.java new file mode 100644 index 0000000..d14f722 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/convert/channel/PayChannelConvert.java @@ -0,0 +1,28 @@ +package cn.aagro.pp.module.pay.convert.channel; + +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.pay.controller.admin.channel.vo.PayChannelCreateReqVO; +import cn.aagro.pp.module.pay.controller.admin.channel.vo.PayChannelRespVO; +import cn.aagro.pp.module.pay.controller.admin.channel.vo.PayChannelUpdateReqVO; +import cn.aagro.pp.module.pay.dal.dataobject.channel.PayChannelDO; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface PayChannelConvert { + + PayChannelConvert INSTANCE = Mappers.getMapper(PayChannelConvert.class); + + @Mapping(target = "config",ignore = true) + PayChannelDO convert(PayChannelCreateReqVO bean); + + @Mapping(target = "config",ignore = true) + PayChannelDO convert(PayChannelUpdateReqVO bean); + + @Mapping(target = "config",expression = "java(cn.aagro.pp.framework.common.util.json.JsonUtils.toJsonString(bean.getConfig()))") + PayChannelRespVO convert(PayChannelDO bean); + + PageResult convertPage(PageResult page); + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/convert/order/PayOrderConvert.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/convert/order/PayOrderConvert.java new file mode 100644 index 0000000..da4c1b9 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/convert/order/PayOrderConvert.java @@ -0,0 +1,71 @@ +package cn.aagro.pp.module.pay.convert.order; + +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.common.util.collection.CollectionUtils; +import cn.aagro.pp.framework.common.util.collection.MapUtils; +import cn.aagro.pp.module.pay.api.order.dto.PayOrderCreateReqDTO; +import cn.aagro.pp.module.pay.api.order.dto.PayOrderRespDTO; +import cn.aagro.pp.module.pay.controller.admin.order.vo.*; +import cn.aagro.pp.module.pay.dal.dataobject.app.PayAppDO; +import cn.aagro.pp.module.pay.dal.dataobject.order.PayOrderDO; +import cn.aagro.pp.module.pay.dal.dataobject.order.PayOrderExtensionDO; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +import java.util.List; +import java.util.Map; + +/** + * 支付订单 Convert + * + * @author aquan + */ +@Mapper +public interface PayOrderConvert { + + PayOrderConvert INSTANCE = Mappers.getMapper(PayOrderConvert.class); + + PayOrderRespVO convert(PayOrderDO bean); + + PayOrderRespDTO convert2(PayOrderDO order); + + default PayOrderDetailsRespVO convert(PayOrderDO order, PayOrderExtensionDO orderExtension, PayAppDO app) { + PayOrderDetailsRespVO respVO = convertDetail(order); + respVO.setExtension(convert(orderExtension)); + if (app != null) { + respVO.setAppName(app.getName()); + } + return respVO; + } + PayOrderDetailsRespVO convertDetail(PayOrderDO bean); + PayOrderDetailsRespVO.PayOrderExtension convert(PayOrderExtensionDO bean); + + default PageResult convertPage(PageResult page, Map appMap) { + PageResult result = convertPage(page); + result.getList().forEach(order -> MapUtils.findAndThen(appMap, order.getAppId(), app -> order.setAppName(app.getName()))); + return result; + } + PageResult convertPage(PageResult page); + + default List convertList(List list, Map appMap) { + return CollectionUtils.convertList(list, order -> { + PayOrderExcelVO excelVO = convertExcel(order); + MapUtils.findAndThen(appMap, order.getAppId(), app -> excelVO.setAppName(app.getName())); + return excelVO; + }); + } + PayOrderExcelVO convertExcel(PayOrderDO bean); + + PayOrderDO convert(PayOrderCreateReqDTO bean); + + @Mapping(target = "id", ignore = true) + PayOrderExtensionDO convert(PayOrderSubmitReqVO bean, String userIp); + + PayOrderUnifiedReqDTO convert2(PayOrderSubmitReqVO reqVO, String userIp); + + @Mapping(source = "order.status", target = "status") + PayOrderSubmitRespVO convert(PayOrderDO order, cn.aagro.pp.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO respDTO); + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/convert/package-info.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/convert/package-info.java new file mode 100644 index 0000000..2d993aa --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/convert/package-info.java @@ -0,0 +1,6 @@ +/** + * 提供 POJO 类的实体转换 + * + * 目前使用 MapStruct 框架 + */ +package cn.aagro.pp.module.pay.convert; diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/convert/refund/PayRefundConvert.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/convert/refund/PayRefundConvert.java new file mode 100644 index 0000000..2f98d43 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/convert/refund/PayRefundConvert.java @@ -0,0 +1,53 @@ +package cn.aagro.pp.module.pay.convert.refund; + +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.common.util.collection.CollectionUtils; +import cn.aagro.pp.framework.common.util.collection.MapUtils; +import cn.aagro.pp.module.pay.api.refund.dto.PayRefundCreateReqDTO; +import cn.aagro.pp.module.pay.controller.admin.refund.vo.PayRefundDetailsRespVO; +import cn.aagro.pp.module.pay.controller.admin.refund.vo.PayRefundExcelVO; +import cn.aagro.pp.module.pay.controller.admin.refund.vo.PayRefundPageItemRespVO; +import cn.aagro.pp.module.pay.dal.dataobject.app.PayAppDO; +import cn.aagro.pp.module.pay.dal.dataobject.order.PayOrderDO; +import cn.aagro.pp.module.pay.dal.dataobject.refund.PayRefundDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; +import java.util.Map; + +@Mapper +public interface PayRefundConvert { + + PayRefundConvert INSTANCE = Mappers.getMapper(PayRefundConvert.class); + + + default PayRefundDetailsRespVO convert(PayRefundDO refund, PayAppDO app) { + PayRefundDetailsRespVO respVO = convert(refund); + if (app != null) { + respVO.setAppName(app.getName()); + } + return respVO; + } + PayRefundDetailsRespVO convert(PayRefundDO bean); + PayRefundDetailsRespVO.Order convert(PayOrderDO bean); + + default PageResult convertPage(PageResult page, Map appMap) { + PageResult result = convertPage(page); + result.getList().forEach(order -> MapUtils.findAndThen(appMap, order.getAppId(), app -> order.setAppName(app.getName()))); + return result; + } + PageResult convertPage(PageResult page); + + PayRefundDO convert(PayRefundCreateReqDTO bean); + + default List convertList(List list, Map appMap) { + return CollectionUtils.convertList(list, order -> { + PayRefundExcelVO excelVO = convertExcel(order); + MapUtils.findAndThen(appMap, order.getAppId(), app -> excelVO.setAppName(app.getName())); + return excelVO; + }); + } + PayRefundExcelVO convertExcel(PayRefundDO bean); + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/convert/wallet/PayWalletConvert.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/convert/wallet/PayWalletConvert.java new file mode 100644 index 0000000..16c4ef2 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/convert/wallet/PayWalletConvert.java @@ -0,0 +1,21 @@ +package cn.aagro.pp.module.pay.convert.wallet; + +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.pay.controller.admin.wallet.vo.wallet.PayWalletRespVO; +import cn.aagro.pp.module.pay.controller.app.wallet.vo.wallet.AppPayWalletRespVO; +import cn.aagro.pp.module.pay.dal.dataobject.wallet.PayWalletDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface PayWalletConvert { + + PayWalletConvert INSTANCE = Mappers.getMapper(PayWalletConvert.class); + + AppPayWalletRespVO convert(PayWalletDO bean); + + PayWalletRespVO convert02(PayWalletDO bean); + + PageResult convertPage(PageResult page); + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/convert/wallet/PayWalletRechargeConvert.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/convert/wallet/PayWalletRechargeConvert.java new file mode 100644 index 0000000..dffd249 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/convert/wallet/PayWalletRechargeConvert.java @@ -0,0 +1,43 @@ +package cn.aagro.pp.module.pay.convert.wallet; + +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.common.util.collection.CollectionUtils; +import cn.aagro.pp.framework.common.util.collection.MapUtils; +import cn.aagro.pp.framework.common.util.object.BeanUtils; +import cn.aagro.pp.framework.dict.core.DictFrameworkUtils; +import cn.aagro.pp.module.pay.controller.app.wallet.vo.recharge.AppPayWalletRechargeCreateRespVO; +import cn.aagro.pp.module.pay.controller.app.wallet.vo.recharge.AppPayWalletRechargeRespVO; +import cn.aagro.pp.module.pay.dal.dataobject.order.PayOrderDO; +import cn.aagro.pp.module.pay.dal.dataobject.wallet.PayWalletRechargeDO; +import cn.aagro.pp.module.pay.enums.DictTypeConstants; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +import java.util.List; +import java.util.Map; + +@Mapper +public interface PayWalletRechargeConvert { + + PayWalletRechargeConvert INSTANCE = Mappers.getMapper(PayWalletRechargeConvert.class); + + @Mapping(target = "totalPrice", expression = "java( payPrice + bonusPrice)") + PayWalletRechargeDO convert(Long walletId, Integer payPrice, Integer bonusPrice, Long packageId); + + AppPayWalletRechargeCreateRespVO convert(PayWalletRechargeDO bean); + + default PageResult convertPage(PageResult pageResult, + List payOrderList) { + PageResult voPageResult = BeanUtils.toBean(pageResult, AppPayWalletRechargeRespVO.class); + Map payOrderMap = CollectionUtils.convertMap(payOrderList, PayOrderDO::getId); + voPageResult.getList().forEach(recharge -> { + recharge.setPayChannelName(DictFrameworkUtils.parseDictDataLabel( + DictTypeConstants.CHANNEL_CODE, recharge.getPayChannelCode())); + MapUtils.findAndThen(payOrderMap, recharge.getPayOrderId(), + order -> recharge.setPayOrderChannelOrderNo(order.getChannelOrderNo())); + }); + return voPageResult; + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/convert/wallet/PayWalletRechargePackageConvert.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/convert/wallet/PayWalletRechargePackageConvert.java new file mode 100644 index 0000000..94e40a5 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/convert/wallet/PayWalletRechargePackageConvert.java @@ -0,0 +1,29 @@ +package cn.aagro.pp.module.pay.convert.wallet; + +import java.util.*; + +import cn.aagro.pp.framework.common.pojo.PageResult; + +import cn.aagro.pp.module.pay.controller.admin.wallet.vo.rechargepackage.WalletRechargePackageCreateReqVO; +import cn.aagro.pp.module.pay.controller.admin.wallet.vo.rechargepackage.WalletRechargePackageRespVO; +import cn.aagro.pp.module.pay.controller.admin.wallet.vo.rechargepackage.WalletRechargePackageUpdateReqVO; +import cn.aagro.pp.module.pay.dal.dataobject.wallet.PayWalletRechargePackageDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface PayWalletRechargePackageConvert { + + PayWalletRechargePackageConvert INSTANCE = Mappers.getMapper(PayWalletRechargePackageConvert.class); + + PayWalletRechargePackageDO convert(WalletRechargePackageCreateReqVO bean); + + PayWalletRechargePackageDO convert(WalletRechargePackageUpdateReqVO bean); + + WalletRechargePackageRespVO convert(PayWalletRechargePackageDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/convert/wallet/PayWalletTransactionConvert.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/convert/wallet/PayWalletTransactionConvert.java new file mode 100644 index 0000000..7de4602 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/convert/wallet/PayWalletTransactionConvert.java @@ -0,0 +1,20 @@ +package cn.aagro.pp.module.pay.convert.wallet; + +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.pay.controller.admin.wallet.vo.transaction.PayWalletTransactionRespVO; +import cn.aagro.pp.module.pay.controller.app.wallet.vo.transaction.AppPayWalletTransactionRespVO; +import cn.aagro.pp.module.pay.dal.dataobject.wallet.PayWalletTransactionDO; +import cn.aagro.pp.module.pay.service.wallet.bo.WalletTransactionCreateReqBO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface PayWalletTransactionConvert { + + PayWalletTransactionConvert INSTANCE = Mappers.getMapper(PayWalletTransactionConvert.class); + + PageResult convertPage2(PageResult page); + + PayWalletTransactionDO convert(WalletTransactionCreateReqBO bean); + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/convert/《芋道 Spring Boot 对象转换 MapStruct 入门》.md b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/convert/《芋道 Spring Boot 对象转换 MapStruct 入门》.md new file mode 100644 index 0000000..a209217 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/convert/《芋道 Spring Boot 对象转换 MapStruct 入门》.md @@ -0,0 +1 @@ + diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/dataobject/app/PayAppDO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/dataobject/app/PayAppDO.java new file mode 100644 index 0000000..9599cf0 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/dataobject/app/PayAppDO.java @@ -0,0 +1,66 @@ +package cn.aagro.pp.module.pay.dal.dataobject.app; + +import cn.aagro.pp.framework.common.enums.CommonStatusEnum; +import cn.aagro.pp.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * 支付应用 DO + * 一个商户下,可能会有多个支付应用。例如说,京东有京东商城、京东到家等等 + * 不过一般来说,一个商户,只有一个应用哈~ + * + * 即 PayMerchantDO : PayAppDO = 1 : n + * + * @author 芋道源码 + */ +@TableName("pay_app") +@KeySequence("pay_app_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PayAppDO extends BaseDO { + + /** + * 应用编号,数据库自增 + */ + @TableId + private Long id; + /** + * 应用标识 + */ + private String appKey; + /** + * 应用名 + */ + private String name; + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 备注 + */ + private String remark; + /** + * 支付结果的回调地址 + */ + private String orderNotifyUrl; + /** + * 退款结果的回调地址 + */ + private String refundNotifyUrl; + + /** + * 转账结果的回调地址 + */ + private String transferNotifyUrl; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/dataobject/channel/PayChannelDO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/dataobject/channel/PayChannelDO.java new file mode 100644 index 0000000..5a8e4af --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/dataobject/channel/PayChannelDO.java @@ -0,0 +1,116 @@ +package cn.aagro.pp.module.pay.dal.dataobject.channel; + +import cn.hutool.core.util.StrUtil; +import cn.aagro.pp.framework.common.enums.CommonStatusEnum; +import cn.aagro.pp.framework.common.util.json.JsonUtils; +import cn.aagro.pp.framework.tenant.core.db.TenantBaseDO; +import cn.aagro.pp.module.pay.dal.dataobject.app.PayAppDO; +import cn.aagro.pp.module.pay.enums.PayChannelEnum; +import cn.aagro.pp.module.pay.framework.pay.core.client.PayClientConfig; +import cn.aagro.pp.module.pay.framework.pay.core.client.impl.NonePayClientConfig; +import cn.aagro.pp.module.pay.framework.pay.core.client.impl.alipay.AlipayPayClientConfig; +import cn.aagro.pp.module.pay.framework.pay.core.client.impl.weixin.WxPayClientConfig; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.AbstractJsonTypeHandler; +import com.fasterxml.jackson.core.type.TypeReference; +import lombok.*; + +import java.lang.reflect.Field; + +/** + * 支付渠道 DO + * 一个应用下,会有多种支付渠道,例如说微信支付、支付宝支付等等 + * + * 即 PayAppDO : PayChannelDO = 1 : n + * + * @author 芋道源码 + */ +@TableName(value = "pay_channel", autoResultMap = true) +@KeySequence("pay_channel_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PayChannelDO extends TenantBaseDO { + + /** + * 渠道编号,数据库自增 + */ + private Long id; + /** + * 渠道编码 + * + * 枚举 {@link PayChannelEnum} + */ + private String code; + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 渠道费率,单位:百分比 + */ + private Double feeRate; + /** + * 备注 + */ + private String remark; + + /** + * 应用编号 + * + * 关联 {@link PayAppDO#getId()} + */ + private Long appId; + /** + * 支付渠道配置 + */ + @TableField(typeHandler = PayClientConfigTypeHandler.class) + private PayClientConfig config; + + public static class PayClientConfigTypeHandler extends AbstractJsonTypeHandler { + + public PayClientConfigTypeHandler(Class type) { + super(type); + } + + public PayClientConfigTypeHandler(Class type, Field field) { + super(type, field); + } + + @Override + public Object parse(String json) { + PayClientConfig config = JsonUtils.parseObjectQuietly(json, new TypeReference() {}); + if (config != null) { + return config; + } + + // 兼容老版本的包路径 + String className = JsonUtils.parseObject(json, "@class", String.class); + className = StrUtil.subAfter(className, ".", true); + switch (className) { + case "AlipayPayClientConfig": + return JsonUtils.parseObject2(json, AlipayPayClientConfig.class); + case "WxPayClientConfig": + return JsonUtils.parseObject2(json, WxPayClientConfig.class); + case "NonePayClientConfig": + return JsonUtils.parseObject2(json, NonePayClientConfig.class); + default: + throw new IllegalArgumentException("未知的 PayClientConfig 类型:" + json); + } + } + + @Override + public String toJson(Object obj) { + return JsonUtils.toJsonString(obj); + } + + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/dataobject/demo/PayDemoOrderDO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/dataobject/demo/PayDemoOrderDO.java new file mode 100644 index 0000000..6b479cf --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/dataobject/demo/PayDemoOrderDO.java @@ -0,0 +1,87 @@ +package cn.aagro.pp.module.pay.dal.dataobject.demo; + +import cn.aagro.pp.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.time.LocalDateTime; + +/** + * 示例订单 + * + * 演示业务系统的订单,如何接入 pay 系统的支付与退款 + * + * @author 芋道源码 + */ +@TableName("pay_demo_order") +@KeySequence("pay_demo_order_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PayDemoOrderDO extends BaseDO { + + /** + * 订单编号,自增 + */ + @TableId + private Long id; + /** + * 用户编号 + */ + private Long userId; + /** + * 商品编号 + */ + private Long spuId; + /** + * 商品名称 + */ + private String spuName; + /** + * 价格,单位:分 + */ + private Integer price; + + // ========== 支付相关字段 ========== + + /** + * 是否支付 + */ + private Boolean payStatus; + /** + * 支付订单编号 + * + * 对接 pay-module-biz 支付服务的支付订单编号,即 PayOrderDO 的 id 编号 + */ + private Long payOrderId; + /** + * 付款时间 + */ + private LocalDateTime payTime; + /** + * 支付渠道 + * + * 对应 PayChannelEnum 枚举 + */ + private String payChannelCode; + + // ========== 退款相关字段 ========== + /** + * 支付退款单号 + */ + private Long payRefundId; + /** + * 退款金额,单位:分 + */ + private Integer refundPrice; + /** + * 退款完成时间 + */ + private LocalDateTime refundTime; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/dataobject/demo/PayDemoWithdrawDO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/dataobject/demo/PayDemoWithdrawDO.java new file mode 100644 index 0000000..aed8a03 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/dataobject/demo/PayDemoWithdrawDO.java @@ -0,0 +1,84 @@ +package cn.aagro.pp.module.pay.dal.dataobject.demo; + +import cn.aagro.pp.framework.mybatis.core.dataobject.BaseDO; +import cn.aagro.pp.module.pay.dal.dataobject.transfer.PayTransferDO; +import cn.aagro.pp.module.pay.enums.demo.PayDemoWithdrawStatusEnum; +import cn.aagro.pp.module.pay.enums.demo.PayDemoWithdrawTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 示例提现订单 + * + * 演示业务系统的转账业务 + */ +@TableName(value ="pay_demo_withdraw", autoResultMap = true) +@KeySequence("pay_demo_withdraw_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +public class PayDemoWithdrawDO extends BaseDO { + + /** + * 提现单编号,自增 + */ + @TableId + private Long id; + + /** + * 提现标题 + */ + private String subject; + /** + * 提现金额,单位:分 + */ + private Integer price; + + /** + * 收款人账号 + */ + private String userAccount; + /** + * 收款人姓名 + */ + private String userName; + + /** + * 提现方式 + * + * 枚举 {@link PayDemoWithdrawTypeEnum} + */ + private Integer type; + /** + * 提现状态 + * + * 枚举 {@link PayDemoWithdrawStatusEnum} + */ + private Integer status; + + // ========== 转账相关字段 ========== + + /** + * 转账单编号 + * + * 关联 {@link PayTransferDO#getId()} + */ + private Long payTransferId; + /** + * 转账渠道 + * + * 枚举 {@link cn.aagro.pp.module.pay.enums.PayChannelEnum} + */ + private String transferChannelCode; + /** + * 转账成功时间 + */ + private LocalDateTime transferTime; + /** + * 转账错误提示 + */ + private String transferErrorMsg; + +} \ No newline at end of file diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/dataobject/notify/PayNotifyLogDO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/dataobject/notify/PayNotifyLogDO.java new file mode 100644 index 0000000..b625352 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/dataobject/notify/PayNotifyLogDO.java @@ -0,0 +1,51 @@ +package cn.aagro.pp.module.pay.dal.dataobject.notify; + +import cn.aagro.pp.module.pay.enums.notify.PayNotifyStatusEnum; +import cn.aagro.pp.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * 商户支付、退款等的通知 Log + * 每次通知时,都会在该表中,记录一次 Log,方便排查问题 + * + * @author 芋道源码 + */ +@TableName("pay_notify_log") +@KeySequence("pay_notify_log_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PayNotifyLogDO extends BaseDO { + + /** + * 日志编号,自增 + */ + private Long id; + /** + * 通知任务编号 + * + * 关联 {@link PayNotifyTaskDO#getId()} + */ + private Long taskId; + /** + * 第几次被通知 + * + * 对应到 {@link PayNotifyTaskDO#getNotifyTimes()} + */ + private Integer notifyTimes; + /** + * HTTP 响应结果 + */ + private String response; + /** + * 支付通知状态 + * + * 枚举 {@link PayNotifyStatusEnum} + */ + private Integer status; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/dataobject/notify/PayNotifyTaskDO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/dataobject/notify/PayNotifyTaskDO.java new file mode 100644 index 0000000..66d2576 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/dataobject/notify/PayNotifyTaskDO.java @@ -0,0 +1,102 @@ +package cn.aagro.pp.module.pay.dal.dataobject.notify; + +import cn.aagro.pp.framework.tenant.core.db.TenantBaseDO; +import cn.aagro.pp.module.pay.dal.dataobject.app.PayAppDO; +import cn.aagro.pp.module.pay.dal.dataobject.order.PayOrderDO; +import cn.aagro.pp.module.pay.dal.dataobject.refund.PayRefundDO; +import cn.aagro.pp.module.pay.dal.dataobject.transfer.PayTransferDO; +import cn.aagro.pp.module.pay.enums.notify.PayNotifyStatusEnum; +import cn.aagro.pp.module.pay.enums.notify.PayNotifyTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 支付通知 + * 在支付系统收到支付渠道的支付、退款的结果后,需要不断的通知到业务系统,直到成功。 + * + * @author 芋道源码 + */ +@TableName("pay_notify_task") +@KeySequence("pay_notify_task_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +public class PayNotifyTaskDO extends TenantBaseDO { + + /** + * 通知频率,单位为秒。 + * + * 算上首次的通知,实际是一共 1 + 8 = 9 次。 + */ + public static final Integer[] NOTIFY_FREQUENCY = new Integer[]{ + 15, 15, 30, 180, + 1800, 1800, 1800, 3600 + }; + + /** + * 编号,自增 + */ + @TableId + private Long id; + /** + * 应用编号 + * + * 关联 {@link PayAppDO#getId()} + */ + private Long appId; + /** + * 通知类型 + * + * 枚举 {@link PayNotifyTypeEnum} + */ + private Integer type; + /** + * 数据编号,根据不同 type 进行关联: + * + * 1. {@link PayNotifyTypeEnum#ORDER} 时,关联 {@link PayOrderDO#getId()} + * 2. {@link PayNotifyTypeEnum#REFUND} 时,关联 {@link PayRefundDO#getId()} + * 3. {@link PayNotifyTypeEnum#TRANSFER} 时,关联 {@link PayTransferDO#getId()} + */ + private Long dataId; + /** + * 商户订单编号 + */ + private String merchantOrderId; + /** + * 商户退款编号 + */ + private String merchantRefundId; + /** + * 商户转账编号 + */ + private String merchantTransferId; + /** + * 通知状态 + * + * 枚举 {@link PayNotifyStatusEnum} + */ + private Integer status; + /** + * 下一次通知时间 + */ + private LocalDateTime nextNotifyTime; + /** + * 最后一次执行时间 + */ + private LocalDateTime lastExecuteTime; + /** + * 当前通知次数 + */ + private Integer notifyTimes; + /** + * 最大可通知次数 + */ + private Integer maxNotifyTimes; + /** + * 通知地址 + */ + private String notifyUrl; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/dataobject/order/PayOrderDO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/dataobject/order/PayOrderDO.java new file mode 100644 index 0000000..e2117c0 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/dataobject/order/PayOrderDO.java @@ -0,0 +1,147 @@ +package cn.aagro.pp.module.pay.dal.dataobject.order; + +import cn.aagro.pp.framework.mybatis.core.dataobject.BaseDO; +import cn.aagro.pp.module.pay.dal.dataobject.app.PayAppDO; +import cn.aagro.pp.module.pay.dal.dataobject.channel.PayChannelDO; +import cn.aagro.pp.module.pay.enums.PayChannelEnum; +import cn.aagro.pp.module.pay.enums.order.PayOrderStatusEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.time.LocalDateTime; + +/** + * 支付订单 DO + * + * @author 芋道源码 + */ +@TableName("pay_order") +@KeySequence("pay_order_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PayOrderDO extends BaseDO { + + /** + * 订单编号,数据库自增 + */ + private Long id; + /** + * 应用编号 + * + * 关联 {@link PayAppDO#getId()} + */ + private Long appId; + /** + * 渠道编号 + * + * 关联 {@link PayChannelDO#getId()} + */ + private Long channelId; + /** + * 渠道编码 + * + * 枚举 {@link PayChannelEnum} + */ + private String channelCode; + + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + */ + private Integer userType; + + // ========== 商户相关字段 ========== + + /** + * 商户订单编号 + * + * 例如说,内部系统 A 的订单号,需要保证每个 PayAppDO 唯一 + */ + private String merchantOrderId; + /** + * 商品标题 + */ + private String subject; + /** + * 商品描述信息 + */ + private String body; + /** + * 异步通知地址 + */ + private String notifyUrl; + + // ========== 订单相关字段 ========== + + /** + * 支付金额,单位:分 + */ + private Integer price; + /** + * 渠道手续费,单位:百分比 + * + * 冗余 {@link PayChannelDO#getFeeRate()} + */ + private Double channelFeeRate; + /** + * 渠道手续金额,单位:分 + */ + private Integer channelFeePrice; + /** + * 支付状态 + * + * 枚举 {@link PayOrderStatusEnum} + */ + private Integer status; + /** + * 用户 IP + */ + private String userIp; + /** + * 订单失效时间 + */ + private LocalDateTime expireTime; + /** + * 订单支付成功时间 + */ + private LocalDateTime successTime; + /** + * 支付成功的订单拓展单编号 + * + * 关联 {@link PayOrderExtensionDO#getId()} + */ + private Long extensionId; + /** + * 支付成功的外部订单号 + * + * 关联 {@link PayOrderExtensionDO#getNo()} + */ + private String no; + + // ========== 退款相关字段 ========== + /** + * 退款总金额,单位:分 + */ + private Integer refundPrice; + + // ========== 渠道相关字段 ========== + /** + * 渠道用户编号 + * + * 例如说,微信 openid、支付宝账号 + */ + private String channelUserId; + /** + * 渠道订单号 + */ + private String channelOrderNo; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/dataobject/order/PayOrderExtensionDO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/dataobject/order/PayOrderExtensionDO.java new file mode 100644 index 0000000..876155b --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/dataobject/order/PayOrderExtensionDO.java @@ -0,0 +1,96 @@ +package cn.aagro.pp.module.pay.dal.dataobject.order; + +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO; +import cn.aagro.pp.module.pay.dal.dataobject.channel.PayChannelDO; +import cn.aagro.pp.module.pay.enums.order.PayOrderStatusEnum; +import cn.aagro.pp.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.*; + +import java.util.Map; + +/** + * 支付订单拓展 DO + * + * 每次调用支付渠道,都会生成一条对应记录 + * + * @author 芋道源码 + */ +@TableName(value = "pay_order_extension",autoResultMap = true) +@KeySequence("pay_order_extension_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PayOrderExtensionDO extends BaseDO { + + /** + * 订单拓展编号,数据库自增 + */ + private Long id; + /** + * 外部订单号,根据规则生成 + * + * 调用支付渠道时,使用该字段作为对接的订单号: + * 1. 微信支付:对应 JSAPI 支付 的 out_trade_no 字段 + * 2. 支付宝支付:对应 电脑网站支付 的 out_trade_no 字段 + * + * 例如说,P202110132239124200055 + */ + private String no; + /** + * 订单号 + * + * 关联 {@link PayOrderDO#getId()} + */ + private Long orderId; + /** + * 渠道编号 + * + * 关联 {@link PayChannelDO#getId()} + */ + private Long channelId; + /** + * 渠道编码 + */ + private String channelCode; + /** + * 用户 IP + */ + private String userIp; + /** + * 支付状态 + * + * 枚举 {@link PayOrderStatusEnum} + */ + private Integer status; + /** + * 支付渠道的额外参数 + * + * 参见 参数说明 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private Map channelExtras; + + /** + * 调用渠道的错误码 + */ + private String channelErrorCode; + /** + * 调用渠道报错时,错误信息 + */ + private String channelErrorMsg; + + /** + * 支付渠道的同步/异步通知的内容 + * + * 对应 {@link PayOrderRespDTO#getRawData()} + */ + private String channelNotifyData; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/dataobject/refund/PayRefundDO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/dataobject/refund/PayRefundDO.java new file mode 100644 index 0000000..32b6ba9 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/dataobject/refund/PayRefundDO.java @@ -0,0 +1,169 @@ +package cn.aagro.pp.module.pay.dal.dataobject.refund; + +import cn.aagro.pp.framework.mybatis.core.dataobject.BaseDO; +import cn.aagro.pp.module.pay.enums.PayChannelEnum; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.refund.PayRefundRespDTO; +import cn.aagro.pp.module.pay.dal.dataobject.app.PayAppDO; +import cn.aagro.pp.module.pay.dal.dataobject.channel.PayChannelDO; +import cn.aagro.pp.module.pay.dal.dataobject.order.PayOrderDO; +import cn.aagro.pp.module.pay.enums.refund.PayRefundStatusEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.time.LocalDateTime; + +/** + * 支付退款单 DO + * 一个支付订单,可以拥有多个支付退款单 + * + * 即 PayOrderDO : PayRefundDO = 1 : n + * + * @author 芋道源码 + */ +@TableName("pay_refund") +@KeySequence("pay_refund_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PayRefundDO extends BaseDO { + + /** + * 退款单编号,数据库自增 + */ + @TableId + private Long id; + /** + * 外部退款号,根据规则生成 + * + * 调用支付渠道时,使用该字段作为对接的退款号: + * 1. 微信退款:对应 申请退款 的 out_refund_no 字段 + * 2. 支付宝退款:对应 的 out_request_no 字段 + */ + private String no; + + /** + * 应用编号 + * + * 关联 {@link PayAppDO#getId()} + */ + private Long appId; + /** + * 渠道编号 + * + * 关联 {@link PayChannelDO#getId()} + */ + private Long channelId; + /** + * 渠道编码 + * + * 枚举 {@link PayChannelEnum} + */ + private String channelCode; + /** + * 订单编号 + * + * 关联 {@link PayOrderDO#getId()} + */ + private Long orderId; + /** + * 支付订单编号 + * + * 冗余 {@link PayOrderDO#getNo()} + */ + private String orderNo; + + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + */ + private Integer userType; + + // ========== 商户相关字段 ========== + /** + * 商户订单编号 + * + * 例如说,内部系统 A 的订单号,需要保证每个 PayAppDO 唯一 + */ + private String merchantOrderId; + /** + * 商户退款订单号 + * + * 例如说,内部系统 A 的订单号,需要保证每个 PayAppDO 唯一 + */ + private String merchantRefundId; + /** + * 异步通知地址 + */ + private String notifyUrl; + + // ========== 退款相关字段 ========== + /** + * 退款状态 + * + * 枚举 {@link PayRefundStatusEnum} + */ + private Integer status; + + /** + * 支付金额,单位:分 + */ + private Integer payPrice; + /** + * 退款金额,单位:分 + */ + private Integer refundPrice; + + /** + * 退款原因 + */ + private String reason; + + /** + * 用户 IP + */ + private String userIp; + + // ========== 渠道相关字段 ========== + /** + * 渠道订单号 + * + * 冗余 {@link PayOrderDO#getChannelOrderNo()} + */ + private String channelOrderNo; + /** + * 渠道退款单号 + * + * 1. 微信退款:对应 申请退款 的 refund_id 字段 + * 2. 支付宝退款:没有字段 + */ + private String channelRefundNo; + /** + * 退款成功时间 + */ + private LocalDateTime successTime; + + /** + * 调用渠道的错误码 + */ + private String channelErrorCode; + /** + * 调用渠道的错误提示 + */ + private String channelErrorMsg; + + /** + * 支付渠道的同步/异步通知的内容 + * + * 对应 {@link PayRefundRespDTO#getRawData()} + */ + private String channelNotifyData; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/dataobject/transfer/PayTransferDO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/dataobject/transfer/PayTransferDO.java new file mode 100644 index 0000000..d0df1d7 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/dataobject/transfer/PayTransferDO.java @@ -0,0 +1,152 @@ +package cn.aagro.pp.module.pay.dal.dataobject.transfer; + +import cn.aagro.pp.framework.mybatis.core.dataobject.BaseDO; +import cn.aagro.pp.module.pay.enums.PayChannelEnum; +import cn.aagro.pp.module.pay.dal.dataobject.app.PayAppDO; +import cn.aagro.pp.module.pay.dal.dataobject.channel.PayChannelDO; +import cn.aagro.pp.module.pay.enums.transfer.PayTransferStatusEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.Map; + +/** + * 转账单 DO + * + * @author jason + */ +@TableName(value ="pay_transfer", autoResultMap = true) +@KeySequence("pay_transfer_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +public class PayTransferDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 转账单号 + */ + private String no; + + /** + * 应用编号 + * + * 关联 {@link PayAppDO#getId()} + */ + private Long appId; + /** + * 转账渠道编号 + * + * 关联 {@link PayChannelDO#getId()} + */ + private Long channelId; + /** + * 转账渠道编码 + * + * 枚举 {@link PayChannelEnum} + */ + private String channelCode; + + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + */ + private Integer userType; + + // ========== 商户相关字段 ========== + /** + * 商户转账单编号 + * + * 例如说,内部系统 A 的订单号,需要保证每个 PayAppDO 唯一 + */ + private String merchantTransferId; + + // ========== 转账相关字段 ========== + + /** + * 转账标题 + */ + private String subject; + + /** + * 转账金额,单位:分 + */ + private Integer price; + + /** + * 收款人账号 + */ + private String userAccount; + /** + * 收款人姓名 + */ + private String userName; + + /** + * 转账状态 + * + * 枚举 {@link PayTransferStatusEnum} + */ + private Integer status; + + /** + * 订单转账成功时间 + */ + private LocalDateTime successTime; + + // ========== 其它字段 ========== + + /** + * 异步通知地址 + */ + private String notifyUrl; + + /** + * 用户 IP + */ + private String userIp; + + /** + * 渠道的额外参数 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private Map channelExtras; + + /** + * 渠道转账单号 + */ + private String channelTransferNo; + + /** + * 调用渠道的错误码 + */ + private String channelErrorCode; + /** + * 调用渠道的错误提示 + */ + private String channelErrorMsg; + + /** + * 渠道的同步/异步通知的内容 + */ + private String channelNotifyData; + + /** + * 渠道 package 信息 + * + * 特殊:目前只有微信转账有这个东西!!! + * @see JSAPI 调起用户确认收款 + */ + private String channelPackageInfo; + +} \ No newline at end of file diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/dataobject/wallet/PayWalletDO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/dataobject/wallet/PayWalletDO.java new file mode 100644 index 0000000..1ee5a01 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/dataobject/wallet/PayWalletDO.java @@ -0,0 +1,59 @@ +package cn.aagro.pp.module.pay.dal.dataobject.wallet; + +import cn.aagro.pp.framework.common.enums.UserTypeEnum; +import cn.aagro.pp.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +/** + * 会员钱包 DO + * + * @author jason + */ +@TableName(value ="pay_wallet") +@KeySequence("pay_wallet_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +public class PayWalletDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + + /** + * 用户 id + * + * 关联 MemberUserDO 的 id 编号 + * 关联 AdminUserDO 的 id 编号 + */ + private Long userId; + /** + * 用户类型, 预留 多商户转帐可能需要用到 + * + * 关联 {@link UserTypeEnum} + */ + private Integer userType; + + /** + * 余额,单位分 + */ + private Integer balance; + + /** + * 冻结金额,单位分 + */ + private Integer freezePrice; + + /** + * 累计支出,单位分 + */ + private Integer totalExpense; + /** + * 累计充值,单位分 + */ + private Integer totalRecharge; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/dataobject/wallet/PayWalletRechargeDO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/dataobject/wallet/PayWalletRechargeDO.java new file mode 100644 index 0000000..6fe96c0 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/dataobject/wallet/PayWalletRechargeDO.java @@ -0,0 +1,116 @@ +package cn.aagro.pp.module.pay.dal.dataobject.wallet; + +import cn.aagro.pp.framework.mybatis.core.dataobject.BaseDO; +import cn.aagro.pp.module.pay.dal.dataobject.order.PayOrderDO; +import cn.aagro.pp.module.pay.dal.dataobject.refund.PayRefundDO; +import cn.aagro.pp.module.pay.enums.refund.PayRefundStatusEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 会员钱包充值 + */ +@TableName(value ="pay_wallet_recharge") +@KeySequence("pay_wallet_recharge_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +public class PayWalletRechargeDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + + /** + * 钱包编号 + * + * 关联 {@link PayWalletDO#getId()} + */ + private Long walletId; + + /** + * 用户实际到账余额 + * + * 例如充 100 送 20,则该值是 120 + */ + private Integer totalPrice; + /** + * 实际支付金额 + */ + private Integer payPrice; + /** + * 钱包赠送金额 + */ + private Integer bonusPrice; + + /** + * 充值套餐编号 + * + * 关联 {@link PayWalletRechargeDO#getPackageId()} 字段 + */ + private Long packageId; + + /** + * 是否已支付 + * + * true - 已支付 + * false - 未支付 + */ + private Boolean payStatus; + + /** + * 支付订单编号 + * + * 关联 {@link PayOrderDO#getId()} + */ + private Long payOrderId; + + /** + * 支付成功的支付渠道 + * + * 冗余 {@link PayOrderDO#getChannelCode()} + */ + private String payChannelCode; + /** + * 订单支付时间 + */ + private LocalDateTime payTime; + + /** + * 支付退款单编号 + * + * 关联 {@link PayRefundDO#getId()} + */ + private Long payRefundId; + + /** + * 退款金额,包含赠送金额 + */ + private Integer refundTotalPrice; + /** + * 退款支付金额 + */ + private Integer refundPayPrice; + + /** + * 退款钱包赠送金额 + */ + private Integer refundBonusPrice; + + /** + * 退款时间 + */ + private LocalDateTime refundTime; + + /** + * 退款状态 + * + * 枚举 {@link PayRefundStatusEnum} + */ + private Integer refundStatus; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/dataobject/wallet/PayWalletRechargePackageDO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/dataobject/wallet/PayWalletRechargePackageDO.java new file mode 100644 index 0000000..5a16382 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/dataobject/wallet/PayWalletRechargePackageDO.java @@ -0,0 +1,47 @@ +package cn.aagro.pp.module.pay.dal.dataobject.wallet; + +import cn.aagro.pp.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +/** + * 会员钱包充值套餐 DO + * + * 通过充值套餐时,可以赠送一定金额; + * + * @author 芋道源码 + */ +@TableName(value ="pay_wallet_recharge_package") +@KeySequence("pay_wallet_recharge_package_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +public class PayWalletRechargePackageDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 套餐名 + */ + private String name; + + /** + * 支付金额 + */ + private Integer payPrice; + /** + * 赠送金额 + */ + private Integer bonusPrice; + + /** + * 状态 + * + * 枚举 {@link cn.aagro.pp.framework.common.enums.CommonStatusEnum} + */ + private Integer status; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/dataobject/wallet/PayWalletTransactionDO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/dataobject/wallet/PayWalletTransactionDO.java new file mode 100644 index 0000000..ca637c0 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/dataobject/wallet/PayWalletTransactionDO.java @@ -0,0 +1,66 @@ +package cn.aagro.pp.module.pay.dal.dataobject.wallet; + +import cn.aagro.pp.framework.mybatis.core.dataobject.BaseDO; +import cn.aagro.pp.module.pay.enums.wallet.PayWalletBizTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +/** + * 会员钱包流水 DO + * + * @author jason + */ +@TableName(value ="pay_wallet_transaction") +@KeySequence("pay_wallet_transaction_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +public class PayWalletTransactionDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + + /** + * 流水号 + */ + private String no; + + /** + * 钱包编号 + * + * 关联 {@link PayWalletDO#getId()} + */ + private Long walletId; + + /** + * 关联业务分类 + * + * 枚举 {@link PayWalletBizTypeEnum#getType()} + */ + private Integer bizType; + + /** + * 关联业务编号 + */ + private String bizId; + + /** + * 流水说明 + */ + private String title; + + /** + * 交易金额,单位分 + * + * 正值表示余额增加,负值表示余额减少 + */ + private Integer price; + + /** + * 交易后余额,单位分 + */ + private Integer balance; +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/mysql/app/PayAppMapper.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/mysql/app/PayAppMapper.java new file mode 100644 index 0000000..ebf2e87 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/mysql/app/PayAppMapper.java @@ -0,0 +1,26 @@ +package cn.aagro.pp.module.pay.dal.mysql.app; + +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.mybatis.core.mapper.BaseMapperX; +import cn.aagro.pp.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.aagro.pp.module.pay.controller.admin.app.vo.PayAppPageReqVO; +import cn.aagro.pp.module.pay.dal.dataobject.app.PayAppDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface PayAppMapper extends BaseMapperX { + + default PageResult selectPage(PayAppPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(PayAppDO::getName, reqVO.getName()) + .likeIfPresent(PayAppDO::getAppKey, reqVO.getAppKey()) + .eqIfPresent(PayAppDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(PayAppDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(PayAppDO::getId)); + } + + default PayAppDO selectByAppKey(String appKey) { + return selectOne(PayAppDO::getAppKey, appKey); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/mysql/channel/PayChannelMapper.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/mysql/channel/PayChannelMapper.java new file mode 100644 index 0000000..8ab03f7 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/mysql/channel/PayChannelMapper.java @@ -0,0 +1,31 @@ +package cn.aagro.pp.module.pay.dal.mysql.channel; + +import cn.aagro.pp.framework.mybatis.core.mapper.BaseMapperX; +import cn.aagro.pp.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.aagro.pp.module.pay.dal.dataobject.channel.PayChannelDO; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Select; + +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.List; + +@Mapper +public interface PayChannelMapper extends BaseMapperX { + + default PayChannelDO selectByAppIdAndCode(Long appId, String code) { + return selectOne(PayChannelDO::getAppId, appId, PayChannelDO::getCode, code); + } + + default List selectListByAppIds(Collection appIds){ + return selectList(PayChannelDO::getAppId, appIds); + } + + default List selectListByAppId(Long appId, Integer status) { + return selectList(new LambdaQueryWrapperX() + .eq(PayChannelDO::getAppId, appId) + .eq(PayChannelDO::getStatus, status)); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/mysql/demo/PayDemoOrderMapper.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/mysql/demo/PayDemoOrderMapper.java new file mode 100644 index 0000000..b73c2aa --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/mysql/demo/PayDemoOrderMapper.java @@ -0,0 +1,28 @@ +package cn.aagro.pp.module.pay.dal.mysql.demo; + +import cn.aagro.pp.framework.common.pojo.PageParam; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.mybatis.core.mapper.BaseMapperX; +import cn.aagro.pp.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.aagro.pp.module.pay.dal.dataobject.demo.PayDemoOrderDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * 示例订单 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface PayDemoOrderMapper extends BaseMapperX { + + default PageResult selectPage(PageParam reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .orderByDesc(PayDemoOrderDO::getId)); + } + + default int updateByIdAndPayed(Long id, boolean wherePayed, PayDemoOrderDO updateObj) { + return update(updateObj, new LambdaQueryWrapperX() + .eq(PayDemoOrderDO::getId, id).eq(PayDemoOrderDO::getPayStatus, wherePayed)); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/mysql/demo/PayDemoWithdrawMapper.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/mysql/demo/PayDemoWithdrawMapper.java new file mode 100644 index 0000000..fa09508 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/mysql/demo/PayDemoWithdrawMapper.java @@ -0,0 +1,24 @@ +package cn.aagro.pp.module.pay.dal.mysql.demo; + +import cn.aagro.pp.framework.common.pojo.PageParam; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.mybatis.core.mapper.BaseMapperX; +import cn.aagro.pp.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.aagro.pp.module.pay.dal.dataobject.demo.PayDemoWithdrawDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface PayDemoWithdrawMapper extends BaseMapperX { + + default PageResult selectPage(PageParam pageParam){ + return selectPage(pageParam, new LambdaQueryWrapperX() + .orderByDesc(PayDemoWithdrawDO::getId)); + } + + default int updateByIdAndStatus(Long id, Integer whereStatus, PayDemoWithdrawDO updateObj) { + return update(updateObj, new LambdaQueryWrapperX() + .eq(PayDemoWithdrawDO::getId, id) + .eq(PayDemoWithdrawDO::getStatus, whereStatus)); + } + +} \ No newline at end of file diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/mysql/notify/PayNotifyLogMapper.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/mysql/notify/PayNotifyLogMapper.java new file mode 100644 index 0000000..348c32f --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/mysql/notify/PayNotifyLogMapper.java @@ -0,0 +1,16 @@ +package cn.aagro.pp.module.pay.dal.mysql.notify; + +import cn.aagro.pp.module.pay.dal.dataobject.notify.PayNotifyLogDO; +import cn.aagro.pp.framework.mybatis.core.mapper.BaseMapperX; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface PayNotifyLogMapper extends BaseMapperX { + + default List selectListByTaskId(Long taskId) { + return selectList(PayNotifyLogDO::getTaskId, taskId); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/mysql/notify/PayNotifyTaskMapper.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/mysql/notify/PayNotifyTaskMapper.java new file mode 100644 index 0000000..208081a --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/mysql/notify/PayNotifyTaskMapper.java @@ -0,0 +1,46 @@ +package cn.aagro.pp.module.pay.dal.mysql.notify; + +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.mybatis.core.mapper.BaseMapperX; +import cn.aagro.pp.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.aagro.pp.module.pay.controller.admin.notify.vo.PayNotifyTaskPageReqVO; +import cn.aagro.pp.module.pay.dal.dataobject.notify.PayNotifyTaskDO; +import cn.aagro.pp.module.pay.enums.notify.PayNotifyStatusEnum; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.time.LocalDateTime; +import java.util.List; + +@Mapper +public interface PayNotifyTaskMapper extends BaseMapperX { + + /** + * 获得需要通知的 PayNotifyTaskDO 记录。需要满足如下条件: + * + * 1. status 非成功 + * 2. nextNotifyTime 小于当前时间 + * + * @return PayTransactionNotifyTaskDO 数组 + */ + default List selectListByNotify() { + return selectList(new LambdaQueryWrapper() + .in(PayNotifyTaskDO::getStatus, PayNotifyStatusEnum.WAITING.getStatus(), + PayNotifyStatusEnum.REQUEST_SUCCESS.getStatus(), PayNotifyStatusEnum.REQUEST_FAILURE.getStatus()) + .le(PayNotifyTaskDO::getNextNotifyTime, LocalDateTime.now())); + } + + default PageResult selectPage(PayNotifyTaskPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(PayNotifyTaskDO::getAppId, reqVO.getAppId()) + .eqIfPresent(PayNotifyTaskDO::getType, reqVO.getType()) + .eqIfPresent(PayNotifyTaskDO::getDataId, reqVO.getDataId()) + .eqIfPresent(PayNotifyTaskDO::getStatus, reqVO.getStatus()) + .eqIfPresent(PayNotifyTaskDO::getMerchantOrderId, reqVO.getMerchantOrderId()) + .eqIfPresent(PayNotifyTaskDO::getMerchantRefundId, reqVO.getMerchantRefundId()) + .eqIfPresent(PayNotifyTaskDO::getMerchantTransferId, reqVO.getMerchantTransferId()) + .betweenIfPresent(PayNotifyTaskDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(PayNotifyTaskDO::getId)); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/mysql/order/PayOrderExtensionMapper.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/mysql/order/PayOrderExtensionMapper.java new file mode 100644 index 0000000..c8985e9 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/mysql/order/PayOrderExtensionMapper.java @@ -0,0 +1,38 @@ +package cn.aagro.pp.module.pay.dal.mysql.order; + +import cn.aagro.pp.framework.mybatis.core.mapper.BaseMapperX; +import cn.aagro.pp.module.pay.dal.dataobject.order.PayOrderExtensionDO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.time.LocalDateTime; +import java.util.List; + +@Mapper +public interface PayOrderExtensionMapper extends BaseMapperX { + + default PayOrderExtensionDO selectByNo(String no) { + return selectOne(PayOrderExtensionDO::getNo, no); + } + + default int updateByIdAndStatus(Long id, Integer status, PayOrderExtensionDO update) { + return update(update, new LambdaQueryWrapper() + .eq(PayOrderExtensionDO::getId, id).eq(PayOrderExtensionDO::getStatus, status)); + } + + default List selectListByOrderId(Long orderId) { + return selectList(PayOrderExtensionDO::getOrderId, orderId); + } + + default List selectListByOrderIdAndStatus(Long orderId, Integer status) { + return selectList(PayOrderExtensionDO::getOrderId, orderId, + PayOrderExtensionDO::getStatus, status); + } + + default List selectListByStatusAndCreateTimeGe(Integer status, LocalDateTime minCreateTime) { + return selectList(new LambdaQueryWrapper() + .eq(PayOrderExtensionDO::getStatus, status) + .ge(PayOrderExtensionDO::getCreateTime, minCreateTime)); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/mysql/order/PayOrderMapper.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/mysql/order/PayOrderMapper.java new file mode 100644 index 0000000..1771adb --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/mysql/order/PayOrderMapper.java @@ -0,0 +1,62 @@ +package cn.aagro.pp.module.pay.dal.mysql.order; + +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.mybatis.core.mapper.BaseMapperX; +import cn.aagro.pp.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.aagro.pp.module.pay.controller.admin.order.vo.PayOrderExportReqVO; +import cn.aagro.pp.module.pay.controller.admin.order.vo.PayOrderPageReqVO; +import cn.aagro.pp.module.pay.dal.dataobject.order.PayOrderDO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.time.LocalDateTime; +import java.util.List; + +@Mapper +public interface PayOrderMapper extends BaseMapperX { + + default PageResult selectPage(PayOrderPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(PayOrderDO::getAppId, reqVO.getAppId()) + .eqIfPresent(PayOrderDO::getChannelCode, reqVO.getChannelCode()) + .likeIfPresent(PayOrderDO::getMerchantOrderId, reqVO.getMerchantOrderId()) + .likeIfPresent(PayOrderDO::getChannelOrderNo, reqVO.getChannelOrderNo()) + .likeIfPresent(PayOrderDO::getNo, reqVO.getNo()) + .eqIfPresent(PayOrderDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(PayOrderDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(PayOrderDO::getId)); + } + + default List selectList(PayOrderExportReqVO reqVO) { + return selectList(new LambdaQueryWrapperX() + .eqIfPresent(PayOrderDO::getAppId, reqVO.getAppId()) + .eqIfPresent(PayOrderDO::getChannelCode, reqVO.getChannelCode()) + .likeIfPresent(PayOrderDO::getMerchantOrderId, reqVO.getMerchantOrderId()) + .likeIfPresent(PayOrderDO::getChannelOrderNo, reqVO.getChannelOrderNo()) + .likeIfPresent(PayOrderDO::getNo, reqVO.getNo()) + .eqIfPresent(PayOrderDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(PayOrderDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(PayOrderDO::getId)); + } + + default Long selectCountByAppId(Long appId) { + return selectCount(PayOrderDO::getAppId, appId); + } + + default PayOrderDO selectByAppIdAndMerchantOrderId(Long appId, String merchantOrderId) { + return selectOne(PayOrderDO::getAppId, appId, + PayOrderDO::getMerchantOrderId, merchantOrderId); + } + + default int updateByIdAndStatus(Long id, Integer status, PayOrderDO update) { + return update(update, new LambdaQueryWrapper() + .eq(PayOrderDO::getId, id).eq(PayOrderDO::getStatus, status)); + } + + default List selectListByStatusAndExpireTimeLt(Integer status, LocalDateTime expireTime) { + return selectList(new LambdaQueryWrapper() + .eq(PayOrderDO::getStatus, status) + .lt(PayOrderDO::getExpireTime, expireTime)); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/mysql/refund/PayRefundMapper.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/mysql/refund/PayRefundMapper.java new file mode 100644 index 0000000..8232b9c --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/mysql/refund/PayRefundMapper.java @@ -0,0 +1,78 @@ +package cn.aagro.pp.module.pay.dal.mysql.refund; + +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.mybatis.core.mapper.BaseMapperX; +import cn.aagro.pp.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.aagro.pp.module.pay.controller.admin.refund.vo.PayRefundExportReqVO; +import cn.aagro.pp.module.pay.controller.admin.refund.vo.PayRefundPageReqVO; +import cn.aagro.pp.module.pay.dal.dataobject.refund.PayRefundDO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface PayRefundMapper extends BaseMapperX { + + default Long selectCountByAppId(Long appId) { + return selectCount(PayRefundDO::getAppId, appId); + } + + default PayRefundDO selectByAppIdAndMerchantRefundId(Long appId, String merchantRefundId) { + return selectOne(new LambdaQueryWrapperX() + .eq(PayRefundDO::getAppId, appId) + .eq(PayRefundDO::getMerchantRefundId, merchantRefundId)); + } + + default Long selectCountByAppIdAndOrderId(Long appId, Long orderId, Integer status) { + return selectCount(new LambdaQueryWrapperX() + .eq(PayRefundDO::getAppId, appId) + .eq(PayRefundDO::getOrderId, orderId) + .eq(PayRefundDO::getStatus, status)); + } + + default PayRefundDO selectByAppIdAndNo(Long appId, String no) { + return selectOne(new LambdaQueryWrapperX() + .eq(PayRefundDO::getAppId, appId) + .eq(PayRefundDO::getNo, no)); + } + + default PayRefundDO selectByNo(String no) { + return selectOne(PayRefundDO::getNo, no); + } + + default int updateByIdAndStatus(Long id, Integer status, PayRefundDO update) { + return update(update, new LambdaQueryWrapper() + .eq(PayRefundDO::getId, id).eq(PayRefundDO::getStatus, status)); + } + + default PageResult selectPage(PayRefundPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(PayRefundDO::getAppId, reqVO.getAppId()) + .eqIfPresent(PayRefundDO::getChannelCode, reqVO.getChannelCode()) + .likeIfPresent(PayRefundDO::getMerchantOrderId, reqVO.getMerchantOrderId()) + .likeIfPresent(PayRefundDO::getMerchantRefundId, reqVO.getMerchantRefundId()) + .likeIfPresent(PayRefundDO::getChannelOrderNo, reqVO.getChannelOrderNo()) + .likeIfPresent(PayRefundDO::getChannelRefundNo, reqVO.getChannelRefundNo()) + .eqIfPresent(PayRefundDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(PayRefundDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(PayRefundDO::getId)); + } + + default List selectList(PayRefundExportReqVO reqVO) { + return selectList(new LambdaQueryWrapperX() + .eqIfPresent(PayRefundDO::getAppId, reqVO.getAppId()) + .eqIfPresent(PayRefundDO::getChannelCode, reqVO.getChannelCode()) + .likeIfPresent(PayRefundDO::getMerchantOrderId, reqVO.getMerchantOrderId()) + .likeIfPresent(PayRefundDO::getMerchantRefundId, reqVO.getMerchantRefundId()) + .likeIfPresent(PayRefundDO::getChannelOrderNo, reqVO.getChannelOrderNo()) + .likeIfPresent(PayRefundDO::getChannelRefundNo, reqVO.getChannelRefundNo()) + .eqIfPresent(PayRefundDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(PayRefundDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(PayRefundDO::getId)); + } + + default List selectListByStatus(Integer status) { + return selectList(PayRefundDO::getStatus, status); + } +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/mysql/transfer/PayTransferMapper.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/mysql/transfer/PayTransferMapper.java new file mode 100644 index 0000000..a56c568 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/mysql/transfer/PayTransferMapper.java @@ -0,0 +1,65 @@ +package cn.aagro.pp.module.pay.dal.mysql.transfer; + +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.mybatis.core.mapper.BaseMapperX; +import cn.aagro.pp.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.aagro.pp.module.pay.controller.admin.transfer.vo.PayTransferPageReqVO; +import cn.aagro.pp.module.pay.dal.dataobject.transfer.PayTransferDO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +@Mapper +public interface PayTransferMapper extends BaseMapperX { + + default int updateByIdAndStatus(Long id, List whereStatuses, PayTransferDO updateObj) { + return update(updateObj, new LambdaQueryWrapper() + .eq(PayTransferDO::getId, id) + .in(PayTransferDO::getStatus, whereStatuses)); + } + + default int updateByIdAndStatus(Long id, Integer whereStatus, PayTransferDO updateObj) { + return update(updateObj, new LambdaQueryWrapper() + .eq(PayTransferDO::getId, id) + .eq(PayTransferDO::getStatus, whereStatus)); + } + + default PayTransferDO selectByAppIdAndMerchantOrderId(Long appId, String merchantOrderId) { + return selectOne(PayTransferDO::getAppId, appId, + PayTransferDO::getMerchantTransferId, merchantOrderId); + } + + default PageResult selectPage(PayTransferPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(PayTransferDO::getNo, reqVO.getNo()) + .eqIfPresent(PayTransferDO::getAppId, reqVO.getAppId()) + .eqIfPresent(PayTransferDO::getChannelCode, reqVO.getChannelCode()) + .eqIfPresent(PayTransferDO::getMerchantTransferId, reqVO.getMerchantOrderId()) + .eqIfPresent(PayTransferDO::getStatus, reqVO.getStatus()) + .likeIfPresent(PayTransferDO::getUserName, reqVO.getUserName()) + .likeIfPresent(PayTransferDO::getUserAccount, reqVO.getUserAccount()) + .eqIfPresent(PayTransferDO::getChannelTransferNo, reqVO.getChannelTransferNo()) + .betweenIfPresent(PayTransferDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(PayTransferDO::getId)); + } + + default List selectListByStatus(Collection statuses) { + return selectList(PayTransferDO::getStatus, statuses); + } + + default PayTransferDO selectByAppIdAndNo(Long appId, String no) { + return selectOne(PayTransferDO::getAppId, appId, + PayTransferDO::getNo, no); + } + + default PayTransferDO selectByNo(String no) { + return selectOne(PayTransferDO::getNo, no); + } + +} + + + + diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/mysql/wallet/PayWalletMapper.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/mysql/wallet/PayWalletMapper.java new file mode 100644 index 0000000..d18c9db --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/mysql/wallet/PayWalletMapper.java @@ -0,0 +1,134 @@ +package cn.aagro.pp.module.pay.dal.mysql.wallet; + + +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.mybatis.core.mapper.BaseMapperX; +import cn.aagro.pp.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.aagro.pp.module.pay.controller.admin.wallet.vo.wallet.PayWalletPageReqVO; +import cn.aagro.pp.module.pay.dal.dataobject.wallet.PayWalletDO; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface PayWalletMapper extends BaseMapperX { + + default PayWalletDO selectByUserIdAndType(Long userId, Integer userType) { + return selectOne(PayWalletDO::getUserId, userId, + PayWalletDO::getUserType, userType); + } + + default PageResult selectPage(PayWalletPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(PayWalletDO::getUserId, reqVO.getUserId()) + .eqIfPresent(PayWalletDO::getUserType, reqVO.getUserType()) + .betweenIfPresent(PayWalletDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(PayWalletDO::getId)); + } + + /** + * 当消费退款时候, 更新钱包 + * + * @param id 钱包 id + * @param price 消费金额 + */ + default int updateWhenConsumptionRefund(Long id, Integer price) { + LambdaUpdateWrapper lambdaUpdateWrapper = new LambdaUpdateWrapper() + .setSql(" balance = balance + " + price + + ", total_expense = total_expense - " + price) + .eq(PayWalletDO::getId, id); + return update(null, lambdaUpdateWrapper); + } + + /** + * 当消费时候, 更新钱包 + * + * @param price 消费金额 + * @param id 钱包 id + */ + default int updateWhenConsumption(Long id, Integer price){ + LambdaUpdateWrapper lambdaUpdateWrapper = new LambdaUpdateWrapper() + .setSql(" balance = balance - " + price + + ", total_expense = total_expense + " + price) + .eq(PayWalletDO::getId, id) + .ge(PayWalletDO::getBalance, price); // cas 逻辑 + return update(null, lambdaUpdateWrapper); + } + + /** + * 当充值的时候,更新钱包 + * + * @param id 钱包 id + * @param price 钱包金额 + */ + default int updateWhenRecharge(Long id, Integer price){ + LambdaUpdateWrapper lambdaUpdateWrapper = new LambdaUpdateWrapper() + .setSql(" balance = balance + " + price + + ", total_recharge = total_recharge + " + price) + .eq(PayWalletDO::getId, id); + return update(null, lambdaUpdateWrapper); + } + + /** + * 增加余额的时候,更新钱包 + * + * @param id 钱包 id + * @param price 钱包金额 + */ + default void updateWhenAdd(Long id, Integer price) { + LambdaUpdateWrapper lambdaUpdateWrapper = new LambdaUpdateWrapper() + .setSql(" balance = balance + " + price) + .eq(PayWalletDO::getId, id); + update(null, lambdaUpdateWrapper); + } + + /** + * 冻结钱包部分余额 + * + * @param id 钱包 id + * @param price 冻结金额 + */ + default int freezePrice(Long id, Integer price){ + LambdaUpdateWrapper lambdaUpdateWrapper = new LambdaUpdateWrapper() + .setSql(" balance = balance - " + price + + ", freeze_price = freeze_price + " + price) + .eq(PayWalletDO::getId, id) + .ge(PayWalletDO::getBalance, price); // cas 逻辑 + return update(null, lambdaUpdateWrapper); + } + + /** + * 解冻钱包余额 + * + * @param id 钱包 id + * @param price 解冻金额 + */ + default int unFreezePrice(Long id, Integer price){ + LambdaUpdateWrapper lambdaUpdateWrapper = new LambdaUpdateWrapper() + .setSql(" balance = balance + " + price + + ", freeze_price = freeze_price - " + price) + .eq(PayWalletDO::getId, id) + .ge(PayWalletDO::getFreezePrice, price); // cas 逻辑 + return update(null, lambdaUpdateWrapper); + } + + /** + * 当充值退款时, 更新钱包 + * + * @param id 钱包 id + * @param price 退款金额 + */ + default int updateWhenRechargeRefund(Long id, Integer price){ + LambdaUpdateWrapper lambdaUpdateWrapper = new LambdaUpdateWrapper() + .setSql(" freeze_price = freeze_price - " + price + + ", total_recharge = total_recharge - " + price) + .eq(PayWalletDO::getId, id) + .ge(PayWalletDO::getFreezePrice, price) + .ge(PayWalletDO::getTotalRecharge, price);// cas 逻辑 + return update(null, lambdaUpdateWrapper); + } + +} + + + + diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/mysql/wallet/PayWalletRechargeMapper.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/mysql/wallet/PayWalletRechargeMapper.java new file mode 100644 index 0000000..105e70b --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/mysql/wallet/PayWalletRechargeMapper.java @@ -0,0 +1,31 @@ +package cn.aagro.pp.module.pay.dal.mysql.wallet; + +import cn.aagro.pp.framework.common.pojo.PageParam; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.mybatis.core.mapper.BaseMapperX; +import cn.aagro.pp.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.aagro.pp.module.pay.dal.dataobject.wallet.PayWalletRechargeDO; +import cn.aagro.pp.module.pay.dal.dataobject.wallet.PayWalletRechargePackageDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface PayWalletRechargeMapper extends BaseMapperX { + + default int updateByIdAndPaid(Long id, boolean wherePayStatus, PayWalletRechargeDO updateObj) { + return update(updateObj, new LambdaQueryWrapperX() + .eq(PayWalletRechargeDO::getId, id).eq(PayWalletRechargeDO::getPayStatus, wherePayStatus)); + } + + default int updateByIdAndRefunded(Long id, Integer whereRefundStatus, PayWalletRechargeDO updateObj) { + return update(updateObj, new LambdaQueryWrapperX() + .eq(PayWalletRechargeDO::getId, id).eq(PayWalletRechargeDO::getRefundStatus, whereRefundStatus)); + } + + default PageResult selectPage(PageParam pageReqVO, Long walletId, Boolean payStatus) { + return selectPage(pageReqVO, new LambdaQueryWrapperX() + .eq(PayWalletRechargeDO::getWalletId, walletId) + .eq(PayWalletRechargeDO::getPayStatus, payStatus) + .orderByDesc(PayWalletRechargeDO::getId)); + } + +} \ No newline at end of file diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/mysql/wallet/PayWalletRechargePackageMapper.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/mysql/wallet/PayWalletRechargePackageMapper.java new file mode 100644 index 0000000..b4b95d5 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/mysql/wallet/PayWalletRechargePackageMapper.java @@ -0,0 +1,32 @@ +package cn.aagro.pp.module.pay.dal.mysql.wallet; + + +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.mybatis.core.mapper.BaseMapperX; +import cn.aagro.pp.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.aagro.pp.module.pay.controller.admin.wallet.vo.rechargepackage.WalletRechargePackagePageReqVO; +import cn.aagro.pp.module.pay.dal.dataobject.wallet.PayWalletRechargePackageDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface PayWalletRechargePackageMapper extends BaseMapperX { + + default PageResult selectPage(WalletRechargePackagePageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(PayWalletRechargePackageDO::getName, reqVO.getName()) + .eqIfPresent(PayWalletRechargePackageDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(PayWalletRechargePackageDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(PayWalletRechargePackageDO::getPayPrice)); + } + + default PayWalletRechargePackageDO selectByName(String name) { + return selectOne(PayWalletRechargePackageDO::getName, name); + } + + default List selectListByStatus(Integer status) { + return selectList(PayWalletRechargePackageDO::getStatus, status); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/mysql/wallet/PayWalletTransactionMapper.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/mysql/wallet/PayWalletTransactionMapper.java new file mode 100644 index 0000000..52a2c15 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/mysql/wallet/PayWalletTransactionMapper.java @@ -0,0 +1,67 @@ +package cn.aagro.pp.module.pay.dal.mysql.wallet; + + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.aagro.pp.framework.common.pojo.PageParam; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.mybatis.core.mapper.BaseMapperX; +import cn.aagro.pp.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.aagro.pp.framework.mybatis.core.query.QueryWrapperX; +import cn.aagro.pp.module.pay.controller.app.wallet.vo.transaction.AppPayWalletTransactionPageReqVO; +import cn.aagro.pp.module.pay.dal.dataobject.wallet.PayWalletTransactionDO; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Consumer; + +import static cn.aagro.pp.module.pay.controller.app.wallet.vo.transaction.AppPayWalletTransactionPageReqVO.*; + +@Mapper +public interface PayWalletTransactionMapper extends BaseMapperX { + + default PageResult selectPage(Long walletId, Integer type, + PageParam pageParam, LocalDateTime[] createTime) { + LambdaQueryWrapperX query = new LambdaQueryWrapperX() + .eqIfPresent(PayWalletTransactionDO::getWalletId, walletId); + if (Objects.equals(type, TYPE_INCOME)) { + query.gt(PayWalletTransactionDO::getPrice, 0); + } else if (Objects.equals(type, TYPE_EXPENSE)) { + query.lt(PayWalletTransactionDO::getPrice, 0); + } + query.betweenIfPresent(PayWalletTransactionDO::getCreateTime, createTime); + query.orderByDesc(PayWalletTransactionDO::getId); + return selectPage(pageParam, query); + } + + default Integer selectPriceSum(Long walletId, Integer type, LocalDateTime[] createTime) { + // SQL sum 查询 + List> result = selectMaps(new QueryWrapperX() + .select("SUM(price) AS priceSum") + .gt(Objects.equals(type, TYPE_INCOME), "price", 0) // 收入 + .lt(Objects.equals(type, TYPE_EXPENSE), "price", 0) // 支出 + .eq("wallet_id", walletId) + .between("create_time", createTime[0], createTime[1])); + // 获得 sum 结果 + Map first = CollUtil.getFirst(result); + return MapUtil.getInt(first, "priceSum", 0); + } + + default PayWalletTransactionDO selectByNo(String no) { + return selectOne(PayWalletTransactionDO::getNo, no); + } + + default PayWalletTransactionDO selectByBiz(String bizId, Integer bizType) { + return selectOne(PayWalletTransactionDO::getBizId, bizId, + PayWalletTransactionDO::getBizType, bizType); + } + +} + + + + diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/redis/RedisKeyConstants.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/redis/RedisKeyConstants.java new file mode 100644 index 0000000..dde84d3 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/redis/RedisKeyConstants.java @@ -0,0 +1,36 @@ +package cn.aagro.pp.module.pay.dal.redis; + +/** + * 支付 Redis Key 枚举类 + * + * @author 芋道源码 + */ +public interface RedisKeyConstants { + + /** + * 通知任务的分布式锁 + * + * KEY 格式:pay_notify:lock:%d // 参数来自 DefaultLockKeyBuilder 类 + * VALUE 数据格式:HASH // RLock.class:Redisson 的 Lock 锁,使用 Hash 数据结构 + * 过期时间:不固定 + */ + String PAY_NOTIFY_LOCK = "pay_notify:lock:%d"; + + /** + * 支付钱包的分布式锁 + * + * KEY 格式:pay_wallet:lock:%d + * VALUE 数据格式:HASH // RLock.class:Redisson 的 Lock 锁,使用 Hash 数据结构 + * 过期时间:不固定 + */ + String PAY_WALLET_LOCK = "pay_wallet:lock:%d"; + + /** + * 支付序号的缓存 + * + * KEY 格式:pay_no:{prefix} + * VALUE 数据格式:编号自增 + */ + String PAY_NO = "pay_no:"; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/redis/no/PayNoRedisDAO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/redis/no/PayNoRedisDAO.java new file mode 100644 index 0000000..4e8858d --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/redis/no/PayNoRedisDAO.java @@ -0,0 +1,39 @@ +package cn.aagro.pp.module.pay.dal.redis.no; + +import cn.hutool.core.date.DatePattern;import cn.hutool.core.date.DateUtil; +import cn.aagro.pp.module.pay.dal.redis.RedisKeyConstants; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Repository; + +import javax.annotation.Resource; +import java.time.Duration; +import java.time.LocalDateTime; + +/** + * 支付序号的 Redis DAO + * + * @author 芋道源码 + */ +@Repository +public class PayNoRedisDAO { + + @Resource + private StringRedisTemplate stringRedisTemplate; + + /** + * 生成序号 + * + * @param prefix 前缀 + * @return 序号 + */ + public String generate(String prefix) { + // 递增序号 + String noPrefix = prefix + DateUtil.format(LocalDateTime.now(), DatePattern.PURE_DATETIME_PATTERN); + String key = RedisKeyConstants.PAY_NO + noPrefix; + Long no = stringRedisTemplate.opsForValue().increment(key); + // 设置过期时间 + stringRedisTemplate.expire(key, Duration.ofMinutes(1L)); + return noPrefix + no; + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/redis/notify/PayNotifyLockRedisDAO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/redis/notify/PayNotifyLockRedisDAO.java new file mode 100644 index 0000000..4be8d3f --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/redis/notify/PayNotifyLockRedisDAO.java @@ -0,0 +1,39 @@ +package cn.aagro.pp.module.pay.dal.redis.notify; + +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.stereotype.Repository; + +import javax.annotation.Resource; +import java.util.concurrent.TimeUnit; + +import static cn.aagro.pp.module.pay.dal.redis.RedisKeyConstants.PAY_NOTIFY_LOCK; + +/** + * 支付通知的锁 Redis DAO + * + * @author 芋道源码 + */ +@Repository +public class PayNotifyLockRedisDAO { + + @Resource + private RedissonClient redissonClient; + + public void lock(Long id, Long timeoutMillis, Runnable runnable) { + String lockKey = formatKey(id); + RLock lock = redissonClient.getLock(lockKey); + try { + lock.lock(timeoutMillis, TimeUnit.MILLISECONDS); + // 执行逻辑 + runnable.run(); + } finally { + lock.unlock(); + } + } + + private static String formatKey(Long id) { + return String.format(PAY_NOTIFY_LOCK, id); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/redis/wallet/PayWalletLockRedisDAO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/redis/wallet/PayWalletLockRedisDAO.java new file mode 100644 index 0000000..a5ca435 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/dal/redis/wallet/PayWalletLockRedisDAO.java @@ -0,0 +1,42 @@ +package cn.aagro.pp.module.pay.dal.redis.wallet; + +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.stereotype.Repository; + +import javax.annotation.Resource; +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; + +import static cn.aagro.pp.module.pay.dal.redis.RedisKeyConstants.PAY_WALLET_LOCK; + +/** + * 支付钱包的锁 Redis DAO + * + * @author 芋道源码 + */ +@Repository +public class PayWalletLockRedisDAO { + + @Resource + private RedissonClient redissonClient; + + public V lock(Long id, Long timeoutMillis, Callable callable) throws Exception { + String lockKey = formatKey(id); + RLock lock = redissonClient.getLock(lockKey); + try { + lock.lock(timeoutMillis, TimeUnit.MILLISECONDS); + // 执行逻辑 + return callable.call(); + } catch (Exception e) { + throw e; + } finally { + lock.unlock(); + } + } + + private static String formatKey(Long id) { + return String.format(PAY_WALLET_LOCK, id); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/enums/DictTypeConstants.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/enums/DictTypeConstants.java new file mode 100644 index 0000000..0cea2c1 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/enums/DictTypeConstants.java @@ -0,0 +1,20 @@ +package cn.aagro.pp.module.pay.enums; + +/** + * Pay 字典类型的枚举类 + * + * @author 芋道源码 + */ +public interface DictTypeConstants { + + String CHANNEL_CODE = "pay_channel_code"; // 支付渠道编码 + + String ORDER_STATUS = "pay_order_status"; // 支付渠道 + + String REFUND_STATUS = "pay_order_status"; // 退款状态 + + String NOTIFY_STATUS = "pay_notify_status"; // 回调状态 + + String TRANSFER_STATUS = "pay_transfer_status"; // 转账状态 + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/enums/ErrorCodeConstants.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/enums/ErrorCodeConstants.java new file mode 100644 index 0000000..6c08489 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/enums/ErrorCodeConstants.java @@ -0,0 +1,97 @@ +package cn.aagro.pp.module.pay.enums; + +import cn.aagro.pp.framework.common.exception.ErrorCode; + +/** + * Pay 错误码 Core 枚举类 + * + * pay 系统,使用 1-007-000-000 段 + */ +public interface ErrorCodeConstants { + + // ========== APP 模块 1-007-000-000 ========== + ErrorCode APP_NOT_FOUND = new ErrorCode(1_007_000_000, "App 不存在"); + ErrorCode APP_IS_DISABLE = new ErrorCode(1_007_000_002, "App 已经被禁用"); + ErrorCode APP_EXIST_ORDER_CANT_DELETE = new ErrorCode(1_007_000_003, "支付应用存在支付订单,无法删除"); + ErrorCode APP_EXIST_REFUND_CANT_DELETE = new ErrorCode(1_007_000_004, "支付应用存在退款订单,无法删除"); + ErrorCode APP_KEY_EXISTS = new ErrorCode(1_007_000_005, "支付应用标识已经存在"); + + // ========== CHANNEL 模块 1-007-001-000 ========== + ErrorCode CHANNEL_NOT_FOUND = new ErrorCode(1_007_001_000, "支付渠道的配置不存在"); + ErrorCode CHANNEL_IS_DISABLE = new ErrorCode(1_007_001_001, "支付渠道已经禁用"); + ErrorCode CHANNEL_EXIST_SAME_CHANNEL_ERROR = new ErrorCode(1_007_001_004, "已存在相同的渠道"); + + // ========== ORDER 模块 1-007-002-000 ========== + ErrorCode PAY_ORDER_NOT_FOUND = new ErrorCode(1_007_002_000, "支付订单不存在"); + ErrorCode PAY_ORDER_STATUS_IS_NOT_WAITING = new ErrorCode(1_007_002_001, "支付订单不处于待支付"); + ErrorCode PAY_ORDER_STATUS_IS_SUCCESS = new ErrorCode(1_007_002_002, "订单已支付,请刷新页面"); + ErrorCode PAY_ORDER_IS_EXPIRED = new ErrorCode(1_007_002_003, "支付订单已经过期"); + ErrorCode PAY_ORDER_SUBMIT_CHANNEL_ERROR = new ErrorCode(1_007_002_004, "发起支付报错,错误码:{},错误提示:{}"); + ErrorCode PAY_ORDER_REFUND_FAIL_STATUS_ERROR = new ErrorCode(1_007_002_005, "支付订单退款失败,原因:状态不是已支付或已退款"); + + // ========== ORDER 模块(拓展单) 1-007-003-000 ========== + ErrorCode PAY_ORDER_EXTENSION_NOT_FOUND = new ErrorCode(1_007_003_000, "支付交易拓展单不存在"); + ErrorCode PAY_ORDER_EXTENSION_STATUS_IS_NOT_WAITING = new ErrorCode(1_007_003_001, "支付交易拓展单不处于待支付"); + ErrorCode PAY_ORDER_EXTENSION_IS_PAID = new ErrorCode(1_007_003_002, "订单已支付,请等待支付结果"); + + // ========== 支付模块(退款) 1-007-006-000 ========== + ErrorCode REFUND_PRICE_EXCEED = new ErrorCode(1_007_006_000, "退款金额超过订单可退款金额"); + ErrorCode REFUND_HAS_REFUNDING = new ErrorCode(1_007_006_002, "已经有退款在处理中"); + ErrorCode REFUND_EXISTS = new ErrorCode(1_007_006_003, "已经存在退款单"); + ErrorCode REFUND_NOT_FOUND = new ErrorCode(1_007_006_004, "支付退款单不存在"); + ErrorCode REFUND_STATUS_IS_NOT_WAITING = new ErrorCode(1_007_006_005, "支付退款单不处于待退款"); + + // ========== 钱包模块 1-007-007-000 ========== + ErrorCode WALLET_NOT_FOUND = new ErrorCode(1_007_007_000, "用户钱包不存在"); + ErrorCode WALLET_BALANCE_NOT_ENOUGH = new ErrorCode(1_007_007_001, "钱包余额不足"); + ErrorCode WALLET_TRANSACTION_NOT_FOUND = new ErrorCode(1_007_007_002, "未找到对应的钱包交易"); + ErrorCode WALLET_REFUND_EXIST = new ErrorCode(1_007_007_003, "已经存在钱包退款"); + ErrorCode WALLET_FREEZE_PRICE_NOT_ENOUGH = new ErrorCode(1_007_007_004, "钱包冻结余额不足"); + + // ========== 钱包充值模块 1-007-008-000 ========== + ErrorCode WALLET_RECHARGE_NOT_FOUND = new ErrorCode(1_007_008_000, "钱包充值记录不存在"); + ErrorCode WALLET_RECHARGE_UPDATE_PAID_STATUS_NOT_UNPAID = new ErrorCode(1_007_008_001, "钱包充值更新支付状态失败,钱包充值记录不是【未支付】状态"); + ErrorCode WALLET_RECHARGE_UPDATE_PAID_PAY_ORDER_ID_ERROR = new ErrorCode(1_007_008_002, "钱包充值更新支付状态失败,支付单编号不匹配"); + ErrorCode WALLET_RECHARGE_UPDATE_PAID_PAY_ORDER_STATUS_NOT_SUCCESS = new ErrorCode(1_007_008_003, "钱包充值更新支付状态失败,支付单状态不是【支付成功】状态"); + ErrorCode WALLET_RECHARGE_UPDATE_PAID_PAY_PRICE_NOT_MATCH = new ErrorCode(1_007_008_004, "钱包充值更新支付状态失败,支付单金额不匹配"); + ErrorCode WALLET_RECHARGE_REFUND_FAIL_NOT_PAID = new ErrorCode(1_007_008_005, "钱包发起退款失败,钱包充值订单未支付"); + ErrorCode WALLET_RECHARGE_REFUND_FAIL_REFUNDED = new ErrorCode(1_007_008_006, "钱包发起退款失败,钱包充值订单已退款"); + ErrorCode WALLET_RECHARGE_REFUND_BALANCE_NOT_ENOUGH = new ErrorCode(1_007_008_007, "钱包发起退款失败,钱包余额不足"); + ErrorCode WALLET_RECHARGE_REFUND_FAIL_REFUND_ORDER_ID_ERROR = new ErrorCode(1_007_008_008, "钱包退款更新失败,钱包退款单编号不匹配"); + ErrorCode WALLET_RECHARGE_REFUND_FAIL_REFUND_NOT_FOUND = new ErrorCode(1_007_008_009, "钱包退款更新失败,退款订单不存在"); + ErrorCode WALLET_RECHARGE_REFUND_FAIL_REFUND_PRICE_NOT_MATCH = new ErrorCode(1_007_008_010, "钱包退款更新失败,退款单金额不匹配"); + ErrorCode WALLET_RECHARGE_PACKAGE_NOT_FOUND = new ErrorCode(1_007_008_011, "钱包充值套餐不存在"); + ErrorCode WALLET_RECHARGE_PACKAGE_IS_DISABLE = new ErrorCode(1_007_008_012, "钱包充值套餐已禁用"); + ErrorCode WALLET_RECHARGE_PACKAGE_NAME_EXISTS = new ErrorCode(1_007_008_013, "钱包充值套餐名称已存在"); + + // ========== 转账模块 1-007-009-000 ========== + ErrorCode PAY_TRANSFER_NOT_FOUND = new ErrorCode(1_007_009_001, "转账单不存在"); + ErrorCode PAY_TRANSFER_CREATE_CHANNEL_NOT_MATCH = new ErrorCode(1_007_009_002, "转账发起失败,原因:两次相同转账请求的类型不匹配"); + ErrorCode PAY_TRANSFER_CREATE_PRICE_NOT_MATCH = new ErrorCode(1_007_009_003, "转账发起失败,原因:两次相同转账请求的金额不匹配"); + ErrorCode PAY_TRANSFER_CREATE_FAIL_STATUS_NOT_CLOSED = new ErrorCode(1_007_009_004, "转账发起失败,原因:已经存在相同的转账单,且状态不是已关闭"); + ErrorCode PAY_TRANSFER_NOTIFY_FAIL_STATUS_IS_NOT_WAITING = new ErrorCode(1_007_009_006, "通知转账结果失败,原因:转账单不处于待转账"); + ErrorCode PAY_TRANSFER_NOTIFY_FAIL_STATUS_NOT_WAITING_OR_PROCESSING = new ErrorCode(1_007_009_007, "通知转账结果失败,原因:转账单不处于待转账或转账中"); + + // ========== 示例订单 1-007-900-000 ========== + ErrorCode DEMO_ORDER_NOT_FOUND = new ErrorCode(1_007_900_000, "示例订单不存在"); + ErrorCode DEMO_ORDER_UPDATE_PAID_STATUS_NOT_UNPAID = new ErrorCode(1_007_900_001, "示例订单更新支付状态失败,订单不是【未支付】状态"); + ErrorCode DEMO_ORDER_UPDATE_PAID_FAIL_PAY_ORDER_ID_ERROR = new ErrorCode(1_007_900_002, "示例订单更新支付状态失败,支付单编号不匹配"); + ErrorCode DEMO_ORDER_UPDATE_PAID_FAIL_PAY_ORDER_STATUS_NOT_SUCCESS = new ErrorCode(1_007_900_003, "示例订单更新支付状态失败,支付单状态不是【支付成功】状态"); + ErrorCode DEMO_ORDER_UPDATE_PAID_FAIL_PAY_PRICE_NOT_MATCH = new ErrorCode(1_007_900_004, "示例订单更新支付状态失败,支付单金额不匹配"); + ErrorCode DEMO_ORDER_REFUND_FAIL_NOT_PAID = new ErrorCode(1_007_900_005, "发起退款失败,示例订单未支付"); + ErrorCode DEMO_ORDER_REFUND_FAIL_REFUNDED = new ErrorCode(1_007_900_006, "发起退款失败,示例订单已退款"); + ErrorCode DEMO_ORDER_REFUND_FAIL_REFUND_NOT_FOUND = new ErrorCode(1_007_900_007, "发起退款失败,退款订单不存在"); + ErrorCode DEMO_ORDER_REFUND_FAIL_REFUND_NOT_SUCCESS = new ErrorCode(1_007_900_008, "发起退款失败,退款订单未退款成功"); + ErrorCode DEMO_ORDER_REFUND_FAIL_REFUND_ORDER_ID_ERROR = new ErrorCode(1_007_900_009, "发起退款失败,退款单编号不匹配"); + ErrorCode DEMO_ORDER_REFUND_FAIL_REFUND_PRICE_NOT_MATCH = new ErrorCode(1_007_900_010, "发起退款失败,退款单金额不匹配"); + + // ========== 示例提现单 1-007-901-000 ========== + ErrorCode DEMO_WITHDRAW_NOT_FOUND = new ErrorCode(1_007_901_000, "示例提现单不存在"); + ErrorCode DEMO_WITHDRAW_UPDATE_STATUS_FAIL_PAY_TRANSFER_ID_ERROR = new ErrorCode(1_007_901_001, "更新示例提现单状态失败,支付转账单编号不匹配"); + ErrorCode DEMO_WITHDRAW_UPDATE_STATUS_FAIL_PAY_TRANSFER_STATUS_NOT_SUCCESS_OR_CLOSED = new ErrorCode(1_007_901_002, "更新示例提现单状态失败,支付转账单状态不是【转账成功】或【转账失败】状态"); + ErrorCode DEMO_WITHDRAW_UPDATE_STATUS_FAIL_PAY_PRICE_NOT_MATCH = new ErrorCode(1_007_901_003, "更新示例提现单状态失败,支付转账单金额不匹配"); + ErrorCode DEMO_WITHDRAW_UPDATE_STATUS_FAIL_PAY_MERCHANT_EXISTS = new ErrorCode(1_007_901_004, "更新示例提现单状态失败,支付转账单商户订单号不匹配"); + ErrorCode DEMO_WITHDRAW_UPDATE_STATUS_FAIL_PAY_CHANNEL_NOT_MATCH = new ErrorCode(1_007_901_005, "更新示例提现单状态失败,支付转账单渠道不匹配"); + ErrorCode DEMO_WITHDRAW_TRANSFER_FAIL_STATUS_NOT_WAITING_OR_CLOSED = new ErrorCode(1_007_901_008, "发起转账失败,原因:示例提现单状态不是【等待提现】或【提现关闭】"); + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/enums/MessageTemplateConstants.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/enums/MessageTemplateConstants.java new file mode 100644 index 0000000..858a0ab --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/enums/MessageTemplateConstants.java @@ -0,0 +1,14 @@ +package cn.aagro.pp.module.pay.enums; + +/** + * 通知模板枚举类 + * + * @author HUIHUI + */ +public interface MessageTemplateConstants { + + // ======================= 小程序订阅消息 ======================= + + String WXA_WALLET_RECHARGER_PAID = "充值成功通知"; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/enums/PayChannelEnum.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/enums/PayChannelEnum.java new file mode 100644 index 0000000..4ee4bfb --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/enums/PayChannelEnum.java @@ -0,0 +1,67 @@ +package cn.aagro.pp.module.pay.enums; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; +import cn.aagro.pp.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 支付渠道的编码的枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum PayChannelEnum implements ArrayValuable { + + WX_PUB("wx_pub", "微信 JSAPI 支付"), // 公众号网页 + WX_LITE("wx_lite", "微信小程序支付"), + WX_APP("wx_app", "微信 App 支付"), + WX_NATIVE("wx_native", "微信 Native 支付"), + WX_WAP("wx_wap", "微信 Wap 网站支付"), // H5 网页 + WX_BAR("wx_bar", "微信付款码支付"), + + ALIPAY_PC("alipay_pc", "支付宝 PC 网站支付"), + ALIPAY_WAP("alipay_wap", "支付宝 Wap 网站支付"), + ALIPAY_APP("alipay_app", "支付宝App 支付"), + ALIPAY_QR("alipay_qr", "支付宝扫码支付"), + ALIPAY_BAR("alipay_bar", "支付宝条码支付"), + + MOCK("mock", "模拟支付"), + + WALLET("wallet", "钱包支付"); + + public static final String[] ARRAYS = Arrays.stream(values()).map(PayChannelEnum::getCode).toArray(String[]::new); + + /** + * 编码 + * + * 参考 支付渠道属性值 + */ + private final String code; + /** + * 名字 + */ + private final String name; + + @Override + public String[] array() { + return ARRAYS; + } + + public static PayChannelEnum getByCode(String code) { + return ArrayUtil.firstMatch(o -> o.getCode().equals(code), values()); + } + + public static boolean isAlipay(String channelCode) { + return StrUtil.startWith(channelCode, "alipay_"); + } + + public static boolean isWeixin(String channelCode) { + return StrUtil.startWith(channelCode, "wx_"); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/enums/demo/PayDemoWithdrawStatusEnum.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/enums/demo/PayDemoWithdrawStatusEnum.java new file mode 100644 index 0000000..084e3d0 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/enums/demo/PayDemoWithdrawStatusEnum.java @@ -0,0 +1,42 @@ +package cn.aagro.pp.module.pay.enums.demo; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Objects; + +/** + * 示例提现单的状态枚举 + * + * @author jason + */ +@Getter +@AllArgsConstructor +public enum PayDemoWithdrawStatusEnum { + + WAITING(0, "等待提现"), + SUCCESS(10, "提现成功"), + CLOSED(20, "提现关闭"); + + /** + * 状态 + */ + private final Integer status; + /** + * 状态名 + */ + private final String name; + + public static boolean isSuccess(Integer status) { + return Objects.equals(status, SUCCESS.getStatus()); + } + + public static boolean isClosed(Integer status) { + return Objects.equals(status, CLOSED.getStatus()); + } + + public static boolean isWaiting(Integer status) { + return Objects.equals(status, WAITING.getStatus()); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/enums/demo/PayDemoWithdrawTypeEnum.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/enums/demo/PayDemoWithdrawTypeEnum.java new file mode 100644 index 0000000..1fa854c --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/enums/demo/PayDemoWithdrawTypeEnum.java @@ -0,0 +1,39 @@ +package cn.aagro.pp.module.pay.enums.demo; + +import cn.aagro.pp.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 示例提现单的类型枚举 + * + * @author owen + */ +@AllArgsConstructor +@Getter +public enum PayDemoWithdrawTypeEnum implements ArrayValuable { + + WECHAT(2, "微信"), + ALIPAY(1, "支付宝"), + WALLET(3, "钱包"), + ; + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(PayDemoWithdrawTypeEnum::getType).toArray(Integer[]::new); + + /** + * 类型 + */ + private final Integer type; + /** + * 名字 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/enums/notify/PayNotifyStatusEnum.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/enums/notify/PayNotifyStatusEnum.java new file mode 100644 index 0000000..b28544d --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/enums/notify/PayNotifyStatusEnum.java @@ -0,0 +1,32 @@ +package cn.aagro.pp.module.pay.enums.notify; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 支付通知状态枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum PayNotifyStatusEnum { + + WAITING(0, "等待通知"), + SUCCESS(10, "通知成功"), + FAILURE(20, "通知失败"), // 多次尝试,彻底失败 + REQUEST_SUCCESS(21, "请求成功,但是结果失败"), + REQUEST_FAILURE(22, "请求失败"), + + ; + + /** + * 状态 + */ + private final Integer status; + /** + * 名字 + */ + private final String name; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/enums/notify/PayNotifyTypeEnum.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/enums/notify/PayNotifyTypeEnum.java new file mode 100644 index 0000000..af23f8f --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/enums/notify/PayNotifyTypeEnum.java @@ -0,0 +1,29 @@ +package cn.aagro.pp.module.pay.enums.notify; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 支付通知类型 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum PayNotifyTypeEnum { + + ORDER(1, "支付单"), + REFUND(2, "退款单"), + TRANSFER(3, "转账单") + ; + + /** + * 类型 + */ + private final Integer type; + /** + * 名字 + */ + private final String name; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/enums/order/PayOrderStatusEnum.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/enums/order/PayOrderStatusEnum.java new file mode 100644 index 0000000..a235365 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/enums/order/PayOrderStatusEnum.java @@ -0,0 +1,84 @@ +package cn.aagro.pp.module.pay.enums.order; + +import cn.aagro.pp.framework.common.core.ArrayValuable; +import cn.aagro.pp.framework.common.util.object.ObjectUtils; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Objects; + +/** + * 支付订单的状态枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum PayOrderStatusEnum implements ArrayValuable { + + WAITING(0, "未支付"), + SUCCESS(10, "支付成功"), + REFUND(20, "已退款"), + CLOSED(30, "支付关闭"), // 注意:全部退款后,还是 REFUND 状态 + ; + + private final Integer status; + private final String name; + + @Override + public Integer[] array() { + return new Integer[0]; + } + + /** + * 判断是否等待支付 + * + * @param status 状态 + * @return 是否等待支付 + */ + public static boolean isWaiting(Integer status) { + return Objects.equals(status, WAITING.getStatus()); + } + + /** + * 判断是否支付成功 + * + * @param status 状态 + * @return 是否支付成功 + */ + public static boolean isSuccess(Integer status) { + return Objects.equals(status, SUCCESS.getStatus()); + } + + /** + * 判断是否已退款 + * + * @param status 状态 + * @return 是否已退款 + */ + public static boolean isRefund(Integer status) { + return Objects.equals(status, REFUND.getStatus()); + } + + /** + * 判断是否支付成功或者已退款 + * + * @param status 状态 + * @return 是否支付成功或者已退款 + */ + public static boolean isSuccessOrRefund(Integer status) { + return ObjectUtils.equalsAny(status, + SUCCESS.getStatus(), REFUND.getStatus()); + } + + /** + * 判断是否支付关闭 + * + * @param status 状态 + * @return 是否支付关闭 + */ + public static boolean isClosed(Integer status) { + return Objects.equals(status, CLOSED.getStatus()); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/enums/refund/PayRefundStatusEnum.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/enums/refund/PayRefundStatusEnum.java new file mode 100644 index 0000000..41a47a1 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/enums/refund/PayRefundStatusEnum.java @@ -0,0 +1,32 @@ +package cn.aagro.pp.module.pay.enums.refund; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Objects; + +/** + * 渠道的退款状态枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum PayRefundStatusEnum { + + WAITING(0, "未退款"), + SUCCESS(10, "退款成功"), + FAILURE(20, "退款失败"); + + private final Integer status; + private final String name; + + public static boolean isSuccess(Integer status) { + return Objects.equals(status, SUCCESS.getStatus()); + } + + public static boolean isFailure(Integer status) { + return Objects.equals(status, FAILURE.getStatus()); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/enums/transfer/PayTransferStatusEnum.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/enums/transfer/PayTransferStatusEnum.java new file mode 100644 index 0000000..5062957 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/enums/transfer/PayTransferStatusEnum.java @@ -0,0 +1,67 @@ +package cn.aagro.pp.module.pay.enums.transfer; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Objects; + +/** + * 渠道的转账状态枚举 + * + * @author jason + */ +@Getter +@AllArgsConstructor +public enum PayTransferStatusEnum { + + WAITING(0, "等待转账"), + PROCESSING(5, "转账进行中"), + SUCCESS(10, "转账成功"), + CLOSED(20, "转账关闭"); + + /** + * 状态 + */ + private final Integer status; + /** + * 状态名 + */ + private final String name; + + public static boolean isSuccess(Integer status) { + return Objects.equals(status, SUCCESS.getStatus()); + } + + public static boolean isClosed(Integer status) { + return Objects.equals(status, CLOSED.getStatus()); + } + + public static boolean isWaiting(Integer status) { + return Objects.equals(status, WAITING.getStatus()); + } + + public static boolean isProcessing(Integer status) { + return Objects.equals(status, PROCESSING.getStatus()); + } + + /** + * 是否处于待转账或者转账中的状态 + * + * @param status 状态 + * @return 是否 + */ + public static boolean isWaitingOrProcessing(Integer status) { + return isWaiting(status) || isProcessing(status); + } + + /** + * 是否处于成功或者关闭中的状态 + * + * @param status 状态 + * @return 是否 + */ + public static boolean isSuccessOrClosed(Integer status) { + return isSuccess(status) || isClosed(status); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/enums/wallet/PayWalletBizTypeEnum.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/enums/wallet/PayWalletBizTypeEnum.java new file mode 100644 index 0000000..b27853d --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/enums/wallet/PayWalletBizTypeEnum.java @@ -0,0 +1,45 @@ +package cn.aagro.pp.module.pay.enums.wallet; + +import cn.aagro.pp.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 钱包交易业务分类 + * + * @author jason + */ +@AllArgsConstructor +@Getter +public enum PayWalletBizTypeEnum implements ArrayValuable { + + RECHARGE(1, "充值"), + RECHARGE_REFUND(2, "充值退款"), + PAYMENT(3, "支付"), + PAYMENT_REFUND(4, "支付退款"), + UPDATE_BALANCE(5, "更新余额"), + TRANSFER(6, "转账"); + + /** + * 业务分类 + */ + private final Integer type; + /** + * 说明 + */ + private final String description; + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(PayWalletBizTypeEnum::getType).toArray(Integer[]::new); + + @Override + public Integer[] array() { + return ARRAYS; + } + + public static PayWalletBizTypeEnum valueOf(Integer type) { + return Arrays.stream(values()).filter(item -> item.getType().equals(type)).findFirst().orElse(null); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/job/config/PayJobConfiguration.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/job/config/PayJobConfiguration.java new file mode 100644 index 0000000..818a737 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/job/config/PayJobConfiguration.java @@ -0,0 +1,28 @@ +package cn.aagro.pp.module.pay.framework.job.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import java.util.concurrent.ThreadPoolExecutor; + +@Configuration(proxyBeanMethods = false) +public class PayJobConfiguration { + + public static final String NOTIFY_THREAD_POOL_TASK_EXECUTOR = "NOTIFY_THREAD_POOL_TASK_EXECUTOR"; + + @Bean(NOTIFY_THREAD_POOL_TASK_EXECUTOR) + public ThreadPoolTaskExecutor notifyThreadPoolTaskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(8); // 设置核心线程数 + executor.setMaxPoolSize(16); // 设置最大线程数 + executor.setKeepAliveSeconds(60); // 设置空闲时间 + executor.setQueueCapacity(100); // 设置队列大小 + executor.setThreadNamePrefix("notify-task-"); // 配置线程池的前缀 + executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + // 进行加载 + executor.initialize(); + return executor; + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/job/core/package-info.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/job/core/package-info.java new file mode 100644 index 0000000..0a0e7b7 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/job/core/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位 + */ +package cn.aagro.pp.module.pay.framework.job.core; diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/package-info.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/package-info.java new file mode 100644 index 0000000..5996c02 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/package-info.java @@ -0,0 +1,6 @@ +/** + * 属于 pay 模块的 framework 封装 + * + * @author 芋道源码 + */ +package cn.aagro.pp.module.pay.framework; diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/config/PayConfiguration.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/config/PayConfiguration.java new file mode 100644 index 0000000..2476977 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/config/PayConfiguration.java @@ -0,0 +1,18 @@ +package cn.aagro.pp.module.pay.framework.pay.config; + +import cn.aagro.pp.module.pay.framework.pay.core.client.PayClientFactory; +import cn.aagro.pp.module.pay.framework.pay.core.client.impl.PayClientFactoryImpl; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties(PayProperties.class) +public class PayConfiguration { + + @Bean + public PayClientFactory payClientFactory() { + return new PayClientFactoryImpl(); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/config/PayProperties.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/config/PayProperties.java new file mode 100644 index 0000000..516cd7e --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/config/PayProperties.java @@ -0,0 +1,69 @@ +package cn.aagro.pp.module.pay.framework.pay.config; + +import lombok.Data; +import org.hibernate.validator.constraints.URL; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.NotEmpty; + +@ConfigurationProperties(prefix = "aagro.pay") +@Validated +@Data +public class PayProperties { + + private static final String ORDER_NO_PREFIX = "P"; + private static final String REFUND_NO_PREFIX = "R"; + + private static final String WALLET_PAY_APP_KEY_DEFAULT = "wallet"; + + /** + * 支付回调地址 + * + * 实际上,对应的 PayNotifyController 的 notifyOrder 方法的 URL + * + * 回调顺序:支付渠道(支付宝支付、微信支付) => aagro-module-pay 的 orderNotifyUrl 地址 => 业务的 PayAppDO.orderNotifyUrl 地址 + */ + @NotEmpty(message = "支付回调地址不能为空") + @URL(message = "支付回调地址的格式必须是 URL") + private String orderNotifyUrl; + + /** + * 退款回调地址 + * + * 实际上,对应的 PayNotifyController 的 notifyRefund 方法的 URL + * + * 回调顺序:支付渠道(支付宝支付、微信支付) => aagro-module-pay 的 refundNotifyUrl 地址 => 业务的 PayAppDO.notifyRefundUrl 地址 + */ + @NotEmpty(message = "支付回调地址不能为空") + @URL(message = "支付回调地址的格式必须是 URL") + private String refundNotifyUrl; + + /** + * 转账回调地址 + * + * 实际上,对应的 PayNotifyController 的 notifyTransfer 方法的 URL + * + * 回调顺序:支付渠道(支付宝支付、微信支付) => aagro-module-pay 的 transferNotifyUrl 地址 => 业务的 PayAppDO.transferNotifyUrl 地址 + */ + private String transferNotifyUrl; + + /** + * 支付订单 no 的前缀 + */ + @NotEmpty(message = "支付订单 no 的前缀不能为空") + private String orderNoPrefix = ORDER_NO_PREFIX; + + /** + * 退款订单 no 的前缀 + */ + @NotEmpty(message = "退款订单 no 的前缀不能为空") + private String refundNoPrefix = REFUND_NO_PREFIX; + + /** + * 钱包支付应用 AppKey + */ + @NotEmpty(message = "钱包支付应用 AppKey 不能为空") + private String walletPayAppKey = WALLET_PAY_APP_KEY_DEFAULT; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/PayClient.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/PayClient.java new file mode 100644 index 0000000..78105a6 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/PayClient.java @@ -0,0 +1,118 @@ +package cn.aagro.pp.module.pay.framework.pay.core.client; + +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.refund.PayRefundRespDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.transfer.PayTransferRespDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO; + +import java.util.Map; + +/** + * 支付客户端,用于对接各支付渠道的 SDK,实现发起支付、退款等功能 + * + * @author 芋道源码 + */ +public interface PayClient { + + /** + * 获得渠道编号 + * + * @return 渠道编号 + */ + Long getId(); + + /** + * 获得渠道配置 + * + * @return 渠道配置 + */ + Config getConfig(); + + // ============ 支付相关 ========== + + /** + * 调用支付渠道,统一下单 + * + * @param reqDTO 下单信息 + * @return 支付订单信息 + */ + PayOrderRespDTO unifiedOrder(PayOrderUnifiedReqDTO reqDTO); + + /** + * 解析 order 回调数据 + * + * @param params HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数 + * @param body HTTP 回调接口的 request body + * @param headers HTTP 回调接口的 request headers + * @return 支付订单信息 + */ + PayOrderRespDTO parseOrderNotify(Map params, String body, Map headers); + + /** + * 获得支付订单信息 + * + * @param outTradeNo 外部订单号 + * @return 支付订单信息 + */ + PayOrderRespDTO getOrder(String outTradeNo); + + // ============ 退款相关 ========== + + /** + * 调用支付渠道,进行退款 + * + * @param reqDTO 统一退款请求信息 + * @return 退款信息 + */ + PayRefundRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO); + + /** + * 解析 refund 回调数据 + * + * @param params HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数 + * @param body HTTP 回调接口的 request body + * @param headers HTTP 回调接口的 request headers + * @return 支付订单信息 + */ + PayRefundRespDTO parseRefundNotify(Map params, String body, Map headers); + + /** + * 获得退款订单信息 + * + * @param outTradeNo 外部订单号 + * @param outRefundNo 外部退款号 + * @return 退款订单信息 + */ + PayRefundRespDTO getRefund(String outTradeNo, String outRefundNo); + + // ============ 转账相关 ========== + + /** + * 调用渠道,进行转账 + * + * @param reqDTO 统一转账请求信息 + * @return 转账信息 + */ + PayTransferRespDTO unifiedTransfer(PayTransferUnifiedReqDTO reqDTO); + + /** + * 获得转账订单信息 + * + * @param outTradeNo 外部订单号 + * @return 转账信息 + */ + PayTransferRespDTO getTransfer(String outTradeNo); + + /** + * 解析 transfer 回调数据 + * + * @param params HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数 + * @param body HTTP 回调接口的 request body + * @param headers HTTP 回调接口的 request headers + * @return 转账信息 + */ + PayTransferRespDTO parseTransferNotify(Map params, String body, Map headers); + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/PayClientConfig.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/PayClientConfig.java new file mode 100644 index 0000000..0afba1a --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/PayClientConfig.java @@ -0,0 +1,27 @@ +package cn.aagro.pp.module.pay.framework.pay.core.client; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import javax.validation.Validator; + +/** + * 支付客户端的配置,本质是支付渠道的配置 + * 每个不同的渠道,需要不同的配置,通过子类来定义 + * + * @author 芋道源码 + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +// @JsonTypeInfo 注解的作用,Jackson 多态 +// 1. 序列化到时数据库时,增加 @class 属性。 +// 2. 反序列化到内存对象时,通过 @class 属性,可以创建出正确的类型 +@JsonIgnoreProperties(ignoreUnknown = true) // 目的:忽略未知的属性,避免反序列化失败 +public interface PayClientConfig { + + /** + * 参数校验 + * + * @param validator 校验对象 + */ + void validate(Validator validator); + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/PayClientFactory.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/PayClientFactory.java new file mode 100644 index 0000000..34de262 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/PayClientFactory.java @@ -0,0 +1,28 @@ +package cn.aagro.pp.module.pay.framework.pay.core.client; + +/** + * 支付客户端的工厂接口 + * + * @author 芋道源码 + */ +public interface PayClientFactory { + + /** + * 获得支付客户端 + * + * @param channelId 渠道编号 + * @return 支付客户端 + */ + PayClient getPayClient(Long channelId); + + /** + * 创建支付客户端 + * + * @param channelId 渠道编号 + * @param channelCode 渠道编码 + * @param config 支付配置 + * @return 支付客户端 + */ + PayClient createOrUpdatePayClient(Long channelId, String channelCode, + Config config); +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/dto/order/PayOrderRespDTO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/dto/order/PayOrderRespDTO.java new file mode 100644 index 0000000..f6b7253 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/dto/order/PayOrderRespDTO.java @@ -0,0 +1,141 @@ +package cn.aagro.pp.module.pay.framework.pay.core.client.dto.order; + +import cn.aagro.pp.module.pay.enums.order.PayOrderStatusEnum; +import cn.aagro.pp.module.pay.framework.pay.core.client.exception.PayClientException; +import cn.aagro.pp.module.pay.framework.pay.core.enums.PayOrderDisplayModeEnum; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 渠道支付订单 Response DTO + * + * @author 芋道源码 + */ +@Data +public class PayOrderRespDTO { + + /** + * 支付状态 + * + * 枚举:{@link PayOrderStatusEnum} + */ + private Integer status; + + /** + * 外部订单号 + * + * 对应 PayOrderExtensionDO 的 no 字段 + */ + private String outTradeNo; + + /** + * 支付渠道编号 + */ + private String channelOrderNo; + /** + * 支付渠道用户编号 + */ + private String channelUserId; + + /** + * 支付成功时间 + */ + private LocalDateTime successTime; + + /** + * 原始的同步/异步通知结果 + */ + private Object rawData; + + // ========== 主动发起支付时,会返回的字段 ========== + + /** + * 展示模式 + * + * 枚举 {@link PayOrderDisplayModeEnum} 类 + */ + private String displayMode; + /** + * 展示内容 + */ + private String displayContent; + + /** + * 调用渠道的错误码 + * + * 注意:这里返回的是业务异常,而是不系统异常。 + * 如果是系统异常,则会抛出 {@link PayClientException} + */ + private String channelErrorCode; + /** + * 调用渠道报错时,错误信息 + */ + private String channelErrorMsg; + + public PayOrderRespDTO() { + } + + /** + * 创建【WAITING】状态的订单返回 + */ + public static PayOrderRespDTO waitingOf(String displayMode, String displayContent, + String outTradeNo, Object rawData) { + PayOrderRespDTO respDTO = new PayOrderRespDTO(); + respDTO.status = PayOrderStatusEnum.WAITING.getStatus(); + respDTO.displayMode = displayMode; + respDTO.displayContent = displayContent; + // 相对通用的字段 + respDTO.outTradeNo = outTradeNo; + respDTO.rawData = rawData; + return respDTO; + } + + /** + * 创建【SUCCESS】状态的订单返回 + */ + public static PayOrderRespDTO successOf(String channelOrderNo, String channelUserId, LocalDateTime successTime, + String outTradeNo, Object rawData) { + PayOrderRespDTO respDTO = new PayOrderRespDTO(); + respDTO.status = PayOrderStatusEnum.SUCCESS.getStatus(); + respDTO.channelOrderNo = channelOrderNo; + respDTO.channelUserId = channelUserId; + respDTO.successTime = successTime; + // 相对通用的字段 + respDTO.outTradeNo = outTradeNo; + respDTO.rawData = rawData; + return respDTO; + } + + /** + * 创建指定状态的订单返回,适合支付渠道回调时 + */ + public static PayOrderRespDTO of(Integer status, String channelOrderNo, String channelUserId, LocalDateTime successTime, + String outTradeNo, Object rawData) { + PayOrderRespDTO respDTO = new PayOrderRespDTO(); + respDTO.status = status; + respDTO.channelOrderNo = channelOrderNo; + respDTO.channelUserId = channelUserId; + respDTO.successTime = successTime; + // 相对通用的字段 + respDTO.outTradeNo = outTradeNo; + respDTO.rawData = rawData; + return respDTO; + } + + /** + * 创建【CLOSED】状态的订单返回,适合调用支付渠道失败时 + */ + public static PayOrderRespDTO closedOf(String channelErrorCode, String channelErrorMsg, + String outTradeNo, Object rawData) { + PayOrderRespDTO respDTO = new PayOrderRespDTO(); + respDTO.status = PayOrderStatusEnum.CLOSED.getStatus(); + respDTO.channelErrorCode = channelErrorCode; + respDTO.channelErrorMsg = channelErrorMsg; + // 相对通用的字段 + respDTO.outTradeNo = outTradeNo; + respDTO.rawData = rawData; + return respDTO; + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/dto/order/PayOrderUnifiedReqDTO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/dto/order/PayOrderUnifiedReqDTO.java new file mode 100644 index 0000000..81b96c3 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/dto/order/PayOrderUnifiedReqDTO.java @@ -0,0 +1,92 @@ +package cn.aagro.pp.module.pay.framework.pay.core.client.dto.order; + +import cn.aagro.pp.module.pay.framework.pay.core.enums.PayOrderDisplayModeEnum; +import lombok.Data; +import org.hibernate.validator.constraints.Length; +import org.hibernate.validator.constraints.URL; + +import javax.validation.constraints.DecimalMin; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; +import java.util.Map; + +/** + * 统一下单 Request DTO + * + * @author 芋道源码 + */ +@Data +public class PayOrderUnifiedReqDTO { + + /** + * 用户 IP + */ + @NotEmpty(message = "用户 IP 不能为空") + private String userIp; + + // ========== 商户相关字段 ========== + + /** + * 外部订单号 + * + * 对应 PayOrderExtensionDO 的 no 字段 + */ + @NotEmpty(message = "外部订单编号不能为空") + private String outTradeNo; + /** + * 商品标题 + */ + @NotEmpty(message = "商品标题不能为空") + @Length(max = 32, message = "商品标题不能超过 32") + private String subject; + /** + * 商品描述信息 + */ + @Length(max = 128, message = "商品描述信息长度不能超过128") + private String body; + /** + * 支付结果的 notify 回调地址 + */ + @NotEmpty(message = "支付结果的回调地址不能为空") + @URL(message = "支付结果的 notify 回调地址必须是 URL 格式") + private String notifyUrl; + /** + * 支付结果的 return 回调地址 + */ + @URL(message = "支付结果的 return 回调地址必须是 URL 格式") + private String returnUrl; + + // ========== 订单相关字段 ========== + + /** + * 支付金额,单位:分 + */ + @NotNull(message = "支付金额不能为空") + @DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零") + private Integer price; + + /** + * 支付过期时间 + */ + @NotNull(message = "支付过期时间不能为空") + private LocalDateTime expireTime; + + // ========== 拓展参数 ========== + /** + * 支付渠道的额外参数 + * + * 例如说,微信公众号需要传递 openid 参数 + */ + private Map channelExtras; + + /** + * 展示模式 + * + * 如果不传递,则每个支付渠道使用默认的方式 + * + * 枚举 {@link PayOrderDisplayModeEnum} + */ + private String displayMode; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/dto/refund/PayRefundRespDTO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/dto/refund/PayRefundRespDTO.java new file mode 100644 index 0000000..51d1def --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/dto/refund/PayRefundRespDTO.java @@ -0,0 +1,115 @@ +package cn.aagro.pp.module.pay.framework.pay.core.client.dto.refund; + +import cn.aagro.pp.module.pay.enums.refund.PayRefundStatusEnum; +import cn.aagro.pp.module.pay.framework.pay.core.client.exception.PayClientException; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 渠道退款订单 Response DTO + * + * @author jason + */ +@Data +public class PayRefundRespDTO { + + /** + * 退款状态 + * + * 枚举 {@link PayRefundStatusEnum} + */ + private Integer status; + + /** + * 外部退款号 + * + * 对应 PayRefundDO 的 no 字段 + */ + private String outRefundNo; + + /** + * 渠道退款单号 + * + * 对应 PayRefundDO.channelRefundNo 字段 + */ + private String channelRefundNo; + + /** + * 退款成功时间 + */ + private LocalDateTime successTime; + + /** + * 原始的异步通知结果 + */ + private Object rawData; + + /** + * 调用渠道的错误码 + * + * 注意:这里返回的是业务异常,而是不系统异常。 + * 如果是系统异常,则会抛出 {@link PayClientException} + */ + private String channelErrorCode; + /** + * 调用渠道报错时,错误信息 + */ + private String channelErrorMsg; + + private PayRefundRespDTO() { + } + + /** + * 创建【WAITING】状态的退款返回 + */ + public static PayRefundRespDTO waitingOf(String channelRefundNo, + String outRefundNo, Object rawData) { + PayRefundRespDTO respDTO = new PayRefundRespDTO(); + respDTO.status = PayRefundStatusEnum.WAITING.getStatus(); + respDTO.channelRefundNo = channelRefundNo; + // 相对通用的字段 + respDTO.outRefundNo = outRefundNo; + respDTO.rawData = rawData; + return respDTO; + } + + /** + * 创建【SUCCESS】状态的退款返回 + */ + public static PayRefundRespDTO successOf(String channelRefundNo, LocalDateTime successTime, + String outRefundNo, Object rawData) { + PayRefundRespDTO respDTO = new PayRefundRespDTO(); + respDTO.status = PayRefundStatusEnum.SUCCESS.getStatus(); + respDTO.channelRefundNo = channelRefundNo; + respDTO.successTime = successTime; + // 相对通用的字段 + respDTO.outRefundNo = outRefundNo; + respDTO.rawData = rawData; + return respDTO; + } + + /** + * 创建【FAILURE】状态的退款返回 + */ + public static PayRefundRespDTO failureOf(String outRefundNo, Object rawData) { + return failureOf(null, null, + outRefundNo, rawData); + } + + /** + * 创建【FAILURE】状态的退款返回 + */ + public static PayRefundRespDTO failureOf(String channelErrorCode, String channelErrorMsg, + String outRefundNo, Object rawData) { + PayRefundRespDTO respDTO = new PayRefundRespDTO(); + respDTO.status = PayRefundStatusEnum.FAILURE.getStatus(); + respDTO.channelErrorCode = channelErrorCode; + respDTO.channelErrorMsg = channelErrorMsg; + // 相对通用的字段 + respDTO.outRefundNo = outRefundNo; + respDTO.rawData = rawData; + return respDTO; + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/dto/refund/PayRefundUnifiedReqDTO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/dto/refund/PayRefundUnifiedReqDTO.java new file mode 100644 index 0000000..89e9bf6 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/dto/refund/PayRefundUnifiedReqDTO.java @@ -0,0 +1,68 @@ +package cn.aagro.pp.module.pay.framework.pay.core.client.dto.refund; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.URL; + +import javax.validation.constraints.DecimalMin; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + * 统一 退款 Request DTO + * + * @author jason + */ +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class PayRefundUnifiedReqDTO { + + /** + * 外部订单号 + * + * 对应 PayOrderExtensionDO 的 no 字段 + */ + @NotEmpty(message = "外部订单编号不能为空") + private String outTradeNo; + + /** + * 外部退款号 + * + * 对应 PayRefundDO 的 no 字段 + */ + @NotEmpty(message = "退款请求单号不能为空") + private String outRefundNo; + + /** + * 退款原因 + */ + @NotEmpty(message = "退款原因不能为空") + private String reason; + + /** + * 支付金额,单位:分 + * + * 目前微信支付在退款的时候,必须传递该字段 + */ + @NotNull(message = "支付金额不能为空") + @DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零") + private Integer payPrice; + /** + * 退款金额,单位:分 + */ + @NotNull(message = "退款金额不能为空") + @DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零") + private Integer refundPrice; + + /** + * 退款结果的 notify 回调地址 + */ + @NotEmpty(message = "支付结果的回调地址不能为空") + @URL(message = "支付结果的 notify 回调地址必须是 URL 格式") + private String notifyUrl; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/dto/transfer/PayTransferRespDTO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/dto/transfer/PayTransferRespDTO.java new file mode 100644 index 0000000..6cb1723 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/dto/transfer/PayTransferRespDTO.java @@ -0,0 +1,116 @@ +package cn.aagro.pp.module.pay.framework.pay.core.client.dto.transfer; + +import cn.aagro.pp.module.pay.enums.transfer.PayTransferStatusEnum; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 统一转账 Response DTO + * + * @author jason + */ +@Data +public class PayTransferRespDTO { + + /** + * 转账状态 + * + * 关联 {@link PayTransferStatusEnum} + */ + private Integer status; + + /** + * 外部转账单号 + */ + private String outTransferNo; + + /** + * 支付渠道编号 + */ + private String channelTransferNo; + + /** + * 支付成功时间 + */ + private LocalDateTime successTime; + + /** + * 原始的返回结果 + */ + private Object rawData; + + /** + * 调用渠道的错误码 + */ + private String channelErrorCode; + /** + * 调用渠道报错时,错误信息 + */ + private String channelErrorMsg; + + /** + * 渠道 package 信息 + * + * 特殊:目前只有微信转账有这个东西!!! + * @see JSAPI 调起用户确认收款 + */ + private String channelPackageInfo; + + /** + * 创建【WAITING】状态的转账返回 + */ + public static PayTransferRespDTO waitingOf(String channelTransferNo, + String outTransferNo, Object rawData) { + PayTransferRespDTO respDTO = new PayTransferRespDTO(); + respDTO.status = PayTransferStatusEnum.WAITING.getStatus(); + respDTO.channelTransferNo = channelTransferNo; + respDTO.outTransferNo = outTransferNo; + respDTO.rawData = rawData; + return respDTO; + } + + /** + * 创建【IN_PROGRESS】状态的转账返回 + */ + public static PayTransferRespDTO processingOf(String channelTransferNo, + String outTransferNo, Object rawData) { + PayTransferRespDTO respDTO = new PayTransferRespDTO(); + respDTO.status = PayTransferStatusEnum.PROCESSING.getStatus(); + respDTO.channelTransferNo = channelTransferNo; + respDTO.outTransferNo = outTransferNo; + respDTO.rawData = rawData; + return respDTO; + } + + /** + * 创建【CLOSED】状态的转账返回 + */ + public static PayTransferRespDTO closedOf(String channelErrorCode, String channelErrorMsg, + String outTransferNo, Object rawData) { + PayTransferRespDTO respDTO = new PayTransferRespDTO(); + respDTO.status = PayTransferStatusEnum.CLOSED.getStatus(); + respDTO.channelErrorCode = channelErrorCode; + respDTO.channelErrorMsg = channelErrorMsg; + // 相对通用的字段 + respDTO.outTransferNo = outTransferNo; + respDTO.rawData = rawData; + return respDTO; + } + + /** + * 创建【SUCCESS】状态的转账返回 + */ + public static PayTransferRespDTO successOf(String channelTransferNo, LocalDateTime successTime, + String outTransferNo, Object rawData) { + PayTransferRespDTO respDTO = new PayTransferRespDTO(); + respDTO.status = PayTransferStatusEnum.SUCCESS.getStatus(); + respDTO.channelTransferNo = channelTransferNo; + respDTO.successTime = successTime; + // 相对通用的字段 + respDTO.outTransferNo = outTransferNo; + respDTO.rawData = rawData; + return respDTO; + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/dto/transfer/PayTransferUnifiedReqDTO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/dto/transfer/PayTransferUnifiedReqDTO.java new file mode 100644 index 0000000..8614e8b --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/dto/transfer/PayTransferUnifiedReqDTO.java @@ -0,0 +1,73 @@ +package cn.aagro.pp.module.pay.framework.pay.core.client.dto.transfer; + +import lombok.Data; +import org.hibernate.validator.constraints.Length; +import org.hibernate.validator.constraints.URL; + +import javax.validation.constraints.Min; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.Map; + +/** + * 统一转账 Request DTO + * + * @author jason + */ +@Data +public class PayTransferUnifiedReqDTO { + + /** + * 用户 IP + */ + @NotEmpty(message = "用户 IP 不能为空") + private String userIp; + + /** + * 外部转账单编号 + */ + @NotEmpty(message = "外部转账单编号不能为空") + private String outTransferNo; + + /** + * 转账金额,单位:分 + */ + @NotNull(message = "转账金额不能为空") + @Min(value = 1, message = "转账金额必须大于零") + private Integer price; + + /** + * 转账标题 + */ + @NotEmpty(message = "转账标题不能为空") + @Length(max = 128, message = "转账标题不能超过 128") + private String subject; + + /** + * 收款人账号 + * + * 微信场景下:openid + * 支付宝场景下:支付宝账号 + */ + @NotEmpty(message = "收款人账号不能为空") + private String userAccount; + /** + * 收款人姓名 + */ + private String userName; + + /** + * 支付渠道的额外参数 + * + * 微信支付:sceneId 和 scene_report_infos 字段,必须传递;参考 按转账场景报备背景信息 + */ + private Map channelExtras; + + /** + * 转账结果的 notify 回调地址 + */ + @NotEmpty(message = "转账结果的回调地址不能为空") + @URL(message = "转账结果的 notify 回调地址必须是 URL 格式") + private String notifyUrl; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/exception/PayClientException.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/exception/PayClientException.java new file mode 100644 index 0000000..6ef653a --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/exception/PayClientException.java @@ -0,0 +1,17 @@ +package cn.aagro.pp.module.pay.framework.pay.core.client.exception; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 支付系统异常 Exception + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class PayClientException extends RuntimeException { + + public PayClientException(Throwable cause) { + super(cause); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/AbstractPayClient.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/AbstractPayClient.java new file mode 100644 index 0000000..cadb898 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/AbstractPayClient.java @@ -0,0 +1,251 @@ +package cn.aagro.pp.module.pay.framework.pay.core.client.impl; + +import cn.aagro.pp.framework.common.exception.ServiceException; +import cn.aagro.pp.framework.common.util.validation.ValidationUtils; +import cn.aagro.pp.module.pay.framework.pay.core.client.PayClient; +import cn.aagro.pp.module.pay.framework.pay.core.client.PayClientConfig; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.refund.PayRefundRespDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.transfer.PayTransferRespDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.exception.PayClientException; +import lombok.extern.slf4j.Slf4j; + +import java.util.Map; + +import static cn.aagro.pp.framework.common.util.json.JsonUtils.toJsonString; + +/** + * 支付客户端的抽象类,提供模板方法,减少子类的冗余代码 + * + * @author 芋道源码 + */ +@Slf4j +public abstract class AbstractPayClient implements PayClient { + + /** + * 渠道编号 + */ + private final Long channelId; + /** + * 渠道编码 + */ + @SuppressWarnings("FieldCanBeLocal") + private final String channelCode; + /** + * 支付配置 + */ + protected Config config; + + public AbstractPayClient(Long channelId, String channelCode, Config config) { + this.channelId = channelId; + this.channelCode = channelCode; + this.config = config; + } + + /** + * 初始化 + */ + public final void init() { + doInit(); + log.debug("[init][客户端({}) 初始化完成]", getId()); + } + + /** + * 自定义初始化 + */ + protected abstract void doInit(); + + public final void refresh(Config config) { + // 判断是否更新 + if (config.equals(this.config)) { + return; + } + log.info("[refresh][客户端({})发生变化,重新初始化]", getId()); + this.config = config; + // 初始化 + this.init(); + } + + @Override + public Long getId() { + return channelId; + } + + @Override + public Config getConfig() { + return config; + } + + // ============ 支付相关 ========== + + @Override + public final PayOrderRespDTO unifiedOrder(PayOrderUnifiedReqDTO reqDTO) { + ValidationUtils.validate(reqDTO); + // 执行统一下单 + PayOrderRespDTO resp; + try { + resp = doUnifiedOrder(reqDTO); + } catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可 + throw ex; + } catch (Throwable ex) { + // 系统异常,则包装成 PayException 异常抛出 + log.error("[unifiedOrder][客户端({}) request({}) 发起支付异常]", + getId(), toJsonString(reqDTO), ex); + throw buildPayException(ex); + } + return resp; + } + + protected abstract PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) + throws Throwable; + + @Override + public final PayOrderRespDTO parseOrderNotify(Map params, String body, Map headers) { + try { + return doParseOrderNotify(params, body, headers); + } catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可 + throw ex; + } catch (Throwable ex) { + log.error("[parseOrderNotify][客户端({}) params({}) body({}) headers({}) 解析失败]", + getId(), params, body, headers, ex); + throw buildPayException(ex); + } + } + + protected abstract PayOrderRespDTO doParseOrderNotify(Map params, String body, Map headers) + throws Throwable; + + @Override + public final PayOrderRespDTO getOrder(String outTradeNo) { + try { + return doGetOrder(outTradeNo); + } catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可 + throw ex; + } catch (Throwable ex) { + log.error("[getOrder][客户端({}) outTradeNo({}) 查询支付单异常]", + getId(), outTradeNo, ex); + throw buildPayException(ex); + } + } + + protected abstract PayOrderRespDTO doGetOrder(String outTradeNo) + throws Throwable; + + // ============ 退款相关 ========== + + @Override + public final PayRefundRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO) { + ValidationUtils.validate(reqDTO); + // 执行统一退款 + PayRefundRespDTO resp; + try { + resp = doUnifiedRefund(reqDTO); + } catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可 + throw ex; + } catch (Throwable ex) { + // 系统异常,则包装成 PayException 异常抛出 + log.error("[unifiedRefund][客户端({}) request({}) 发起退款异常]", + getId(), toJsonString(reqDTO), ex); + throw buildPayException(ex); + } + return resp; + } + + protected abstract PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable; + + @Override + public final PayRefundRespDTO parseRefundNotify(Map params, String body, Map headers) { + try { + return doParseRefundNotify(params, body, headers); + } catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可 + throw ex; + } catch (Throwable ex) { + log.error("[parseRefundNotify][客户端({}) params({}) body({}) headers({}) 解析失败]", + getId(), params, body, headers, ex); + throw buildPayException(ex); + } + } + + protected abstract PayRefundRespDTO doParseRefundNotify(Map params, String body, Map headers) + throws Throwable; + + @Override + public final PayRefundRespDTO getRefund(String outTradeNo, String outRefundNo) { + try { + return doGetRefund(outTradeNo, outRefundNo); + } catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可 + throw ex; + } catch (Throwable ex) { + log.error("[getRefund][客户端({}) outTradeNo({}) outRefundNo({}) 查询退款单异常]", + getId(), outTradeNo, outRefundNo, ex); + throw buildPayException(ex); + } + } + + protected abstract PayRefundRespDTO doGetRefund(String outTradeNo, String outRefundNo) + throws Throwable; + + @Override + public final PayTransferRespDTO unifiedTransfer(PayTransferUnifiedReqDTO reqDTO) { + PayTransferRespDTO resp; + try { + resp = doUnifiedTransfer(reqDTO); + } catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可 + throw ex; + } catch (Throwable ex) { + // 系统异常,则包装成 PayException 异常抛出 + log.error("[unifiedTransfer][客户端({}) request({}) 发起转账异常]", + getId(), toJsonString(reqDTO), ex); + throw buildPayException(ex); + } + return resp; + } + + @Override + public final PayTransferRespDTO parseTransferNotify(Map params, String body, Map headers) { + try { + return doParseTransferNotify(params, body, headers); + } catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可 + throw ex; + } catch (Throwable ex) { + log.error("[doParseTransferNotify][客户端({}) params({}) body({}) headers({}) 解析失败]", + getId(), params, body, headers, ex); + throw buildPayException(ex); + } + } + + protected abstract PayTransferRespDTO doParseTransferNotify(Map params, String body, Map headers) + throws Throwable; + + @Override + public final PayTransferRespDTO getTransfer(String outTradeNo) { + try { + return doGetTransfer(outTradeNo); + } catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可 + throw ex; + } catch (Throwable ex) { + log.error("[getTransfer][客户端({}) outTradeNo({}) 查询转账单异常]", + getId(), outTradeNo, ex); + throw buildPayException(ex); + } + } + + protected abstract PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) + throws Throwable; + + protected abstract PayTransferRespDTO doGetTransfer(String outTradeNo) + throws Throwable; + + // ========== 各种工具方法 ========== + + private PayClientException buildPayException(Throwable ex) { + if (ex instanceof PayClientException) { + return (PayClientException) ex; + } + throw new PayClientException(ex); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/NonePayClientConfig.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/NonePayClientConfig.java new file mode 100644 index 0000000..4d266e4 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/NonePayClientConfig.java @@ -0,0 +1,31 @@ +package cn.aagro.pp.module.pay.framework.pay.core.client.impl; + +import cn.aagro.pp.module.pay.framework.pay.core.client.PayClientConfig; +import lombok.Data; + +import javax.validation.Validator; + +/** + * 无需任何配置 PayClientConfig 实现类 + * + * @author jason + */ +@Data +public class NonePayClientConfig implements PayClientConfig { + + /** + * 配置名称 + *

+ * 如果不加任何属性,JsonUtils.parseObject2 解析会报错,所以暂时加个名称 + */ + private String name; + + public NonePayClientConfig(){ + this.name = "none-config"; + } + + @Override + public void validate(Validator validator) { + // 无任何配置不需要校验 + } +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/PayClientFactoryImpl.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/PayClientFactoryImpl.java new file mode 100644 index 0000000..637a2cd --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/PayClientFactoryImpl.java @@ -0,0 +1,97 @@ +package cn.aagro.pp.module.pay.framework.pay.core.client.impl; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.TypeUtil; +import cn.aagro.pp.module.pay.enums.PayChannelEnum; +import cn.aagro.pp.module.pay.framework.pay.core.client.PayClient; +import cn.aagro.pp.module.pay.framework.pay.core.client.PayClientConfig; +import cn.aagro.pp.module.pay.framework.pay.core.client.PayClientFactory; +import cn.aagro.pp.module.pay.framework.pay.core.client.impl.alipay.*; +import cn.aagro.pp.module.pay.framework.pay.core.client.impl.wallet.WalletPayClient; +import cn.aagro.pp.module.pay.framework.pay.core.client.impl.weixin.*; +import cn.aagro.pp.module.pay.framework.pay.core.client.impl.mock.MockPayClient; +import lombok.extern.slf4j.Slf4j; + +import java.lang.reflect.Type; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import static cn.aagro.pp.module.pay.enums.PayChannelEnum.*; + +/** + * 支付客户端的工厂实现类 + * + * @author 芋道源码 + */ +@Slf4j +public class PayClientFactoryImpl implements PayClientFactory { + + /** + * 支付客户端 Map + * + * key:渠道编号 + */ + private final ConcurrentMap> clients = new ConcurrentHashMap<>(); + + /** + * 支付客户端 Class Map + */ + private final Map>> clientClass = new ConcurrentHashMap<>(); + + public PayClientFactoryImpl() { + // 微信支付客户端 + clientClass.put(WX_PUB, WxPubPayClient.class); + clientClass.put(WX_LITE, WxLitePayClient.class); + clientClass.put(WX_APP, WxAppPayClient.class); + clientClass.put(WX_BAR, WxBarPayClient.class); + clientClass.put(WX_NATIVE, WxNativePayClient.class); + clientClass.put(WX_WAP, WxWapPayClient.class); + // 支付包支付客户端 + clientClass.put(ALIPAY_WAP, AlipayWapPayClient.class); + clientClass.put(ALIPAY_QR, AlipayQrPayClient.class); + clientClass.put(ALIPAY_APP, AlipayAppPayClient.class); + clientClass.put(ALIPAY_PC, AlipayPcPayClient.class); + clientClass.put(ALIPAY_BAR, AlipayBarPayClient.class); + // 钱包支付客户端 + clientClass.put(WALLET, WalletPayClient.class); + // Mock 支付客户端 + clientClass.put(MOCK, MockPayClient.class); + } + + @Override + public PayClient getPayClient(Long channelId) { + AbstractPayClient client = clients.get(channelId); + if (client == null) { + log.error("[getPayClient][渠道编号({}) 找不到客户端]", channelId); + } + return client; + } + + @Override + @SuppressWarnings("unchecked") + public PayClient createOrUpdatePayClient(Long channelId, String channelCode, + Config config) { + AbstractPayClient client = (AbstractPayClient) clients.get(channelId); + if (client == null) { + client = this.createPayClient(channelId, channelCode, config); + client.init(); + clients.put(client.getId(), client); + } else { + client.refresh(config); + } + return client; + } + + @SuppressWarnings("unchecked") + private AbstractPayClient createPayClient(Long channelId, String channelCode, + Config config) { + PayChannelEnum channelEnum = PayChannelEnum.getByCode(channelCode); + Assert.notNull(channelEnum, String.format("支付渠道(%s) 为空", channelCode)); + Class payClientClass = clientClass.get(channelEnum); + Assert.notNull(payClientClass, String.format("支付渠道(%s) Class 为空", channelCode)); + return (AbstractPayClient) ReflectUtil.newInstance(payClientClass, channelId, config); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java new file mode 100644 index 0000000..9dc1f74 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java @@ -0,0 +1,379 @@ +package cn.aagro.pp.module.pay.framework.pay.core.client.impl.alipay; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HttpUtil; +import cn.aagro.pp.framework.common.util.json.JsonUtils; +import cn.aagro.pp.framework.common.util.object.ObjectUtils; +import cn.aagro.pp.module.pay.enums.order.PayOrderStatusEnum; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.refund.PayRefundRespDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.transfer.PayTransferRespDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.impl.AbstractPayClient; +import com.alipay.api.AlipayApiException; +import com.alipay.api.AlipayConfig; +import com.alipay.api.AlipayResponse; +import com.alipay.api.DefaultAlipayClient; +import com.alipay.api.domain.*; +import com.alipay.api.internal.util.AlipaySignature; +import com.alipay.api.internal.util.AntCertificationUtil; +import com.alipay.api.internal.util.codec.Base64; +import com.alipay.api.request.*; +import com.alipay.api.response.*; +import lombok.Getter; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; + +import java.nio.charset.StandardCharsets; +import java.security.cert.X509Certificate; +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; +import java.util.function.Supplier; + +import static cn.hutool.core.date.DatePattern.NORM_DATETIME_FORMATTER; +import static cn.aagro.pp.module.pay.framework.pay.core.client.impl.alipay.AlipayPayClientConfig.MODE_CERTIFICATE; +import static cn.aagro.pp.module.pay.framework.pay.core.client.impl.alipay.AlipayPayClientConfig.MODE_PUBLIC_KEY; + +/** + * 支付宝抽象类,实现支付宝统一的接口、以及部分实现(退款) + * + * @author jason + */ +@Slf4j +public abstract class AbstractAlipayPayClient extends AbstractPayClient { + + @Getter // 仅用于单测场景 + protected DefaultAlipayClient client; + + public AbstractAlipayPayClient(Long channelId, String channelCode, AlipayPayClientConfig config) { + super(channelId, channelCode, config); + } + + @Override + @SneakyThrows + protected void doInit() { + AlipayConfig alipayConfig = new AlipayConfig(); + BeanUtil.copyProperties(config, alipayConfig, false); + this.client = new DefaultAlipayClient(alipayConfig); + } + + // ============ 支付相关 ========== + + /** + * 构造支付关闭的 {@link PayOrderRespDTO} 对象 + * + * @return 支付关闭的 {@link PayOrderRespDTO} 对象 + */ + protected PayOrderRespDTO buildClosedPayOrderRespDTO(PayOrderUnifiedReqDTO reqDTO, AlipayResponse response) { + Assert.isFalse(response.isSuccess()); + return PayOrderRespDTO.closedOf(response.getSubCode(), response.getSubMsg(), + reqDTO.getOutTradeNo(), response); + } + + @Override + public PayOrderRespDTO doParseOrderNotify(Map params, String body, Map headers) throws Throwable { + // 1. 校验回调数据 + verifyNotifyData(params); + + // 2. 解析订单的状态 + // 额外说明:支付宝不仅仅支付成功会回调,再各种触发支付单数据变化时,都会进行回调,所以这里 status 的解析会写的比较复杂 + Map bodyObj = HttpUtil.decodeParamMap(body, StandardCharsets.UTF_8); + Integer status = parseStatus(bodyObj.get("trade_status")); + // 特殊逻辑: 支付宝没有退款成功的状态,所以,如果有退款金额,我们认为是退款成功 + if (MapUtil.getDouble(bodyObj, "refund_fee", 0D) > 0) { + status = PayOrderStatusEnum.REFUND.getStatus(); + } + Assert.notNull(status, (Supplier) () -> { + throw new IllegalArgumentException(StrUtil.format("body({}) 的 trade_status 不正确", body)); + }); + return PayOrderRespDTO.of(status, bodyObj.get("trade_no"), bodyObj.get("seller_id"), parseTime(params.get("gmt_payment")), + bodyObj.get("out_trade_no"), body); + } + + @Override + protected PayOrderRespDTO doGetOrder(String outTradeNo) throws Throwable { + // 1.1 构建 AlipayTradeRefundModel 请求 + AlipayTradeQueryModel model = new AlipayTradeQueryModel(); + model.setOutTradeNo(outTradeNo); + // 1.2 构建 AlipayTradeQueryRequest 请求 + AlipayTradeQueryRequest request = new AlipayTradeQueryRequest(); + request.setBizModel(model); + AlipayTradeQueryResponse response; + if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) { + // 证书模式 + response = client.certificateExecute(request); + } else { + response = client.execute(request); + } + if (!response.isSuccess()) { // 不成功,例如说订单不存在 + return PayOrderRespDTO.closedOf(response.getSubCode(), response.getSubMsg(), + outTradeNo, response); + } + // 2.2 解析订单的状态 + Integer status = parseStatus(response.getTradeStatus()); + Assert.notNull(status, () -> { + throw new IllegalArgumentException(StrUtil.format("body({}) 的 trade_status 不正确", response.getBody())); + }); + return PayOrderRespDTO.of(status, response.getTradeNo(), response.getBuyerUserId(), LocalDateTimeUtil.of(response.getSendPayDate()), + outTradeNo, response); + } + + private static Integer parseStatus(String tradeStatus) { + return Objects.equals("WAIT_BUYER_PAY", tradeStatus) ? PayOrderStatusEnum.WAITING.getStatus() + : ObjectUtils.equalsAny(tradeStatus, "TRADE_FINISHED", "TRADE_SUCCESS") ? PayOrderStatusEnum.SUCCESS.getStatus() + : Objects.equals("TRADE_CLOSED", tradeStatus) ? PayOrderStatusEnum.CLOSED.getStatus() : null; + } + + // ============ 退款相关 ========== + + /** + * 支付宝统一的退款接口 alipay.trade.refund + * + * @param reqDTO 退款请求 request DTO + * @return 退款请求 Response + */ + @Override + protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws AlipayApiException { + // 1.1 构建 AlipayTradeRefundModel 请求 + AlipayTradeRefundModel model = new AlipayTradeRefundModel(); + model.setOutTradeNo(reqDTO.getOutTradeNo()); + model.setOutRequestNo(reqDTO.getOutRefundNo()); + model.setRefundAmount(formatAmount(reqDTO.getRefundPrice())); + model.setRefundReason(reqDTO.getReason()); + // 1.2 构建 AlipayTradePayRequest 请求 + AlipayTradeRefundRequest request = new AlipayTradeRefundRequest(); + request.setBizModel(model); + + // 2.1 执行请求 + AlipayTradeRefundResponse response; + if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) { // 证书模式 + response = client.certificateExecute(request); + } else { + response = client.execute(request); + } + if (!response.isSuccess()) { + // 当出现 ACQ.SYSTEM_ERROR, 退款可能成功也可能失败。 返回 WAIT 状态. 后续 job 会轮询 + if (ObjectUtils.equalsAny(response.getSubCode(), "ACQ.SYSTEM_ERROR", "SYSTEM_ERROR")) { + return PayRefundRespDTO.waitingOf(null, reqDTO.getOutRefundNo(), response); + } + return PayRefundRespDTO.failureOf(response.getSubCode(), response.getSubMsg(), reqDTO.getOutRefundNo(), response); + } + // 2.2 创建返回结果 + // 支付宝只要退款调用返回 success,就认为退款成功,不需要回调。具体可见 parseNotify 方法的说明。 + // 另外,支付宝没有退款单号,所以不用设置 + return PayRefundRespDTO.successOf(null, LocalDateTimeUtil.of(response.getGmtRefundPay()), + reqDTO.getOutRefundNo(), response); + } + + @Override + public PayRefundRespDTO doParseRefundNotify(Map params, String body, Map headers) { + // 补充说明:支付宝退款时,没有回调,这点和微信支付是不同的。并且,退款分成部分退款、和全部退款。 + // ① 部分退款:是会有回调,但是它回调的是订单状态的同步回调,不是退款订单的回调 + // ② 全部退款:Wap 支付有订单状态的同步回调,但是 PC/扫码又没有 + // 所以,这里在解析时,即使是退款导致的订单状态同步,我们也忽略不做为“退款同步”,而是订单的回调。 + // 实际上,支付宝退款只要发起成功,就可以认为退款成功,不需要等待回调。 + throw new UnsupportedOperationException("支付宝无退款回调"); + } + + @Override + protected PayRefundRespDTO doGetRefund(String outTradeNo, String outRefundNo) throws AlipayApiException { + // 1.1 构建 AlipayTradeFastpayRefundQueryModel 请求 + AlipayTradeFastpayRefundQueryModel model = new AlipayTradeFastpayRefundQueryModel(); + model.setOutTradeNo(outTradeNo); + model.setOutRequestNo(outRefundNo); + model.setQueryOptions(Collections.singletonList("gmt_refund_pay")); + // 1.2 构建 AlipayTradeFastpayRefundQueryRequest 请求 + AlipayTradeFastpayRefundQueryRequest request = new AlipayTradeFastpayRefundQueryRequest(); + request.setBizModel(model); + + // 2.1 执行请求 + AlipayTradeFastpayRefundQueryResponse response; + if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) { // 证书模式 + response = client.certificateExecute(request); + } else { + response = client.execute(request); + } + if (!response.isSuccess()) { + // 明确不存在的情况,应该就是失败,可进行关闭 + if (ObjectUtils.equalsAny(response.getSubCode(), "TRADE_NOT_EXIST", "ACQ.TRADE_NOT_EXIST")) { + return PayRefundRespDTO.failureOf(outRefundNo, response); + } + // 可能存在“ACQ.SYSTEM_ERROR”系统错误等情况,所以返回 WAIT 继续等待 + return PayRefundRespDTO.waitingOf(null, outRefundNo, response); + } + // 2.2 创建返回结果 + if (Objects.equals(response.getRefundStatus(), "REFUND_SUCCESS")) { + return PayRefundRespDTO.successOf(null, LocalDateTimeUtil.of(response.getGmtRefundPay()), + outRefundNo, response); + } + return PayRefundRespDTO.waitingOf(null, outRefundNo, response); + } + + @Override + protected PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) throws AlipayApiException { + // 补充说明:https://opendocs.alipay.com/open/03dcrm?pathHash=4ba3b20b + // 沙箱环境:可通过 公钥模式 或 公钥证书模式 加签进行调试 + // 生产环境:必须使用 公钥证书模式 加签请求强校验请求 + + // 1.1 构建 AlipayFundTransUniTransferModel + AlipayFundTransUniTransferModel model = new AlipayFundTransUniTransferModel(); + // ① 通用的参数 + model.setTransAmount(formatAmount(reqDTO.getPrice())); // 转账金额 + model.setOrderTitle(reqDTO.getSubject()); // 转账业务的标题,用于在支付宝用户的账单里显示。 + model.setOutBizNo(reqDTO.getOutTransferNo()); + model.setProductCode("TRANS_ACCOUNT_NO_PWD"); // 销售产品码。单笔无密转账固定为 TRANS_ACCOUNT_NO_PWD + model.setBizScene("DIRECT_TRANSFER"); // 业务场景 单笔无密转账固定为 DIRECT_TRANSFER + if (reqDTO.getChannelExtras() != null) { + model.setBusinessParams(JsonUtils.toJsonString(reqDTO.getChannelExtras())); + } + // ② 个性化的参数 + Participant payeeInfo = new Participant(); + payeeInfo.setIdentityType("ALIPAY_LOGON_ID"); // 暂时只考虑转账到支付宝,银行没有权限 https://opendocs.alipay.com/open/02byvc?scene=66dd06f5a923403393b85de68d3c0055 + payeeInfo.setIdentity(reqDTO.getUserAccount()); // 支付宝登录号 + payeeInfo.setName(reqDTO.getUserName()); // 支付宝账号姓名 + model.setPayeeInfo(payeeInfo); + // 1.2 构建 AlipayFundTransUniTransferRequest + AlipayFundTransUniTransferRequest request = new AlipayFundTransUniTransferRequest(); + request.setBizModel(model); + + // 2.1 执行请求 + AlipayFundTransUniTransferResponse response; + if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) { // 证书模式 + response = client.certificateExecute(request); + } else { + response = client.execute(request); + } + if (!response.isSuccess()) { + // 当出现 SYSTEM_ERROR, 转账可能成功也可能失败。 返回 WAIT 状态. 后续 job 会轮询,或相同 outBizNo 重新发起转账 + // 发现 outBizNo 相同 两次请求参数相同. 会返回 "PAYMENT_INFO_INCONSISTENCY", 不知道哪里的问题. 暂时返回 WAIT. 后续job 会轮询 + if (ObjectUtils.equalsAny(response.getSubCode(),"PAYMENT_INFO_INCONSISTENCY", "SYSTEM_ERROR", "ACQ.SYSTEM_ERROR")) { + return PayTransferRespDTO.waitingOf(null, reqDTO.getOutTransferNo(), response); + } + return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(), + reqDTO.getOutTransferNo(), response); + } + // 2.2 处理结果 + if (ObjectUtils.equalsAny(response.getStatus(), "REFUND", "FAIL")) { // 转账到银行卡会出现 "REFUND" "FAIL" + return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(), + reqDTO.getOutTransferNo(), response); + } + if (Objects.equals(response.getStatus(), "DEALING")) { // 转账到银行卡会出现 "DEALING" 处理中 + return PayTransferRespDTO.processingOf(response.getOrderId(), reqDTO.getOutTransferNo(), response); + } + return PayTransferRespDTO.successOf(response.getOrderId(), parseTime(response.getTransDate()), + response.getOutBizNo(), response); + } + + @Override + protected PayTransferRespDTO doGetTransfer(String outTradeNo) throws Throwable { + // 1.1 构建 AlipayFundTransCommonQueryModel + AlipayFundTransCommonQueryModel model = new AlipayFundTransCommonQueryModel(); + model.setProductCode("TRANS_ACCOUNT_NO_PWD"); + model.setBizScene("DIRECT_TRANSFER"); //业务场景 + model.setOutBizNo(outTradeNo); + // 1.2 构建 AlipayFundTransCommonQueryRequest + AlipayFundTransCommonQueryRequest request = new AlipayFundTransCommonQueryRequest(); + request.setBizModel(model); + + // 2.1 执行请求 + AlipayFundTransCommonQueryResponse response; + if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) { // 证书模式 + response = client.certificateExecute(request); + } else { + response = client.execute(request); + } + if (!response.isSuccess()) { + // 当出现 SYSTEM_ERROR, 转账可能成功也可能失败。 返回 WAIT 状态. 后续 job 会轮询, 或相同 outBizNo 重新发起转账 + // 当出现 ORDER_NOT_EXIST 可能是转账还在处理中,也可能是转账处理失败. 返回 WAIT 状态. 后续 job 会轮询, 或相同 outBizNo 重新发起转账 + if (ObjectUtils.equalsAny(response.getSubCode(), "ORDER_NOT_EXIST", "SYSTEM_ERROR", "ACQ.SYSTEM_ERROR")) { + return PayTransferRespDTO.waitingOf(null, outTradeNo, response); + } + return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(), + outTradeNo, response); + } + // 2.2 处理返回结果 + if (ObjectUtils.equalsAny(response.getStatus(), "REFUND", "FAIL")) { // 转账到银行卡会出现 "REFUND" "FAIL" + return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(), + outTradeNo, response); + } + if (Objects.equals(response.getStatus(), "DEALING")) { // 转账到银行卡会出现 "DEALING" 处理中 + return PayTransferRespDTO.processingOf(response.getOrderId(), outTradeNo, response); + } + return PayTransferRespDTO.successOf(response.getOrderId(), parseTime(response.getPayDate()), + response.getOutBizNo(), response); + } + + // TODO @芋艿:由于支付宝一直没触发回调,这个方法暂时没办法测试 + @Override + protected PayTransferRespDTO doParseTransferNotify(Map params, String body, Map headers) + throws Throwable { + // 1. 校验回调数据 + verifyNotifyData(params); + + // 2. 解析转账状态 + Map bodyObj = HttpUtil.decodeParamMap(body, StandardCharsets.UTF_8); + String status = bodyObj.get("status"); + String outBizNo = bodyObj.get("out_biz_no"); + String orderId = bodyObj.get("order_id"); + String payDate = bodyObj.get("pay_date"); + + // 3. 根据状态返回对应的结果 + if (Objects.equals(status, "SUCCESS")) { + return PayTransferRespDTO.successOf(orderId, parseTime(payDate), outBizNo, bodyObj); + } + if (Objects.equals(status, "DEALING")) { + return PayTransferRespDTO.processingOf(orderId, outBizNo, bodyObj); + } + if (ObjectUtils.equalsAny(status, "REFUND", "FAIL")) { + return PayTransferRespDTO.closedOf(bodyObj.get("sub_code"), bodyObj.get("sub_msg"), + outBizNo, bodyObj); + } + return PayTransferRespDTO.waitingOf(orderId, outBizNo, bodyObj); + } + + /** + * 校验回调数据 + * + * @param params 回调参数 + * @throws Throwable 验签失败时抛出异常 + */ + protected void verifyNotifyData(Map params) throws Throwable { + boolean verify; + if (Objects.equals(config.getMode(), MODE_PUBLIC_KEY)) { + verify = AlipaySignature.rsaCheckV1(params, config.getAlipayPublicKey(), + StandardCharsets.UTF_8.name(), config.getSignType()); + } else if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) { + // 由于 rsaCertCheckV1 的第二个参数是 path,所以不能这么调用!!!通过阅读源码,发现可以采用如下方式! + X509Certificate cert = AntCertificationUtil.getCertFromContent(config.getAlipayPublicCertContent()); + String publicKey = Base64.encodeBase64String(cert.getPublicKey().getEncoded()); + verify = AlipaySignature.rsaCheckV1(params, publicKey, + StandardCharsets.UTF_8.name(), config.getSignType()); + } else { + throw new IllegalArgumentException("未知的公钥类型:" + config.getMode()); + } + Assert.isTrue(verify, "验签结果不通过"); + } + + // ========== 各种工具方法 ========== + + protected String formatAmount(Integer amount) { + return String.valueOf(amount / 100.0); + } + + protected String formatTime(LocalDateTime time) { + return LocalDateTimeUtil.format(time, NORM_DATETIME_FORMATTER); + } + + protected LocalDateTime parseTime(String str) { + return LocalDateTimeUtil.parse(str, NORM_DATETIME_FORMATTER); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/alipay/AlipayAppPayClient.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/alipay/AlipayAppPayClient.java new file mode 100644 index 0000000..ddaf497 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/alipay/AlipayAppPayClient.java @@ -0,0 +1,60 @@ +package cn.aagro.pp.module.pay.framework.pay.core.client.impl.alipay; + +import cn.aagro.pp.module.pay.enums.PayChannelEnum; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import cn.aagro.pp.module.pay.framework.pay.core.enums.PayOrderDisplayModeEnum; +import com.alipay.api.AlipayApiException; +import com.alipay.api.domain.AlipayTradeAppPayModel; +import com.alipay.api.request.AlipayTradeAppPayRequest; +import com.alipay.api.response.AlipayTradeAppPayResponse; +import lombok.extern.slf4j.Slf4j; + +/** + * 支付宝【App 支付】的 PayClient 实现类 + * + * 文档:App 支付 + * + * // TODO 芋艿:未详细测试,因为手头没 App + * + * @author 芋道源码 + */ +@Slf4j +public class AlipayAppPayClient extends AbstractAlipayPayClient { + + public AlipayAppPayClient(Long channelId, AlipayPayClientConfig config) { + super(channelId, PayChannelEnum.ALIPAY_APP.getCode(), config); + } + + @Override + public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException { + // 1.1 构建 AlipayTradeAppPayModel 请求 + AlipayTradeAppPayModel model = new AlipayTradeAppPayModel(); + // ① 通用的参数 + model.setOutTradeNo(reqDTO.getOutTradeNo()); + model.setSubject(reqDTO.getSubject()); + model.setBody(reqDTO.getBody() + "test"); + model.setTotalAmount(formatAmount(reqDTO.getPrice())); + model.setTimeExpire(formatTime(reqDTO.getExpireTime())); + model.setProductCode("QUICK_MSECURITY_PAY"); // 销售产品码:无线快捷支付产品 + // ② 个性化的参数【无】 + // ③ 支付宝扫码支付只有一种展示 + String displayMode = PayOrderDisplayModeEnum.APP.getMode(); + + // 1.2 构建 AlipayTradePrecreateRequest 请求 + AlipayTradeAppPayRequest request = new AlipayTradeAppPayRequest(); + request.setBizModel(model); + request.setNotifyUrl(reqDTO.getNotifyUrl()); + request.setReturnUrl(reqDTO.getReturnUrl()); + + // 2.1 执行请求 + AlipayTradeAppPayResponse response = client.sdkExecute(request); + // 2.2 处理结果 + if (!response.isSuccess()) { + return buildClosedPayOrderRespDTO(reqDTO, response); + } + return PayOrderRespDTO.waitingOf(displayMode, response.getBody(), + reqDTO.getOutTradeNo(), response); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/alipay/AlipayBarPayClient.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/alipay/AlipayBarPayClient.java new file mode 100644 index 0000000..14e3876 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/alipay/AlipayBarPayClient.java @@ -0,0 +1,86 @@ +package cn.aagro.pp.module.pay.framework.pay.core.client.impl.alipay; + +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import cn.aagro.pp.module.pay.enums.PayChannelEnum; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import cn.aagro.pp.module.pay.framework.pay.core.enums.PayOrderDisplayModeEnum; +import com.alipay.api.AlipayApiException; +import com.alipay.api.domain.AlipayTradePayModel; +import com.alipay.api.request.AlipayTradePayRequest; +import com.alipay.api.response.AlipayTradePayResponse; +import lombok.extern.slf4j.Slf4j; + +import java.time.LocalDateTime; +import java.util.Objects; + +import static cn.aagro.pp.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; +import static cn.aagro.pp.framework.common.exception.util.ServiceExceptionUtil.exception0; +import static cn.aagro.pp.module.pay.framework.pay.core.client.impl.alipay.AlipayPayClientConfig.MODE_CERTIFICATE; + +/** + * 支付宝【条码支付】的 PayClient 实现类 + * + * 文档:当面付 + * + * @author 芋道源码 + */ +@Slf4j +public class AlipayBarPayClient extends AbstractAlipayPayClient { + + public AlipayBarPayClient(Long channelId, AlipayPayClientConfig config) { + super(channelId, PayChannelEnum.ALIPAY_BAR.getCode(), config); + } + + @Override + public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException { + String authCode = MapUtil.getStr(reqDTO.getChannelExtras(), "auth_code"); + if (StrUtil.isEmpty(authCode)) { + throw exception0(BAD_REQUEST.getCode(), "条形码不能为空"); + } + + // 1.1 构建 AlipayTradePayModel 请求 + AlipayTradePayModel model = new AlipayTradePayModel(); + // ① 通用的参数 + model.setOutTradeNo(reqDTO.getOutTradeNo()); + model.setSubject(reqDTO.getSubject()); + model.setBody(reqDTO.getBody()); + model.setTotalAmount(formatAmount(reqDTO.getPrice())); + model.setScene("bar_code"); // 当面付条码支付场景 + // ② 个性化的参数 + model.setAuthCode(authCode); + // ③ 支付宝条码支付只有一种展示 + String displayMode = PayOrderDisplayModeEnum.BAR_CODE.getMode(); + + // 1.2 构建 AlipayTradePayRequest 请求 + AlipayTradePayRequest request = new AlipayTradePayRequest(); + request.setBizModel(model); + request.setNotifyUrl(reqDTO.getNotifyUrl()); + request.setReturnUrl(reqDTO.getReturnUrl()); + + // 2.1 执行请求 + AlipayTradePayResponse response; + if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) { + // 证书模式 + response = client.certificateExecute(request); + } else { + response = client.execute(request); + } + // 2.2 处理结果 + if (!response.isSuccess()) { + return buildClosedPayOrderRespDTO(reqDTO, response); + } + if ("10000".equals(response.getCode())) { // 免密支付 + LocalDateTime successTime = LocalDateTimeUtil.of(response.getGmtPayment()); + return PayOrderRespDTO.successOf(response.getTradeNo(), response.getBuyerUserId(), successTime, + response.getOutTradeNo(), response) + .setDisplayMode(displayMode).setDisplayContent(""); + } + // 大额支付,需要用户输入密码,所以返回 waiting。此时,前端一般会进行轮询 + return PayOrderRespDTO.waitingOf(displayMode, "", + reqDTO.getOutTradeNo(), response); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/alipay/AlipayPayClientConfig.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/alipay/AlipayPayClientConfig.java new file mode 100644 index 0000000..bd88026 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/alipay/AlipayPayClientConfig.java @@ -0,0 +1,130 @@ +package cn.aagro.pp.module.pay.framework.pay.core.client.impl.alipay; + +import cn.aagro.pp.framework.common.util.validation.ValidationUtils; +import cn.aagro.pp.module.pay.framework.pay.core.client.PayClientConfig; +import lombok.Data; + +import javax.validation.ConstraintViolation; +import javax.validation.Validator; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.util.Set; + +/** + * 支付宝的 PayClientConfig 实现类 + * 属性主要来自 {@link com.alipay.api.AlipayConfig} 的必要属性 + * + * @author 芋道源码 + */ +@Data +public class AlipayPayClientConfig implements PayClientConfig { + + /** + * 公钥类型 - 公钥模式 + */ + public static final Integer MODE_PUBLIC_KEY = 1; + /** + * 公钥类型 - 证书模式 + */ + public static final Integer MODE_CERTIFICATE = 2; + + /** + * 接口内容加密方式 - AES 加密 + */ + public static final String ENC_TYPE_AES = "AES"; + + /** + * 签名算法类型 - RSA + */ + public static final String SIGN_TYPE_DEFAULT = "RSA2"; + + /** + * 网关地址 + * + * 1. 生产环境 + * 2. 沙箱环境 + */ + @NotBlank(message = "网关地址不能为空", groups = {ModePublicKey.class, ModeCertificate.class}) + private String serverUrl; + + /** + * 开放平台上创建的应用的 ID + */ + @NotBlank(message = "开放平台上创建的应用的 ID不能为空", groups = {ModePublicKey.class, ModeCertificate.class}) + private String appId; + + /** + * 签名算法类型,推荐:RSA2 + *

+ * {@link #SIGN_TYPE_DEFAULT} + */ + @NotBlank(message = "签名算法类型不能为空", groups = {ModePublicKey.class, ModeCertificate.class}) + private String signType; + + /** + * 公钥类型 + * 1. {@link #MODE_PUBLIC_KEY} 情况,privateKey + alipayPublicKey + * 2. {@link #MODE_CERTIFICATE} 情况,appCertContent + alipayPublicCertContent + rootCertContent + */ + @NotNull(message = "公钥类型不能为空", groups = {ModePublicKey.class, ModeCertificate.class}) + private Integer mode; + + // ========== 公钥模式 ========== + /** + * 商户私钥 + */ + @NotBlank(message = "商户私钥不能为空", groups = {ModePublicKey.class}) + private String privateKey; + + /** + * 支付宝公钥字符串 + */ + @NotBlank(message = "支付宝公钥字符串不能为空", groups = {ModePublicKey.class}) + private String alipayPublicKey; + + // ========== 证书模式 ========== + /** + * 指定商户公钥应用证书内容字符串 + */ + @NotBlank(message = "指定商户公钥应用证书内容不能为空", groups = {ModeCertificate.class}) + private String appCertContent; + /** + * 指定支付宝公钥证书内容字符串 + */ + @NotBlank(message = "指定支付宝公钥证书内容不能为空", groups = {ModeCertificate.class}) + private String alipayPublicCertContent; + /** + * 指定根证书内容字符串 + */ + @NotBlank(message = "指定根证书内容字符串不能为空", groups = {ModeCertificate.class}) + private String rootCertContent; + + /** + * 接口内容加密方式 + * + * 1. 如果为空,将使用无加密方式 + * 2. 如果要加密,目前支付宝只有 AES 一种加密方式 + * + * @see 支付宝开放平台 + * @see AlipayPayClientConfig#ENC_TYPE_AES + */ + private String encryptType; + + /** + * 接口内容加密的私钥 + */ + private String encryptKey; + + public interface ModePublicKey { + } + + public interface ModeCertificate { + } + + @Override + public void validate(Validator validator) { + ValidationUtils.validate(validator, this, + MODE_PUBLIC_KEY.equals(this.getMode()) ? ModePublicKey.class : ModeCertificate.class); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/alipay/AlipayPcPayClient.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/alipay/AlipayPcPayClient.java new file mode 100644 index 0000000..f4405e6 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/alipay/AlipayPcPayClient.java @@ -0,0 +1,70 @@ +package cn.aagro.pp.module.pay.framework.pay.core.client.impl.alipay; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.http.Method; +import cn.aagro.pp.module.pay.enums.PayChannelEnum; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import cn.aagro.pp.module.pay.framework.pay.core.enums.PayOrderDisplayModeEnum; +import com.alipay.api.AlipayApiException; +import com.alipay.api.domain.AlipayTradePagePayModel; +import com.alipay.api.request.AlipayTradePagePayRequest; +import com.alipay.api.response.AlipayTradePagePayResponse; +import lombok.extern.slf4j.Slf4j; + +import java.util.Objects; + +/** + * 支付宝【PC 网站】的 PayClient 实现类 + * + * 文档:电脑网站支付 + * + * @author XGD + */ +@Slf4j +public class AlipayPcPayClient extends AbstractAlipayPayClient { + + public AlipayPcPayClient(Long channelId, AlipayPayClientConfig config) { + super(channelId, PayChannelEnum.ALIPAY_PC.getCode(), config); + } + + @Override + public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException { + // 1.1 构建 AlipayTradePagePayModel 请求 + AlipayTradePagePayModel model = new AlipayTradePagePayModel(); + // ① 通用的参数 + model.setOutTradeNo(reqDTO.getOutTradeNo()); + model.setSubject(reqDTO.getSubject()); + model.setBody(reqDTO.getBody()); + model.setTotalAmount(formatAmount(reqDTO.getPrice())); + model.setTimeExpire(formatTime(reqDTO.getExpireTime())); + model.setProductCode("FAST_INSTANT_TRADE_PAY"); // 销售产品码. 目前 PC 支付场景下仅支持 FAST_INSTANT_TRADE_PAY + // ② 个性化的参数 + // 如果想弄更多个性化的参数,可参考 https://www.pingxx.com/api/支付渠道 extra 参数说明.html 的 alipay_pc_direct 部分进行拓展 + model.setQrPayMode("2"); // 跳转模式 - 订单码,效果参见:https://help.pingxx.com/article/1137360/ + // ③ 支付宝 PC 支付有两种展示模式:FORM、URL + String displayMode = ObjectUtil.defaultIfNull(reqDTO.getDisplayMode(), + PayOrderDisplayModeEnum.URL.getMode()); + + // 1.2 构建 AlipayTradePagePayRequest 请求 + AlipayTradePagePayRequest request = new AlipayTradePagePayRequest(); + request.setBizModel(model); + request.setNotifyUrl(reqDTO.getNotifyUrl()); + request.setReturnUrl(reqDTO.getReturnUrl()); + + // 2.1 执行请求 + AlipayTradePagePayResponse response; + if (Objects.equals(displayMode, PayOrderDisplayModeEnum.FORM.getMode())) { + response = client.pageExecute(request, Method.POST.name()); // 需要特殊使用 POST 请求 + } else { + response = client.pageExecute(request, Method.GET.name()); + } + // 2.2 处理结果 + if (!response.isSuccess()) { + return buildClosedPayOrderRespDTO(reqDTO, response); + } + return PayOrderRespDTO.waitingOf(displayMode, response.getBody(), + reqDTO.getOutTradeNo(), response); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/alipay/AlipayQrPayClient.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/alipay/AlipayQrPayClient.java new file mode 100644 index 0000000..97352dd --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/alipay/AlipayQrPayClient.java @@ -0,0 +1,66 @@ +package cn.aagro.pp.module.pay.framework.pay.core.client.impl.alipay; + +import cn.aagro.pp.module.pay.enums.PayChannelEnum; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import cn.aagro.pp.module.pay.framework.pay.core.enums.PayOrderDisplayModeEnum; +import com.alipay.api.AlipayApiException; +import com.alipay.api.domain.AlipayTradePrecreateModel; +import com.alipay.api.request.AlipayTradePrecreateRequest; +import com.alipay.api.response.AlipayTradePrecreateResponse; +import lombok.extern.slf4j.Slf4j; + +import java.util.Objects; + +import static cn.aagro.pp.module.pay.framework.pay.core.client.impl.alipay.AlipayPayClientConfig.MODE_CERTIFICATE; + +/** + * 支付宝【扫码支付】的 PayClient 实现类 + * + * 文档:扫码支付 + * + * @author 芋道源码 + */ +@Slf4j +public class AlipayQrPayClient extends AbstractAlipayPayClient { + + public AlipayQrPayClient(Long channelId, AlipayPayClientConfig config) { + super(channelId, PayChannelEnum.ALIPAY_QR.getCode(), config); + } + + @Override + public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException { + // 1.1 构建 AlipayTradePrecreateModel 请求 + AlipayTradePrecreateModel model = new AlipayTradePrecreateModel(); + // ① 通用的参数 + model.setOutTradeNo(reqDTO.getOutTradeNo()); + model.setSubject(reqDTO.getSubject()); + model.setBody(reqDTO.getBody()); + model.setTotalAmount(formatAmount(reqDTO.getPrice())); + model.setProductCode("FACE_TO_FACE_PAYMENT"); // 销售产品码. 目前扫码支付场景下仅支持 FACE_TO_FACE_PAYMENT + // ② 个性化的参数【无】 + // ③ 支付宝扫码支付只有一种展示,考虑到前端可能希望二维码扫描后,手机打开 + String displayMode = PayOrderDisplayModeEnum.QR_CODE.getMode(); + + // 1.2 构建 AlipayTradePrecreateRequest 请求 + AlipayTradePrecreateRequest request = new AlipayTradePrecreateRequest(); + request.setBizModel(model); + request.setNotifyUrl(reqDTO.getNotifyUrl()); + request.setReturnUrl(reqDTO.getReturnUrl()); + + // 2.1 执行请求 + AlipayTradePrecreateResponse response; + if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) { + // 证书模式 + response = client.certificateExecute(request); + } else { + response = client.execute(request); + } + // 2.2 处理结果 + if (!response.isSuccess()) { + return buildClosedPayOrderRespDTO(reqDTO, response); + } + return PayOrderRespDTO.waitingOf(displayMode, response.getQrCode(), + reqDTO.getOutTradeNo(), response); + } +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/alipay/AlipayWapPayClient.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/alipay/AlipayWapPayClient.java new file mode 100644 index 0000000..c6a3b8c --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/alipay/AlipayWapPayClient.java @@ -0,0 +1,59 @@ +package cn.aagro.pp.module.pay.framework.pay.core.client.impl.alipay; + +import cn.hutool.http.Method; +import cn.aagro.pp.module.pay.enums.PayChannelEnum; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import cn.aagro.pp.module.pay.framework.pay.core.enums.PayOrderDisplayModeEnum; +import com.alipay.api.AlipayApiException; +import com.alipay.api.domain.AlipayTradeWapPayModel; +import com.alipay.api.request.AlipayTradeWapPayRequest; +import com.alipay.api.response.AlipayTradeWapPayResponse; +import lombok.extern.slf4j.Slf4j; + +/** + * 支付宝【Wap 网站】的 PayClient 实现类 + * + * 文档:手机网站支付接口 + * + * @author 芋道源码 + */ +@Slf4j +public class AlipayWapPayClient extends AbstractAlipayPayClient { + + public AlipayWapPayClient(Long channelId, AlipayPayClientConfig config) { + super(channelId, PayChannelEnum.ALIPAY_WAP.getCode(), config); + } + + @Override + public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException { + // 1.1 构建 AlipayTradeWapPayModel 请求 + AlipayTradeWapPayModel model = new AlipayTradeWapPayModel(); + // ① 通用的参数 + model.setOutTradeNo(reqDTO.getOutTradeNo()); + model.setSubject(reqDTO.getSubject()); + model.setBody(reqDTO.getBody()); + model.setTotalAmount(formatAmount(reqDTO.getPrice())); + model.setProductCode("QUICK_WAP_PAY"); // 销售产品码. 目前 Wap 支付场景下仅支持 QUICK_WAP_PAY + // ② 个性化的参数【无】 + // ③ 支付宝 Wap 支付只有一种展示:URL + String displayMode = PayOrderDisplayModeEnum.URL.getMode(); + + // 1.2 构建 AlipayTradeWapPayRequest 请求 + AlipayTradeWapPayRequest request = new AlipayTradeWapPayRequest(); + request.setBizModel(model); + request.setNotifyUrl(reqDTO.getNotifyUrl()); + request.setReturnUrl(reqDTO.getReturnUrl()); + model.setQuitUrl(reqDTO.getReturnUrl()); + model.setTimeExpire(formatTime(reqDTO.getExpireTime())); + + // 2.1 执行请求 + AlipayTradeWapPayResponse response = client.pageExecute(request, Method.GET.name()); + // 2.2 处理结果 + if (!response.isSuccess()) { + return buildClosedPayOrderRespDTO(reqDTO, response); + } + return PayOrderRespDTO.waitingOf(displayMode, response.getBody(), + reqDTO.getOutTradeNo(), response); + } +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/mock/MockPayClient.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/mock/MockPayClient.java new file mode 100644 index 0000000..42881bb --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/mock/MockPayClient.java @@ -0,0 +1,84 @@ +package cn.aagro.pp.module.pay.framework.pay.core.client.impl.mock; + +import cn.aagro.pp.module.pay.enums.PayChannelEnum; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.refund.PayRefundRespDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.transfer.PayTransferRespDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.impl.AbstractPayClient; +import cn.aagro.pp.module.pay.framework.pay.core.client.impl.NonePayClientConfig; + +import java.time.LocalDateTime; +import java.util.Map; + +/** + * 模拟支付的 PayClient 实现类 + * + * 模拟支付返回结果都是成功,方便大家日常流畅 + * + * @author jason + */ +public class MockPayClient extends AbstractPayClient { + + private static final String MOCK_RESP_SUCCESS_DATA = "MOCK_SUCCESS"; + + public MockPayClient(Long channelId, NonePayClientConfig config) { + super(channelId, PayChannelEnum.MOCK.getCode(), config); + } + + @Override + protected void doInit() { + } + + @Override + protected PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) { + return PayOrderRespDTO.successOf("MOCK-P-" + reqDTO.getOutTradeNo(), "", LocalDateTime.now(), + reqDTO.getOutTradeNo(), MOCK_RESP_SUCCESS_DATA); + } + + @Override + protected PayOrderRespDTO doGetOrder(String outTradeNo) { + return PayOrderRespDTO.successOf("MOCK-P-" + outTradeNo, "", LocalDateTime.now(), + outTradeNo, MOCK_RESP_SUCCESS_DATA); + } + + @Override + protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) { + return PayRefundRespDTO.successOf("MOCK-R-" + reqDTO.getOutRefundNo(), LocalDateTime.now(), + reqDTO.getOutRefundNo(), MOCK_RESP_SUCCESS_DATA); + } + + @Override + protected PayRefundRespDTO doGetRefund(String outTradeNo, String outRefundNo) { + return PayRefundRespDTO.successOf("MOCK-R-" + outRefundNo, LocalDateTime.now(), + outRefundNo, MOCK_RESP_SUCCESS_DATA); + } + + @Override + protected PayTransferRespDTO doParseTransferNotify(Map params, String body, Map headers) { + throw new UnsupportedOperationException("未实现"); + } + + @Override + protected PayRefundRespDTO doParseRefundNotify(Map params, String body, Map headers) { + throw new UnsupportedOperationException("模拟支付无退款回调"); + } + + @Override + protected PayOrderRespDTO doParseOrderNotify(Map params, String body, Map headers) { + throw new UnsupportedOperationException("模拟支付无支付回调"); + } + + @Override + protected PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) { + throw new UnsupportedOperationException("待实现"); + } + + @Override + protected PayTransferRespDTO doGetTransfer(String outTradeNo) { + throw new UnsupportedOperationException("待实现"); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/wallet/WalletPayClient.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/wallet/WalletPayClient.java new file mode 100644 index 0000000..e8f144a --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/wallet/WalletPayClient.java @@ -0,0 +1,251 @@ +package cn.aagro.pp.module.pay.framework.pay.core.client.impl.wallet; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.aagro.pp.framework.common.exception.ServiceException; +import cn.aagro.pp.module.pay.enums.PayChannelEnum; +import cn.aagro.pp.module.pay.enums.refund.PayRefundStatusEnum; +import cn.aagro.pp.module.pay.enums.transfer.PayTransferStatusEnum; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.refund.PayRefundRespDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.transfer.PayTransferRespDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.impl.AbstractPayClient; +import cn.aagro.pp.module.pay.framework.pay.core.client.impl.NonePayClientConfig; +import cn.aagro.pp.module.pay.dal.dataobject.order.PayOrderExtensionDO; +import cn.aagro.pp.module.pay.dal.dataobject.refund.PayRefundDO; +import cn.aagro.pp.module.pay.dal.dataobject.transfer.PayTransferDO; +import cn.aagro.pp.module.pay.dal.dataobject.wallet.PayWalletTransactionDO; +import cn.aagro.pp.module.pay.enums.order.PayOrderStatusEnum; +import cn.aagro.pp.module.pay.enums.wallet.PayWalletBizTypeEnum; +import cn.aagro.pp.module.pay.service.order.PayOrderService; +import cn.aagro.pp.module.pay.service.refund.PayRefundService; +import cn.aagro.pp.module.pay.service.transfer.PayTransferService; +import cn.aagro.pp.module.pay.service.wallet.PayWalletService; +import cn.aagro.pp.module.pay.service.wallet.PayWalletTransactionService; +import lombok.extern.slf4j.Slf4j; + +import java.util.Map; + +import static cn.aagro.pp.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; +import static cn.aagro.pp.module.pay.enums.ErrorCodeConstants.PAY_ORDER_EXTENSION_NOT_FOUND; +import static cn.aagro.pp.module.pay.enums.ErrorCodeConstants.REFUND_NOT_FOUND; + +/** + * 钱包支付的 PayClient 实现类 + * + * @author jason + */ +@Slf4j +public class WalletPayClient extends AbstractPayClient { + + public static final String WALLET_ID_KEY = "walletId"; + + private PayWalletService wallService; + private PayWalletTransactionService walletTransactionService; + + private PayOrderService orderService; + private PayRefundService refundService; + private PayTransferService transferService; + + public WalletPayClient(Long channelId, NonePayClientConfig config) { + super(channelId, PayChannelEnum.WALLET.getCode(), config); + } + + @Override + protected void doInit() { + if (wallService == null) { + wallService = SpringUtil.getBean(PayWalletService.class); + } + if (walletTransactionService == null) { + walletTransactionService = SpringUtil.getBean(PayWalletTransactionService.class); + } + } + + @Override + @SuppressWarnings("PatternVariableCanBeUsed") + protected PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) { + try { + Long walletId = MapUtil.getLong(reqDTO.getChannelExtras(), WALLET_ID_KEY); + Assert.notNull(walletId, "钱包编号"); + PayWalletTransactionDO transaction = wallService.orderPay(walletId, + reqDTO.getOutTradeNo(), reqDTO.getPrice()); + return PayOrderRespDTO.successOf(transaction.getNo(), transaction.getCreator(), + transaction.getCreateTime(), + reqDTO.getOutTradeNo(), transaction); + } catch (Throwable ex) { + log.error("[doUnifiedOrder][reqDTO({}) 异常]", reqDTO, ex); + Integer errorCode = INTERNAL_SERVER_ERROR.getCode(); + String errorMsg = INTERNAL_SERVER_ERROR.getMsg(); + if (ex instanceof ServiceException) { + ServiceException serviceException = (ServiceException) ex; + errorCode = serviceException.getCode(); + errorMsg = serviceException.getMessage(); + } + return PayOrderRespDTO.closedOf(String.valueOf(errorCode), errorMsg, + reqDTO.getOutTradeNo(), ""); + } + } + + @Override + protected PayOrderRespDTO doParseOrderNotify(Map params, String body, Map headers) { + throw new UnsupportedOperationException("钱包支付无支付回调"); + } + + @Override + protected PayOrderRespDTO doGetOrder(String outTradeNo) { + if (orderService == null) { + orderService = SpringUtil.getBean(PayOrderService.class); + } + PayOrderExtensionDO orderExtension = orderService.getOrderExtensionByNo(outTradeNo); + // 支付交易拓展单不存在, 返回关闭状态 + if (orderExtension == null) { + return PayOrderRespDTO.closedOf(String.valueOf(PAY_ORDER_EXTENSION_NOT_FOUND.getCode()), + PAY_ORDER_EXTENSION_NOT_FOUND.getMsg(), outTradeNo, ""); + } + // 关闭状态 + if (PayOrderStatusEnum.isClosed(orderExtension.getStatus())) { + return PayOrderRespDTO.closedOf(orderExtension.getChannelErrorCode(), + orderExtension.getChannelErrorMsg(), outTradeNo, ""); + } + // 成功状态 + if (PayOrderStatusEnum.isSuccess(orderExtension.getStatus())) { + PayWalletTransactionDO walletTransaction = walletTransactionService.getWalletTransaction( + String.valueOf(orderExtension.getOrderId()), PayWalletBizTypeEnum.PAYMENT); + Assert.notNull(walletTransaction, "支付单 {} 钱包流水不能为空", outTradeNo); + return PayOrderRespDTO.successOf(walletTransaction.getNo(), walletTransaction.getCreator(), + walletTransaction.getCreateTime(), outTradeNo, walletTransaction); + } + // 其它状态为无效状态 + log.error("[doGetOrder] 支付单 {} 的状态不正确", outTradeNo); + throw new IllegalStateException(String.format("支付单[%s] 状态不正确", outTradeNo)); + } + + @Override + @SuppressWarnings("PatternVariableCanBeUsed") + protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) { + try { + PayWalletTransactionDO payWalletTransaction = wallService.orderRefund(reqDTO.getOutRefundNo(), + reqDTO.getRefundPrice(), reqDTO.getReason()); + return PayRefundRespDTO.successOf(payWalletTransaction.getNo(), payWalletTransaction.getCreateTime(), + reqDTO.getOutRefundNo(), payWalletTransaction); + } catch (Throwable ex) { + log.error("[doUnifiedRefund][reqDOT({}) 异常]", reqDTO, ex); + Integer errorCode = INTERNAL_SERVER_ERROR.getCode(); + String errorMsg = INTERNAL_SERVER_ERROR.getMsg(); + if (ex instanceof ServiceException) { + ServiceException serviceException = (ServiceException) ex; + errorCode = serviceException.getCode(); + errorMsg = serviceException.getMessage(); + } + return PayRefundRespDTO.failureOf(String.valueOf(errorCode), errorMsg, + reqDTO.getOutRefundNo(), ""); + } + } + + @Override + protected PayRefundRespDTO doParseRefundNotify(Map params, String body, Map headers) { + throw new UnsupportedOperationException("钱包支付无退款回调"); + } + + @Override + protected PayRefundRespDTO doGetRefund(String outTradeNo, String outRefundNo) { + if (refundService == null) { + refundService = SpringUtil.getBean(PayRefundService.class); + } + PayRefundDO payRefund = refundService.getRefundByNo(outRefundNo); + // 支付退款单不存在, 返回退款失败状态 + if (payRefund == null) { + return PayRefundRespDTO.failureOf(String.valueOf(REFUND_NOT_FOUND), REFUND_NOT_FOUND.getMsg(), + outRefundNo, ""); + } + // 退款失败 + if (PayRefundStatusEnum.isFailure(payRefund.getStatus())) { + return PayRefundRespDTO.failureOf(payRefund.getChannelErrorCode(), payRefund.getChannelErrorMsg(), + outRefundNo, ""); + } + // 退款成功 + if (PayRefundStatusEnum.isSuccess(payRefund.getStatus())) { + PayWalletTransactionDO walletTransaction = walletTransactionService.getWalletTransaction( + String.valueOf(payRefund.getId()), PayWalletBizTypeEnum.PAYMENT_REFUND); + Assert.notNull(walletTransaction, "支付退款单 {} 钱包流水不能为空", outRefundNo); + return PayRefundRespDTO.successOf(walletTransaction.getNo(), walletTransaction.getCreateTime(), + outRefundNo, walletTransaction); + } + // 其它状态为无效状态 + log.error("[doGetRefund] 支付退款单 {} 的状态不正确", outRefundNo); + throw new IllegalStateException(String.format("支付退款单[%s] 状态不正确", outRefundNo)); + } + + @Override + @SuppressWarnings("PatternVariableCanBeUsed") + public PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) { + try { + Long walletId = Long.parseLong(reqDTO.getUserAccount()); + PayWalletTransactionDO transaction = wallService.addWalletBalance(walletId, String.valueOf(reqDTO.getOutTransferNo()), + PayWalletBizTypeEnum.TRANSFER, reqDTO.getPrice()); + return PayTransferRespDTO.successOf(transaction.getNo(), transaction.getCreateTime(), + reqDTO.getOutTransferNo(), transaction); + } catch (Throwable ex) { + log.error("[doUnifiedTransfer][reqDTO({}) 异常]", reqDTO, ex); + Integer errorCode = INTERNAL_SERVER_ERROR.getCode(); + String errorMsg = INTERNAL_SERVER_ERROR.getMsg(); + if (ex instanceof ServiceException) { + ServiceException serviceException = (ServiceException) ex; + errorCode = serviceException.getCode(); + errorMsg = serviceException.getMessage(); + } + return PayTransferRespDTO.closedOf(String.valueOf(errorCode), errorMsg, + reqDTO.getOutTransferNo(), ""); + } + } + + @Override + protected PayTransferRespDTO doParseTransferNotify(Map params, String body, Map headers) { + throw new UnsupportedOperationException("钱包支付无转账回调"); + } + + @Override + protected PayTransferRespDTO doGetTransfer(String outTradeNo) { + if (transferService == null) { + transferService = SpringUtil.getBean(PayTransferService.class); + } + // 获取转账单 + PayTransferDO transfer = transferService.getTransferByNo(outTradeNo); + // 转账单不存在,返回关闭状态 + if (transfer == null) { + return PayTransferRespDTO.closedOf(String.valueOf(PAY_ORDER_EXTENSION_NOT_FOUND.getCode()), + PAY_ORDER_EXTENSION_NOT_FOUND.getMsg(), outTradeNo, ""); + } + // 关闭状态 + if (PayTransferStatusEnum.isClosed(transfer.getStatus())) { + return PayTransferRespDTO.closedOf(transfer.getChannelErrorCode(), + transfer.getChannelErrorMsg(), outTradeNo, ""); + } + // 成功状态 + if (PayTransferStatusEnum.isSuccess(transfer.getStatus())) { + PayWalletTransactionDO walletTransaction = walletTransactionService.getWalletTransaction( + String.valueOf(transfer.getId()), PayWalletBizTypeEnum.TRANSFER); + Assert.notNull(walletTransaction, "转账单 {} 钱包流水不能为空", outTradeNo); + return PayTransferRespDTO.successOf(walletTransaction.getNo(), walletTransaction.getCreateTime(), + outTradeNo, walletTransaction); + } + // 处理中状态 + if (PayTransferStatusEnum.isProcessing(transfer.getStatus())) { + return PayTransferRespDTO.processingOf(transfer.getChannelTransferNo(), + outTradeNo, transfer); + } + // 等待状态 + if (PayTransferStatusEnum.isWaiting(transfer.getStatus())) { + return PayTransferRespDTO.waitingOf(transfer.getChannelTransferNo(), + outTradeNo, transfer); + } + // 其它状态为无效状态 + log.error("[doGetTransfer] 转账单 {} 的状态不正确", outTradeNo); + throw new IllegalStateException(String.format("转账单[%s] 状态不正确", outTradeNo)); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java new file mode 100644 index 0000000..8efe478 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java @@ -0,0 +1,599 @@ +package cn.aagro.pp.module.pay.framework.pay.core.client.impl.weixin; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.codec.Base64; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.date.TemporalAccessorUtil; +import cn.hutool.core.util.StrUtil; +import cn.aagro.pp.framework.common.util.io.FileUtils; +import cn.aagro.pp.framework.common.util.json.JsonUtils; +import cn.aagro.pp.framework.common.util.object.ObjectUtils; +import cn.aagro.pp.module.pay.enums.order.PayOrderStatusEnum; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.refund.PayRefundRespDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.transfer.PayTransferRespDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.impl.AbstractPayClient; +import com.github.binarywang.wxpay.bean.notify.*; +import com.github.binarywang.wxpay.bean.request.*; +import com.github.binarywang.wxpay.bean.result.*; +import com.github.binarywang.wxpay.bean.transfer.TransferBillsGetResult; +import com.github.binarywang.wxpay.bean.transfer.TransferBillsNotifyResult; +import com.github.binarywang.wxpay.bean.transfer.TransferBillsRequest; +import com.github.binarywang.wxpay.bean.transfer.TransferBillsResult; +import com.github.binarywang.wxpay.config.WxPayConfig; +import com.github.binarywang.wxpay.exception.WxPayException; +import com.github.binarywang.wxpay.service.WxPayService; +import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl; +import lombok.extern.slf4j.Slf4j; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Map; +import java.util.Objects; + +import static cn.hutool.core.date.DatePattern.*; +import static cn.aagro.pp.module.pay.framework.pay.core.client.impl.weixin.WxPayClientConfig.API_VERSION_V2; +import static cn.aagro.pp.module.pay.framework.pay.core.client.impl.weixin.WxPayClientConfig.API_VERSION_V3; + +/** + * 微信支付抽象类,实现微信统一的接口、以及部分实现(退款) + * + * @author 芋道源码 + */ +@Slf4j +public abstract class AbstractWxPayClient extends AbstractPayClient { + + protected WxPayService client; + + public AbstractWxPayClient(Long channelId, String channelCode, WxPayClientConfig config) { + super(channelId, channelCode, config); + } + + /** + * 初始化 client 客户端 + * + * @param tradeType 交易类型 + */ + protected void doInit(String tradeType) { + // 创建 config 配置 + WxPayConfig payConfig = new WxPayConfig(); + BeanUtil.copyProperties(config, payConfig, "keyContent", "privateKeyContent", "publicKeyContent"); + payConfig.setTradeType(tradeType); + // weixin-pay-java 无法设置内容,只允许读取文件,所以这里要创建临时文件来解决 + if (Objects.equals(config.getApiVersion(), API_VERSION_V2)) { + payConfig.setKeyPath(FileUtils.createTempFile(Base64.decode(config.getKeyContent())).getPath()); + } else if (Objects.equals(config.getApiVersion(), API_VERSION_V3)) { + payConfig.setPrivateKeyPath(FileUtils.createTempFile(config.getPrivateKeyContent()).getPath()); + // 参考 https://gitee.com/aagrocode/aagro-ui-admin-vue3/issues/ICUE53 和 https://t.zsxq.com/ODR5V + if (StrUtil.isNotBlank(config.getPublicKeyContent())) { + payConfig.setPrivateCertPath(FileUtils.createTempFile(Base64.decode(config.getPublicKeyContent())).getPath()); + } + // 特殊:强制使用微信公钥模式,避免灰度期间的问题!!! + payConfig.setStrictlyNeedWechatPaySerial(true); + } + + // 创建 client 客户端 + client = new WxPayServiceImpl(); + client.setConfig(payConfig); + } + + // ============ 支付相关 ========== + + @Override + protected PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws Exception { + try { + switch (config.getApiVersion()) { + case API_VERSION_V2: + return doUnifiedOrderV2(reqDTO); + case API_VERSION_V3: + // TODO @芋艿:【可能是 wxjava 的 bug】参考 https://github.com/binarywang/WxJava/issues/1557 + client.getConfig().setApiV3HttpClient(null); + return doUnifiedOrderV3(reqDTO); + default: + throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion())); + } + } catch (WxPayException e) { + log.error("[doUnifiedOrder][支付({}) 发起微信支付异常", reqDTO, e); + String errorCode = getErrorCode(e); + String errorMessage = getErrorMessage(e); + return PayOrderRespDTO.closedOf(errorCode, errorMessage, + reqDTO.getOutTradeNo(), e.getXmlString()); + } + } + + /** + * 【V2】调用支付渠道,统一下单 + * + * @param reqDTO 下单信息 + * @return 各支付渠道的返回结果 + */ + protected abstract PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) + throws Exception; + + /** + * 【V3】调用支付渠道,统一下单 + * + * @param reqDTO 下单信息 + * @return 各支付渠道的返回结果 + */ + protected abstract PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) + throws WxPayException; + + /** + * 【V2】创建微信下单请求 + * + * @param reqDTO 下信息 + * @return 下单请求 + */ + protected WxPayUnifiedOrderRequest buildPayUnifiedOrderRequestV2(PayOrderUnifiedReqDTO reqDTO) { + return WxPayUnifiedOrderRequest.newBuilder() + .outTradeNo(reqDTO.getOutTradeNo()) + .body(reqDTO.getSubject()) + .detail(reqDTO.getBody()) + .totalFee(reqDTO.getPrice()) // 单位分 + .timeExpire(formatDateV2(reqDTO.getExpireTime())) + .spbillCreateIp(reqDTO.getUserIp()) + .notifyUrl(reqDTO.getNotifyUrl()) + .build(); + } + + /** + * 【V3】创建微信下单请求 + * + * @param reqDTO 下信息 + * @return 下单请求 + */ + protected WxPayUnifiedOrderV3Request buildPayUnifiedOrderRequestV3(PayOrderUnifiedReqDTO reqDTO) { + WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request(); + request.setOutTradeNo(reqDTO.getOutTradeNo()); + request.setDescription(reqDTO.getSubject()); + request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(reqDTO.getPrice())); // 单位分 + request.setTimeExpire(formatDateV3(reqDTO.getExpireTime())); + request.setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp(reqDTO.getUserIp())); + request.setNotifyUrl(reqDTO.getNotifyUrl()); + return request; + } + + @Override + public PayOrderRespDTO doParseOrderNotify(Map params, String body, Map headers) throws WxPayException { + switch (config.getApiVersion()) { + case API_VERSION_V2: + return doParseOrderNotifyV2(body); + case API_VERSION_V3: + return doParseOrderNotifyV3(body, headers); + default: + throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion())); + } + } + + private PayOrderRespDTO doParseOrderNotifyV2(String body) throws WxPayException { + // 1. 解析回调 + WxPayOrderNotifyResult response = client.parseOrderNotifyResult(body); + // 2. 构建结果 + // V2 微信支付的回调,只有 SUCCESS 支付成功、CLOSED 支付失败两种情况,无需像支付宝一样解析的比较复杂 + Integer status = Objects.equals(response.getResultCode(), "SUCCESS") ? + PayOrderStatusEnum.SUCCESS.getStatus() : PayOrderStatusEnum.CLOSED.getStatus(); + return PayOrderRespDTO.of(status, response.getTransactionId(), response.getOpenid(), parseDateV2(response.getTimeEnd()), + response.getOutTradeNo(), body); + } + + private PayOrderRespDTO doParseOrderNotifyV3(String body, Map headers) throws WxPayException { + // 1. 解析回调 + SignatureHeader signatureHeader = getRequestHeader(headers); + WxPayNotifyV3Result response = client.parseOrderNotifyV3Result(body, signatureHeader); + WxPayNotifyV3Result.DecryptNotifyResult result = response.getResult(); + // 2. 构建结果 + Integer status = parseStatus(result.getTradeState()); + String openid = result.getPayer() != null ? result.getPayer().getOpenid() : null; + return PayOrderRespDTO.of(status, result.getTransactionId(), openid, parseDateV3(result.getSuccessTime()), + result.getOutTradeNo(), body); + } + + @Override + protected PayOrderRespDTO doGetOrder(String outTradeNo) throws Throwable { + try { + switch (config.getApiVersion()) { + case API_VERSION_V2: + return doGetOrderV2(outTradeNo); + case API_VERSION_V3: + return doGetOrderV3(outTradeNo); + default: + throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion())); + } + } catch (WxPayException e) { + if (ObjectUtils.equalsAny(e.getErrCode(), "ORDERNOTEXIST", "ORDER_NOT_EXIST")) { + String errorCode = getErrorCode(e); + String errorMessage = getErrorMessage(e); + return PayOrderRespDTO.closedOf(errorCode, errorMessage, + outTradeNo, e.getXmlString()); + } + throw e; + } + } + + private PayOrderRespDTO doGetOrderV2(String outTradeNo) throws WxPayException { + // 构建 WxPayUnifiedOrderRequest 对象 + WxPayOrderQueryRequest request = WxPayOrderQueryRequest.newBuilder() + .outTradeNo(outTradeNo).build(); + // 执行请求 + WxPayOrderQueryResult response = client.queryOrder(request); + + // 转换结果 + Integer status = parseStatus(response.getTradeState()); + return PayOrderRespDTO.of(status, response.getTransactionId(), response.getOpenid(), parseDateV2(response.getTimeEnd()), + outTradeNo, response); + } + + private PayOrderRespDTO doGetOrderV3(String outTradeNo) throws WxPayException { + // 构建 WxPayUnifiedOrderRequest 对象 + WxPayOrderQueryV3Request request = new WxPayOrderQueryV3Request() + .setOutTradeNo(outTradeNo); + // 执行请求 + WxPayOrderQueryV3Result response = client.queryOrderV3(request); + + // 转换结果 + Integer status = parseStatus(response.getTradeState()); + String openid = response.getPayer() != null ? response.getPayer().getOpenid() : null; + return PayOrderRespDTO.of(status, response.getTransactionId(), openid, parseDateV3(response.getSuccessTime()), + outTradeNo, response); + } + + private static Integer parseStatus(String tradeState) { + switch (tradeState) { + case "NOTPAY": + case "USERPAYING": // 支付中,等待用户输入密码(条码支付独有) + return PayOrderStatusEnum.WAITING.getStatus(); + case "SUCCESS": + return PayOrderStatusEnum.SUCCESS.getStatus(); + case "REFUND": + return PayOrderStatusEnum.REFUND.getStatus(); + case "CLOSED": + case "REVOKED": // 已撤销(刷卡支付独有) + case "PAYERROR": // 支付失败(其它原因,如银行返回失败) + return PayOrderStatusEnum.CLOSED.getStatus(); + default: + throw new IllegalArgumentException(StrUtil.format("未知的支付状态({})", tradeState)); + } + } + + // ============ 退款相关 ========== + + @Override + protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable { + try { + switch (config.getApiVersion()) { + case API_VERSION_V2: + return doUnifiedRefundV2(reqDTO); + case API_VERSION_V3: + return doUnifiedRefundV3(reqDTO); + default: + throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion())); + } + } catch (WxPayException e) { + String errorCode = getErrorCode(e); + String errorMessage = getErrorMessage(e); + return PayRefundRespDTO.failureOf(errorCode, errorMessage, + reqDTO.getOutRefundNo(), e.getXmlString()); + } + } + + private PayRefundRespDTO doUnifiedRefundV2(PayRefundUnifiedReqDTO reqDTO) throws Throwable { + // 1. 构建 WxPayRefundRequest 请求 + WxPayRefundRequest request = new WxPayRefundRequest() + .setOutTradeNo(reqDTO.getOutTradeNo()) + .setOutRefundNo(reqDTO.getOutRefundNo()) + .setRefundFee(reqDTO.getRefundPrice()) + .setRefundDesc(reqDTO.getReason()) + .setTotalFee(reqDTO.getPayPrice()) + .setNotifyUrl(reqDTO.getNotifyUrl()); + // 2.1 执行请求 + WxPayRefundResult response = client.refundV2(request); + // 2.2 创建返回结果 + if (Objects.equals("SUCCESS", response.getResultCode())) { // V2 情况下,不直接返回退款成功,而是等待异步通知 + return PayRefundRespDTO.waitingOf(response.getRefundId(), + reqDTO.getOutRefundNo(), response); + } + return PayRefundRespDTO.failureOf(reqDTO.getOutRefundNo(), response); + } + + private PayRefundRespDTO doUnifiedRefundV3(PayRefundUnifiedReqDTO reqDTO) throws Throwable { + // 1. 构建 WxPayRefundRequest 请求 + WxPayRefundV3Request request = new WxPayRefundV3Request() + .setOutTradeNo(reqDTO.getOutTradeNo()) + .setOutRefundNo(reqDTO.getOutRefundNo()) + .setAmount(new WxPayRefundV3Request.Amount().setRefund(reqDTO.getRefundPrice()) + .setTotal(reqDTO.getPayPrice()).setCurrency("CNY")) + .setReason(reqDTO.getReason()) + .setNotifyUrl(reqDTO.getNotifyUrl()); + // 2.1 执行请求 + WxPayRefundV3Result response = client.refundV3(request); + // 2.2 创建返回结果 + if (Objects.equals("SUCCESS", response.getStatus())) { + return PayRefundRespDTO.successOf(response.getRefundId(), parseDateV3(response.getSuccessTime()), + reqDTO.getOutRefundNo(), response); + } + if (Objects.equals("PROCESSING", response.getStatus())) { + return PayRefundRespDTO.waitingOf(response.getRefundId(), + reqDTO.getOutRefundNo(), response); + } + return PayRefundRespDTO.failureOf(reqDTO.getOutRefundNo(), response); + } + + @Override + public PayRefundRespDTO doParseRefundNotify(Map params, String body, Map headers) throws WxPayException { + switch (config.getApiVersion()) { + case API_VERSION_V2: + return doParseRefundNotifyV2(body); + case API_VERSION_V3: + return parseRefundNotifyV3(body, headers); + default: + throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion())); + } + } + + private PayRefundRespDTO doParseRefundNotifyV2(String body) throws WxPayException { + // 1. 解析回调 + WxPayRefundNotifyResult response = client.parseRefundNotifyResult(body); + WxPayRefundNotifyResult.ReqInfo result = response.getReqInfo(); + // 2. 构建结果 + if (Objects.equals("SUCCESS", result.getRefundStatus())) { + return PayRefundRespDTO.successOf(result.getRefundId(), parseDateV2B(result.getSuccessTime()), + result.getOutRefundNo(), response); + } + return PayRefundRespDTO.failureOf(result.getOutRefundNo(), response); + } + + private PayRefundRespDTO parseRefundNotifyV3(String body, Map headers) throws WxPayException { + // 1. 解析回调 + SignatureHeader signatureHeader = getRequestHeader(headers); + WxPayRefundNotifyV3Result response = client.parseRefundNotifyV3Result(body, signatureHeader); + WxPayRefundNotifyV3Result.DecryptNotifyResult result = response.getResult(); + // 2. 构建结果 + if (Objects.equals("SUCCESS", result.getRefundStatus())) { + return PayRefundRespDTO.successOf(result.getRefundId(), parseDateV3(result.getSuccessTime()), + result.getOutRefundNo(), response); + } + return PayRefundRespDTO.failureOf(result.getOutRefundNo(), response); + } + + @Override + protected PayRefundRespDTO doGetRefund(String outTradeNo, String outRefundNo) throws WxPayException { + try { + switch (config.getApiVersion()) { + case API_VERSION_V2: + return doGetRefundV2(outTradeNo, outRefundNo); + case API_VERSION_V3: + return doGetRefundV3(outTradeNo, outRefundNo); + default: + throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion())); + } + } catch (WxPayException e) { + if (ObjectUtils.equalsAny(e.getErrCode(), "REFUNDNOTEXIST", "RESOURCE_NOT_EXISTS")) { + String errorCode = getErrorCode(e); + String errorMessage = getErrorMessage(e); + return PayRefundRespDTO.failureOf(errorCode, errorMessage, + outRefundNo, e.getXmlString()); + } + throw e; + } + } + + private PayRefundRespDTO doGetRefundV2(String outTradeNo, String outRefundNo) throws WxPayException { + // 1. 构建 WxPayRefundRequest 请求 + WxPayRefundQueryRequest request = WxPayRefundQueryRequest.newBuilder() + .outTradeNo(outTradeNo) + .outRefundNo(outRefundNo) + .build(); + // 2.1 执行请求 + WxPayRefundQueryResult response = client.refundQuery(request); + // 2.2 创建返回结果 + if (!Objects.equals("SUCCESS", response.getResultCode())) { + return PayRefundRespDTO.waitingOf(null, + outRefundNo, response); + } + WxPayRefundQueryResult.RefundRecord refund = CollUtil.findOne(response.getRefundRecords(), + record -> record.getOutRefundNo().equals(outRefundNo)); + if (refund == null) { + return PayRefundRespDTO.failureOf(outRefundNo, response); + } + switch (refund.getRefundStatus()) { + case "SUCCESS": + return PayRefundRespDTO.successOf(refund.getRefundId(), parseDateV2B(refund.getRefundSuccessTime()), + outRefundNo, response); + case "PROCESSING": + return PayRefundRespDTO.waitingOf(refund.getRefundId(), + outRefundNo, response); + case "CHANGE": // 退款到银行发现用户的卡作废或者冻结了,导致原路退款银行卡失败,资金回流到商户的现金帐号,需要商户人工干预,通过线下或者财付通转账的方式进行退款 + case "FAIL": + return PayRefundRespDTO.failureOf(outRefundNo, response); + default: + throw new IllegalArgumentException(String.format("未知的退款状态(%s)", refund.getRefundStatus())); + } + } + + private PayRefundRespDTO doGetRefundV3(String outTradeNo, String outRefundNo) throws WxPayException { + // 1. 构建 WxPayRefundRequest 请求 + WxPayRefundQueryV3Request request = new WxPayRefundQueryV3Request(); + request.setOutRefundNo(outRefundNo); + // 2.1 执行请求 + WxPayRefundQueryV3Result response = client.refundQueryV3(request); + // 2.2 创建返回结果 + switch (response.getStatus()) { + case "SUCCESS": + return PayRefundRespDTO.successOf(response.getRefundId(), parseDateV3(response.getSuccessTime()), + outRefundNo, response); + case "PROCESSING": + return PayRefundRespDTO.waitingOf(response.getRefundId(), + outRefundNo, response); + case "ABNORMAL": // 退款异常 + case "CLOSED": + return PayRefundRespDTO.failureOf(outRefundNo, response); + default: + throw new IllegalArgumentException(String.format("未知的退款状态(%s)", response.getStatus())); + } + } + + @Override + protected PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) throws WxPayException { + // 1. 构建 TransferBillsRequest 请求 + TransferBillsRequest request = TransferBillsRequest.newBuilder() + .appid(this.config.getAppId()) + .outBillNo(reqDTO.getOutTransferNo()) + .transferAmount(reqDTO.getPrice()) + .transferRemark(reqDTO.getSubject()) + .transferSceneId(reqDTO.getChannelExtras().get("sceneId")) + .openid(reqDTO.getUserAccount()) + .userName(reqDTO.getUserName()) + .transferSceneReportInfos(JsonUtils.parseArray(reqDTO.getChannelExtras().get("sceneReportInfos"), + TransferBillsRequest.TransferSceneReportInfo.class)) + .notifyUrl(reqDTO.getNotifyUrl()) + .build(); + // 特殊:微信转账,必须 0.3 元起,才允许传入姓名 + if (reqDTO.getPrice() < 30) { + request.setUserName(null); + } + + // 2.1 执行请求 + try { + TransferBillsResult response = client.getTransferService().transferBills(request); + + // 2.2 创建返回结果 + String state = response.getState(); + if (ObjectUtils.equalsAny(state, "ACCEPTED", "PROCESSING", "WAIT_USER_CONFIRM", "TRANSFERING")) { + return PayTransferRespDTO.processingOf(response.getTransferBillNo(), response.getOutBillNo(), response) + .setChannelPackageInfo(response.getPackageInfo()); // 一般情况下,只有 WAIT_USER_CONFIRM 会有! + } + if (Objects.equals("SUCCESS", state)) { + return PayTransferRespDTO.successOf(response.getTransferBillNo(), parseDateV3(response.getCreateTime()), + response.getOutBillNo(), response); + } + return PayTransferRespDTO.closedOf(state, response.getFailReason(), + response.getOutBillNo(), response); + } catch (WxPayException e) { + log.error("[doUnifiedTransfer][转账({}) 发起微信支付异常", reqDTO, e); + String errorCode = getErrorCode(e); + String errorMessage = getErrorMessage(e); + return PayTransferRespDTO.closedOf(errorCode, errorMessage, + reqDTO.getOutTransferNo(), e.getXmlString()); + } + } + + @Override + protected PayTransferRespDTO doGetTransfer(String outTradeNo) throws WxPayException { + // 1. 执行请求 + TransferBillsGetResult response = client.getTransferService().getBillsByOutBillNo(outTradeNo); + + // 2. 创建返回结果 + String state = response.getState(); + if (ObjectUtils.equalsAny(state, "ACCEPTED", "PROCESSING", "WAIT_USER_CONFIRM", "TRANSFERING")) { + return PayTransferRespDTO.processingOf(response.getTransferBillNo(), response.getOutBillNo(), response); + } + if (Objects.equals("SUCCESS", state)) { + return PayTransferRespDTO.successOf(response.getTransferBillNo(), parseDateV3(response.getUpdateTime()), + response.getOutBillNo(), response); + } + return PayTransferRespDTO.closedOf(state, response.getFailReason(), + response.getOutBillNo(), response); + } + + @Override + public PayTransferRespDTO doParseTransferNotify(Map params, String body, Map headers) throws WxPayException { + switch (config.getApiVersion()) { + case API_VERSION_V3: + return parseTransferNotifyV3(body, headers); + case API_VERSION_V2: + throw new UnsupportedOperationException("V2 版本暂不支持,建议使用 V3 版本"); + default: + throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion())); + } + } + + private PayTransferRespDTO parseTransferNotifyV3(String body, Map headers) throws WxPayException { + // 1. 解析回调 + SignatureHeader signatureHeader = getRequestHeader(headers); + TransferBillsNotifyResult response = client.getTransferService().parseTransferBillsNotifyResult(body, signatureHeader); + TransferBillsNotifyResult.DecryptNotifyResult result = response.getResult(); + + // 2. 创建返回结果 + String state = result.getState(); + if (ObjectUtils.equalsAny(state, "ACCEPTED", "PROCESSING", "WAIT_USER_CONFIRM", "TRANSFERING")) { + return PayTransferRespDTO.processingOf(result.getTransferBillNo(), result.getOutBillNo(), response); + } + if (Objects.equals("SUCCESS", state)) { + return PayTransferRespDTO.successOf(result.getTransferBillNo(), parseDateV3(result.getUpdateTime()), + result.getOutBillNo(), response); + } + return PayTransferRespDTO.closedOf(state, result.getFailReason(), + result.getOutBillNo(), response); + } + + // ========== 各种工具方法 ========== + + /** + * 组装请求头重的签名信息 + * + * @see 官方示例 + */ + private SignatureHeader getRequestHeader(Map headers) { + // 参见 https://gitee.com/zhijiantianya/aagro-cloud/issues/ICSFL6 + return SignatureHeader.builder() + .signature(getHeaderValue(headers, "Wechatpay-Signature", "wechatpay-signature")) + .nonce(getHeaderValue(headers, "Wechatpay-Nonce", "wechatpay-nonce")) + .serial(getHeaderValue(headers, "Wechatpay-Serial", "wechatpay-serial")) + .timeStamp(getHeaderValue(headers, "Wechatpay-Timestamp", "wechatpay-timestamp")) + .build(); + } + + private String getHeaderValue(Map headers, String capitalizedKey, String lowercaseKey) { + String value = headers.get(capitalizedKey); + if (value != null) { + return value; + } + return headers.get(lowercaseKey); + } + + static String formatDateV2(LocalDateTime time) { + return TemporalAccessorUtil.format(time.atZone(ZoneId.systemDefault()), PURE_DATETIME_PATTERN); + } + + static LocalDateTime parseDateV2(String time) { + return LocalDateTimeUtil.parse(time, PURE_DATETIME_PATTERN); + } + + static LocalDateTime parseDateV2B(String time) { + return LocalDateTimeUtil.parse(time, NORM_DATETIME_PATTERN); + } + + static String formatDateV3(LocalDateTime time) { + return TemporalAccessorUtil.format(time.atZone(ZoneId.systemDefault()), UTC_WITH_XXX_OFFSET_PATTERN); + } + + static LocalDateTime parseDateV3(String time) { + return LocalDateTimeUtil.parse(time, UTC_WITH_XXX_OFFSET_PATTERN); + } + + static String getErrorCode(WxPayException e) { + if (StrUtil.isNotEmpty(e.getErrCode())) { + return e.getErrCode(); + } + if (StrUtil.isNotEmpty(e.getCustomErrorMsg())) { + return "CUSTOM_ERROR"; + } + return e.getReturnCode(); + } + + static String getErrorMessage(WxPayException e) { + if (StrUtil.isNotEmpty(e.getErrCode())) { + return e.getErrCodeDes(); + } + if (StrUtil.isNotEmpty(e.getCustomErrorMsg())) { + return e.getCustomErrorMsg(); + } + return e.getReturnMsg(); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/weixin/WxAppPayClient.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/weixin/WxAppPayClient.java new file mode 100644 index 0000000..4e0942e --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/weixin/WxAppPayClient.java @@ -0,0 +1,63 @@ +package cn.aagro.pp.module.pay.framework.pay.core.client.impl.weixin; + +import cn.aagro.pp.module.pay.enums.PayChannelEnum; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import cn.aagro.pp.module.pay.framework.pay.core.enums.PayOrderDisplayModeEnum; +import com.github.binarywang.wxpay.bean.order.WxPayAppOrderResult; +import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest; +import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request; +import com.github.binarywang.wxpay.bean.result.WxPayUnifiedOrderV3Result; +import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum; +import com.github.binarywang.wxpay.constant.WxPayConstants; +import com.github.binarywang.wxpay.exception.WxPayException; +import lombok.extern.slf4j.Slf4j; + +import static cn.aagro.pp.framework.common.util.json.JsonUtils.toJsonString; + +/** + * 微信支付【App 支付】的 PayClient 实现类 + * + * 文档:App 支付 + * + * // TODO 芋艿:未详细测试,因为手头没 App + * + * @author 芋道源码 + */ +@Slf4j +public class WxAppPayClient extends AbstractWxPayClient { + + public WxAppPayClient(Long channelId, WxPayClientConfig config) { + super(channelId, PayChannelEnum.WX_APP.getCode(), config); + } + + @Override + protected void doInit() { + super.doInit(WxPayConstants.TradeType.APP); + } + + @Override + protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { + // 构建 WxPayUnifiedOrderRequest 对象 + WxPayUnifiedOrderRequest request = buildPayUnifiedOrderRequestV2(reqDTO); + // 执行请求 + WxPayAppOrderResult response = client.createOrder(request); + + // 转换结果 + return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.APP.getMode(), toJsonString(response), + reqDTO.getOutTradeNo(), response); + } + + @Override + protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { + // 构建 WxPayUnifiedOrderV3Request 对象 + WxPayUnifiedOrderV3Request request = buildPayUnifiedOrderRequestV3(reqDTO); + // 执行请求 + WxPayUnifiedOrderV3Result.AppResult response = client.createOrderV3(TradeTypeEnum.APP, request); + + // 转换结果 + return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.APP.getMode(), toJsonString(response), + reqDTO.getOutTradeNo(), response); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/weixin/WxBarPayClient.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/weixin/WxBarPayClient.java new file mode 100644 index 0000000..ac5cf2d --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/weixin/WxBarPayClient.java @@ -0,0 +1,107 @@ +package cn.aagro.pp.module.pay.framework.pay.core.client.impl.weixin; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.thread.ThreadUtil; +import cn.hutool.core.util.StrUtil; +import cn.aagro.pp.framework.common.util.date.LocalDateTimeUtils; +import cn.aagro.pp.module.pay.enums.PayChannelEnum; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import cn.aagro.pp.module.pay.framework.pay.core.enums.PayOrderDisplayModeEnum; +import com.github.binarywang.wxpay.bean.request.WxPayMicropayRequest; +import com.github.binarywang.wxpay.bean.result.WxPayMicropayResult; +import com.github.binarywang.wxpay.constant.WxPayConstants; +import com.github.binarywang.wxpay.exception.WxPayException; +import lombok.extern.slf4j.Slf4j; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.concurrent.TimeUnit; + +import static cn.aagro.pp.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; +import static cn.aagro.pp.framework.common.util.json.JsonUtils.toJsonString; + +/** + * 微信支付【付款码支付】的 PayClient 实现类 + * + * 文档:付款码支付 + * + * @author 芋道源码 + */ +@Slf4j +public class WxBarPayClient extends AbstractWxPayClient { + + /** + * 微信付款码的过期时间 + */ + private static final Duration AUTH_CODE_EXPIRE = Duration.ofMinutes(3); + + public WxBarPayClient(Long channelId, WxPayClientConfig config) { + super(channelId, PayChannelEnum.WX_BAR.getCode(), config); + } + + @Override + protected void doInit() { + super.doInit(WxPayConstants.TradeType.MICROPAY); + } + + @Override + protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { + // 由于付款码需要不断轮询,所以需要在较短的时间完成支付 + LocalDateTime expireTime = LocalDateTimeUtils.addTime(AUTH_CODE_EXPIRE); + if (expireTime.isAfter(reqDTO.getExpireTime())) { + expireTime = reqDTO.getExpireTime(); + } + // 构建 WxPayMicropayRequest 对象 + WxPayMicropayRequest request = WxPayMicropayRequest.newBuilder() + .outTradeNo(reqDTO.getOutTradeNo()) + .body(reqDTO.getSubject()) + .detail(reqDTO.getBody()) + .totalFee(reqDTO.getPrice()) // 单位分 + .timeExpire(formatDateV2(expireTime)) + .spbillCreateIp(reqDTO.getUserIp()) + .authCode(getAuthCode(reqDTO)) + .build(); + // 执行请求,重试直到失败(过期),或者成功 + WxPayException lastWxPayException = null; + for (int i = 1; i < Byte.MAX_VALUE; i++) { + try { + WxPayMicropayResult response = client.micropay(request); + // 支付成功,例如说:1)用户输入了密码;2)用户免密支付 + return PayOrderRespDTO.successOf(response.getTransactionId(), response.getOpenid(), parseDateV2(response.getTimeEnd()), + response.getOutTradeNo(), response) + .setDisplayMode(PayOrderDisplayModeEnum.BAR_CODE.getMode()); + } catch (WxPayException ex) { + lastWxPayException = ex; + // 如果不满足这 3 种任一的,则直接抛出 WxPayException 异常,不仅需处理 + // 1. SYSTEMERROR:接口返回错误:请立即调用被扫订单结果查询API,查询当前订单状态,并根据订单的状态决定下一步的操作。 + // 2. USERPAYING:用户支付中,需要输入密码:等待 5 秒,然后调用被扫订单结果查询 API,查询当前订单的不同状态,决定下一步的操作。 + // 3. BANKERROR:银行系统异常:请立即调用被扫订单结果查询 API,查询当前订单的不同状态,决定下一步的操作。 + if (!StrUtil.equalsAny(ex.getErrCode(), "SYSTEMERROR", "USERPAYING", "BANKERROR")) { + throw ex; + } + // 等待 5 秒,继续下一轮重新发起支付 + log.info("[doUnifiedOrderV2][发起微信 Bar 支付第({})失败,等待下一轮重试,请求({}),响应({})]", i, + toJsonString(request), ex.getMessage()); + ThreadUtil.sleep(5, TimeUnit.SECONDS); + } + } + throw lastWxPayException; + } + + @Override + protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { + return doUnifiedOrderV2(reqDTO); + } + + // ========== 各种工具方法 ========== + + static String getAuthCode(PayOrderUnifiedReqDTO reqDTO) { + String authCode = MapUtil.getStr(reqDTO.getChannelExtras(), "authCode"); + if (StrUtil.isEmpty(authCode)) { + throw invalidParamException("支付请求的 authCode 不能为空!"); + } + return authCode; + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/weixin/WxLitePayClient.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/weixin/WxLitePayClient.java new file mode 100644 index 0000000..3a2171c --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/weixin/WxLitePayClient.java @@ -0,0 +1,22 @@ +package cn.aagro.pp.module.pay.framework.pay.core.client.impl.weixin; + +import cn.aagro.pp.module.pay.enums.PayChannelEnum; +import lombok.extern.slf4j.Slf4j; + +/** + * 微信支付【小程序】的 PayClient 实现类 + * + * 由于公众号和小程序的微信支付逻辑一致,所以直接进行继承 + * + * 文档:JSAPI 下单 + * + * @author zwy + */ +@Slf4j +public class WxLitePayClient extends WxPubPayClient { + + public WxLitePayClient(Long channelId, WxPayClientConfig config) { + super(channelId, PayChannelEnum.WX_LITE.getCode(), config); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/weixin/WxNativePayClient.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/weixin/WxNativePayClient.java new file mode 100644 index 0000000..196bf06 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/weixin/WxNativePayClient.java @@ -0,0 +1,59 @@ +package cn.aagro.pp.module.pay.framework.pay.core.client.impl.weixin; + +import cn.aagro.pp.module.pay.enums.PayChannelEnum; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import cn.aagro.pp.module.pay.framework.pay.core.enums.PayOrderDisplayModeEnum; +import com.github.binarywang.wxpay.bean.order.WxPayNativeOrderResult; +import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest; +import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request; +import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum; +import com.github.binarywang.wxpay.constant.WxPayConstants; +import com.github.binarywang.wxpay.exception.WxPayException; +import lombok.extern.slf4j.Slf4j; + +/** + * 微信支付【Native 二维码】的 PayClient 实现类 + * + * 文档:Native 下单 + * + * @author zwy + */ +@Slf4j +public class WxNativePayClient extends AbstractWxPayClient { + + public WxNativePayClient(Long channelId, WxPayClientConfig config) { + super(channelId, PayChannelEnum.WX_NATIVE.getCode(), config); + } + + @Override + protected void doInit() { + super.doInit(WxPayConstants.TradeType.NATIVE); + } + + @Override + protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { + // 构建 WxPayUnifiedOrderRequest 对象 + WxPayUnifiedOrderRequest request = buildPayUnifiedOrderRequestV2(reqDTO) + .setProductId(reqDTO.getOutTradeNo()); // V2 必须传递 productId,无需在微信配置。该参数在 V3 简化,无需传递! + // 执行请求 + WxPayNativeOrderResult response = client.createOrder(request); + + // 转换结果 + return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.QR_CODE.getMode(), response.getCodeUrl(), + reqDTO.getOutTradeNo(), response); + } + + @Override + protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { + // 构建 WxPayUnifiedOrderV3Request 对象 + WxPayUnifiedOrderV3Request request = buildPayUnifiedOrderRequestV3(reqDTO); + // 执行请求 + String response = client.createOrderV3(TradeTypeEnum.NATIVE, request); + + // 转换结果 + return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.QR_CODE.getMode(), response, + reqDTO.getOutTradeNo(), response); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/weixin/WxPayClientConfig.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/weixin/WxPayClientConfig.java new file mode 100644 index 0000000..28f9cb2 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/weixin/WxPayClientConfig.java @@ -0,0 +1,107 @@ +package cn.aagro.pp.module.pay.framework.pay.core.client.impl.weixin; + +import cn.aagro.pp.framework.common.util.validation.ValidationUtils; +import cn.aagro.pp.module.pay.framework.pay.core.client.PayClientConfig; +import lombok.Data; + +import javax.validation.Validator; +import javax.validation.constraints.NotBlank; + +/** + * 微信支付的 PayClientConfig 实现类 + * 属性主要来自 {@link com.github.binarywang.wxpay.config.WxPayConfig} 的必要属性 + * + * @author 芋道源码 + */ +@Data +public class WxPayClientConfig implements PayClientConfig { + + /** + * API 版本 - V2 + * + * V2 协议说明 + */ + public static final String API_VERSION_V2 = "v2"; + /** + * API 版本 - V3 + * + * V3 协议说明 + */ + public static final String API_VERSION_V3 = "v3"; + + /** + * 公众号或者小程序的 appid + * + * 只有公众号或小程序需要该字段 + */ + @NotBlank(message = "APPID 不能为空", groups = {V2.class, V3.class}) + private String appId; + /** + * 商户号 + */ + @NotBlank(message = "商户号不能为空", groups = {V2.class, V3.class}) + private String mchId; + /** + * API 版本 + */ + @NotBlank(message = "API 版本不能为空", groups = {V2.class, V3.class}) + private String apiVersion; + + // ========== V2 版本的参数 ========== + + /** + * 商户密钥 + */ + @NotBlank(message = "商户密钥不能为空", groups = V2.class) + private String mchKey; + /** + * apiclient_cert.p12 证书文件的对应字符串【base64 格式】 + * + * 为什么采用 base64 格式?因为 p12 读取后是二进制,需要转换成 base64 格式才好传输和存储 + */ + @NotBlank(message = "apiclient_cert.p12 不能为空", groups = V2.class) + private String keyContent; + + // ========== V3 版本的参数 ========== + /** + * apiclient_key.pem 证书文件的对应字符串 + */ + @NotBlank(message = "apiclient_key 不能为空", groups = V3.class) + private String privateKeyContent; + /** + * apiV3 密钥值 + */ + @NotBlank(message = "apiV3 密钥值不能为空", groups = V3.class) + private String apiV3Key; + /** + * 证书序列号(merchantSerialNumber) + */ + @NotBlank(message = "证书序列号不能为空", groups = V3.class) + private String certSerialNo; + + /** + * pub_key.pem 证书文件的对应字符串 + */ + private String publicKeyContent; + @NotBlank(message = "publicKeyId 不能为空", groups = V3.class) + private String publicKeyId; + + /** + * 分组校验 v2版本 + */ + public interface V2 { + } + + /** + * 分组校验 v3版本 + */ + public interface V3 { + } + + @Override + public void validate(Validator validator) { + ValidationUtils.validate(validator, this, + API_VERSION_V2.equals(this.getApiVersion()) ? V2.class : V3.class); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/weixin/WxPubPayClient.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/weixin/WxPubPayClient.java new file mode 100644 index 0000000..ce96de7 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/weixin/WxPubPayClient.java @@ -0,0 +1,81 @@ +package cn.aagro.pp.module.pay.framework.pay.core.client.impl.weixin; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import cn.aagro.pp.module.pay.enums.PayChannelEnum; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import cn.aagro.pp.module.pay.framework.pay.core.enums.PayOrderDisplayModeEnum; +import com.github.binarywang.wxpay.bean.order.WxPayMpOrderResult; +import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest; +import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request; +import com.github.binarywang.wxpay.bean.result.WxPayUnifiedOrderV3Result; +import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum; +import com.github.binarywang.wxpay.constant.WxPayConstants; +import com.github.binarywang.wxpay.exception.WxPayException; +import lombok.extern.slf4j.Slf4j; + +import static cn.aagro.pp.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; +import static cn.aagro.pp.framework.common.util.json.JsonUtils.toJsonString; + +/** + * 微信支付(公众号)的 PayClient 实现类 + * + * 文档:JSAPI 下单 + * + * @author 芋道源码 + */ +@Slf4j +public class WxPubPayClient extends AbstractWxPayClient { + + @SuppressWarnings("unused") // 反射会调用到,所以不能删除 + public WxPubPayClient(Long channelId, WxPayClientConfig config) { + super(channelId, PayChannelEnum.WX_PUB.getCode(), config); + } + + protected WxPubPayClient(Long channelId, String channelCode, WxPayClientConfig config) { + super(channelId, channelCode, config); + } + + @Override + protected void doInit() { + super.doInit(WxPayConstants.TradeType.JSAPI); + } + + @Override + protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { + // 构建 WxPayUnifiedOrderRequest 对象 + WxPayUnifiedOrderRequest request = buildPayUnifiedOrderRequestV2(reqDTO) + .setOpenid(getOpenid(reqDTO)); + // 执行请求 + WxPayMpOrderResult response = client.createOrder(request); + + // 转换结果 + return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.APP.getMode(), toJsonString(response), + reqDTO.getOutTradeNo(), response); + } + + @Override + protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { + // 构建 WxPayUnifiedOrderRequest 对象 + WxPayUnifiedOrderV3Request request = buildPayUnifiedOrderRequestV3(reqDTO) + .setPayer(new WxPayUnifiedOrderV3Request.Payer().setOpenid(getOpenid(reqDTO))); + // 执行请求 + WxPayUnifiedOrderV3Result.JsapiResult response = client.createOrderV3(TradeTypeEnum.JSAPI, request); + + // 转换结果 + return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.APP.getMode(), toJsonString(response), + reqDTO.getOutTradeNo(), response); + } + + // ========== 各种工具方法 ========== + + static String getOpenid(PayOrderUnifiedReqDTO reqDTO) { + String openid = MapUtil.getStr(reqDTO.getChannelExtras(), "openid"); + if (StrUtil.isEmpty(openid)) { + throw invalidParamException("支付请求的 openid 不能为空!"); + } + return openid; + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/weixin/WxWapPayClient.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/weixin/WxWapPayClient.java new file mode 100644 index 0000000..88f33ba --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/weixin/WxWapPayClient.java @@ -0,0 +1,62 @@ +package cn.aagro.pp.module.pay.framework.pay.core.client.impl.weixin; + +import cn.aagro.pp.module.pay.enums.PayChannelEnum; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import cn.aagro.pp.module.pay.framework.pay.core.enums.PayOrderDisplayModeEnum; +import com.github.binarywang.wxpay.bean.order.WxPayMwebOrderResult; +import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest; +import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request; +import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum; +import com.github.binarywang.wxpay.constant.WxPayConstants; +import com.github.binarywang.wxpay.exception.WxPayException; +import lombok.extern.slf4j.Slf4j; + +/** + * 微信支付(H5 网页)的 PayClient 实现类 + * + * 文档:H5下单API + * + * @author YYQ + */ +@Slf4j +public class WxWapPayClient extends AbstractWxPayClient { + + public WxWapPayClient(Long channelId, WxPayClientConfig config) { + super(channelId, PayChannelEnum.WX_WAP.getCode(), config); + } + + protected WxWapPayClient(Long channelId, String channelCode, WxPayClientConfig config) { + super(channelId, channelCode, config); + } + + @Override + protected void doInit() { + super.doInit(WxPayConstants.TradeType.MWEB); + } + + @Override + protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { + // 构建 WxPayUnifiedOrderRequest 对象 + WxPayUnifiedOrderRequest request = buildPayUnifiedOrderRequestV2(reqDTO); + // 执行请求 + WxPayMwebOrderResult response = client.createOrder(request); + + // 转换结果 + return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.URL.getMode(), response.getMwebUrl(), + reqDTO.getOutTradeNo(), response); + } + + @Override + protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { + // 构建 WxPayUnifiedOrderRequest 对象 + WxPayUnifiedOrderV3Request request = buildPayUnifiedOrderRequestV3(reqDTO); + // 执行请求 + String response = client.createOrderV3(TradeTypeEnum.H5, request); + + // 转换结果 + return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.URL.getMode(), response, + reqDTO.getOutTradeNo(), response); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/enums/PayOrderDisplayModeEnum.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/enums/PayOrderDisplayModeEnum.java new file mode 100644 index 0000000..5609e15 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/core/enums/PayOrderDisplayModeEnum.java @@ -0,0 +1,29 @@ +package cn.aagro.pp.module.pay.framework.pay.core.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 支付 UI 展示模式 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum PayOrderDisplayModeEnum { + + URL("url"), // Redirect 跳转链接的方式 + IFRAME("iframe"), // IFrame 内嵌链接的方式【目前暂时用不到】 + FORM("form"), // HTML 表单提交 + QR_CODE("qr_code"), // 二维码的文字内容 + QR_CODE_URL("qr_code_url"), // 二维码的图片链接 + BAR_CODE("bar_code"), // 条形码 + APP("app"), // 应用:Android、iOS、微信小程序、微信公众号等,需要做自定义处理的 + ; + + /** + * 展示模式 + */ + private final String mode; + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/package-info.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/package-info.java new file mode 100644 index 0000000..cda8168 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/pay/package-info.java @@ -0,0 +1,6 @@ +/** + * 支付客户端,接入国内多个支付渠道: + * 1. 支付宝,基于官方 SDK 接入 + * 2. 微信支付,基于 weixin-java-pay 接入 + */ +package cn.aagro.pp.module.pay.framework.pay; \ No newline at end of file diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/web/config/PayWebConfiguration.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/web/config/PayWebConfiguration.java new file mode 100644 index 0000000..fd3d737 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/web/config/PayWebConfiguration.java @@ -0,0 +1,24 @@ +package cn.aagro.pp.module.pay.framework.web.config; + +import cn.aagro.pp.framework.swagger.config.AagroSwaggerAutoConfiguration; +import org.springdoc.core.GroupedOpenApi; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * pay 模块的 web 组件的 Configuration + * + * @author 芋道源码 + */ +@Configuration(proxyBeanMethods = false) +public class PayWebConfiguration { + + /** + * pay 模块的 API 分组 + */ + @Bean + public GroupedOpenApi payGroupedOpenApi() { + return AagroSwaggerAutoConfiguration.buildGroupedOpenApi("pay"); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/web/package-info.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/web/package-info.java new file mode 100644 index 0000000..33e3a2d --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/framework/web/package-info.java @@ -0,0 +1,4 @@ +/** + * pay 模块的 web 配置 + */ +package cn.aagro.pp.module.pay.framework.web; diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/job/notify/PayNotifyJob.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/job/notify/PayNotifyJob.java new file mode 100644 index 0000000..2692125 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/job/notify/PayNotifyJob.java @@ -0,0 +1,31 @@ +package cn.aagro.pp.module.pay.job.notify; + +import cn.aagro.pp.framework.quartz.core.handler.JobHandler; +import cn.aagro.pp.framework.tenant.core.job.TenantJob; +import cn.aagro.pp.module.pay.service.notify.PayNotifyService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * 支付通知 Job + * 通过不断扫描待通知的 PayNotifyTaskDO 记录,回调业务线的回调接口 + * + * @author 芋道源码 + */ +@Component +@Slf4j +public class PayNotifyJob implements JobHandler { + + @Resource + private PayNotifyService payNotifyService; + + @Override + @TenantJob + public String execute(String param) throws Exception { + int notifyCount = payNotifyService.executeNotify(); + return String.format("执行支付通知 %s 个", notifyCount); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/job/order/PayOrderExpireJob.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/job/order/PayOrderExpireJob.java new file mode 100644 index 0000000..3d8edd1 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/job/order/PayOrderExpireJob.java @@ -0,0 +1,31 @@ +package cn.aagro.pp.module.pay.job.order; + +import cn.hutool.core.util.StrUtil; +import cn.aagro.pp.framework.quartz.core.handler.JobHandler; +import cn.aagro.pp.framework.tenant.core.job.TenantJob; +import cn.aagro.pp.module.pay.service.order.PayOrderService; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * 支付订单的过期 Job + * + * 支付超过过期时间时,支付渠道是不会通知进行过期,所以需要定时进行过期关闭。 + * + * @author 芋道源码 + */ +@Component +public class PayOrderExpireJob implements JobHandler { + + @Resource + private PayOrderService orderService; + + @Override + @TenantJob + public String execute(String param) { + int count = orderService.expireOrder(); + return StrUtil.format("支付过期 {} 个", count); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/job/order/PayOrderSyncJob.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/job/order/PayOrderSyncJob.java new file mode 100644 index 0000000..22d16b2 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/job/order/PayOrderSyncJob.java @@ -0,0 +1,43 @@ +package cn.aagro.pp.module.pay.job.order; + +import cn.hutool.core.util.StrUtil; +import cn.aagro.pp.framework.quartz.core.handler.JobHandler; +import cn.aagro.pp.framework.tenant.core.job.TenantJob; +import cn.aagro.pp.module.pay.service.order.PayOrderService; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.time.Duration; +import java.time.LocalDateTime; + +/** + * 支付订单的同步 Job + * + * 由于支付订单的状态,是由支付渠道异步通知进行同步,考虑到异步通知可能会失败(小概率),所以需要定时进行同步。 + * + * @author 芋道源码 + */ +@Component +public class PayOrderSyncJob implements JobHandler { + + /** + * 同步创建时间在 N 分钟之内的订单 + * + * 为什么同步 10 分钟之内的订单? + * 因为一个订单发起支付,到支付成功,大多数在 10 分钟内,需要保证轮询到。 + * 如果设置为 30、60 或者更大时间范围,会导致轮询的订单太多,影响性能。当然,你也可以根据自己的业务情况来处理。 + */ + private static final Duration CREATE_TIME_DURATION_BEFORE = Duration.ofMinutes(10); + + @Resource + private PayOrderService orderService; + + @Override + @TenantJob + public String execute(String param) { + LocalDateTime minCreateTime = LocalDateTime.now().minus(CREATE_TIME_DURATION_BEFORE); + int count = orderService.syncOrder(minCreateTime); + return StrUtil.format("同步支付订单 {} 个", count); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/job/refund/PayRefundSyncJob.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/job/refund/PayRefundSyncJob.java new file mode 100644 index 0000000..6f774d2 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/job/refund/PayRefundSyncJob.java @@ -0,0 +1,31 @@ +package cn.aagro.pp.module.pay.job.refund; + +import cn.hutool.core.util.StrUtil; +import cn.aagro.pp.framework.quartz.core.handler.JobHandler; +import cn.aagro.pp.framework.tenant.core.job.TenantJob; +import cn.aagro.pp.module.pay.service.refund.PayRefundService; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * 退款订单的同步 Job + * + * 由于退款订单的状态,是由支付渠道异步通知进行同步,考虑到异步通知可能会失败(小概率),所以需要定时进行同步。 + * + * @author 芋道源码 + */ +@Component +public class PayRefundSyncJob implements JobHandler { + + @Resource + private PayRefundService refundService; + + @Override + @TenantJob + public String execute(String param) { + int count = refundService.syncRefund(); + return StrUtil.format("同步退款订单 {} 个", count); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/job/transfer/PayTransferSyncJob.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/job/transfer/PayTransferSyncJob.java new file mode 100644 index 0000000..ddcc2b0 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/job/transfer/PayTransferSyncJob.java @@ -0,0 +1,30 @@ +package cn.aagro.pp.module.pay.job.transfer; + +import cn.hutool.core.util.StrUtil; +import cn.aagro.pp.framework.quartz.core.handler.JobHandler; +import cn.aagro.pp.framework.tenant.core.job.TenantJob; +import cn.aagro.pp.module.pay.service.transfer.PayTransferService; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * 转账订单的同步 Job + * + * 由于转账订单的转账结果,有些渠道是异步通知进行同步的,考虑到异步通知可能会失败(小概率),所以需要定时进行同步。 + * + * @author jason + */ +@Component +public class PayTransferSyncJob implements JobHandler { + + @Resource + private PayTransferService transferService; + + @Override + @TenantJob + public String execute(String param) { + int count = transferService.syncTransfer(); + return StrUtil.format("同步转账订单 {} 个", count); + } +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/package-info.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/package-info.java new file mode 100644 index 0000000..b1e6d79 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/package-info.java @@ -0,0 +1,10 @@ +/** + * pay 模块,我们放支付业务,提供业务的支付能力。 + * 例如说:商户、应用、支付、退款等等 + * + * 1. Controller URL:以 /pay/ 开头,避免和其它 Module 冲突 + * 2. DataObject 表名:以 pay_ 开头,方便在数据库中区分 + * + * 注意,由于 Pay 模块和 Trade 模块,容易重名,所以类名都加载 Pay 的前缀~ + */ +package cn.aagro.pp.module.pay; diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/app/PayAppService.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/app/PayAppService.java new file mode 100644 index 0000000..ef28d95 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/app/PayAppService.java @@ -0,0 +1,115 @@ +package cn.aagro.pp.module.pay.service.app; + +import cn.aagro.pp.framework.common.exception.ServiceException; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.common.util.collection.CollectionUtils; +import cn.aagro.pp.module.pay.controller.admin.app.vo.PayAppCreateReqVO; +import cn.aagro.pp.module.pay.controller.admin.app.vo.PayAppPageReqVO; +import cn.aagro.pp.module.pay.controller.admin.app.vo.PayAppUpdateReqVO; +import cn.aagro.pp.module.pay.dal.dataobject.app.PayAppDO; + +import javax.validation.Valid; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * 支付应用 Service 接口 + * + * @author 芋艿 + */ +public interface PayAppService { + + /** + * 创建支付应用 + * + * @param createReqVO 创建 + * @return 编号 + */ + Long createApp(@Valid PayAppCreateReqVO createReqVO); + + /** + * 更新支付应用 + * + * @param updateReqVO 更新 + */ + void updateApp(@Valid PayAppUpdateReqVO updateReqVO); + + /** + * 修改应用状态 + * + * @param id 应用编号 + * @param status 状态 + */ + void updateAppStatus(Long id, Integer status); + + /** + * 删除支付应用 + * + * @param id 编号 + */ + void deleteApp(Long id); + + /** + * 获得支付应用 + * + * @param id 编号 + * @return 支付应用 + */ + PayAppDO getApp(Long id); + + /** + * 获得支付应用列表 + * + * @param ids 编号 + * @return 支付应用列表 + */ + List getAppList(Collection ids); + + /** + * 获得支付应用列表 + * + * @return 支付应用列表 + */ + List getAppList(); + + /** + * 获得支付应用分页 + * + * @param pageReqVO 分页查询 + * @return 支付应用分页 + */ + PageResult getAppPage(PayAppPageReqVO pageReqVO); + + /** + * 获得指定编号的商户 Map + * + * @param ids 应用编号集合 + * @return 商户 Map + */ + default Map getAppMap(Collection ids) { + List list = getAppList(ids); + return CollectionUtils.convertMap(list, PayAppDO::getId); + } + + /** + * 支付应用的合法性 + *

+ * 如果不合法,抛出 {@link ServiceException} 业务异常 + * + * @param id 应用编号 + * @return 应用 + */ + PayAppDO validPayApp(Long id); + + /** + * 支付应用的合法性 + *

+ * 如果不合法,抛出 {@link ServiceException} 业务异常 + * + * @param appKey 应用标识 + * @return 应用 + */ + PayAppDO validPayApp(String appKey); + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/app/PayAppServiceImpl.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/app/PayAppServiceImpl.java new file mode 100644 index 0000000..a1280c9 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/app/PayAppServiceImpl.java @@ -0,0 +1,167 @@ +package cn.aagro.pp.module.pay.service.app; + +import cn.hutool.core.collection.CollUtil; +import cn.aagro.pp.framework.common.enums.CommonStatusEnum; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.pay.controller.admin.app.vo.PayAppCreateReqVO; +import cn.aagro.pp.module.pay.controller.admin.app.vo.PayAppPageReqVO; +import cn.aagro.pp.module.pay.controller.admin.app.vo.PayAppUpdateReqVO; +import cn.aagro.pp.module.pay.convert.app.PayAppConvert; +import cn.aagro.pp.module.pay.dal.dataobject.app.PayAppDO; +import cn.aagro.pp.module.pay.dal.mysql.app.PayAppMapper; +import cn.aagro.pp.module.pay.enums.ErrorCodeConstants; +import cn.aagro.pp.module.pay.service.order.PayOrderService; +import cn.aagro.pp.module.pay.service.refund.PayRefundService; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import static cn.aagro.pp.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.aagro.pp.module.pay.enums.ErrorCodeConstants.*; + +/** + * 支付应用 Service 实现类 + * + * @author aquan + */ +@Service +@Validated +public class PayAppServiceImpl implements PayAppService { + + @Resource + private PayAppMapper appMapper; + + @Resource + @Lazy // 延迟加载,避免循环依赖报错 + private PayOrderService orderService; + @Resource + @Lazy // 延迟加载,避免循环依赖报错 + private PayRefundService refundService; + + @Override + public Long createApp(PayAppCreateReqVO createReqVO) { + // 验证 appKey 是否重复 + validateAppKeyUnique(null, createReqVO.getAppKey()); + + // 插入 + PayAppDO app = PayAppConvert.INSTANCE.convert(createReqVO); + appMapper.insert(app); + // 返回 + return app.getId(); + } + + @Override + public void updateApp(PayAppUpdateReqVO updateReqVO) { + // 校验存在 + validateAppExists(updateReqVO.getId()); + // 验证 appKey 是否重复 + validateAppKeyUnique(updateReqVO.getId(), updateReqVO.getAppKey()); + + // 更新 + PayAppDO updateObj = PayAppConvert.INSTANCE.convert(updateReqVO); + appMapper.updateById(updateObj); + } + + void validateAppKeyUnique(Long id, String appKey) { + PayAppDO app = appMapper.selectByAppKey(appKey); + if (app == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 appKey 的应用 + if (id == null) { + throw exception(APP_KEY_EXISTS); + } + if (!app.getId().equals(id)) { + throw exception(APP_KEY_EXISTS); + } + } + + @Override + public void updateAppStatus(Long id, Integer status) { + // 校验商户存在 + validateAppExists(id); + // 更新状态 + appMapper.updateById(new PayAppDO().setId(id).setStatus(status)); + } + + @Override + public void deleteApp(Long id) { + // 校验存在 + validateAppExists(id); + // 校验关联数据是否存在 + if (orderService.getOrderCountByAppId(id) > 0) { + throw exception(APP_EXIST_ORDER_CANT_DELETE); + } + if (refundService.getRefundCountByAppId(id) > 0) { + throw exception(APP_EXIST_REFUND_CANT_DELETE); + } + + // 删除 + appMapper.deleteById(id); + } + + private void validateAppExists(Long id) { + if (appMapper.selectById(id) == null) { + throw exception(APP_NOT_FOUND); + } + } + + @Override + public PayAppDO getApp(Long id) { + return appMapper.selectById(id); + } + + @Override + public List getAppList(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return Collections.emptyList(); + } + return appMapper.selectByIds(ids); + } + + @Override + public List getAppList() { + return appMapper.selectList(); + } + + @Override + public PageResult getAppPage(PayAppPageReqVO pageReqVO) { + return appMapper.selectPage(pageReqVO); + } + + @Override + public PayAppDO validPayApp(Long appId) { + PayAppDO app = appMapper.selectById(appId); + return validatePayApp(app); + } + + @Override + public PayAppDO validPayApp(String appKey) { + PayAppDO app = appMapper.selectByAppKey(appKey); + return validatePayApp(app); + } + + /** + * 校验支付应用实体的有效性:存在 + 开启 + * + * @param app 待校验的支付应用实体 + * @return 校验通过的支付应用实体 + */ + private PayAppDO validatePayApp(PayAppDO app) { + // 校验是否存在 + if (app == null) { + throw exception(ErrorCodeConstants.APP_NOT_FOUND); + } + // 校验是否禁用 + if (CommonStatusEnum.isDisable(app.getStatus())) { + throw exception(ErrorCodeConstants.APP_IS_DISABLE); + } + return app; + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/channel/PayChannelService.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/channel/PayChannelService.java new file mode 100644 index 0000000..04877b9 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/channel/PayChannelService.java @@ -0,0 +1,104 @@ +package cn.aagro.pp.module.pay.service.channel; + +import cn.aagro.pp.framework.common.exception.ServiceException; +import cn.aagro.pp.module.pay.framework.pay.core.client.PayClient; +import cn.aagro.pp.module.pay.controller.admin.channel.vo.PayChannelCreateReqVO; +import cn.aagro.pp.module.pay.controller.admin.channel.vo.PayChannelUpdateReqVO; +import cn.aagro.pp.module.pay.dal.dataobject.channel.PayChannelDO; + +import javax.validation.Valid; +import java.util.Collection; +import java.util.List; + +/** + * 支付渠道 Service 接口 + * + * @author aquan + */ +public interface PayChannelService { + + /** + * 创建支付渠道 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createChannel(@Valid PayChannelCreateReqVO createReqVO); + + /** + * 更新支付渠道 + * + * @param updateReqVO 更新信息 + */ + void updateChannel(@Valid PayChannelUpdateReqVO updateReqVO); + + /** + * 删除支付渠道 + * + * @param id 编号 + */ + void deleteChannel(Long id); + + /** + * 获得支付渠道 + * + * @param id 编号 + * @return 支付渠道 + */ + PayChannelDO getChannel(Long id); + + /** + * 根据支付应用 ID 集合,获得支付渠道列表 + * + * @param appIds 应用编号集合 + * @return 支付渠道列表 + */ + List getChannelListByAppIds(Collection appIds); + + /** + * 根据条件获取渠道 + * + * @param appId 应用编号 + * @param code 渠道编码 + * @return 数量 + */ + PayChannelDO getChannelByAppIdAndCode(Long appId, String code); + + /** + * 支付渠道的合法性 + * + * 如果不合法,抛出 {@link ServiceException} 业务异常 + * + * @param id 渠道编号 + * @return 渠道信息 + */ + PayChannelDO validPayChannel(Long id); + + /** + * 支付渠道的合法性 + * + * 如果不合法,抛出 {@link ServiceException} 业务异常 + * + * @param appId 应用编号 + * @param code 支付渠道 + * @return 渠道信息 + */ + PayChannelDO validPayChannel(Long appId, String code); + + /** + * 获得指定应用的开启的渠道列表 + * + * @param appId 应用编号 + * @return 渠道列表 + */ + List getEnableChannelList(Long appId); + + /** + * 获得指定编号的支付客户端 + * + * @param id 编号 + * @return 支付客户端 + */ + PayClient getPayClient(Long id); + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/channel/PayChannelServiceImpl.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/channel/PayChannelServiceImpl.java new file mode 100644 index 0000000..ed6f35c --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/channel/PayChannelServiceImpl.java @@ -0,0 +1,165 @@ +package cn.aagro.pp.module.pay.service.channel; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ObjectUtil; +import cn.aagro.pp.framework.common.enums.CommonStatusEnum; +import cn.aagro.pp.framework.common.util.json.JsonUtils; +import cn.aagro.pp.module.pay.controller.admin.channel.vo.PayChannelCreateReqVO; +import cn.aagro.pp.module.pay.controller.admin.channel.vo.PayChannelUpdateReqVO; +import cn.aagro.pp.module.pay.convert.channel.PayChannelConvert; +import cn.aagro.pp.module.pay.dal.dataobject.channel.PayChannelDO; +import cn.aagro.pp.module.pay.dal.mysql.channel.PayChannelMapper; +import cn.aagro.pp.module.pay.enums.PayChannelEnum; +import cn.aagro.pp.module.pay.framework.pay.core.client.PayClient; +import cn.aagro.pp.module.pay.framework.pay.core.client.PayClientConfig; +import cn.aagro.pp.module.pay.framework.pay.core.client.PayClientFactory; +import cn.aagro.pp.module.pay.framework.pay.core.client.impl.NonePayClientConfig; +import cn.aagro.pp.module.pay.framework.pay.core.client.impl.alipay.AlipayPayClientConfig; +import cn.aagro.pp.module.pay.framework.pay.core.client.impl.weixin.WxPayClientConfig; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import javax.validation.Validator; +import java.util.Collection; +import java.util.List; + +import static cn.aagro.pp.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.aagro.pp.module.pay.enums.ErrorCodeConstants.*; + +/** + * 支付渠道 Service 实现类 + * + * @author aquan + */ +@Service +@Slf4j +@Validated +public class PayChannelServiceImpl implements PayChannelService { + + @Resource + private PayClientFactory payClientFactory; + + @Resource + private PayChannelMapper payChannelMapper; + + @Resource + private Validator validator; + + @Override + public Long createChannel(PayChannelCreateReqVO reqVO) { + // 断言是否有重复的 + PayChannelDO dbChannel = getChannelByAppIdAndCode(reqVO.getAppId(), reqVO.getCode()); + if (dbChannel != null) { + throw exception(CHANNEL_EXIST_SAME_CHANNEL_ERROR); + } + + // 新增渠道 + PayChannelDO channel = PayChannelConvert.INSTANCE.convert(reqVO) + .setConfig(parseConfig(reqVO.getCode(), reqVO.getConfig())); + payChannelMapper.insert(channel); + return channel.getId(); + } + + @Override + public void updateChannel(PayChannelUpdateReqVO updateReqVO) { + // 校验存在 + PayChannelDO dbChannel = validateChannelExists(updateReqVO.getId()); + + // 更新 + PayChannelDO channel = PayChannelConvert.INSTANCE.convert(updateReqVO) + .setConfig(parseConfig(dbChannel.getCode(), updateReqVO.getConfig())); + payChannelMapper.updateById(channel); + } + + /** + * 解析并校验配置 + * + * @param code 渠道编码 + * @param configStr 配置 + * @return 支付配置 + */ + private PayClientConfig parseConfig(String code, String configStr) { + // 解析配置 + Class payClass = PayChannelEnum.isAlipay(code) ? AlipayPayClientConfig.class + : PayChannelEnum.isWeixin(code) ? WxPayClientConfig.class + : NonePayClientConfig.class; + if (ObjectUtil.isNull(payClass)) { + throw exception(CHANNEL_NOT_FOUND); + } + PayClientConfig config = JsonUtils.parseObject2(configStr, payClass); + Assert.notNull(config); + + // 验证参数 + config.validate(validator); + return config; + } + + @Override + public void deleteChannel(Long id) { + // 校验存在 + validateChannelExists(id); + + // 删除 + payChannelMapper.deleteById(id); + } + + private PayChannelDO validateChannelExists(Long id) { + PayChannelDO channel = payChannelMapper.selectById(id); + if (channel == null) { + throw exception(CHANNEL_NOT_FOUND); + } + return channel; + } + + @Override + public PayChannelDO getChannel(Long id) { + return payChannelMapper.selectById(id); + } + + @Override + public List getChannelListByAppIds(Collection appIds) { + return payChannelMapper.selectListByAppIds(appIds); + } + + @Override + public PayChannelDO getChannelByAppIdAndCode(Long appId, String code) { + return payChannelMapper.selectByAppIdAndCode(appId, code); + } + + @Override + public PayChannelDO validPayChannel(Long id) { + PayChannelDO channel = payChannelMapper.selectById(id); + validPayChannel(channel); + return channel; + } + + @Override + public PayChannelDO validPayChannel(Long appId, String code) { + PayChannelDO channel = payChannelMapper.selectByAppIdAndCode(appId, code); + validPayChannel(channel); + return channel; + } + + private void validPayChannel(PayChannelDO channel) { + if (channel == null) { + throw exception(CHANNEL_NOT_FOUND); + } + if (CommonStatusEnum.DISABLE.getStatus().equals(channel.getStatus())) { + throw exception(CHANNEL_IS_DISABLE); + } + } + + @Override + public List getEnableChannelList(Long appId) { + return payChannelMapper.selectListByAppId(appId, CommonStatusEnum.ENABLE.getStatus()); + } + + @Override + public PayClient getPayClient(Long id) { + PayChannelDO channel = validPayChannel(id); + return payClientFactory.createOrUpdatePayClient(id, channel.getCode(), channel.getConfig()); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/demo/PayDemoOrderService.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/demo/PayDemoOrderService.java new file mode 100644 index 0000000..5008c61 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/demo/PayDemoOrderService.java @@ -0,0 +1,67 @@ +package cn.aagro.pp.module.pay.service.demo; + +import cn.aagro.pp.framework.common.pojo.PageParam; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.pay.controller.admin.demo.vo.order.PayDemoOrderCreateReqVO; +import cn.aagro.pp.module.pay.dal.dataobject.demo.PayDemoOrderDO; + +import javax.validation.Valid; + +/** + * 示例订单 Service 接口 + * + * @author 芋道源码 + */ +public interface PayDemoOrderService { + + /** + * 创建示例订单 + * + * @param userId 用户编号 + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createDemoOrder(Long userId, @Valid PayDemoOrderCreateReqVO createReqVO); + + /** + * 获得示例订单 + * + * @param id 编号 + * @return 示例订单 + */ + PayDemoOrderDO getDemoOrder(Long id); + + /** + * 获得示例订单分页 + * + * @param pageReqVO 分页查询 + * @return 示例订单分页 + */ + PageResult getDemoOrderPage(PageParam pageReqVO); + + /** + * 更新示例订单为已支付 + * + * @param id 编号 + * @param payOrderId 支付订单号 + */ + void updateDemoOrderPaid(Long id, Long payOrderId); + + /** + * 发起示例订单的退款 + * + * @param id 编号 + * @param userIp 用户编号 + */ + void refundDemoOrder(Long id, String userIp); + + /** + * 更新示例订单为已退款 + * + * @param id 编号 + * @param refundId 退款编号 + * @param payRefundId 退款订单号 + */ + void updateDemoOrderRefunded(Long id, String refundId, Long payRefundId); + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/demo/PayDemoOrderServiceImpl.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/demo/PayDemoOrderServiceImpl.java new file mode 100644 index 0000000..5af9240 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/demo/PayDemoOrderServiceImpl.java @@ -0,0 +1,268 @@ +package cn.aagro.pp.module.pay.service.demo; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.aagro.pp.framework.common.enums.UserTypeEnum; +import cn.aagro.pp.framework.common.pojo.PageParam; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.pay.api.order.PayOrderApi; +import cn.aagro.pp.module.pay.api.order.dto.PayOrderCreateReqDTO; +import cn.aagro.pp.module.pay.api.order.dto.PayOrderRespDTO; +import cn.aagro.pp.module.pay.api.refund.PayRefundApi; +import cn.aagro.pp.module.pay.api.refund.dto.PayRefundCreateReqDTO; +import cn.aagro.pp.module.pay.api.refund.dto.PayRefundRespDTO; +import cn.aagro.pp.module.pay.controller.admin.demo.vo.order.PayDemoOrderCreateReqVO; +import cn.aagro.pp.module.pay.dal.dataobject.demo.PayDemoOrderDO; +import cn.aagro.pp.module.pay.dal.mysql.demo.PayDemoOrderMapper; +import cn.aagro.pp.module.pay.enums.order.PayOrderStatusEnum; +import cn.aagro.pp.module.pay.enums.refund.PayRefundStatusEnum; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; + +import static cn.hutool.core.util.ObjectUtil.notEqual; +import static cn.aagro.pp.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.aagro.pp.framework.common.util.date.LocalDateTimeUtils.addTime; +import static cn.aagro.pp.framework.common.util.json.JsonUtils.toJsonString; +import static cn.aagro.pp.framework.common.util.servlet.ServletUtils.getClientIP; +import static cn.aagro.pp.module.pay.enums.ErrorCodeConstants.*; + +/** + * 示例订单 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class PayDemoOrderServiceImpl implements PayDemoOrderService { + + /** + * 接入的支付应用标识 + * + * 从 [支付管理 -> 应用信息] 里添加 + */ + private static final String PAY_APP_KEY = "demo"; + + /** + * 商品信息 Map + * + * key:商品编号 + * value:[商品名、商品价格] + */ + private final Map spuNames = new HashMap<>(); + + @Resource + private PayOrderApi payOrderApi; + @Resource + private PayRefundApi payRefundApi; + + @Resource + private PayDemoOrderMapper payDemoOrderMapper; + + public PayDemoOrderServiceImpl() { + spuNames.put(1L, new Object[]{"华为手机", 1}); + spuNames.put(2L, new Object[]{"小米电视", 10}); + spuNames.put(3L, new Object[]{"苹果手表", 100}); + spuNames.put(4L, new Object[]{"华硕笔记本", 1000}); + spuNames.put(5L, new Object[]{"蔚来汽车", 200000}); + } + + @Override + public Long createDemoOrder(Long userId, PayDemoOrderCreateReqVO createReqVO) { + // 1.1 获得商品 + Object[] spu = spuNames.get(createReqVO.getSpuId()); + Assert.notNull(spu, "商品({}) 不存在", createReqVO.getSpuId()); + String spuName = (String) spu[0]; + Integer price = (Integer) spu[1]; + // 1.2 插入 demo 订单 + PayDemoOrderDO demoOrder = new PayDemoOrderDO().setUserId(userId) + .setSpuId(createReqVO.getSpuId()).setSpuName(spuName) + .setPrice(price).setPayStatus(false).setRefundPrice(0); + payDemoOrderMapper.insert(demoOrder); + + // 2.1 创建支付单 + Long payOrderId = payOrderApi.createOrder(new PayOrderCreateReqDTO() + .setAppKey(PAY_APP_KEY).setUserIp(getClientIP()) // 支付应用 + .setUserId(userId).setUserType(UserTypeEnum.ADMIN.getValue()) // 用户信息 + .setMerchantOrderId(demoOrder.getId().toString()) // 业务的订单编号 + .setSubject(spuName).setBody("").setPrice(price) // 价格信息 + .setExpireTime(addTime(Duration.ofHours(2L)))); // 支付的过期时间 + // 2.2 更新支付单到 demo 订单 + payDemoOrderMapper.updateById(new PayDemoOrderDO().setId(demoOrder.getId()) + .setPayOrderId(payOrderId)); + return demoOrder.getId(); + } + + @Override + public PayDemoOrderDO getDemoOrder(Long id) { + return payDemoOrderMapper.selectById(id); + } + + @Override + public PageResult getDemoOrderPage(PageParam pageReqVO) { + return payDemoOrderMapper.selectPage(pageReqVO); + } + + @Override + public void updateDemoOrderPaid(Long id, Long payOrderId) { + // 1.1 校验订单是否存在 + PayDemoOrderDO order = payDemoOrderMapper.selectById(id); + if (order == null) { + log.error("[updateDemoOrderPaid][order({}) payOrder({}) 不存在订单,请进行处理!]", id, payOrderId); + throw exception(DEMO_ORDER_NOT_FOUND); + } + // 1.2 校验订单已支付 + if (order.getPayStatus()) { + // 特殊:支付单号相同,直接返回,说明重复回调 + if (ObjectUtil.equals(order.getPayOrderId(), payOrderId)) { + log.warn("[updateDemoOrderPaid][order({}) 已支付,且支付单号相同({}),直接返回]", order, payOrderId); + return; + } + // 异常:支付单号不同,说明支付单号错误 + log.error("[updateDemoOrderPaid][order({}) 支付单不匹配({}),请进行处理!]", + order, payOrderId); + throw exception(DEMO_ORDER_UPDATE_PAID_FAIL_PAY_ORDER_ID_ERROR); + } + + // 2. 校验支付订单的合法性 + PayOrderRespDTO payOrder = validatePayOrderPaid(order, payOrderId); + + // 3. 更新 PayDemoOrderDO 状态为已支付 + int updateCount = payDemoOrderMapper.updateByIdAndPayed(id, false, + new PayDemoOrderDO().setPayStatus(true).setPayTime(LocalDateTime.now()) + .setPayChannelCode(payOrder.getChannelCode())); + if (updateCount == 0) { + throw exception(DEMO_ORDER_UPDATE_PAID_STATUS_NOT_UNPAID); + } + } + + /** + * 校验支付订单的合法性 + * + * @param order 交易订单 + * @param payOrderId 支付订单编号 + * @return 支付订单 + */ + private PayOrderRespDTO validatePayOrderPaid(PayDemoOrderDO order, Long payOrderId) { + // 1. 校验支付单是否存在 + PayOrderRespDTO payOrder = payOrderApi.getOrder(payOrderId); + if (payOrder == null) { + log.error("[validatePayOrderPaid][order({}) payOrder({}) 不存在,请进行处理!]", order.getId(), payOrderId); + throw exception(PAY_ORDER_NOT_FOUND); + } + // 2.1 校验支付单已支付 + if (!PayOrderStatusEnum.isSuccess(payOrder.getStatus())) { + log.error("[validatePayOrderPaid][order({}) payOrder({}) 未支付,请进行处理!payOrder 数据是:{}]", + order.getId(), payOrderId, toJsonString(payOrder)); + throw exception(DEMO_ORDER_UPDATE_PAID_FAIL_PAY_ORDER_STATUS_NOT_SUCCESS); + } + // 2.1 校验支付金额一致 + if (notEqual(payOrder.getPrice(), order.getPrice())) { + log.error("[validatePayOrderPaid][order({}) payOrder({}) 支付金额不匹配,请进行处理!order 数据是:{},payOrder 数据是:{}]", + order.getId(), payOrderId, toJsonString(order), toJsonString(payOrder)); + throw exception(DEMO_ORDER_UPDATE_PAID_FAIL_PAY_PRICE_NOT_MATCH); + } + // 2.2 校验支付订单匹配(二次) + if (notEqual(payOrder.getMerchantOrderId(), order.getId().toString())) { + log.error("[validatePayOrderPaid][order({}) 支付单不匹配({}),请进行处理!payOrder 数据是:{}]", + order.getId(), payOrderId, toJsonString(payOrder)); + throw exception(DEMO_ORDER_UPDATE_PAID_FAIL_PAY_ORDER_ID_ERROR); + } + return payOrder; + } + + @Override + public void refundDemoOrder(Long id, String userIp) { + // 1. 校验订单是否可以退款 + PayDemoOrderDO order = validateDemoOrderCanRefund(id); + + // 2.1 生成退款单号 + // 一般来说,用户发起退款的时候,都会单独插入一个售后维权表,然后使用该表的 id 作为 refundId + // 这里我们是个简单的 demo,所以没有售后维权表,直接使用订单 id + "-refund" 来演示 + String refundId = order.getId() + "-refund"; + // 2.2 创建退款单 + Long payRefundId = payRefundApi.createRefund(new PayRefundCreateReqDTO() + .setAppKey(PAY_APP_KEY).setUserIp(getClientIP()) // 支付应用 + .setUserId(order.getUserId()).setUserType(UserTypeEnum.ADMIN.getValue()) // 用户信息 + .setMerchantOrderId(String.valueOf(order.getId())) // 支付单号 + .setMerchantRefundId(refundId) + .setReason("想退钱").setPrice(order.getPrice()));// 价格信息 + // 2.3 更新退款单到 demo 订单 + payDemoOrderMapper.updateById(new PayDemoOrderDO().setId(id) + .setPayRefundId(payRefundId).setRefundPrice(order.getPrice())); + } + + private PayDemoOrderDO validateDemoOrderCanRefund(Long id) { + // 校验订单是否存在 + PayDemoOrderDO order = payDemoOrderMapper.selectById(id); + if (order == null) { + throw exception(DEMO_ORDER_NOT_FOUND); + } + // 校验订单是否支付 + if (!order.getPayStatus()) { + throw exception(DEMO_ORDER_REFUND_FAIL_NOT_PAID); + } + // 校验订单是否已退款 + if (order.getPayRefundId() != null) { + throw exception(DEMO_ORDER_REFUND_FAIL_REFUNDED); + } + return order; + } + + @Override + public void updateDemoOrderRefunded(Long id, String refundId, Long payRefundId) { + // 1. 校验并获得退款订单(可退款) + PayRefundRespDTO payRefund = validateDemoOrderCanRefunded(id, refundId, payRefundId); + // 2.2 更新退款单到 demo 订单 + payDemoOrderMapper.updateById(new PayDemoOrderDO().setId(id) + .setRefundTime(payRefund.getSuccessTime())); + } + + private PayRefundRespDTO validateDemoOrderCanRefunded(Long id, String refundId, Long payRefundId) { + // 1.1 校验示例订单 + // 一般来说,这里应该用 refundId 来查询退款单,然后再校验订单是否匹配 + // 这里我们是个简单的 demo,所以没有售后维权表,直接使用订单 id 来查询订单 + PayDemoOrderDO order = payDemoOrderMapper.selectById(id); + if (order == null) { + throw exception(DEMO_ORDER_NOT_FOUND); + } + // 1.2 校验退款订单匹配 + if (ObjUtil.notEqual(order.getPayRefundId(), payRefundId)) { + log.error("[validateDemoOrderCanRefunded][order({}) 退款单不匹配({}),请进行处理!order 数据是:{}]", + id, payRefundId, toJsonString(order)); + throw exception(DEMO_ORDER_REFUND_FAIL_REFUND_ORDER_ID_ERROR); + } + + // 2.1 校验退款订单 + PayRefundRespDTO payRefund = payRefundApi.getRefund(payRefundId); + if (payRefund == null) { + throw exception(DEMO_ORDER_REFUND_FAIL_REFUND_NOT_FOUND); + } + // 2.2 必须是退款成功状态 + if (!PayRefundStatusEnum.isSuccess(payRefund.getStatus())) { + throw exception(DEMO_ORDER_REFUND_FAIL_REFUND_NOT_SUCCESS); + } + // 2.3 校验退款金额一致 + if (notEqual(payRefund.getRefundPrice(), order.getPrice())) { + log.error("[validateDemoOrderCanRefunded][order({}) payRefund({}) 退款金额不匹配,请进行处理!order 数据是:{},payRefund 数据是:{}]", + id, payRefundId, toJsonString(order), toJsonString(payRefund)); + throw exception(DEMO_ORDER_REFUND_FAIL_REFUND_PRICE_NOT_MATCH); + } + // 2.4 校验退款订单匹配(二次) + if (notEqual(payRefund.getMerchantRefundId(), id.toString() + "-refund")) { + log.error("[validateDemoOrderCanRefunded][order({}) 退款单不匹配({}),请进行处理!payRefund 数据是:{}]", + id, payRefundId, toJsonString(payRefund)); + throw exception(DEMO_ORDER_REFUND_FAIL_REFUND_ORDER_ID_ERROR); + } + return payRefund; + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/demo/PayDemoWithdrawService.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/demo/PayDemoWithdrawService.java new file mode 100644 index 0000000..bf728cb --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/demo/PayDemoWithdrawService.java @@ -0,0 +1,49 @@ +package cn.aagro.pp.module.pay.service.demo; + +import cn.aagro.pp.framework.common.pojo.PageParam; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.pay.controller.admin.demo.vo.withdraw.PayDemoWithdrawCreateReqVO; +import cn.aagro.pp.module.pay.dal.dataobject.demo.PayDemoWithdrawDO; + +import javax.validation.Valid; + +/** + * 示例提现单 Service 接口 + * + * @author jason + */ +public interface PayDemoWithdrawService { + + /** + * 创建示例提现单 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createDemoWithdraw(@Valid PayDemoWithdrawCreateReqVO createReqVO); + + /** + * 提现单转账 + * + * @param id 提现单编号 + * @param userId 用户编号 + * @return 转账编号 + */ + Long transferDemoWithdraw(Long id, Long userId); + + /** + * 获得示例提现单分页 + * + * @param pageVO 分页查询参数 + */ + PageResult getDemoWithdrawPage(PageParam pageVO); + + /** + * 更新示例提现单的状态 + * + * @param id 编号 + * @param payTransferId 转账单编号 + */ + void updateDemoWithdrawTransferred(Long id, Long payTransferId); + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/demo/PayDemoWithdrawServiceImpl.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/demo/PayDemoWithdrawServiceImpl.java new file mode 100644 index 0000000..5393ca2 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/demo/PayDemoWithdrawServiceImpl.java @@ -0,0 +1,198 @@ +package cn.aagro.pp.module.pay.service.demo; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ObjectUtil; +import cn.aagro.pp.framework.common.enums.UserTypeEnum; +import cn.aagro.pp.framework.common.pojo.PageParam; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.common.util.json.JsonUtils; +import cn.aagro.pp.framework.common.util.object.BeanUtils; +import cn.aagro.pp.module.pay.api.transfer.PayTransferApi; +import cn.aagro.pp.module.pay.api.transfer.dto.PayTransferCreateReqDTO; +import cn.aagro.pp.module.pay.api.transfer.dto.PayTransferCreateRespDTO; +import cn.aagro.pp.module.pay.api.transfer.dto.PayTransferRespDTO; +import cn.aagro.pp.module.pay.controller.admin.demo.vo.withdraw.PayDemoWithdrawCreateReqVO; +import cn.aagro.pp.module.pay.dal.dataobject.demo.PayDemoWithdrawDO; +import cn.aagro.pp.module.pay.dal.mysql.demo.PayDemoWithdrawMapper; +import cn.aagro.pp.module.pay.enums.PayChannelEnum; +import cn.aagro.pp.module.pay.enums.demo.PayDemoWithdrawStatusEnum; +import cn.aagro.pp.module.pay.enums.demo.PayDemoWithdrawTypeEnum; +import cn.aagro.pp.module.pay.enums.transfer.PayTransferStatusEnum; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import javax.validation.Valid; + +import static cn.aagro.pp.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.aagro.pp.framework.common.util.servlet.ServletUtils.getClientIP; +import static cn.aagro.pp.module.pay.enums.ErrorCodeConstants.*; + +/** + * 示例转账业务 Service 实现类 + * + * @author jason + */ +@Service +@Validated +@Slf4j +public class PayDemoWithdrawServiceImpl implements PayDemoWithdrawService { + + /** + * 接入的支付应用标识 + * + * 从 [支付管理 -> 应用信息] 里添加 + */ + private static final String PAY_APP_KEY = "demo"; + + @Resource + private PayDemoWithdrawMapper demoTransferMapper; + + @Resource + private PayTransferApi payTransferApi; + + @Override + public Long createDemoWithdraw(@Valid PayDemoWithdrawCreateReqVO reqVO) { + PayDemoWithdrawDO withdraw = BeanUtils.toBean(reqVO, PayDemoWithdrawDO.class) + .setTransferChannelCode(getTransferChannelCode(reqVO.getType())) + .setStatus(PayDemoWithdrawStatusEnum.WAITING.getStatus()); + demoTransferMapper.insert(withdraw); + return withdraw.getId(); + } + + @Override + public Long transferDemoWithdraw(Long id, Long userId) { + // 1.1 校验提现单 + PayDemoWithdrawDO withdraw = validateDemoWithdrawCanTransfer(id); + // 1.2 特殊:如果是转账失败的情况,需要充值下 + if (PayDemoWithdrawStatusEnum.isClosed(withdraw.getStatus())) { + int updateCount = demoTransferMapper.updateByIdAndStatus(withdraw.getId(), withdraw.getStatus(), + new PayDemoWithdrawDO().setStatus(PayDemoWithdrawStatusEnum.WAITING.getStatus()).setTransferErrorMsg("")); + if (updateCount == 0) { + throw exception(DEMO_WITHDRAW_TRANSFER_FAIL_STATUS_NOT_WAITING_OR_CLOSED); + } + withdraw.setStatus(PayDemoWithdrawStatusEnum.WAITING.getStatus()); + } + + // 2.1 创建支付单 + PayTransferCreateReqDTO transferReqDTO = new PayTransferCreateReqDTO() + .setAppKey(PAY_APP_KEY).setChannelCode(withdraw.getTransferChannelCode()).setUserIp(getClientIP()) // 支付应用 + .setUserId(userId).setUserType(UserTypeEnum.ADMIN.getValue()) // 用户信息 + .setMerchantTransferId(String.valueOf(withdraw.getId())) // 业务的订单编号 + .setSubject(withdraw.getSubject()).setPrice(withdraw.getPrice()) // 价格信息 + .setUserAccount(withdraw.getUserAccount()).setUserName(withdraw.getUserName()); // 收款信息 + if (ObjectUtil.equal(withdraw.getType(), PayDemoWithdrawTypeEnum.WECHAT.getType())) { + // 注意:微信很特殊!提现需要写明用途!!! + transferReqDTO.setChannelExtras(PayTransferCreateReqDTO.buildWeiXinChannelExtra1000( + "测试活动", "测试奖励")); + } + PayTransferCreateRespDTO transferRespDTO = payTransferApi.createTransfer(transferReqDTO); + + // 2.2 更新转账单到 demo 示例提现单,并将状态更新为转账中 + demoTransferMapper.updateByIdAndStatus(withdraw.getId(), withdraw.getStatus(), + new PayDemoWithdrawDO().setPayTransferId(transferRespDTO.getId())); + return transferRespDTO.getId(); + } + + private PayDemoWithdrawDO validateDemoWithdrawCanTransfer(Long id) { + // 校验存在 + PayDemoWithdrawDO withdraw = demoTransferMapper.selectById(id); + if (withdraw == null) { + throw exception(DEMO_WITHDRAW_NOT_FOUND); + } + // 校验状态,只有等待中或转账失败的订单,才能发起转账 + if (!PayDemoWithdrawStatusEnum.isWaiting(withdraw.getStatus()) + && !PayDemoWithdrawStatusEnum.isClosed(withdraw.getStatus())) { + throw exception(DEMO_WITHDRAW_TRANSFER_FAIL_STATUS_NOT_WAITING_OR_CLOSED); + } + return withdraw; + } + + private String getTransferChannelCode(Integer type) { + if (ObjectUtil.equal(type, PayDemoWithdrawTypeEnum.ALIPAY.getType())) { + return PayChannelEnum.ALIPAY_PC.getCode(); + } + if (ObjectUtil.equal(type, PayDemoWithdrawTypeEnum.WECHAT.getType())) { + return PayChannelEnum.WX_LITE.getCode(); + } + if (ObjectUtil.equal(type, PayDemoWithdrawTypeEnum.WALLET.getType())) { + return PayChannelEnum.WALLET.getCode(); + } + throw new IllegalArgumentException("未知提现方式:" + type); + } + + @Override + public PageResult getDemoWithdrawPage(PageParam pageVO) { + return demoTransferMapper.selectPage(pageVO); + } + + @Override + public void updateDemoWithdrawTransferred(Long id, Long payTransferId) { + // 1.1 校验转账单是否存在 + PayDemoWithdrawDO withdraw = demoTransferMapper.selectById(id); + if (withdraw == null) { + log.error("[updateDemoWithdrawStatus][withdraw({}) payOrder({}) 不存在提现单,请进行处理!]", id, payTransferId); + throw exception(DEMO_WITHDRAW_NOT_FOUND); + } + // 1.2 校验转账单已成结束(成功或失败) + if (PayDemoWithdrawStatusEnum.isSuccess(withdraw.getStatus()) + || PayDemoWithdrawStatusEnum.isClosed(withdraw.getStatus())) { + // 特殊:转账单编号相同,直接返回,说明重复回调 + if (ObjectUtil.equal(withdraw.getPayTransferId(), payTransferId)) { + log.warn("[updateDemoWithdrawStatus][withdraw({}) 已结束,且转账单编号相同({}),直接返回]", withdraw, payTransferId); + return; + } + // 异常:转账单编号不同,说明转账单编号错误 + log.error("[updateDemoWithdrawStatus][withdraw({}) 转账单不匹配({}),请进行处理!]", withdraw, payTransferId); + throw exception(DEMO_WITHDRAW_UPDATE_STATUS_FAIL_PAY_TRANSFER_ID_ERROR); + } + + // 2. 校验提现单的合法性 + PayTransferRespDTO payTransfer = validateDemoTransferStatusCanUpdate(withdraw, payTransferId); + + // 3. 更新示例订单状态 + Integer newStatus = PayTransferStatusEnum.isSuccess(payTransfer.getStatus()) ? PayDemoWithdrawStatusEnum.SUCCESS.getStatus() : + PayTransferStatusEnum.isClosed(payTransfer.getStatus()) ? PayDemoWithdrawStatusEnum.CLOSED.getStatus() : null; + Assert.notNull(newStatus, "转账单状态({}) 不合法", payTransfer.getStatus()); + demoTransferMapper.updateByIdAndStatus(withdraw.getId(), withdraw.getStatus(), + new PayDemoWithdrawDO().setStatus(newStatus).setTransferTime(payTransfer.getSuccessTime()) + .setTransferErrorMsg(payTransfer.getChannelErrorMsg())); + } + + private PayTransferRespDTO validateDemoTransferStatusCanUpdate(PayDemoWithdrawDO withdraw, Long payTransferId) { + // 1. 校验转账单是否存在 + PayTransferRespDTO payTransfer = payTransferApi.getTransfer(payTransferId); + if (payTransfer == null) { + log.error("[validateDemoTransferStatusCanUpdate][withdraw({}) payTransfer({}) 不存在,请进行处理!]", withdraw.getId(), payTransferId); + throw exception(PAY_TRANSFER_NOT_FOUND); + } + + // 2.1 校验转账单已成功 + if (!PayTransferStatusEnum.isSuccessOrClosed(payTransfer.getStatus())) { + log.error("[validateDemoTransferStatusCanUpdate][withdraw({}) payTransfer({}) 未结束,请进行处理!payTransfer 数据是:{}]", + withdraw.getId(), payTransferId, JsonUtils.toJsonString(payTransfer)); + throw exception(DEMO_WITHDRAW_UPDATE_STATUS_FAIL_PAY_TRANSFER_STATUS_NOT_SUCCESS_OR_CLOSED); + } + // 2.2 校验转账金额一致 + if (ObjectUtil.notEqual(payTransfer.getPrice(), withdraw.getPrice())) { + log.error("[validateDemoTransferStatusCanUpdate][withdraw({}) payTransfer({}) 转账金额不匹配,请进行处理!withdraw 数据是:{},payTransfer 数据是:{}]", + withdraw.getId(), payTransferId, JsonUtils.toJsonString(withdraw), JsonUtils.toJsonString(payTransfer)); + throw exception(DEMO_WITHDRAW_UPDATE_STATUS_FAIL_PAY_PRICE_NOT_MATCH); + } + // 2.3 校验转账订单匹配(二次) + if (ObjectUtil.notEqual(payTransfer.getMerchantTransferId(), withdraw.getId().toString())) { + log.error("[validateDemoTransferStatusCanUpdate][withdraw({}) 转账单不匹配({}),请进行处理!payTransfer 数据是:{}]", + withdraw.getId(), payTransferId, JsonUtils.toJsonString(payTransfer)); + throw exception(DEMO_WITHDRAW_UPDATE_STATUS_FAIL_PAY_MERCHANT_EXISTS); + } + // 2.4 校验转账渠道一致 + if (ObjectUtil.notEqual(payTransfer.getChannelCode(), withdraw.getTransferChannelCode())) { + log.error("[validateDemoTransferStatusCanUpdate][withdraw({}) payTransfer({}) 转账渠道不匹配,请进行处理!withdraw 数据是:{},payTransfer 数据是:{}]", + withdraw.getId(), payTransferId, JsonUtils.toJsonString(withdraw), JsonUtils.toJsonString(payTransfer)); + throw exception(DEMO_WITHDRAW_UPDATE_STATUS_FAIL_PAY_CHANNEL_NOT_MATCH); + } + return payTransfer; + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/notify/PayNotifyService.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/notify/PayNotifyService.java new file mode 100644 index 0000000..38a473d --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/notify/PayNotifyService.java @@ -0,0 +1,57 @@ +package cn.aagro.pp.module.pay.service.notify; + +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.pay.controller.admin.notify.vo.PayNotifyTaskPageReqVO; +import cn.aagro.pp.module.pay.dal.dataobject.notify.PayNotifyLogDO; +import cn.aagro.pp.module.pay.dal.dataobject.notify.PayNotifyTaskDO; + +import java.util.List; + +/** + * 回调通知 Service 接口 + * + * @author 芋道源码 + */ +public interface PayNotifyService { + + /** + * 创建回调通知任务 + * + * @param type 类型 + * @param dataId 数据编号 + */ + void createPayNotifyTask(Integer type, Long dataId); + + /** + * 执行回调通知 + * + * 注意,该方法提供给定时任务调用。目前是 aagro-server 进行调用 + * @return 通知数量 + */ + int executeNotify() throws InterruptedException; + + /** + * 获得回调通知 + * + * @param id 编号 + * @return 回调通知 + */ + PayNotifyTaskDO getNotifyTask(Long id); + + /** + * 获得回调通知分页 + * + * @param pageReqVO 分页查询 + * @return 回调通知分页 + */ + PageResult getNotifyTaskPage(PayNotifyTaskPageReqVO pageReqVO); + + /** + * 获得回调日志列表 + * + * @param taskId 任务编号 + * @return 日志列表 + */ + List getNotifyLogList(Long taskId); + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/notify/PayNotifyServiceImpl.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/notify/PayNotifyServiceImpl.java new file mode 100644 index 0000000..8a3f1c4 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/notify/PayNotifyServiceImpl.java @@ -0,0 +1,323 @@ +package cn.aagro.pp.module.pay.service.notify; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.hutool.http.HttpResponse; +import cn.hutool.http.HttpUtil; +import cn.aagro.pp.framework.common.pojo.CommonResult; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.common.util.date.DateUtils; +import cn.aagro.pp.framework.common.util.json.JsonUtils; +import cn.aagro.pp.framework.tenant.core.util.TenantUtils; +import cn.aagro.pp.module.pay.api.notify.dto.PayOrderNotifyReqDTO; +import cn.aagro.pp.module.pay.api.notify.dto.PayRefundNotifyReqDTO; +import cn.aagro.pp.module.pay.api.notify.dto.PayTransferNotifyReqDTO; +import cn.aagro.pp.module.pay.controller.admin.notify.vo.PayNotifyTaskPageReqVO; +import cn.aagro.pp.module.pay.dal.dataobject.notify.PayNotifyLogDO; +import cn.aagro.pp.module.pay.dal.dataobject.notify.PayNotifyTaskDO; +import cn.aagro.pp.module.pay.dal.dataobject.order.PayOrderDO; +import cn.aagro.pp.module.pay.dal.dataobject.refund.PayRefundDO; +import cn.aagro.pp.module.pay.dal.dataobject.transfer.PayTransferDO; +import cn.aagro.pp.module.pay.dal.mysql.notify.PayNotifyLogMapper; +import cn.aagro.pp.module.pay.dal.mysql.notify.PayNotifyTaskMapper; +import cn.aagro.pp.module.pay.dal.redis.notify.PayNotifyLockRedisDAO; +import cn.aagro.pp.module.pay.enums.notify.PayNotifyStatusEnum; +import cn.aagro.pp.module.pay.enums.notify.PayNotifyTypeEnum; +import cn.aagro.pp.module.pay.service.order.PayOrderService; +import cn.aagro.pp.module.pay.service.refund.PayRefundService; +import cn.aagro.pp.module.pay.service.transfer.PayTransferService; +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Lazy; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static cn.aagro.pp.framework.common.util.date.LocalDateTimeUtils.addTime; +import static cn.aagro.pp.module.pay.framework.job.config.PayJobConfiguration.NOTIFY_THREAD_POOL_TASK_EXECUTOR; + +/** + * 支付通知 Core Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Valid +@Slf4j +public class PayNotifyServiceImpl implements PayNotifyService { + + /** + * 通知超时时间,单位:秒 + */ + public static final int NOTIFY_TIMEOUT = 120; + /** + * {@link #NOTIFY_TIMEOUT} 的毫秒 + */ + public static final long NOTIFY_TIMEOUT_MILLIS = 120 * DateUtils.SECOND_MILLIS; + + @Resource + @Lazy // 循环依赖,避免报错 + private PayOrderService orderService; + @Resource + @Lazy // 循环依赖,避免报错 + private PayRefundService refundService; + @Resource + @Lazy // 循环依赖,避免报错 + private PayTransferService transferService; + + @Resource + private PayNotifyTaskMapper notifyTaskMapper; + @Resource + private PayNotifyLogMapper notifyLogMapper; + + @Resource(name = NOTIFY_THREAD_POOL_TASK_EXECUTOR) + private ThreadPoolTaskExecutor threadPoolTaskExecutor; + + @Resource + private PayNotifyLockRedisDAO notifyLockCoreRedisDAO; + + @Override + @Transactional(rollbackFor = Exception.class) + public void createPayNotifyTask(Integer type, Long dataId) { + PayNotifyTaskDO task = new PayNotifyTaskDO().setType(type).setDataId(dataId); + task.setStatus(PayNotifyStatusEnum.WAITING.getStatus()).setNextNotifyTime(LocalDateTime.now()) + .setNotifyTimes(0).setMaxNotifyTimes(PayNotifyTaskDO.NOTIFY_FREQUENCY.length + 1); + // 补充 appId + notifyUrl + merchant* 字段 + if (Objects.equals(task.getType(), PayNotifyTypeEnum.ORDER.getType())) { + PayOrderDO order = orderService.getOrder(task.getDataId()); // 不进行非空判断,有问题直接异常 + task.setAppId(order.getAppId()).setNotifyUrl(order.getNotifyUrl()) + .setMerchantOrderId(order.getMerchantOrderId()); + } else if (Objects.equals(task.getType(), PayNotifyTypeEnum.REFUND.getType())) { + PayRefundDO refund = refundService.getRefund(task.getDataId()); + task.setAppId(refund.getAppId()).setNotifyUrl(refund.getNotifyUrl()) + .setMerchantOrderId(refund.getMerchantOrderId()).setMerchantRefundId(refund.getMerchantRefundId()); + } else if (Objects.equals(task.getType(), PayNotifyTypeEnum.TRANSFER.getType())) { + PayTransferDO transfer = transferService.getTransfer(task.getDataId()); + task.setAppId(transfer.getAppId()).setNotifyUrl(transfer.getNotifyUrl()) + .setMerchantTransferId(transfer.getMerchantTransferId()); + } + + // 执行插入 + notifyTaskMapper.insert(task); + + // 必须在事务提交后,在发起任务,否则 PayNotifyTaskDO 还没入库,就提前回调接入的业务 + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + + @Override + public void afterCommit() { + // 异步的原因:避免阻塞当前事务,无需等待结果 + getSelf().executeNotifyAsync(task); + } + + }); + } + + @Override + public int executeNotify() throws InterruptedException { + // 获得需要通知的任务 + List tasks = notifyTaskMapper.selectListByNotify(); + if (CollUtil.isEmpty(tasks)) { + return 0; + } + + // 遍历,逐个通知 + CountDownLatch latch = new CountDownLatch(tasks.size()); + tasks.forEach(task -> threadPoolTaskExecutor.execute(() -> { + try { + executeNotify(task); + } finally { + latch.countDown(); + } + })); + // 等待完成 + awaitExecuteNotify(latch); + // 返回执行完成的任务数(成功 + 失败) + return tasks.size(); + } + + /** + * 等待全部支付通知的完成 + * 每 1 秒会打印一次剩余任务数量 + * + * @param latch Latch + * @throws InterruptedException 如果被打断 + */ + private void awaitExecuteNotify(CountDownLatch latch) throws InterruptedException { + long size = latch.getCount(); + for (int i = 0; i < NOTIFY_TIMEOUT; i++) { + if (latch.await(1L, TimeUnit.SECONDS)) { + return; + } + log.info("[awaitExecuteNotify][任务处理中, 总任务数({}) 剩余任务数({})]", size, latch.getCount()); + } + log.error("[awaitExecuteNotify][任务未处理完,总任务数({}) 剩余任务数({})]", size, latch.getCount()); + } + + /** + * 异步执行单个支付通知 + * + * @param task 通知任务 + */ + @Async + public void executeNotifyAsync(PayNotifyTaskDO task) { + executeNotify(task); + } + + /** + * 【加锁】执行单个支付通知 + * + * @param task 通知任务 + */ + public void executeNotify(PayNotifyTaskDO task) { + // 分布式锁,避免并发问题 + notifyLockCoreRedisDAO.lock(task.getId(), NOTIFY_TIMEOUT_MILLIS, () -> { + // 校验,当前任务是否已经被通知过 + // 虽然已经通过分布式加锁,但是可能同时满足通知的条件,然后都去获得锁。此时,第一个执行完后,第二个还是能拿到锁,然后会再执行一次。 + // 因此,此处我们通过第 notifyTimes 通知次数是否匹配来判断 + PayNotifyTaskDO dbTask = notifyTaskMapper.selectById(task.getId()); + if (ObjectUtil.notEqual(task.getNotifyTimes(), dbTask.getNotifyTimes())) { + log.warn("[executeNotifySync][task({}) 任务被忽略,原因是它的通知不是第 ({}) 次,可能是因为并发执行了]", + JsonUtils.toJsonString(task), dbTask.getNotifyTimes()); + return; + } + + // 执行通知 + getSelf().executeNotify0(dbTask); + }); + } + + @Transactional(rollbackFor = Exception.class) + public void executeNotify0(PayNotifyTaskDO task) { + // 发起回调 + CommonResult invokeResult = null; + Throwable invokeException = null; + try { + invokeResult = executeNotifyInvoke(task); + } catch (Throwable e) { + invokeException = e; + } + + // 处理结果 + Integer newStatus = processNotifyResult(task, invokeResult, invokeException); + + // 记录 PayNotifyLog 日志 + String response = invokeException != null ? ExceptionUtil.getRootCauseMessage(invokeException) : + JsonUtils.toJsonString(invokeResult); + notifyLogMapper.insert(PayNotifyLogDO.builder().taskId(task.getId()) + .notifyTimes(task.getNotifyTimes() + 1).status(newStatus).response(response).build()); + } + + /** + * 执行单个支付任务的 HTTP 调用 + * + * @param task 通知任务 + * @return HTTP 响应 + */ + private CommonResult executeNotifyInvoke(PayNotifyTaskDO task) { + // 拼接 body 参数 + Object request; + if (Objects.equals(task.getType(), PayNotifyTypeEnum.ORDER.getType())) { + request = PayOrderNotifyReqDTO.builder().merchantOrderId(task.getMerchantOrderId()) + .payOrderId(task.getDataId()).build(); + } else if (Objects.equals(task.getType(), PayNotifyTypeEnum.REFUND.getType())) { + request = PayRefundNotifyReqDTO.builder().merchantOrderId(task.getMerchantOrderId()) + .merchantRefundId(task.getMerchantRefundId()) + .payRefundId(task.getDataId()).build(); + } else if (Objects.equals(task.getType(), PayNotifyTypeEnum.TRANSFER.getType())) { + request = PayTransferNotifyReqDTO.builder().merchantTransferId(task.getMerchantTransferId()) + .payTransferId(task.getDataId()).build(); + } else { + throw new RuntimeException("未知的通知任务类型:" + JsonUtils.toJsonString(task)); + } + // 拼接 header 参数 + Map headers = new HashMap<>(); + TenantUtils.addTenantHeader(headers, task.getTenantId()); + + // 发起请求 + try (HttpResponse response = HttpUtil.createPost(task.getNotifyUrl()) + .body(JsonUtils.toJsonString(request)).addHeaders(headers) + .timeout((int) NOTIFY_TIMEOUT_MILLIS).execute()) { + // 解析结果 + return JsonUtils.parseObject(response.body(), CommonResult.class); + } + } + + /** + * 处理并更新通知结果 + * + * @param task 通知任务 + * @param invokeResult 通知结果 + * @param invokeException 通知异常 + * @return 最终任务的状态 + */ + @VisibleForTesting + Integer processNotifyResult(PayNotifyTaskDO task, CommonResult invokeResult, Throwable invokeException) { + // 设置通用的更新 PayNotifyTaskDO 的字段 + PayNotifyTaskDO updateTask = new PayNotifyTaskDO() + .setId(task.getId()) + .setLastExecuteTime(LocalDateTime.now()) + .setNotifyTimes(task.getNotifyTimes() + 1); + + // 情况一:调用成功 + if (invokeResult != null && invokeResult.isSuccess()) { + updateTask.setStatus(PayNotifyStatusEnum.SUCCESS.getStatus()); + notifyTaskMapper.updateById(updateTask); + return updateTask.getStatus(); + } + + // 情况二:调用失败、调用异常 + // 2.1 超过最大回调次数 + if (updateTask.getNotifyTimes() >= PayNotifyTaskDO.NOTIFY_FREQUENCY.length) { + updateTask.setStatus(PayNotifyStatusEnum.FAILURE.getStatus()); + notifyTaskMapper.updateById(updateTask); + return updateTask.getStatus(); + } + // 2.2 未超过最大回调次数 + updateTask.setNextNotifyTime(addTime(Duration.ofSeconds(PayNotifyTaskDO.NOTIFY_FREQUENCY[updateTask.getNotifyTimes()]))); + updateTask.setStatus(invokeException != null ? PayNotifyStatusEnum.REQUEST_FAILURE.getStatus() + : PayNotifyStatusEnum.REQUEST_SUCCESS.getStatus()); + notifyTaskMapper.updateById(updateTask); + return updateTask.getStatus(); + } + + @Override + public PayNotifyTaskDO getNotifyTask(Long id) { + return notifyTaskMapper.selectById(id); + } + + @Override + public PageResult getNotifyTaskPage(PayNotifyTaskPageReqVO pageReqVO) { + return notifyTaskMapper.selectPage(pageReqVO); + } + + @Override + public List getNotifyLogList(Long taskId) { + return notifyLogMapper.selectListByTaskId(taskId); + } + + /** + * 获得自身的代理对象,解决 AOP 生效问题 + * + * @return 自己 + */ + private PayNotifyServiceImpl getSelf() { + return SpringUtil.getBean(getClass()); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/order/PayOrderService.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/order/PayOrderService.java new file mode 100644 index 0000000..0237a75 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/order/PayOrderService.java @@ -0,0 +1,159 @@ +package cn.aagro.pp.module.pay.service.order; + +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO; +import cn.aagro.pp.module.pay.api.order.dto.PayOrderCreateReqDTO; +import cn.aagro.pp.module.pay.controller.admin.order.vo.PayOrderExportReqVO; +import cn.aagro.pp.module.pay.controller.admin.order.vo.PayOrderPageReqVO; +import cn.aagro.pp.module.pay.controller.admin.order.vo.PayOrderSubmitReqVO; +import cn.aagro.pp.module.pay.controller.admin.order.vo.PayOrderSubmitRespVO; +import cn.aagro.pp.module.pay.dal.dataobject.order.PayOrderDO; +import cn.aagro.pp.module.pay.dal.dataobject.order.PayOrderExtensionDO; + +import javax.validation.Valid; +import javax.validation.constraints.NotEmpty; +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.List; + +/** + * 支付订单 Service 接口 + * + * @author aquan + */ +public interface PayOrderService { + + /** + * 获得支付订单 + * + * @param id 编号 + * @return 支付订单 + */ + PayOrderDO getOrder(Long id); + + /** + * 获得支付订单 + * + * @param appId 应用编号 + * @param merchantOrderId 商户订单编号 + * @return 支付订单 + */ + PayOrderDO getOrder(Long appId, String merchantOrderId); + + /** + * 获得支付订单列表 + * + * @param ids 编号数组 + * @return 支付订单列表 + */ + List getOrderList(Collection ids); + + /** + * 获得指定应用的订单数量 + * + * @param appId 应用编号 + * @return 订单数量 + */ + Long getOrderCountByAppId(Long appId); + + /** + * 获得支付订单分页 + * + * @param pageReqVO 分页查询 + * @return 支付订单分页 + */ + PageResult getOrderPage(PayOrderPageReqVO pageReqVO); + + /** + * 获得支付订单列表, 用于 Excel 导出 + * + * @param exportReqVO 查询条件 + * @return 支付订单列表 + */ + List getOrderList(PayOrderExportReqVO exportReqVO); + + /** + * 创建支付单 + * + * @param reqDTO 创建请求 + * @return 支付单编号 + */ + Long createOrder(@Valid PayOrderCreateReqDTO reqDTO); + + /** + * 提交支付 + * 此时,会发起支付渠道的调用 + * + * @param reqVO 提交请求 + * @param userIp 提交 IP + * @return 提交结果 + */ + PayOrderSubmitRespVO submitOrder(@Valid PayOrderSubmitReqVO reqVO, + @NotEmpty(message = "提交 IP 不能为空") String userIp); + + /** + * 通知支付单成功 + * + * @param channelId 渠道编号 + * @param notify 通知 + */ + void notifyOrder(Long channelId, PayOrderRespDTO notify); + + /** + * 更新支付订单的退款金额 + * + * @param id 编号 + * @param incrRefundPrice 增加的退款金额 + */ + void updateOrderRefundPrice(Long id, Integer incrRefundPrice); + + /** + * 更新支付订单价格 + * + * @param id 支付单编号 + * @param payPrice 支付单价格 + */ + void updatePayOrderPrice(Long id, Integer payPrice); + + /** + * 获得支付订单 + * + * @param id 编号 + * @return 支付订单 + */ + PayOrderExtensionDO getOrderExtension(Long id); + + /** + * 获得支付订单 + * + * @param no 支付订单 no + * @return 支付订单 + */ + PayOrderExtensionDO getOrderExtensionByNo(String no); + + /** + * 同步订单的支付状态 + * + * @param minCreateTime 最小创建时间 + * @return 同步到已支付的订单数量 + */ + int syncOrder(LocalDateTime minCreateTime); + + /** + * 同步订单的支付状态 + * + * 1. Quietly 表示,即使同步失败,也不会抛出异常 + * 2. 什么时候回出现异常?因为是主动同步,可能和支付渠道的异步回调存在并发冲突,导致抛出异常 + * + * @param id 订单编号 + */ + void syncOrderQuietly(Long id); + + /** + * 将已过期的订单,状态修改为已关闭 + * + * @return 过期的订单数量 + */ + int expireOrder(); + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/order/PayOrderServiceImpl.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/order/PayOrderServiceImpl.java new file mode 100644 index 0000000..0128efd --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/order/PayOrderServiceImpl.java @@ -0,0 +1,605 @@ +package cn.aagro.pp.module.pay.service.order; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.common.util.date.LocalDateTimeUtils; +import cn.aagro.pp.framework.common.util.number.MoneyUtils; +import cn.aagro.pp.module.pay.framework.pay.core.client.PayClient; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import cn.aagro.pp.framework.tenant.core.util.TenantUtils; +import cn.aagro.pp.module.pay.api.order.dto.PayOrderCreateReqDTO; +import cn.aagro.pp.module.pay.controller.admin.order.vo.PayOrderExportReqVO; +import cn.aagro.pp.module.pay.controller.admin.order.vo.PayOrderPageReqVO; +import cn.aagro.pp.module.pay.controller.admin.order.vo.PayOrderSubmitReqVO; +import cn.aagro.pp.module.pay.controller.admin.order.vo.PayOrderSubmitRespVO; +import cn.aagro.pp.module.pay.convert.order.PayOrderConvert; +import cn.aagro.pp.module.pay.dal.dataobject.app.PayAppDO; +import cn.aagro.pp.module.pay.dal.dataobject.channel.PayChannelDO; +import cn.aagro.pp.module.pay.dal.dataobject.order.PayOrderDO; +import cn.aagro.pp.module.pay.dal.dataobject.order.PayOrderExtensionDO; +import cn.aagro.pp.module.pay.dal.mysql.order.PayOrderExtensionMapper; +import cn.aagro.pp.module.pay.dal.mysql.order.PayOrderMapper; +import cn.aagro.pp.module.pay.dal.redis.no.PayNoRedisDAO; +import cn.aagro.pp.module.pay.enums.notify.PayNotifyTypeEnum; +import cn.aagro.pp.module.pay.enums.order.PayOrderStatusEnum; +import cn.aagro.pp.module.pay.framework.pay.config.PayProperties; +import cn.aagro.pp.module.pay.service.app.PayAppService; +import cn.aagro.pp.module.pay.service.channel.PayChannelService; +import cn.aagro.pp.module.pay.service.notify.PayNotifyService; +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import static cn.aagro.pp.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.aagro.pp.framework.common.util.json.JsonUtils.toJsonString; +import static cn.aagro.pp.module.pay.enums.ErrorCodeConstants.*; + +/** + * 支付订单 Service 实现类 + * + * @author aquan + */ +@Service +@Validated +@Slf4j +public class PayOrderServiceImpl implements PayOrderService { + + @Resource + private PayProperties payProperties; + + @Resource + private PayOrderMapper orderMapper; + @Resource + private PayOrderExtensionMapper orderExtensionMapper; + @Resource + private PayNoRedisDAO noRedisDAO; + + @Resource + private PayAppService appService; + @Resource + private PayChannelService channelService; + @Resource + private PayNotifyService notifyService; + + @Override + public PayOrderDO getOrder(Long id) { + return orderMapper.selectById(id); + } + + @Override + public PayOrderDO getOrder(Long appId, String merchantOrderId) { + return orderMapper.selectByAppIdAndMerchantOrderId(appId, merchantOrderId); + } + + @Override + public List getOrderList(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return Collections.emptyList(); + } + return orderMapper.selectByIds(ids); + } + + @Override + public Long getOrderCountByAppId(Long appId) { + return orderMapper.selectCountByAppId(appId); + } + + @Override + public PageResult getOrderPage(PayOrderPageReqVO pageReqVO) { + return orderMapper.selectPage(pageReqVO); + } + + @Override + public List getOrderList(PayOrderExportReqVO exportReqVO) { + return orderMapper.selectList(exportReqVO); + } + + @Override + public Long createOrder(PayOrderCreateReqDTO reqDTO) { + // 校验 App + PayAppDO app = appService.validPayApp(reqDTO.getAppKey()); + + // 查询对应的支付交易单是否已经存在。如果是,则直接返回 + PayOrderDO order = orderMapper.selectByAppIdAndMerchantOrderId( + app.getId(), reqDTO.getMerchantOrderId()); + if (order != null) { + log.warn("[createOrder][appId({}) merchantOrderId({}) 已经存在对应的支付单({})]", order.getAppId(), + order.getMerchantOrderId(), toJsonString(order)); // 理论来说,不会出现这个情况 + return order.getId(); + } + + // 创建支付交易单 + order = PayOrderConvert.INSTANCE.convert(reqDTO).setAppId(app.getId()) + // 商户相关字段 + .setNotifyUrl(app.getOrderNotifyUrl()) + // 订单相关字段 + .setStatus(PayOrderStatusEnum.WAITING.getStatus()) + // 退款相关字段 + .setRefundPrice(0); + orderMapper.insert(order); + return order.getId(); + } + + @Override // 注意,这里不能添加事务注解,避免调用支付渠道失败时,将 PayOrderExtensionDO 回滚了 + public PayOrderSubmitRespVO submitOrder(PayOrderSubmitReqVO reqVO, String userIp) { + // 1.1 获得 PayOrderDO ,并校验其是否存在 + PayOrderDO order = validateOrderCanSubmit(reqVO.getId()); + // 1.32 校验支付渠道是否有效 + PayChannelDO channel = validateChannelCanSubmit(order.getAppId(), reqVO.getChannelCode()); + PayClient client = channelService.getPayClient(channel.getId()); + + // 2. 插入 PayOrderExtensionDO + String no = noRedisDAO.generate(payProperties.getOrderNoPrefix()); + PayOrderExtensionDO orderExtension = PayOrderConvert.INSTANCE.convert(reqVO, userIp) + .setOrderId(order.getId()).setNo(no) + .setChannelId(channel.getId()).setChannelCode(channel.getCode()) + .setStatus(PayOrderStatusEnum.WAITING.getStatus()); + orderExtensionMapper.insert(orderExtension); + + // 3. 调用三方接口 + PayOrderUnifiedReqDTO unifiedOrderReqDTO = PayOrderConvert.INSTANCE.convert2(reqVO, userIp) + // 商户相关的字段 + .setOutTradeNo(orderExtension.getNo()) // 注意,此处使用的是 PayOrderExtensionDO.no 属性! + .setSubject(order.getSubject()).setBody(order.getBody()) + .setNotifyUrl(genChannelOrderNotifyUrl(channel)) + .setReturnUrl(reqVO.getReturnUrl()) + // 订单相关字段 + .setPrice(order.getPrice()).setExpireTime(order.getExpireTime()); + PayOrderRespDTO unifiedOrderResp = client.unifiedOrder(unifiedOrderReqDTO); + + // 4. 如果调用直接支付成功,则直接更新支付单状态为成功。例如说:付款码支付,免密支付时,就直接验证支付成功 + if (unifiedOrderResp != null) { + try { + getSelf().notifyOrder(channel, unifiedOrderResp); + } catch (Exception e) { + // 兼容 https://gitee.com/zhijiantianya/aagro-cloud/issues/I8SM9H 场景 + // 支付宝或微信扫码之后时,由于 PayClient 是直接返回支付成功,而支付也会有回调,导致存在并发更新问题,此时一般是可以 try catch 直接忽略 + log.warn("[submitOrder][order({}) channel({}) 支付结果({}) 通知时发生异常,可能是并发问题]", + order, channel, unifiedOrderResp, e); + } + // 如有渠道错误码,则抛出业务异常,提示用户 + if (StrUtil.isNotEmpty(unifiedOrderResp.getChannelErrorCode())) { + throw exception(PAY_ORDER_SUBMIT_CHANNEL_ERROR, unifiedOrderResp.getChannelErrorCode(), + unifiedOrderResp.getChannelErrorMsg()); + } + // 此处需要读取最新的状态 + order = orderMapper.selectById(order.getId()); + } + return PayOrderConvert.INSTANCE.convert(order, unifiedOrderResp); + } + + private PayOrderDO validateOrderCanSubmit(Long id) { + PayOrderDO order = orderMapper.selectById(id); + if (order == null) { // 是否存在 + throw exception(PAY_ORDER_NOT_FOUND); + } + if (PayOrderStatusEnum.isSuccess(order.getStatus())) { // 校验状态,发现已支付 + throw exception(PAY_ORDER_STATUS_IS_SUCCESS); + } + if (!PayOrderStatusEnum.WAITING.getStatus().equals(order.getStatus())) { // 校验状态,必须是待支付 + throw exception(PAY_ORDER_STATUS_IS_NOT_WAITING); + } + if (LocalDateTimeUtils.beforeNow(order.getExpireTime())) { // 校验是否过期 + throw exception(PAY_ORDER_IS_EXPIRED); + } + + // 【重要】校验是否支付拓展单已支付,只是没有回调、或者数据不正常 + validateOrderActuallyPaid(id); + return order; + } + + /** + * 校验支付订单实际已支付 + * + * @param id 支付编号 + */ + @VisibleForTesting + void validateOrderActuallyPaid(Long id) { + List orderExtensions = orderExtensionMapper.selectListByOrderId(id); + orderExtensions.forEach(orderExtension -> { + // 情况一:校验数据库中的 orderExtension 是不是已支付 + if (PayOrderStatusEnum.isSuccess(orderExtension.getStatus())) { + log.warn("[validateOrderCanSubmit][order({}) 的 extension({}) 已支付,可能是数据不一致]", + id, orderExtension.getId()); + throw exception(PAY_ORDER_EXTENSION_IS_PAID); + } + // 情况二:调用三方接口,查询支付单状态,是不是已支付 + PayClient payClient = channelService.getPayClient(orderExtension.getChannelId()); + if (payClient == null) { + log.error("[validateOrderCanSubmit][渠道编号({}) 找不到对应的支付客户端]", orderExtension.getChannelId()); + return; + } + PayOrderRespDTO respDTO = payClient.getOrder(orderExtension.getNo()); + if (respDTO != null && PayOrderStatusEnum.isSuccess(respDTO.getStatus())) { + log.warn("[validateOrderCanSubmit][order({}) 的 PayOrderRespDTO({}) 已支付,可能是回调延迟]", + id, toJsonString(respDTO)); + throw exception(PAY_ORDER_EXTENSION_IS_PAID); + } + }); + } + + private PayChannelDO validateChannelCanSubmit(Long appId, String channelCode) { + // 校验 App + appService.validPayApp(appId); + // 校验支付渠道是否有效 + PayChannelDO channel = channelService.validPayChannel(appId, channelCode); + PayClient client = channelService.getPayClient(channel.getId()); + if (client == null) { + log.error("[validatePayChannelCanSubmit][渠道编号({}) 找不到对应的支付客户端]", channel.getId()); + throw exception(CHANNEL_NOT_FOUND); + } + return channel; + } + + /** + * 根据支付渠道的编码,生成支付渠道的回调地址 + * + * @param channel 支付渠道 + * @return 支付渠道的回调地址 配置地址 + "/" + channel id + */ + private String genChannelOrderNotifyUrl(PayChannelDO channel) { + return payProperties.getOrderNotifyUrl() + "/" + channel.getId(); + } + + @Override + public void notifyOrder(Long channelId, PayOrderRespDTO notify) { + // 校验支付渠道是否有效 + PayChannelDO channel = channelService.validPayChannel(channelId); + // 更新支付订单为已支付 + TenantUtils.execute(channel.getTenantId(), () -> getSelf().notifyOrder(channel, notify)); + } + + /** + * 通知并更新订单的支付结果 + * + * @param channel 支付渠道 + * @param notify 通知 + */ + @Transactional(rollbackFor = Exception.class) + // 注意,如果是方法内调用该方法,需要通过 getSelf().notifyPayOrder(channel, notify) 调用,否则事务不生效 + public void notifyOrder(PayChannelDO channel, PayOrderRespDTO notify) { + // 情况一:支付成功的回调 + if (PayOrderStatusEnum.isSuccess(notify.getStatus())) { + notifyOrderSuccess(channel, notify); + return; + } + // 情况二:支付失败的回调 + if (PayOrderStatusEnum.isClosed(notify.getStatus())) { + notifyOrderClosed(channel, notify); + } + // 情况三:WAITING:无需处理 + // 情况四:REFUND:通过退款回调处理 + } + + private void notifyOrderSuccess(PayChannelDO channel, PayOrderRespDTO notify) { + // 1. 更新 PayOrderExtensionDO 支付成功 + PayOrderExtensionDO orderExtension = updateOrderSuccess(notify); + // 2. 更新 PayOrderDO 支付成功 + Boolean paid = updateOrderSuccess(channel, orderExtension, notify); + if (paid) { // 如果之前已经成功回调,则直接返回,不用重复记录支付通知记录;例如说:支付平台重复回调 + return; + } + + // 3. 插入支付通知记录 + notifyService.createPayNotifyTask(PayNotifyTypeEnum.ORDER.getType(), + orderExtension.getOrderId()); + } + + /** + * 更新 PayOrderExtensionDO 支付成功 + * + * @param notify 通知 + * @return PayOrderExtensionDO 对象 + */ + private PayOrderExtensionDO updateOrderSuccess(PayOrderRespDTO notify) { + // 1. 查询 PayOrderExtensionDO + PayOrderExtensionDO orderExtension = orderExtensionMapper.selectByNo(notify.getOutTradeNo()); + if (orderExtension == null) { + throw exception(PAY_ORDER_EXTENSION_NOT_FOUND); + } + if (PayOrderStatusEnum.isSuccess(orderExtension.getStatus())) { // 如果已经是成功,直接返回,不用重复更新 + log.info("[updateOrderExtensionSuccess][orderExtension({}) 已经是已支付,无需更新]", orderExtension.getId()); + return orderExtension; + } + if (ObjectUtil.notEqual(orderExtension.getStatus(), PayOrderStatusEnum.WAITING.getStatus())) { // 校验状态,必须是待支付 + throw exception(PAY_ORDER_EXTENSION_STATUS_IS_NOT_WAITING); + } + + // 2. 更新 PayOrderExtensionDO + int updateCounts = orderExtensionMapper.updateByIdAndStatus(orderExtension.getId(), orderExtension.getStatus(), + PayOrderExtensionDO.builder().status(PayOrderStatusEnum.SUCCESS.getStatus()).channelNotifyData(toJsonString(notify)).build()); + if (updateCounts == 0) { // 校验状态,必须是待支付 + throw exception(PAY_ORDER_EXTENSION_STATUS_IS_NOT_WAITING); + } + log.info("[updateOrderExtensionSuccess][orderExtension({}) 更新为已支付]", orderExtension.getId()); + return orderExtension; + } + + /** + * 更新 PayOrderDO 支付成功 + * + * @param channel 支付渠道 + * @param orderExtension 支付拓展单 + * @param notify 通知回调 + * @return 是否之前已经成功回调 + */ + private Boolean updateOrderSuccess(PayChannelDO channel, PayOrderExtensionDO orderExtension, + PayOrderRespDTO notify) { + // 1. 判断 PayOrderDO 是否处于待支付 + PayOrderDO order = orderMapper.selectById(orderExtension.getOrderId()); + if (order == null) { + throw exception(PAY_ORDER_NOT_FOUND); + } + if (PayOrderStatusEnum.isSuccess(order.getStatus()) // 如果已经是成功,直接返回,不用重复更新 + && Objects.equals(order.getExtensionId(), orderExtension.getId())) { + log.info("[updateOrderExtensionSuccess][order({}) 已经是已支付,无需更新]", order.getId()); + return true; + } + if (!PayOrderStatusEnum.WAITING.getStatus().equals(order.getStatus())) { // 校验状态,必须是待支付 + throw exception(PAY_ORDER_STATUS_IS_NOT_WAITING); + } + + // 2. 更新 PayOrderDO + int updateCounts = orderMapper.updateByIdAndStatus(order.getId(), PayOrderStatusEnum.WAITING.getStatus(), + PayOrderDO.builder().status(PayOrderStatusEnum.SUCCESS.getStatus()) + .channelId(channel.getId()).channelCode(channel.getCode()) + .successTime(notify.getSuccessTime()).extensionId(orderExtension.getId()).no(orderExtension.getNo()) + .channelOrderNo(notify.getChannelOrderNo()).channelUserId(notify.getChannelUserId()) + .channelFeeRate(channel.getFeeRate()) + .channelFeePrice(MoneyUtils.calculateRatePrice(order.getPrice(), channel.getFeeRate())) + .build()); + if (updateCounts == 0) { // 校验状态,必须是待支付 + throw exception(PAY_ORDER_STATUS_IS_NOT_WAITING); + } + log.info("[updateOrderExtensionSuccess][order({}) 更新为已支付]", order.getId()); + return false; + } + + private void notifyOrderClosed(PayChannelDO channel, PayOrderRespDTO notify) { + updateOrderExtensionClosed(channel, notify); + } + + @SuppressWarnings("unused") + private void updateOrderExtensionClosed(PayChannelDO channel, PayOrderRespDTO notify) { + // 1. 查询 PayOrderExtensionDO + PayOrderExtensionDO orderExtension = orderExtensionMapper.selectByNo(notify.getOutTradeNo()); + if (orderExtension == null) { + throw exception(PAY_ORDER_EXTENSION_NOT_FOUND); + } + if (PayOrderStatusEnum.isClosed(orderExtension.getStatus())) { // 如果已经是关闭,直接返回,不用重复更新 + log.info("[updateOrderExtensionClosed][orderExtension({}) 已经是支付关闭,无需更新]", orderExtension.getId()); + return; + } + // 一般出现先是支付成功,然后支付关闭,都是全部退款导致关闭的场景。这个情况,我们不更新支付拓展单,只通过退款流程,更新支付单 + if (PayOrderStatusEnum.isSuccess(orderExtension.getStatus())) { + log.info("[updateOrderExtensionClosed][orderExtension({}) 是已支付,无需更新为支付关闭]", orderExtension.getId()); + return; + } + if (ObjectUtil.notEqual(orderExtension.getStatus(), PayOrderStatusEnum.WAITING.getStatus())) { // 校验状态,必须是待支付 + throw exception(PAY_ORDER_EXTENSION_STATUS_IS_NOT_WAITING); + } + + // 2. 更新 PayOrderExtensionDO + int updateCounts = orderExtensionMapper.updateByIdAndStatus(orderExtension.getId(), orderExtension.getStatus(), + PayOrderExtensionDO.builder().status(PayOrderStatusEnum.CLOSED.getStatus()).channelNotifyData(toJsonString(notify)) + .channelErrorCode(notify.getChannelErrorCode()).channelErrorMsg(notify.getChannelErrorMsg()).build()); + if (updateCounts == 0) { // 校验状态,必须是待支付 + throw exception(PAY_ORDER_EXTENSION_STATUS_IS_NOT_WAITING); + } + log.info("[updateOrderExtensionClosed][orderExtension({}) 更新为支付关闭]", orderExtension.getId()); + } + + @Override + public void updateOrderRefundPrice(Long id, Integer incrRefundPrice) { + PayOrderDO order = orderMapper.selectById(id); + if (order == null) { + throw exception(PAY_ORDER_NOT_FOUND); + } + if (!PayOrderStatusEnum.isSuccessOrRefund(order.getStatus())) { + throw exception(PAY_ORDER_REFUND_FAIL_STATUS_ERROR); + } + if (order.getRefundPrice() + incrRefundPrice > order.getPrice()) { + throw exception(REFUND_PRICE_EXCEED); + } + + // 更新订单 + PayOrderDO updateObj = new PayOrderDO() + .setRefundPrice(order.getRefundPrice() + incrRefundPrice) + .setStatus(PayOrderStatusEnum.REFUND.getStatus()); + int updateCount = orderMapper.updateByIdAndStatus(id, order.getStatus(), updateObj); + if (updateCount == 0) { + throw exception(PAY_ORDER_REFUND_FAIL_STATUS_ERROR); + } + } + + @Override + public void updatePayOrderPrice(Long id, Integer payPrice) { + PayOrderDO order = orderMapper.selectById(id); + if (order == null) { + throw exception(PAY_ORDER_NOT_FOUND); + } + if (ObjectUtil.notEqual(PayOrderStatusEnum.WAITING.getStatus(), order.getStatus())) { + throw exception(PAY_ORDER_STATUS_IS_NOT_WAITING); + } + if (ObjectUtil.equal(order.getPrice(), payPrice)) { + return; + } + + orderMapper.updateById(new PayOrderDO().setId(order.getId()).setPrice(payPrice)); + } + + @Override + public PayOrderExtensionDO getOrderExtension(Long id) { + return orderExtensionMapper.selectById(id); + } + + @Override + public PayOrderExtensionDO getOrderExtensionByNo(String no) { + return orderExtensionMapper.selectByNo(no); + } + + @Override + public int syncOrder(LocalDateTime minCreateTime) { + // 1. 查询指定创建时间前的待支付订单 + List orderExtensions = orderExtensionMapper.selectListByStatusAndCreateTimeGe( + PayOrderStatusEnum.WAITING.getStatus(), minCreateTime); + if (CollUtil.isEmpty(orderExtensions)) { + return 0; + } + // 2. 遍历执行 + int count = 0; + for (PayOrderExtensionDO orderExtension : orderExtensions) { + count += syncOrder(orderExtension) ? 1 : 0; + } + return count; + } + + @Override + public void syncOrderQuietly(Long id) { + // 1. 查询待支付订单 + List orderExtensions = orderExtensionMapper.selectListByOrderIdAndStatus(id, + PayOrderStatusEnum.WAITING.getStatus()); + + // 2. 遍历执行 + for (PayOrderExtensionDO orderExtension : orderExtensions) { + syncOrder(orderExtension); + } + } + + /** + * 同步单个支付拓展单 + * + * @param orderExtension 支付拓展单 + * @return 是否已支付 + */ + private boolean syncOrder(PayOrderExtensionDO orderExtension) { + try { + // 1.1 查询支付订单信息 + PayClient payClient = channelService.getPayClient(orderExtension.getChannelId()); + if (payClient == null) { + log.error("[syncOrder][渠道编号({}) 找不到对应的支付客户端]", orderExtension.getChannelId()); + return false; + } + PayOrderRespDTO respDTO = payClient.getOrder(orderExtension.getNo()); + // 如果查询到订单不存在,PayClient 返回的状态为关闭。但此时不能关闭订单。存在以下一种场景: + // 拉起渠道支付后,短时间内用户未及时完成支付,但是该订单同步定时任务恰巧自动触发了,主动查询结果为订单不存在。 + // 当用户支付成功之后,该订单状态在渠道的回调中无法从已关闭改为已支付,造成重大影响。 + // 考虑此定时任务是异常场景的兜底操作,因此这里不做变更,优先以回调为准。 + // 让订单自动随着支付渠道那边一起等到过期,确保渠道先过期关闭支付入口,而后通过订单过期定时任务关闭自己的订单。 + if (PayOrderStatusEnum.isClosed(respDTO.getStatus())) { + return false; + } + // 1.2 回调支付结果 + notifyOrder(orderExtension.getChannelId(), respDTO); + + // 2. 如果是已支付,则返回 true + return PayOrderStatusEnum.isSuccess(respDTO.getStatus()); + } catch (Throwable e) { + log.error("[syncOrder][orderExtension({}) 同步支付状态异常]", orderExtension.getId(), e); + return false; + } + } + + @Override + public int expireOrder() { + // 1. 查询过期的待支付订单 + List orders = orderMapper.selectListByStatusAndExpireTimeLt( + PayOrderStatusEnum.WAITING.getStatus(), LocalDateTime.now()); + if (CollUtil.isEmpty(orders)) { + return 0; + } + + // 2. 遍历执行 + int count = 0; + for (PayOrderDO order : orders) { + count += expireOrder(order) ? 1 : 0; + } + return count; + } + + /** + * 同步单个支付单 + * + * @param order 支付单 + * @return 是否已过期 + */ + private boolean expireOrder(PayOrderDO order) { + try { + // 1. 需要先处理关联的支付拓展单,避免错误的过期已支付 or 已退款的订单 + List orderExtensions = orderExtensionMapper.selectListByOrderId(order.getId()); + for (PayOrderExtensionDO orderExtension : orderExtensions) { + if (PayOrderStatusEnum.isClosed(orderExtension.getStatus())) { + continue; + } + // 情况一:校验数据库中的 orderExtension 是不是已支付 + if (PayOrderStatusEnum.isSuccess(orderExtension.getStatus())) { + log.error("[expireOrder][order({}) 的 extension({}) 已支付,可能是数据不一致]", + order.getId(), orderExtension.getId()); + return false; + } + // 情况二:调用三方接口,查询支付单状态,是不是已支付/已退款 + PayClient payClient = channelService.getPayClient(orderExtension.getChannelId()); + if (payClient == null) { + log.error("[expireOrder][渠道编号({}) 找不到对应的支付客户端]", orderExtension.getChannelId()); + return false; + } + PayOrderRespDTO respDTO = payClient.getOrder(orderExtension.getNo()); + if (PayOrderStatusEnum.isRefund(respDTO.getStatus())) { + // 补充说明:按道理,应该是 WAITING => SUCCESS => REFUND 状态,如果直接 WAITING => REFUND 状态,说明中间丢了过程 + // 此时,需要人工介入,手工补齐数据,保持 WAITING => SUCCESS => REFUND 的过程 + log.error("[expireOrder][extension({}) 的 PayOrderRespDTO({}) 已退款,可能是回调延迟]", + orderExtension.getId(), toJsonString(respDTO)); + return false; + } + if (PayOrderStatusEnum.isSuccess(respDTO.getStatus())) { + notifyOrder(orderExtension.getChannelId(), respDTO); + return false; + } + // 兜底逻辑:将支付拓展单更新为已关闭 + PayOrderExtensionDO updateObj = new PayOrderExtensionDO().setStatus(PayOrderStatusEnum.CLOSED.getStatus()) + .setChannelNotifyData(toJsonString(respDTO)); + if (orderExtensionMapper.updateByIdAndStatus(orderExtension.getId(), PayOrderStatusEnum.WAITING.getStatus(), + updateObj) == 0) { + log.error("[expireOrder][extension({}) 更新为支付关闭失败]", orderExtension.getId()); + return false; + } + log.info("[expireOrder][extension({}) 更新为支付关闭成功]", orderExtension.getId()); + } + + // 2. 都没有上述情况,可以安心更新为已关闭 + PayOrderDO updateObj = new PayOrderDO().setStatus(PayOrderStatusEnum.CLOSED.getStatus()); + if (orderMapper.updateByIdAndStatus(order.getId(), order.getStatus(), updateObj) == 0) { + log.error("[expireOrder][order({}) 更新为支付关闭失败]", order.getId()); + return false; + } + log.info("[expireOrder][order({}) 更新为支付关闭成功]", order.getId()); + return true; + } catch (Throwable e) { + log.error("[expireOrder][order({}) 过期订单异常]", order.getId(), e); + return false; + } + } + + /** + * 获得自身的代理对象,解决 AOP 生效问题 + * + * @return 自己 + */ + private PayOrderServiceImpl getSelf() { + return SpringUtil.getBean(getClass()); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/refund/PayRefundService.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/refund/PayRefundService.java new file mode 100644 index 0000000..195493e --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/refund/PayRefundService.java @@ -0,0 +1,82 @@ +package cn.aagro.pp.module.pay.service.refund; + +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.refund.PayRefundRespDTO; +import cn.aagro.pp.module.pay.api.refund.dto.PayRefundCreateReqDTO; +import cn.aagro.pp.module.pay.controller.admin.refund.vo.PayRefundExportReqVO; +import cn.aagro.pp.module.pay.controller.admin.refund.vo.PayRefundPageReqVO; +import cn.aagro.pp.module.pay.dal.dataobject.refund.PayRefundDO; + +import java.util.List; + +/** + * 退款订单 Service 接口 + * + * @author aquan + */ +public interface PayRefundService { + + /** + * 获得退款订单 + * + * @param id 编号 + * @return 退款订单 + */ + PayRefundDO getRefund(Long id); + + /** + * 获得退款订单 + * + * @param no 外部退款单号 + * @return 退款订单 + */ + PayRefundDO getRefundByNo(String no); + + /** + * 获得指定应用的退款数量 + * + * @param appId 应用编号 + * @return 退款数量 + */ + Long getRefundCountByAppId(Long appId); + + /** + * 获得退款订单分页 + * + * @param pageReqVO 分页查询 + * @return 退款订单分页 + */ + PageResult getRefundPage(PayRefundPageReqVO pageReqVO); + + /** + * 获得退款订单列表, 用于 Excel 导出 + * + * @param exportReqVO 查询条件 + * @return 退款订单列表 + */ + List getRefundList(PayRefundExportReqVO exportReqVO); + + /** + * 创建退款申请 + * + * @param reqDTO 退款申请信息 + * @return 退款单号 + */ + Long createRefund(PayRefundCreateReqDTO reqDTO); + + /** + * 渠道的退款通知 + * + * @param channelId 渠道编号 + * @param notify 通知 + */ + void notifyRefund(Long channelId, PayRefundRespDTO notify); + + /** + * 同步渠道退款的退款状态 + * + * @return 同步到状态的退款数量,包括退款成功、退款失败 + */ + int syncRefund(); + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/refund/PayRefundServiceImpl.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/refund/PayRefundServiceImpl.java new file mode 100644 index 0000000..a63537a --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/refund/PayRefundServiceImpl.java @@ -0,0 +1,331 @@ +package cn.aagro.pp.module.pay.service.refund; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.pay.framework.pay.core.client.PayClient; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.refund.PayRefundRespDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO; +import cn.aagro.pp.framework.tenant.core.util.TenantUtils; +import cn.aagro.pp.module.pay.api.refund.dto.PayRefundCreateReqDTO; +import cn.aagro.pp.module.pay.controller.admin.refund.vo.PayRefundExportReqVO; +import cn.aagro.pp.module.pay.controller.admin.refund.vo.PayRefundPageReqVO; +import cn.aagro.pp.module.pay.convert.refund.PayRefundConvert; +import cn.aagro.pp.module.pay.dal.dataobject.app.PayAppDO; +import cn.aagro.pp.module.pay.dal.dataobject.channel.PayChannelDO; +import cn.aagro.pp.module.pay.dal.dataobject.order.PayOrderDO; +import cn.aagro.pp.module.pay.dal.dataobject.refund.PayRefundDO; +import cn.aagro.pp.module.pay.dal.mysql.refund.PayRefundMapper; +import cn.aagro.pp.module.pay.dal.redis.no.PayNoRedisDAO; +import cn.aagro.pp.module.pay.enums.notify.PayNotifyTypeEnum; +import cn.aagro.pp.module.pay.enums.order.PayOrderStatusEnum; +import cn.aagro.pp.module.pay.enums.refund.PayRefundStatusEnum; +import cn.aagro.pp.module.pay.framework.pay.config.PayProperties; +import cn.aagro.pp.module.pay.service.app.PayAppService; +import cn.aagro.pp.module.pay.service.channel.PayChannelService; +import cn.aagro.pp.module.pay.service.notify.PayNotifyService; +import cn.aagro.pp.module.pay.service.order.PayOrderService; +import lombok.extern.slf4j.Slf4j; +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 static cn.aagro.pp.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.aagro.pp.framework.common.util.json.JsonUtils.toJsonString; +import static cn.aagro.pp.module.pay.enums.ErrorCodeConstants.*; + +/** + * 退款订单 Service 实现类 + * + * @author jason + */ +@Service +@Slf4j +@Validated +public class PayRefundServiceImpl implements PayRefundService { + + @Resource + private PayProperties payProperties; + + @Resource + private PayRefundMapper refundMapper; + @Resource + private PayNoRedisDAO noRedisDAO; + + @Resource + private PayOrderService orderService; + @Resource + private PayAppService appService; + @Resource + private PayChannelService channelService; + @Resource + private PayNotifyService notifyService; + + @Override + public PayRefundDO getRefund(Long id) { + return refundMapper.selectById(id); + } + + @Override + public PayRefundDO getRefundByNo(String no) { + return refundMapper.selectByNo(no); + } + + @Override + public Long getRefundCountByAppId(Long appId) { + return refundMapper.selectCountByAppId(appId); + } + + @Override + public PageResult getRefundPage(PayRefundPageReqVO pageReqVO) { + return refundMapper.selectPage(pageReqVO); + } + + @Override + public List getRefundList(PayRefundExportReqVO exportReqVO) { + return refundMapper.selectList(exportReqVO); + } + + @Override + public Long createRefund(PayRefundCreateReqDTO reqDTO) { + // 1.1 校验 App + PayAppDO app = appService.validPayApp(reqDTO.getAppKey()); + // 1.2 校验支付订单 + PayOrderDO order = validatePayOrderCanRefund(reqDTO, app.getId()); + // 1.3 校验支付渠道是否有效 + PayChannelDO channel = channelService.validPayChannel(order.getChannelId()); + PayClient client = channelService.getPayClient(channel.getId()); + if (client == null) { + log.error("[refund][渠道编号({}) 找不到对应的支付客户端]", channel.getId()); + throw exception(CHANNEL_NOT_FOUND); + } + // 1.4 校验退款订单是否已经存在 + PayRefundDO refund = refundMapper.selectByAppIdAndMerchantRefundId( + app.getId(), reqDTO.getMerchantRefundId()); + if (refund != null) { + throw exception(REFUND_EXISTS); + } + + // 2.1 插入退款单 + String no = noRedisDAO.generate(payProperties.getRefundNoPrefix()); + refund = PayRefundConvert.INSTANCE.convert(reqDTO) + .setNo(no).setAppId(app.getId()).setOrderId(order.getId()).setOrderNo(order.getNo()) + .setChannelId(order.getChannelId()).setChannelCode(order.getChannelCode()) + // 商户相关的字段 + .setNotifyUrl(app.getRefundNotifyUrl()) + // 渠道相关字段 + .setChannelOrderNo(order.getChannelOrderNo()) + // 退款相关字段 + .setStatus(PayRefundStatusEnum.WAITING.getStatus()) + .setPayPrice(order.getPrice()).setRefundPrice(reqDTO.getPrice()); + refundMapper.insert(refund); + try { + // 2.2 向渠道发起退款申请 + PayRefundUnifiedReqDTO unifiedReqDTO = new PayRefundUnifiedReqDTO() + .setPayPrice(order.getPrice()) + .setRefundPrice(reqDTO.getPrice()) + .setOutTradeNo(order.getNo()) + .setOutRefundNo(refund.getNo()) + .setNotifyUrl(genChannelRefundNotifyUrl(channel)) + .setReason(reqDTO.getReason()); + PayRefundRespDTO refundRespDTO = client.unifiedRefund(unifiedReqDTO); + // 2.3 处理退款返回 + getSelf().notifyRefund(channel, refundRespDTO); + } catch (Throwable e) { + // 注意:这里仅打印异常,不进行抛出。 + // 原因是:虽然调用支付渠道进行退款发生异常(网络请求超时),实际退款成功。这个结果,后续通过退款回调、或者退款轮询补偿可以拿到。 + // 最终,在异常的情况下,支付中心会异步回调业务的退款回调接口,提供退款结果 + log.error("[createPayRefund][退款 id({}) requestDTO({}) 发生异常]", + refund.getId(), reqDTO, e); + } + + // 返回退款编号 + return refund.getId(); + } + + /** + * 校验支付订单是否可以退款 + * + * @param reqDTO 退款申请信息 + * @return 支付订单 + */ + private PayOrderDO validatePayOrderCanRefund(PayRefundCreateReqDTO reqDTO, Long appId) { + PayOrderDO order = orderService.getOrder(appId, reqDTO.getMerchantOrderId()); + if (order == null) { + throw exception(PAY_ORDER_NOT_FOUND); + } + // 校验状态,必须是已支付、或者已退款 + if (!PayOrderStatusEnum.isSuccessOrRefund(order.getStatus())) { + throw exception(PAY_ORDER_REFUND_FAIL_STATUS_ERROR); + } + + // 校验金额,退款金额不能大于原定的金额 + if (reqDTO.getPrice() + order.getRefundPrice() > order.getPrice()) { + throw exception(REFUND_PRICE_EXCEED); + } + // 是否有退款中的订单 + if (refundMapper.selectCountByAppIdAndOrderId(appId, order.getId(), + PayRefundStatusEnum.WAITING.getStatus()) > 0) { + throw exception(REFUND_HAS_REFUNDING); + } + return order; + } + + /** + * 根据支付渠道的编码,生成支付渠道的回调地址 + * + * @param channel 支付渠道 + * @return 支付渠道的回调地址 配置地址 + "/" + channel id + */ + private String genChannelRefundNotifyUrl(PayChannelDO channel) { + return payProperties.getRefundNotifyUrl() + "/" + channel.getId(); + } + + @Override + public void notifyRefund(Long channelId, PayRefundRespDTO notify) { + // 校验支付渠道是否有效 + PayChannelDO channel = channelService.validPayChannel(channelId); + // 更新退款订单 + TenantUtils.execute(channel.getTenantId(), () -> getSelf().notifyRefund(channel, notify)); + } + + /** + * 通知并更新订单的退款结果 + * + * @param channel 支付渠道 + * @param notify 通知 + */ + // 注意,如果是方法内调用该方法,需要通过 getSelf().notifyRefund(channel, notify) 调用,否则事务不生效 + @Transactional(rollbackFor = Exception.class) + public void notifyRefund(PayChannelDO channel, PayRefundRespDTO notify) { + // 情况一:退款成功 + if (PayRefundStatusEnum.isSuccess(notify.getStatus())) { + notifyRefundSuccess(channel, notify); + return; + } + // 情况二:退款失败 + if (PayRefundStatusEnum.isFailure(notify.getStatus())) { + notifyRefundFailure(channel, notify); + } + } + + private void notifyRefundSuccess(PayChannelDO channel, PayRefundRespDTO notify) { + // 1.1 查询 PayRefundDO + PayRefundDO refund = refundMapper.selectByAppIdAndNo( + channel.getAppId(), notify.getOutRefundNo()); + if (refund == null) { + throw exception(REFUND_NOT_FOUND); + } + if (PayRefundStatusEnum.isSuccess(refund.getStatus())) { // 如果已经是成功,直接返回,不用重复更新 + log.info("[notifyRefundSuccess][退款订单({}) 已经是退款成功,无需更新]", refund.getId()); + return; + } + if (!PayRefundStatusEnum.WAITING.getStatus().equals(refund.getStatus())) { + throw exception(REFUND_STATUS_IS_NOT_WAITING); + } + // 1.2 更新 PayRefundDO + PayRefundDO updateRefundObj = new PayRefundDO() + .setSuccessTime(notify.getSuccessTime()) + .setChannelRefundNo(notify.getChannelRefundNo()) + .setStatus(PayRefundStatusEnum.SUCCESS.getStatus()) + .setChannelNotifyData(toJsonString(notify)); + int updateCounts = refundMapper.updateByIdAndStatus(refund.getId(), refund.getStatus(), updateRefundObj); + if (updateCounts == 0) { // 校验状态,必须是等待状态 + throw exception(REFUND_STATUS_IS_NOT_WAITING); + } + log.info("[notifyRefundSuccess][退款订单({}) 更新为退款成功]", refund.getId()); + + // 2. 更新订单 + orderService.updateOrderRefundPrice(refund.getOrderId(), refund.getRefundPrice()); + + // 3. 插入退款通知记录 + notifyService.createPayNotifyTask(PayNotifyTypeEnum.REFUND.getType(), + refund.getId()); + } + + private void notifyRefundFailure(PayChannelDO channel, PayRefundRespDTO notify) { + // 1.1 查询 PayRefundDO + PayRefundDO refund = refundMapper.selectByAppIdAndNo( + channel.getAppId(), notify.getOutRefundNo()); + if (refund == null) { + throw exception(REFUND_NOT_FOUND); + } + if (PayRefundStatusEnum.isFailure(refund.getStatus())) { // 如果已经是成功,直接返回,不用重复更新 + log.info("[notifyRefundSuccess][退款订单({}) 已经是退款关闭,无需更新]", refund.getId()); + return; + } + if (!PayRefundStatusEnum.WAITING.getStatus().equals(refund.getStatus())) { + throw exception(REFUND_STATUS_IS_NOT_WAITING); + } + // 1.2 更新 PayRefundDO + PayRefundDO updateRefundObj = new PayRefundDO() + .setChannelRefundNo(notify.getChannelRefundNo()) + .setStatus(PayRefundStatusEnum.FAILURE.getStatus()) + .setChannelNotifyData(toJsonString(notify)) + .setChannelErrorCode(notify.getChannelErrorCode()).setChannelErrorMsg(notify.getChannelErrorMsg()); + int updateCounts = refundMapper.updateByIdAndStatus(refund.getId(), refund.getStatus(), updateRefundObj); + if (updateCounts == 0) { // 校验状态,必须是等待状态 + throw exception(REFUND_STATUS_IS_NOT_WAITING); + } + log.info("[notifyRefundFailure][退款订单({}) 更新为退款失败]", refund.getId()); + + // 2. 插入退款通知记录 + notifyService.createPayNotifyTask(PayNotifyTypeEnum.REFUND.getType(), + refund.getId()); + } + + @Override + public int syncRefund() { + // 1. 查询指定创建时间内的待退款订单 + List refunds = refundMapper.selectListByStatus(PayRefundStatusEnum.WAITING.getStatus()); + if (CollUtil.isEmpty(refunds)) { + return 0; + } + // 2. 遍历执行 + int count = 0; + for (PayRefundDO refund : refunds) { + count += syncRefund(refund) ? 1 : 0; + } + return count; + } + + /** + * 同步单个退款订单 + * + * @param refund 退款订单 + * @return 是否同步到 + */ + private boolean syncRefund(PayRefundDO refund) { + try { + // 1.1 查询退款订单信息 + PayClient payClient = channelService.getPayClient(refund.getChannelId()); + if (payClient == null) { + log.error("[syncRefund][渠道编号({}) 找不到对应的支付客户端]", refund.getChannelId()); + return false; + } + PayRefundRespDTO respDTO = payClient.getRefund(refund.getOrderNo(), refund.getNo()); + // 1.2 回调退款结果 + notifyRefund(refund.getChannelId(), respDTO); + + // 2. 如果同步到,则返回 true + return PayRefundStatusEnum.isSuccess(respDTO.getStatus()) + || PayRefundStatusEnum.isFailure(respDTO.getStatus()); + } catch (Throwable e) { + log.error("[syncRefund][refund({}) 同步退款状态异常]", refund.getId(), e); + return false; + } + } + + /** + * 获得自身的代理对象,解决 AOP 生效问题 + * + * @return 自己 + */ + private PayRefundServiceImpl getSelf() { + return SpringUtil.getBean(getClass()); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/transfer/PayTransferService.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/transfer/PayTransferService.java new file mode 100644 index 0000000..df3dc9f --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/transfer/PayTransferService.java @@ -0,0 +1,70 @@ +package cn.aagro.pp.module.pay.service.transfer; + +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.transfer.PayTransferRespDTO; +import cn.aagro.pp.module.pay.api.transfer.dto.PayTransferCreateReqDTO; +import cn.aagro.pp.module.pay.api.transfer.dto.PayTransferCreateRespDTO; +import cn.aagro.pp.module.pay.controller.admin.transfer.vo.PayTransferPageReqVO; +import cn.aagro.pp.module.pay.dal.dataobject.transfer.PayTransferDO; +import javax.validation.Valid; + +/** + * 转账 Service 接口 + * + * @author jason + */ +public interface PayTransferService { + + /** + * 创建转账单,并发起转账 + * + * @param reqDTO 创建请求 + * @return 转账单编号 + */ + PayTransferCreateRespDTO createTransfer(@Valid PayTransferCreateReqDTO reqDTO); + + /** + * 获取转账单 + * @param id 转账单编号 + */ + PayTransferDO getTransfer(Long id); + + /** + * 根据转账单号获取转账单 + * + * @param no 转账单号 + * @return 转账单 + */ + PayTransferDO getTransferByNo(String no); + + /** + * 获得转账单分页 + * + * @param pageReqVO 分页查询 + * @return 转账单分页 + */ + PageResult getTransferPage(PayTransferPageReqVO pageReqVO); + + /** + * 同步渠道转账单状态 + * + * @return 同步到状态的转账数量,包括转账成功、转账失败、转账中的 + */ + int syncTransfer(); + + /** + * 【单个】同步渠道转账单状态 + * + * @param id 转账单编号 + */ + void syncTransfer(Long id); + + /** + * 渠道的转账通知 + * + * @param channelId 渠道编号 + * @param notify 通知 + */ + void notifyTransfer(Long channelId, PayTransferRespDTO notify); + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/transfer/PayTransferServiceImpl.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/transfer/PayTransferServiceImpl.java new file mode 100644 index 0000000..04a9d3c --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/transfer/PayTransferServiceImpl.java @@ -0,0 +1,316 @@ +package cn.aagro.pp.module.pay.service.transfer; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.common.util.json.JsonUtils; +import cn.aagro.pp.framework.common.util.object.BeanUtils; +import cn.aagro.pp.module.pay.framework.pay.core.client.PayClient; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.transfer.PayTransferRespDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO; +import cn.aagro.pp.framework.tenant.core.util.TenantUtils; +import cn.aagro.pp.module.pay.api.transfer.dto.PayTransferCreateReqDTO; +import cn.aagro.pp.module.pay.api.transfer.dto.PayTransferCreateRespDTO; +import cn.aagro.pp.module.pay.controller.admin.transfer.vo.PayTransferPageReqVO; +import cn.aagro.pp.module.pay.dal.dataobject.app.PayAppDO; +import cn.aagro.pp.module.pay.dal.dataobject.channel.PayChannelDO; +import cn.aagro.pp.module.pay.dal.dataobject.transfer.PayTransferDO; +import cn.aagro.pp.module.pay.dal.mysql.transfer.PayTransferMapper; +import cn.aagro.pp.module.pay.dal.redis.no.PayNoRedisDAO; +import cn.aagro.pp.module.pay.enums.notify.PayNotifyTypeEnum; +import cn.aagro.pp.module.pay.enums.transfer.PayTransferStatusEnum; +import cn.aagro.pp.module.pay.framework.pay.config.PayProperties; +import cn.aagro.pp.module.pay.service.app.PayAppService; +import cn.aagro.pp.module.pay.service.channel.PayChannelService; +import cn.aagro.pp.module.pay.service.notify.PayNotifyService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.List; + +import static cn.aagro.pp.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.aagro.pp.module.pay.enums.ErrorCodeConstants.*; + +// TODO @jason:等彻底实现完,单测写写; + +/** + * 转账 Service 实现类 + * + * @author jason + */ +@Service +@Slf4j +public class PayTransferServiceImpl implements PayTransferService { + + private static final String TRANSFER_NO_PREFIX = "T"; + + @Resource + private PayProperties payProperties; + + @Resource + private PayTransferMapper transferMapper; + @Resource + private PayAppService appService; + @Resource + private PayChannelService channelService; + @Resource + private PayNotifyService notifyService; + @Resource + private PayNoRedisDAO noRedisDAO; + + @Override + public PayTransferCreateRespDTO createTransfer(PayTransferCreateReqDTO reqDTO) { + // 1.1 校验 App + PayAppDO payApp = appService.validPayApp(reqDTO.getAppKey()); + // 1.2 校验支付渠道是否有效 + PayChannelDO channel = channelService.validPayChannel(payApp.getId(), reqDTO.getChannelCode()); + PayClient client = channelService.getPayClient(channel.getId()); + if (client == null) { + log.error("[createTransfer][渠道编号({}) 找不到对应的支付客户端]", channel.getId()); + throw exception(CHANNEL_NOT_FOUND); + } + // 1.3 校验转账单已经发起过转账 + PayTransferDO transfer = validateTransferCanCreate(reqDTO, payApp.getId()); + + // 2.1 情况一:不存在创建转账单,则进行创建 + if (transfer == null) { + String no = noRedisDAO.generate(TRANSFER_NO_PREFIX); + transfer = BeanUtils.toBean(reqDTO, PayTransferDO.class) + .setAppId(channel.getAppId()).setChannelId(channel.getId()) + .setNo(no).setStatus(PayTransferStatusEnum.WAITING.getStatus()) + .setNotifyUrl(payApp.getTransferNotifyUrl()); + transferMapper.insert(transfer); + } else { + // 2.2 情况二:存在创建转账单,但是状态为关闭,则更新为等待中 + transferMapper.updateByIdAndStatus(transfer.getId(), transfer.getStatus(), + new PayTransferDO().setStatus(PayTransferStatusEnum.WAITING.getStatus())); + } + PayTransferRespDTO unifiedTransferResp = null; + try { + // 3. 调用三方渠道发起转账 + PayTransferUnifiedReqDTO transferUnifiedReq = BeanUtils.toBean(reqDTO, PayTransferUnifiedReqDTO.class) + .setOutTransferNo(transfer.getNo()) + .setNotifyUrl(genChannelTransferNotifyUrl(channel)); + unifiedTransferResp = client.unifiedTransfer(transferUnifiedReq); + // 4. 通知转账结果 + getSelf().notifyTransfer(channel, unifiedTransferResp); + } catch (Throwable e) { + // 注意这里仅打印异常,不进行抛出。 + // 原因是:虽然调用支付渠道进行转账发生异常(网络请求超时),实际转账成功。这个结果,后续转账轮询可以拿到。 + // 或者,使用相同 no 再次发起转账请求 + log.error("[createTransfer][转账编号({}) requestDTO({}) 发生异常]", transfer.getId(), reqDTO, e); + } + return new PayTransferCreateRespDTO().setId(transfer.getId()) + .setChannelPackageInfo(unifiedTransferResp != null ? unifiedTransferResp.getChannelPackageInfo() : null); + } + + /** + * 根据支付渠道的编码,生成支付渠道的回调地址 + * + * @param channel 支付渠道 + * @return 支付渠道的回调地址 配置地址 + "/" + channel id + */ + private String genChannelTransferNotifyUrl(PayChannelDO channel) { + return payProperties.getTransferNotifyUrl() + "/" + channel.getId(); + } + + private PayTransferDO validateTransferCanCreate(PayTransferCreateReqDTO reqDTO, Long appId) { + PayTransferDO transfer = transferMapper.selectByAppIdAndMerchantOrderId(appId, reqDTO.getMerchantTransferId()); + if (transfer != null) { + // 只有转账单状态为关闭,才能再次发起转账 + if (!PayTransferStatusEnum.isClosed(transfer.getStatus())) { + throw exception(PAY_TRANSFER_CREATE_FAIL_STATUS_NOT_CLOSED); + } + // 校验参数是否一致 + if (ObjectUtil.notEqual(reqDTO.getPrice(), transfer.getPrice())) { + throw exception(PAY_TRANSFER_CREATE_PRICE_NOT_MATCH); + } + if (ObjectUtil.notEqual(reqDTO.getChannelCode(), transfer.getChannelCode())) { + throw exception(PAY_TRANSFER_CREATE_CHANNEL_NOT_MATCH); + } + } + // 如果状态为等待状态:不知道渠道转账是否发起成功 + // 特殊:允许使用相同的 no 再次发起转账,渠道会保证幂等 + return transfer; + } + + @Transactional(rollbackFor = Exception.class) + // 注意,如果是方法内调用该方法,需要通过 getSelf().notifyTransfer(channel, notify) 调用,否则事务不生效 + public void notifyTransfer(PayChannelDO channel, PayTransferRespDTO notify) { + // 转账成功的回调 + if (PayTransferStatusEnum.isSuccess(notify.getStatus())) { + notifyTransferSuccess(channel, notify); + } + // 转账关闭的回调 + if (PayTransferStatusEnum.isClosed(notify.getStatus())) { + notifyTransferClosed(channel, notify); + } + // 转账处理中的回调 + if (PayTransferStatusEnum.isProcessing(notify.getStatus())) { + notifyTransferProgressing(channel, notify); + } + // WAITING 状态无需处理 + } + + private void notifyTransferProgressing(PayChannelDO channel, PayTransferRespDTO notify) { + // 1. 校验 + PayTransferDO transfer = transferMapper.selectByAppIdAndNo(channel.getAppId(), notify.getOutTransferNo()); + if (transfer == null) { + throw exception(PAY_TRANSFER_NOT_FOUND); + } + if (PayTransferStatusEnum.isProcessing(transfer.getStatus())) { // 如果已经是转账中,直接返回,不用重复更新 + log.info("[notifyTransferProgressing][transfer({}) 已经是转账中状态,无需更新]", transfer.getId()); + return; + } + if (!PayTransferStatusEnum.isWaiting(transfer.getStatus())) { + throw exception(PAY_TRANSFER_NOTIFY_FAIL_STATUS_IS_NOT_WAITING); + } + + // 2. 更新状态 + int updateCounts = transferMapper.updateByIdAndStatus(transfer.getId(), + PayTransferStatusEnum.WAITING.getStatus(), + new PayTransferDO().setStatus(PayTransferStatusEnum.PROCESSING.getStatus()) + .setChannelPackageInfo(notify.getChannelPackageInfo())); + if (updateCounts == 0) { + throw exception(PAY_TRANSFER_NOTIFY_FAIL_STATUS_IS_NOT_WAITING); + } + log.info("[notifyTransferProgressing][transfer({}) 更新为转账进行中状态]", transfer.getId()); + } + + private void notifyTransferSuccess(PayChannelDO channel, PayTransferRespDTO notify) { + // 1. 校验状态 + PayTransferDO transfer = transferMapper.selectByAppIdAndNo(channel.getAppId(), notify.getOutTransferNo()); + if (transfer == null) { + throw exception(PAY_TRANSFER_NOT_FOUND); + } + if (PayTransferStatusEnum.isSuccess(transfer.getStatus())) { // 如果已成功,直接返回,不用重复更新 + log.info("[notifyTransferSuccess][transfer({}) 已经是成功状态,无需更新]", transfer.getId()); + return; + } + if (!PayTransferStatusEnum.isWaitingOrProcessing(transfer.getStatus())) { + throw exception(PAY_TRANSFER_NOTIFY_FAIL_STATUS_NOT_WAITING_OR_PROCESSING); + } + + // 2. 更新状态 + int updateCounts = transferMapper.updateByIdAndStatus(transfer.getId(), + CollUtil.newArrayList(PayTransferStatusEnum.WAITING.getStatus(), PayTransferStatusEnum.PROCESSING.getStatus()), + new PayTransferDO().setStatus(PayTransferStatusEnum.SUCCESS.getStatus()) + .setSuccessTime(notify.getSuccessTime()) + .setChannelTransferNo(notify.getChannelTransferNo()) + .setChannelNotifyData(JsonUtils.toJsonString(notify))); + if (updateCounts == 0) { + throw exception(PAY_TRANSFER_NOTIFY_FAIL_STATUS_NOT_WAITING_OR_PROCESSING); + } + log.info("[notifyTransferSuccess][transfer({}) 更新为已转账]", transfer.getId()); + + // 3. 插入转账通知记录 + notifyService.createPayNotifyTask(PayNotifyTypeEnum.TRANSFER.getType(), transfer.getId()); + } + + private void notifyTransferClosed(PayChannelDO channel, PayTransferRespDTO notify) { + // 1. 校验状态 + PayTransferDO transfer = transferMapper.selectByAppIdAndNo(channel.getAppId(), notify.getOutTransferNo()); + if (transfer == null) { + throw exception(PAY_TRANSFER_NOT_FOUND); + } + if (PayTransferStatusEnum.isClosed(transfer.getStatus())) { // 如果已是关闭状态,直接返回,不用重复更新 + log.info("[notifyTransferClosed][transfer({}) 已经是关闭状态,无需更新]", transfer.getId()); + return; + } + if (!PayTransferStatusEnum.isWaitingOrProcessing(transfer.getStatus())) { + throw exception(PAY_TRANSFER_NOTIFY_FAIL_STATUS_NOT_WAITING_OR_PROCESSING); + } + + // 2. 更新状态 + int updateCount = transferMapper.updateByIdAndStatus(transfer.getId(), + CollUtil.newArrayList(PayTransferStatusEnum.WAITING.getStatus(), PayTransferStatusEnum.PROCESSING.getStatus()), + new PayTransferDO().setStatus(PayTransferStatusEnum.CLOSED.getStatus()) + .setChannelTransferNo(notify.getChannelTransferNo()) + .setChannelNotifyData(JsonUtils.toJsonString(notify)) + .setChannelErrorCode(notify.getChannelErrorCode()).setChannelErrorMsg(notify.getChannelErrorMsg())); + if (updateCount == 0) { + throw exception(PAY_TRANSFER_NOTIFY_FAIL_STATUS_NOT_WAITING_OR_PROCESSING); + } + log.info("[notifyTransferClosed][transfer({}) 更新为关闭状态]", transfer.getId()); + + // 3. 插入转账通知记录 + notifyService.createPayNotifyTask(PayNotifyTypeEnum.TRANSFER.getType(), transfer.getId()); + } + + @Override + public PayTransferDO getTransfer(Long id) { + return transferMapper.selectById(id); + } + + @Override + public PayTransferDO getTransferByNo(String no) { + return transferMapper.selectByNo(no); + } + + @Override + public PageResult getTransferPage(PayTransferPageReqVO pageReqVO) { + return transferMapper.selectPage(pageReqVO); + } + + @Override + public int syncTransfer() { + List list = transferMapper.selectListByStatus(CollUtil.newArrayList( + PayTransferStatusEnum.WAITING.getStatus(), PayTransferStatusEnum.PROCESSING.getStatus())); + if (CollUtil.isEmpty(list)) { + return 0; + } + int count = 0; + for (PayTransferDO transfer : list) { + count += syncTransfer(transfer) ? 1 : 0; + } + return count; + } + + @Override + public void syncTransfer(Long id) { + PayTransferDO transfer = transferMapper.selectById(id); + if (transfer == null) { + throw exception(PAY_TRANSFER_NOT_FOUND); + } + syncTransfer(transfer); + } + + private boolean syncTransfer(PayTransferDO transfer) { + try { + // 1. 查询转账订单信息 + PayClient payClient = channelService.getPayClient(transfer.getChannelId()); + if (payClient == null) { + log.error("[syncTransfer][渠道编号({}) 找不到对应的支付客户端]", transfer.getChannelId()); + return false; + } + PayTransferRespDTO resp = payClient.getTransfer(transfer.getNo()); + + // 2. 回调转账结果 + notifyTransfer(transfer.getChannelId(), resp); + return true; + } catch (Throwable ex) { + log.error("[syncTransfer][transfer({}) 同步转账单状态异常]", transfer.getId(), ex); + return false; + } + } + + public void notifyTransfer(Long channelId, PayTransferRespDTO notify) { + // 校验渠道是否有效 + PayChannelDO channel = channelService.validPayChannel(channelId); + // 通知转账结果给对应的业务 + TenantUtils.execute(channel.getTenantId(), () -> getSelf().notifyTransfer(channel, notify)); + } + + /** + * 获得自身的代理对象,解决 AOP 生效问题 + * + * @return 自己 + */ + private PayTransferServiceImpl getSelf() { + return SpringUtil.getBean(getClass()); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/wallet/PayWalletRechargePackageService.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/wallet/PayWalletRechargePackageService.java new file mode 100644 index 0000000..2215fc6 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/wallet/PayWalletRechargePackageService.java @@ -0,0 +1,71 @@ +package cn.aagro.pp.module.pay.service.wallet; + +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.pay.controller.admin.wallet.vo.rechargepackage.WalletRechargePackageCreateReqVO; +import cn.aagro.pp.module.pay.controller.admin.wallet.vo.rechargepackage.WalletRechargePackagePageReqVO; +import cn.aagro.pp.module.pay.controller.admin.wallet.vo.rechargepackage.WalletRechargePackageUpdateReqVO; +import cn.aagro.pp.module.pay.dal.dataobject.wallet.PayWalletRechargePackageDO; + +import javax.validation.Valid; +import java.util.Collection; +import java.util.List; + +/** + * 钱包充值套餐 Service 接口 + * + * @author jason + */ +public interface PayWalletRechargePackageService { + + /** + * 获取钱包充值套餐 + * @param packageId 充值套餐编号 + */ + PayWalletRechargePackageDO getWalletRechargePackage(Long packageId); + + /** + * 校验钱包充值套餐的有效性, 无效的话抛出 ServiceException 异常 + * + * @param packageId 充值套餐编号 + */ + PayWalletRechargePackageDO validWalletRechargePackage(Long packageId); + + /** + * 创建充值套餐 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createWalletRechargePackage(@Valid WalletRechargePackageCreateReqVO createReqVO); + + /** + * 更新充值套餐 + * + * @param updateReqVO 更新信息 + */ + void updateWalletRechargePackage(@Valid WalletRechargePackageUpdateReqVO updateReqVO); + + /** + * 删除充值套餐 + * + * @param id 编号 + */ + void deleteWalletRechargePackage(Long id); + + /** + * 获得充值套餐分页 + * + * @param pageReqVO 分页查询 + * @return 充值套餐分页 + */ + PageResult getWalletRechargePackagePage(WalletRechargePackagePageReqVO pageReqVO); + + /** + * 获得充值套餐列表 + * + * @param status 状态 + * @return 充值套餐列表 + */ + List getWalletRechargePackageList(Integer status); + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/wallet/PayWalletRechargePackageServiceImpl.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/wallet/PayWalletRechargePackageServiceImpl.java new file mode 100644 index 0000000..9a0bba2 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/wallet/PayWalletRechargePackageServiceImpl.java @@ -0,0 +1,113 @@ +package cn.aagro.pp.module.pay.service.wallet; + +import cn.hutool.core.util.StrUtil; +import cn.aagro.pp.framework.common.enums.CommonStatusEnum; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.pay.controller.admin.wallet.vo.rechargepackage.WalletRechargePackageCreateReqVO; +import cn.aagro.pp.module.pay.controller.admin.wallet.vo.rechargepackage.WalletRechargePackagePageReqVO; +import cn.aagro.pp.module.pay.controller.admin.wallet.vo.rechargepackage.WalletRechargePackageUpdateReqVO; +import cn.aagro.pp.module.pay.convert.wallet.PayWalletRechargePackageConvert; +import cn.aagro.pp.module.pay.dal.dataobject.wallet.PayWalletRechargePackageDO; +import cn.aagro.pp.module.pay.dal.mysql.wallet.PayWalletRechargePackageMapper; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; + +import java.util.List; + +import static cn.aagro.pp.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.aagro.pp.module.pay.enums.ErrorCodeConstants.*; + +/** + * 钱包充值套餐 Service 实现类 + * + * @author jason + */ +@Service +public class PayWalletRechargePackageServiceImpl implements PayWalletRechargePackageService { + + @Resource + private PayWalletRechargePackageMapper walletRechargePackageMapper; + + @Override + public PayWalletRechargePackageDO getWalletRechargePackage(Long packageId) { + return walletRechargePackageMapper.selectById(packageId); + } + + @Override + public PayWalletRechargePackageDO validWalletRechargePackage(Long packageId) { + PayWalletRechargePackageDO rechargePackageDO = walletRechargePackageMapper.selectById(packageId); + if (rechargePackageDO == null) { + throw exception(WALLET_RECHARGE_PACKAGE_NOT_FOUND); + } + if (CommonStatusEnum.DISABLE.getStatus().equals(rechargePackageDO.getStatus())) { + throw exception(WALLET_RECHARGE_PACKAGE_IS_DISABLE); + } + return rechargePackageDO; + } + + @Override + public Long createWalletRechargePackage(WalletRechargePackageCreateReqVO createReqVO) { + // 校验套餐名是否唯一 + validateRechargePackageNameUnique(null, createReqVO.getName()); + + // 插入 + PayWalletRechargePackageDO walletRechargePackage = PayWalletRechargePackageConvert.INSTANCE.convert(createReqVO); + walletRechargePackageMapper.insert(walletRechargePackage); + // 返回 + return walletRechargePackage.getId(); + } + + @Override + public void updateWalletRechargePackage(WalletRechargePackageUpdateReqVO updateReqVO) { + // 校验存在 + validateWalletRechargePackageExists(updateReqVO.getId()); + // 校验套餐名是否唯一 + validateRechargePackageNameUnique(updateReqVO.getId(), updateReqVO.getName()); + + // 更新 + PayWalletRechargePackageDO updateObj = PayWalletRechargePackageConvert.INSTANCE.convert(updateReqVO); + walletRechargePackageMapper.updateById(updateObj); + } + + private void validateRechargePackageNameUnique(Long id, String name) { + if (StrUtil.isBlank(name)) { + return; + } + PayWalletRechargePackageDO rechargePackage = walletRechargePackageMapper.selectByName(name); + if (rechargePackage == null) { + return ; + } + if (id == null) { + throw exception(WALLET_RECHARGE_PACKAGE_NAME_EXISTS); + } + if (!id.equals(rechargePackage.getId())) { + throw exception(WALLET_RECHARGE_PACKAGE_NAME_EXISTS); + } + } + + @Override + public void deleteWalletRechargePackage(Long id) { + // 校验存在 + validateWalletRechargePackageExists(id); + // 删除 + walletRechargePackageMapper.deleteById(id); + } + + private void validateWalletRechargePackageExists(Long id) { + if (walletRechargePackageMapper.selectById(id) == null) { + throw exception(WALLET_RECHARGE_PACKAGE_NOT_FOUND); + } + } + + @Override + public PageResult getWalletRechargePackagePage(WalletRechargePackagePageReqVO pageReqVO) { + return walletRechargePackageMapper.selectPage(pageReqVO); + } + + @Override + public List getWalletRechargePackageList(Integer status) { + return walletRechargePackageMapper.selectListByStatus(status); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/wallet/PayWalletRechargeService.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/wallet/PayWalletRechargeService.java new file mode 100644 index 0000000..04ed408 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/wallet/PayWalletRechargeService.java @@ -0,0 +1,64 @@ +package cn.aagro.pp.module.pay.service.wallet; + +import cn.aagro.pp.framework.common.pojo.PageParam; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.pay.controller.app.wallet.vo.recharge.AppPayWalletRechargeCreateReqVO; +import cn.aagro.pp.module.pay.dal.dataobject.wallet.PayWalletRechargeDO; + +/** + * 钱包充值 Service 接口 + * + * @author jason + */ +public interface PayWalletRechargeService { + + /** + * 创建钱包充值记录(发起充值) + * + * @param userId 用户编号 + * @param userType 用户类型 + * @param createReqVO 钱包充值请求 VO + * @param userIp 用户Ip + * @return 钱包充值记录 + */ + PayWalletRechargeDO createWalletRecharge(Long userId, Integer userType, String userIp, + AppPayWalletRechargeCreateReqVO createReqVO); + + /** + * 获得钱包充值记录分页 + * + * @param userId 用户编号 + * @param userType 用户类型 + * @param pageReqVO 分页请求 + * @param payStatus 是否支付 + * @return 钱包充值记录分页 + */ + PageResult getWalletRechargePackagePage(Long userId, Integer userType, + PageParam pageReqVO, Boolean payStatus); + + /** + * 更新钱包充值成功 + * + * @param id 钱包充值记录编号 + * @param payOrderId 支付订单编号 + */ + void updateWalletRechargerPaid(Long id, Long payOrderId); + + /** + * 发起钱包充值退款 + * + * @param id 钱包充值编号 + * @param userIp 用户 ip 地址 + */ + void refundWalletRecharge(Long id, String userIp); + + /** + * 更新钱包充值记录为已退款 + * + * @param id 钱包充值记录编号 + * @param refundId 钱包充值退款编号(实际和 id 相同) + * @param payRefundId 退款单id + */ + void updateWalletRechargeRefunded(Long id, Long refundId, Long payRefundId); + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/wallet/PayWalletRechargeServiceImpl.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/wallet/PayWalletRechargeServiceImpl.java new file mode 100644 index 0000000..b859acc --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/wallet/PayWalletRechargeServiceImpl.java @@ -0,0 +1,351 @@ +package cn.aagro.pp.module.pay.service.wallet; + +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.aagro.pp.framework.common.enums.UserTypeEnum; +import cn.aagro.pp.framework.common.pojo.PageParam; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.pay.api.order.dto.PayOrderCreateReqDTO; +import cn.aagro.pp.module.pay.api.refund.PayRefundApi; +import cn.aagro.pp.module.pay.api.refund.dto.PayRefundCreateReqDTO; +import cn.aagro.pp.module.pay.api.refund.dto.PayRefundRespDTO; +import cn.aagro.pp.module.pay.controller.app.wallet.vo.recharge.AppPayWalletRechargeCreateReqVO; +import cn.aagro.pp.module.pay.dal.dataobject.order.PayOrderDO; +import cn.aagro.pp.module.pay.dal.dataobject.wallet.PayWalletDO; +import cn.aagro.pp.module.pay.dal.dataobject.wallet.PayWalletRechargeDO; +import cn.aagro.pp.module.pay.dal.dataobject.wallet.PayWalletRechargePackageDO; +import cn.aagro.pp.module.pay.dal.mysql.wallet.PayWalletRechargeMapper; +import cn.aagro.pp.module.pay.enums.PayChannelEnum; +import cn.aagro.pp.module.pay.enums.order.PayOrderStatusEnum; +import cn.aagro.pp.module.pay.enums.refund.PayRefundStatusEnum; +import cn.aagro.pp.module.pay.enums.wallet.PayWalletBizTypeEnum; +import cn.aagro.pp.module.pay.framework.pay.config.PayProperties; +import cn.aagro.pp.module.pay.service.order.PayOrderService; +import cn.aagro.pp.module.system.api.social.SocialClientApi; +import cn.aagro.pp.module.system.api.social.dto.SocialWxaOrderUploadShippingInfoReqDTO; +import cn.aagro.pp.module.system.api.social.dto.SocialWxaSubscribeMessageSendReqDTO; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.Objects; + +import static cn.hutool.core.util.ObjectUtil.notEqual; +import static cn.aagro.pp.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.aagro.pp.framework.common.util.date.LocalDateTimeUtils.addTime; +import static cn.aagro.pp.framework.common.util.json.JsonUtils.toJsonString; +import static cn.aagro.pp.framework.common.util.number.MoneyUtils.fenToYuanStr; +import static cn.aagro.pp.module.pay.convert.wallet.PayWalletRechargeConvert.INSTANCE; +import static cn.aagro.pp.module.pay.enums.ErrorCodeConstants.*; +import static cn.aagro.pp.module.pay.enums.MessageTemplateConstants.WXA_WALLET_RECHARGER_PAID; +import static cn.aagro.pp.module.pay.enums.refund.PayRefundStatusEnum.*; + +/** + * 钱包充值 Service 实现类 + * + * @author jason + */ +@Service +@Slf4j +public class PayWalletRechargeServiceImpl implements PayWalletRechargeService { + + private static final String WALLET_RECHARGE_ORDER_SUBJECT = "钱包余额充值"; + + @Resource + private PayWalletRechargeMapper walletRechargeMapper; + @Resource + private PayWalletService payWalletService; + @Resource + private PayOrderService payOrderService; + @Resource + private PayWalletRechargePackageService payWalletRechargePackageService; + + @Resource + public SocialClientApi socialClientApi; + @Resource + private PayRefundApi payRefundApi; + + @Resource + private PayProperties payProperties; + + @Override + @Transactional(rollbackFor = Exception.class) + public PayWalletRechargeDO createWalletRecharge(Long userId, Integer userType, String userIp, + AppPayWalletRechargeCreateReqVO reqVO) { + // 1.1 计算充值金额 + int payPrice; + int bonusPrice = 0; + if (Objects.nonNull(reqVO.getPackageId())) { + PayWalletRechargePackageDO rechargePackage = payWalletRechargePackageService.validWalletRechargePackage(reqVO.getPackageId()); + payPrice = rechargePackage.getPayPrice(); + bonusPrice = rechargePackage.getBonusPrice(); + } else { + payPrice = reqVO.getPayPrice(); + } + // 1.2 插入充值记录 + PayWalletDO wallet = payWalletService.getOrCreateWallet(userId, userType); + PayWalletRechargeDO recharge = INSTANCE.convert(wallet.getId(), payPrice, bonusPrice, reqVO.getPackageId()); + walletRechargeMapper.insert(recharge); + + // 2.1 创建支付单 + Long payOrderId = payOrderService.createOrder(new PayOrderCreateReqDTO() + .setAppKey(payProperties.getWalletPayAppKey()).setUserIp(userIp) + .setUserId(userId).setUserType(userType) // 用户信息 + .setMerchantOrderId(recharge.getId().toString()) // 业务的订单编号 + .setSubject(WALLET_RECHARGE_ORDER_SUBJECT).setBody("") + .setPrice(recharge.getPayPrice()) + .setExpireTime(addTime(Duration.ofHours(2L)))); // TODO @芋艿:支付超时时间 + // 2.2 更新钱包充值记录中支付订单 + walletRechargeMapper.updateById(new PayWalletRechargeDO().setId(recharge.getId()).setPayOrderId(payOrderId)); + recharge.setPayOrderId(payOrderId); + return recharge; + } + + @Override + public PageResult getWalletRechargePackagePage(Long userId, Integer userType, + PageParam pageReqVO, Boolean payStatus) { + PayWalletDO wallet = payWalletService.getOrCreateWallet(userId, userType); + return walletRechargeMapper.selectPage(pageReqVO, wallet.getId(), payStatus); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateWalletRechargerPaid(Long id, Long payOrderId) { + // 1.1 校验钱包充值是否存在 + PayWalletRechargeDO recharge = walletRechargeMapper.selectById(id); + if (recharge == null) { + log.error("[updateWalletRechargerPaid][recharge({}) payOrder({}) 不存在充值订单,请进行处理!]", id, payOrderId); + throw exception(WALLET_RECHARGE_NOT_FOUND); + } + // 1.2 校验钱包充值是否可以支付 + if (recharge.getPayStatus()) { + // 特殊:支付单号相同,直接返回,说明重复回调 + if (ObjectUtil.equals(recharge.getPayOrderId(), payOrderId)) { + log.warn("[updateWalletRechargerPaid][recharge({}) 已支付,且支付单号相同({}),直接返回]", recharge, payOrderId); + return; + } + // 异常:支付单号不同,说明支付单号错误 + log.error("[updateWalletRechargerPaid][recharge({}) 已支付,但是支付单号不同({}),请进行处理!]", recharge, payOrderId); + throw exception(WALLET_RECHARGE_UPDATE_PAID_PAY_ORDER_ID_ERROR); + } + + // 2. 校验支付订单的合法性 + PayOrderDO payOrderDO = validatePayOrderPaid(recharge, payOrderId); + + // 3. 更新钱包充值的支付状态 + int updateCount = walletRechargeMapper.updateByIdAndPaid(id, false, + new PayWalletRechargeDO().setId(id).setPayStatus(true).setPayTime(LocalDateTime.now()) + .setPayChannelCode(payOrderDO.getChannelCode())); + if (updateCount == 0) { + throw exception(WALLET_RECHARGE_UPDATE_PAID_STATUS_NOT_UNPAID); + } + + // 4. 更新钱包余额 + // TODO @jason:这样的话,未来提现会不会把充值的,也提现走哈。类似先充 100,送 110;然后提现 110; + // TODO 需要钱包中加个可提现余额 + payWalletService.addWalletBalance(recharge.getWalletId(), String.valueOf(id), + PayWalletBizTypeEnum.RECHARGE, recharge.getTotalPrice()); + + // 5. 发送订阅消息 + getSelf().sendWalletRechargerPaidMessage(payOrderId, recharge); + } + + @Async + public void sendWalletRechargerPaidMessage(Long payOrderId, PayWalletRechargeDO walletRecharge) { + // 1. 构建并发送模版消息 + PayWalletDO wallet = payWalletService.getWallet(walletRecharge.getWalletId()); + socialClientApi.sendWxaSubscribeMessage(new SocialWxaSubscribeMessageSendReqDTO() + .setUserId(wallet.getUserId()).setUserType(wallet.getUserType()) + .setTemplateTitle(WXA_WALLET_RECHARGER_PAID) + .setPage("pages/user/wallet/money") // 钱包详情界面 + .addMessage("character_string1", String.valueOf(payOrderId)) // 支付单编号 + .addMessage("amount2", fenToYuanStr(walletRecharge.getTotalPrice())) // 充值金额 + .addMessage("time3", LocalDateTimeUtil.formatNormal(walletRecharge.getCreateTime())) // 充值时间 + .addMessage("phrase4", "充值成功")); // 充值状态 + + // 2. 调用接口上传虚拟物品发货信息 + // 注意:只有微信小程序支付的订单,才需要同步 + PayOrderDO payOrder = payOrderService.getOrder(payOrderId); + if (ObjUtil.notEqual(payOrder.getChannelCode(), PayChannelEnum.WX_LITE.getCode())) { + return; + } + SocialWxaOrderUploadShippingInfoReqDTO reqDTO = new SocialWxaOrderUploadShippingInfoReqDTO() + .setTransactionId(payOrder.getChannelOrderNo()) + .setOpenid(payOrder.getChannelUserId()) + .setItemDesc(payOrder.getSubject()) + .setLogisticsType(SocialWxaOrderUploadShippingInfoReqDTO.LOGISTICS_TYPE_VIRTUAL); // 虚拟物品发货类型 + try { + socialClientApi.uploadWxaOrderShippingInfo(UserTypeEnum.MEMBER.getValue(), reqDTO); + } catch (Exception ex) { + log.error("[sendWalletRechargerPaidMessage][订单({}) 上传订单物流信息到微信小程序失败]", payOrder, ex); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void refundWalletRecharge(Long id, String userIp) { + // 1.1 获取钱包充值记录 + PayWalletRechargeDO walletRecharge = walletRechargeMapper.selectById(id); + if (walletRecharge == null) { + log.error("[refundWalletRecharge][钱包充值记录不存在,钱包充值记录 id({})]", id); + throw exception(WALLET_RECHARGE_NOT_FOUND); + } + // 1.2 校验钱包充值是否可以发起退款 + PayWalletDO wallet = validateWalletRechargeCanRefund(walletRecharge); + + // 2. 冻结退款的余额,暂时只处理赠送的余额也全部退回 + payWalletService.freezePrice(wallet.getId(), walletRecharge.getTotalPrice()); + + // 3. 创建退款单 + String walletRechargeId = String.valueOf(id); + String refundId = walletRechargeId + "-refund"; + Long payRefundId = payRefundApi.createRefund(new PayRefundCreateReqDTO() + .setAppKey(payProperties.getWalletPayAppKey()).setUserIp(userIp) + .setUserId(wallet.getUserId()).setUserType(wallet.getUserType()) // 用户信息 + .setMerchantOrderId(walletRechargeId) + .setMerchantRefundId(refundId) + .setReason("想退钱").setPrice(walletRecharge.getPayPrice())); + + // 4. 更新充值记录退款单号 + walletRechargeMapper.updateById(new PayWalletRechargeDO().setPayRefundId(payRefundId) + .setRefundStatus(WAITING.getStatus()).setId(walletRecharge.getId())); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateWalletRechargeRefunded(Long id, Long refundId, Long payRefundId) { + // 1.1 获取钱包充值记录 + // 说明:因为 id 和 refundId 是相同的,所以直接使用 id 查询即可! + PayWalletRechargeDO walletRecharge = walletRechargeMapper.selectById(id); + if (walletRecharge == null) { + log.error("[updateWalletRechargerPaid][钱包充值记录不存在,钱包充值记录 id({})]", id); + throw exception(WALLET_RECHARGE_NOT_FOUND); + } + // 1.2 校验钱包充值是否可以更新已退款 + PayRefundRespDTO payRefund = validateWalletRechargeCanRefunded(walletRecharge, payRefundId); + + // 2. 处理退款结果 + PayWalletRechargeDO updateObj = new PayWalletRechargeDO().setId(id); + // 情况一:退款成功 + if (PayRefundStatusEnum.isSuccess(payRefund.getStatus())) { + // 2.1 更新钱包余额 + payWalletService.reduceWalletBalance(walletRecharge.getWalletId(), id, + PayWalletBizTypeEnum.RECHARGE_REFUND, walletRecharge.getTotalPrice()); + + updateObj.setRefundStatus(SUCCESS.getStatus()).setRefundTime(payRefund.getSuccessTime()) + .setRefundTotalPrice(walletRecharge.getTotalPrice()).setRefundPayPrice(walletRecharge.getPayPrice()) + .setRefundBonusPrice(walletRecharge.getBonusPrice()); + // 情况二:退款失败 + } else if (PayRefundStatusEnum.isFailure(payRefund.getStatus())) { + // 2.2 解冻余额 + payWalletService.unfreezePrice(walletRecharge.getWalletId(), walletRecharge.getTotalPrice()); + + updateObj.setRefundStatus(FAILURE.getStatus()); + } + + // 3. 更新钱包充值的退款字段 + walletRechargeMapper.updateByIdAndRefunded(id, WAITING.getStatus(), updateObj); + } + + private PayRefundRespDTO validateWalletRechargeCanRefunded(PayWalletRechargeDO walletRecharge, Long payRefundId) { + // 1. 校验退款订单匹配 + if (notEqual(walletRecharge.getPayRefundId(), payRefundId)) { + log.error("[validateWalletRechargeCanRefunded][钱包充值({}) 退款单不匹配({}),请进行处理!钱包充值的数据是:{}]", + walletRecharge.getId(), payRefundId, toJsonString(walletRecharge)); + throw exception(WALLET_RECHARGE_REFUND_FAIL_REFUND_ORDER_ID_ERROR); + } + + // 2.1 校验退款订单 + PayRefundRespDTO payRefund = payRefundApi.getRefund(payRefundId); + if (payRefund == null) { + log.error("[validateWalletRechargeCanRefunded][payRefund({})不存在]", payRefundId); + throw exception(WALLET_RECHARGE_REFUND_FAIL_REFUND_NOT_FOUND); + } + // 2.2 校验退款金额一致 + if (notEqual(payRefund.getRefundPrice(), walletRecharge.getPayPrice())) { + log.error("[validateWalletRechargeCanRefunded][钱包({}) payRefund({}) 退款金额不匹配,请进行处理!钱包数据是:{},payRefund 数据是:{}]", + walletRecharge.getId(), payRefundId, toJsonString(walletRecharge), toJsonString(payRefund)); + throw exception(WALLET_RECHARGE_REFUND_FAIL_REFUND_PRICE_NOT_MATCH); + } + // 2.3 校验退款订单商户订单是否匹配 + if (notEqual(payRefund.getMerchantRefundId(), walletRecharge.getId().toString())) { + log.error("[validateWalletRechargeCanRefunded][钱包({}) 退款单不匹配({}),请进行处理!payRefund 数据是:{}]", + walletRecharge.getId(), payRefundId, toJsonString(payRefund)); + throw exception(WALLET_RECHARGE_REFUND_FAIL_REFUND_ORDER_ID_ERROR); + } + return payRefund; + } + + private PayWalletDO validateWalletRechargeCanRefund(PayWalletRechargeDO walletRecharge) { + // 校验充值订单是否支付 + if (!walletRecharge.getPayStatus()) { + throw exception(WALLET_RECHARGE_REFUND_FAIL_NOT_PAID); + } + // 校验充值订单是否已退款 + if (walletRecharge.getPayRefundId() != null) { + throw exception(WALLET_RECHARGE_REFUND_FAIL_REFUNDED); + } + // 校验钱包余额是否足够 + PayWalletDO wallet = payWalletService.getWallet(walletRecharge.getWalletId()); + Assert.notNull(wallet, "用户钱包({}) 不存在", wallet.getId()); + if (wallet.getBalance() < walletRecharge.getTotalPrice()) { + throw exception(WALLET_RECHARGE_REFUND_BALANCE_NOT_ENOUGH); + } + // TODO @芋艿:需要考虑下,赠送的金额,会不会导致提现超过; + return wallet; + } + + /** + * 校验支付订单的合法性 + * + * @param recharge 充值订单 + * @param payOrderId 支付订单编号 + * @return 支付订单 + */ + private PayOrderDO validatePayOrderPaid(PayWalletRechargeDO recharge, Long payOrderId) { + // 1. 校验支付单是否存在 + PayOrderDO payOrder = payOrderService.getOrder(payOrderId); + if (payOrder == null) { + log.error("[validatePayOrderPaid][充值订单({}) payOrder({}) 不存在,请进行处理!]", + recharge.getId(), payOrderId); + throw exception(PAY_ORDER_NOT_FOUND); + } + + // 2.1 校验支付单已支付 + if (!PayOrderStatusEnum.isSuccess(payOrder.getStatus())) { + log.error("[validatePayOrderPaid][充值订单({}) payOrder({}) 未支付,请进行处理!payOrder 数据是:{}]", + recharge.getId(), payOrderId, toJsonString(payOrder)); + throw exception(WALLET_RECHARGE_UPDATE_PAID_PAY_ORDER_STATUS_NOT_SUCCESS); + } + // 2.2 校验支付金额一致 + if (notEqual(payOrder.getPrice(), recharge.getPayPrice())) { + log.error("[validatePayOrderPaid][充值订单({}) payOrder({}) 支付金额不匹配,请进行处理!钱包 数据是:{},payOrder 数据是:{}]", + recharge.getId(), payOrderId, toJsonString(recharge), toJsonString(payOrder)); + throw exception(WALLET_RECHARGE_UPDATE_PAID_PAY_PRICE_NOT_MATCH); + } + // 2.3 校验支付订单的商户订单匹配 + if (notEqual(payOrder.getMerchantOrderId(), recharge.getId().toString())) { + log.error("[validatePayOrderPaid][充值订单({}) 支付单不匹配({}),请进行处理!payOrder 数据是:{}]", + recharge.getId(), payOrderId, toJsonString(payOrder)); + throw exception(WALLET_RECHARGE_UPDATE_PAID_PAY_ORDER_ID_ERROR); + } + return payOrder; + } + + /** + * 获得自身的代理对象,解决 AOP 生效问题 + * + * @return 自己 + */ + private PayWalletRechargeServiceImpl getSelf() { + return SpringUtil.getBean(getClass()); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/wallet/PayWalletService.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/wallet/PayWalletService.java new file mode 100644 index 0000000..05d06ff --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/wallet/PayWalletService.java @@ -0,0 +1,99 @@ +package cn.aagro.pp.module.pay.service.wallet; + +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.pay.controller.admin.wallet.vo.wallet.PayWalletPageReqVO; +import cn.aagro.pp.module.pay.dal.dataobject.wallet.PayWalletDO; +import cn.aagro.pp.module.pay.dal.dataobject.wallet.PayWalletTransactionDO; +import cn.aagro.pp.module.pay.enums.wallet.PayWalletBizTypeEnum; + +/** + * 钱包 Service 接口 + * + * @author jason + */ +public interface PayWalletService { + + /** + * 获取钱包信息 + *

+ * 如果不存在,则创建钱包。由于用户注册时候不会创建钱包 + * + * @param userId 用户编号 + * @param userType 用户类型 + */ + PayWalletDO getOrCreateWallet(Long userId, Integer userType); + + /** + * 获取钱包信息 + * + * @param walletId 钱包 id + */ + PayWalletDO getWallet(Long walletId); + + /** + * 获得会员钱包分页 + * + * @param pageReqVO 分页查询 + * @return 会员钱包分页 + */ + PageResult getWalletPage(PayWalletPageReqVO pageReqVO); + + /** + * 钱包订单支付 + * + * @param walletId 钱包编号 + * @param outTradeNo 外部订单号 + * @param price 金额 + */ + PayWalletTransactionDO orderPay(Long walletId, String outTradeNo, Integer price); + + /** + * 钱包订单支付退款 + * + * @param outRefundNo 外部退款号 + * @param refundPrice 退款金额 + * @param reason 退款原因 + */ + PayWalletTransactionDO orderRefund(String outRefundNo, Integer refundPrice, String reason); + + /** + * 扣减钱包余额 + * + * @param walletId 钱包编号 + * @param bizId 业务关联编号 + * @param bizType 业务关联分类 + * @param price 扣减金额 + * @return 钱包流水 + */ + PayWalletTransactionDO reduceWalletBalance(Long walletId, Long bizId, + PayWalletBizTypeEnum bizType, Integer price); + + /** + * 增加钱包余额 + * + * @param walletId 钱包编号 + * @param bizId 业务关联编号 + * @param bizType 业务关联分类 + * @param price 增加金额 + * @return 钱包流水 + */ + PayWalletTransactionDO addWalletBalance(Long walletId, String bizId, + PayWalletBizTypeEnum bizType, Integer price); + + /** + * 冻结钱包部分余额 + * + * @param id 钱包编号 + * @param price 冻结金额 + */ + void freezePrice(Long id, Integer price); + + /** + * 解冻钱包余额 + * + * @param id 钱包编号 + * @param price 解冻金额 + */ + void unfreezePrice(Long id, Integer price); + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/wallet/PayWalletServiceImpl.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/wallet/PayWalletServiceImpl.java new file mode 100644 index 0000000..8d8bd45 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/wallet/PayWalletServiceImpl.java @@ -0,0 +1,233 @@ +package cn.aagro.pp.module.pay.service.wallet; + +import cn.hutool.core.lang.Assert; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.common.util.date.DateUtils; +import cn.aagro.pp.module.pay.controller.admin.wallet.vo.wallet.PayWalletPageReqVO; +import cn.aagro.pp.module.pay.dal.dataobject.order.PayOrderExtensionDO; +import cn.aagro.pp.module.pay.dal.dataobject.refund.PayRefundDO; +import cn.aagro.pp.module.pay.dal.dataobject.wallet.PayWalletDO; +import cn.aagro.pp.module.pay.dal.dataobject.wallet.PayWalletTransactionDO; +import cn.aagro.pp.module.pay.dal.mysql.wallet.PayWalletMapper; +import cn.aagro.pp.module.pay.dal.redis.wallet.PayWalletLockRedisDAO; +import cn.aagro.pp.module.pay.enums.wallet.PayWalletBizTypeEnum; +import cn.aagro.pp.module.pay.service.order.PayOrderService; +import cn.aagro.pp.module.pay.service.refund.PayRefundService; +import cn.aagro.pp.module.pay.service.wallet.bo.WalletTransactionCreateReqBO; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.time.LocalDateTime; + +import static cn.aagro.pp.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.aagro.pp.module.pay.enums.ErrorCodeConstants.*; +import static cn.aagro.pp.module.pay.enums.wallet.PayWalletBizTypeEnum.PAYMENT; +import static cn.aagro.pp.module.pay.enums.wallet.PayWalletBizTypeEnum.PAYMENT_REFUND; + +/** + * 钱包 Service 实现类 + * + * @author jason + */ +@Service +@Slf4j +public class PayWalletServiceImpl implements PayWalletService { + + /** + * 通知超时时间,单位:毫秒 + */ + public static final long UPDATE_TIMEOUT_MILLIS = 120 * DateUtils.SECOND_MILLIS; + + @Resource + private PayWalletMapper walletMapper; + @Resource + private PayWalletLockRedisDAO lockRedisDAO; + + @Resource + @Lazy // 延迟加载,避免循环依赖 + private PayWalletTransactionService walletTransactionService; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private PayOrderService orderService; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private PayRefundService refundService; + + @Override + public PayWalletDO getOrCreateWallet(Long userId, Integer userType) { + PayWalletDO wallet = walletMapper.selectByUserIdAndType(userId, userType); + if (wallet == null) { + wallet = new PayWalletDO().setUserId(userId).setUserType(userType) + .setBalance(0).setTotalExpense(0).setTotalRecharge(0); + wallet.setCreateTime(LocalDateTime.now()); + walletMapper.insert(wallet); + } + return wallet; + } + + @Override + public PayWalletDO getWallet(Long walletId) { + return walletMapper.selectById(walletId); + } + + @Override + public PageResult getWalletPage(PayWalletPageReqVO pageReqVO) { + return walletMapper.selectPage(pageReqVO); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public PayWalletTransactionDO orderPay(Long walletId, String outTradeNo, Integer price) { + // 1. 判断支付交易拓展单是否存 + PayOrderExtensionDO orderExtension = orderService.getOrderExtensionByNo(outTradeNo); + if (orderExtension == null) { + throw exception(PAY_ORDER_EXTENSION_NOT_FOUND); + } + PayWalletDO wallet = walletMapper.selectById(walletId); + // 2. 扣减余额 + return reduceWalletBalance(wallet.getId(), orderExtension.getOrderId(), PAYMENT, price); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public PayWalletTransactionDO orderRefund(String outRefundNo, Integer refundPrice, String reason) { + // 1.1 判断退款单是否存在 + PayRefundDO payRefund = refundService.getRefundByNo(outRefundNo); + if (payRefund == null) { + throw exception(REFUND_NOT_FOUND); + } + // 1.2 校验是否可以退款 + Long walletId = validateWalletCanRefund(payRefund.getId(), payRefund.getChannelOrderNo()); + PayWalletDO wallet = walletMapper.selectById(walletId); + Assert.notNull(wallet, "钱包 {} 不存在", walletId); + + // 2. 增加余额 + return addWalletBalance(walletId, String.valueOf(payRefund.getId()), PAYMENT_REFUND, refundPrice); + } + + /** + * 校验是否能退款 + * + * @param refundId 支付退款单 id + * @param walletPayNo 钱包支付 no + */ + private Long validateWalletCanRefund(Long refundId, String walletPayNo) { + // 1. 校验钱包支付交易存在 + PayWalletTransactionDO walletTransaction = walletTransactionService.getWalletTransactionByNo(walletPayNo); + if (walletTransaction == null) { + throw exception(WALLET_TRANSACTION_NOT_FOUND); + } + // 2. 校验退款是否存在 + PayWalletTransactionDO refundTransaction = walletTransactionService.getWalletTransaction( + String.valueOf(refundId), PAYMENT_REFUND); + if (refundTransaction != null) { + throw exception(WALLET_REFUND_EXIST); + } + return walletTransaction.getWalletId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @SneakyThrows + public PayWalletTransactionDO reduceWalletBalance(Long walletId, Long bizId, + PayWalletBizTypeEnum bizType, Integer price) { + // 1. 获取钱包 + PayWalletDO payWallet = getWallet(walletId); + if (payWallet == null) { + log.error("[reduceWalletBalance][用户钱包({})不存在]", walletId); + throw exception(WALLET_NOT_FOUND); + } + + // 2. 加锁,更新钱包余额(目的:避免钱包流水的并发更新时,余额变化不连贯) + return lockRedisDAO.lock(walletId, UPDATE_TIMEOUT_MILLIS, () -> { + // 2. 扣除余额 + int updateCounts; + switch (bizType) { + case PAYMENT: { + updateCounts = walletMapper.updateWhenConsumption(payWallet.getId(), price); + break; + } + case RECHARGE_REFUND: { + updateCounts = walletMapper.updateWhenRechargeRefund(payWallet.getId(), price); + break; + } + default: { + // TODO 其它类型待实现 + throw new UnsupportedOperationException("待实现"); + } + } + if (updateCounts == 0) { + throw exception(WALLET_BALANCE_NOT_ENOUGH); + } + + // 3. 生成钱包流水 + Integer afterBalance = payWallet.getBalance() - price; + WalletTransactionCreateReqBO bo = new WalletTransactionCreateReqBO().setWalletId(payWallet.getId()) + .setPrice(-price).setBalance(afterBalance).setBizId(String.valueOf(bizId)) + .setBizType(bizType.getType()).setTitle(bizType.getDescription()); + return walletTransactionService.createWalletTransaction(bo); + }); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @SneakyThrows + public PayWalletTransactionDO addWalletBalance(Long walletId, String bizId, + PayWalletBizTypeEnum bizType, Integer price) { + // 1. 获取钱包 + PayWalletDO payWallet = getWallet(walletId); + if (payWallet == null) { + log.error("[addWalletBalance][用户钱包({})不存在]", walletId); + throw exception(WALLET_NOT_FOUND); + } + + // 2. 加锁,更新钱包余额(目的:避免钱包流水的并发更新时,余额变化不连贯) + return lockRedisDAO.lock(walletId, UPDATE_TIMEOUT_MILLIS, () -> { + // 3. 更新钱包金额 + switch (bizType) { + case PAYMENT_REFUND: { // 退款更新 + walletMapper.updateWhenConsumptionRefund(payWallet.getId(), price); + break; + } + case RECHARGE: { // 充值更新 + walletMapper.updateWhenRecharge(payWallet.getId(), price); + break; + } + case UPDATE_BALANCE: // 更新余额 + case TRANSFER: // 分佣提现 + walletMapper.updateWhenAdd(payWallet.getId(), price); + break; + default: { + throw new UnsupportedOperationException("待实现:" + bizType); + } + } + + // 4. 生成钱包流水 + WalletTransactionCreateReqBO transactionCreateReqBO = new WalletTransactionCreateReqBO() + .setWalletId(payWallet.getId()).setPrice(price).setBalance(payWallet.getBalance() + price) + .setBizId(bizId).setBizType(bizType.getType()).setTitle(bizType.getDescription()); + return walletTransactionService.createWalletTransaction(transactionCreateReqBO); + }); + } + + @Override + public void freezePrice(Long id, Integer price) { + int updateCounts = walletMapper.freezePrice(id, price); + if (updateCounts == 0) { + throw exception(WALLET_BALANCE_NOT_ENOUGH); + } + } + + @Override + public void unfreezePrice(Long id, Integer price) { + int updateCounts = walletMapper.unFreezePrice(id, price); + if (updateCounts == 0) { + throw exception(WALLET_FREEZE_PRICE_NOT_ENOUGH); + } + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/wallet/PayWalletTransactionService.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/wallet/PayWalletTransactionService.java new file mode 100644 index 0000000..e838e33 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/wallet/PayWalletTransactionService.java @@ -0,0 +1,74 @@ +package cn.aagro.pp.module.pay.service.wallet; + +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.pay.controller.admin.wallet.vo.transaction.PayWalletTransactionPageReqVO; +import cn.aagro.pp.module.pay.controller.app.wallet.vo.transaction.AppPayWalletTransactionPageReqVO; +import cn.aagro.pp.module.pay.controller.app.wallet.vo.transaction.AppPayWalletTransactionSummaryRespVO; +import cn.aagro.pp.module.pay.dal.dataobject.wallet.PayWalletTransactionDO; +import cn.aagro.pp.module.pay.enums.wallet.PayWalletBizTypeEnum; +import cn.aagro.pp.module.pay.service.wallet.bo.WalletTransactionCreateReqBO; + +import javax.validation.Valid; + +import java.time.LocalDateTime; + +/** + * 钱包余额流水 Service 接口 + * + * @author jason + */ +public interface PayWalletTransactionService { + + /** + * 查询钱包余额流水分页 + * + * @param userId 用户编号 + * @param userType 用户类型 + * @param pageVO 分页查询参数 + */ + PageResult getWalletTransactionPage(Long userId, Integer userType, + AppPayWalletTransactionPageReqVO pageVO); + + /** + * 查询钱包余额流水分页 + * + * @param pageVO 分页查询参数 + */ + PageResult getWalletTransactionPage(PayWalletTransactionPageReqVO pageVO); + + /** + * 新增钱包余额流水 + * + * @param bo 创建钱包流水 bo + * @return 新建的钱包 do + */ + PayWalletTransactionDO createWalletTransaction(@Valid WalletTransactionCreateReqBO bo); + + /** + * 根据 no,获取钱包余流水 + * + * @param no 流水号 + */ + PayWalletTransactionDO getWalletTransactionByNo(String no); + + /** + * 获取钱包流水 + * + * @param bizId 业务编号 + * @param type 业务类型 + * @return 钱包流水 + */ + PayWalletTransactionDO getWalletTransaction(String bizId, PayWalletBizTypeEnum type); + + /** + * 获得钱包流水统计 + * + * @param userId 用户编号 + * @param userType 用户类型 + * @param createTime 时间段 + * @return 钱包流水统计 + */ + AppPayWalletTransactionSummaryRespVO getWalletTransactionSummary(Long userId, Integer userType, + LocalDateTime[] createTime); + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/wallet/PayWalletTransactionServiceImpl.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/wallet/PayWalletTransactionServiceImpl.java new file mode 100644 index 0000000..26bbfe1 --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/wallet/PayWalletTransactionServiceImpl.java @@ -0,0 +1,95 @@ +package cn.aagro.pp.module.pay.service.wallet; + +import cn.hutool.core.util.ObjectUtil; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.pay.controller.admin.wallet.vo.transaction.PayWalletTransactionPageReqVO; +import cn.aagro.pp.module.pay.controller.app.wallet.vo.transaction.AppPayWalletTransactionPageReqVO; +import cn.aagro.pp.module.pay.controller.app.wallet.vo.transaction.AppPayWalletTransactionSummaryRespVO; +import cn.aagro.pp.module.pay.convert.wallet.PayWalletTransactionConvert; +import cn.aagro.pp.module.pay.dal.dataobject.wallet.PayWalletDO; +import cn.aagro.pp.module.pay.dal.dataobject.wallet.PayWalletTransactionDO; +import cn.aagro.pp.module.pay.dal.mysql.wallet.PayWalletTransactionMapper; +import cn.aagro.pp.module.pay.dal.redis.no.PayNoRedisDAO; +import cn.aagro.pp.module.pay.enums.wallet.PayWalletBizTypeEnum; +import cn.aagro.pp.module.pay.service.wallet.bo.WalletTransactionCreateReqBO; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.time.LocalDateTime; + +import static cn.aagro.pp.module.pay.controller.app.wallet.vo.transaction.AppPayWalletTransactionPageReqVO.TYPE_EXPENSE; +import static cn.aagro.pp.module.pay.controller.app.wallet.vo.transaction.AppPayWalletTransactionPageReqVO.TYPE_INCOME; + +/** + * 钱包流水 Service 实现类 + * + * @author jason + */ +@Service +@Slf4j +@Validated +public class PayWalletTransactionServiceImpl implements PayWalletTransactionService { + + /** + * 钱包流水的 no 前缀 + */ + private static final String WALLET_NO_PREFIX = "W"; + + @Resource + private PayWalletService payWalletService; + @Resource + private PayWalletTransactionMapper payWalletTransactionMapper; + @Resource + private PayNoRedisDAO noRedisDAO; + + @Override + public PageResult getWalletTransactionPage(Long userId, Integer userType, + AppPayWalletTransactionPageReqVO pageVO) { + PayWalletDO wallet = payWalletService.getOrCreateWallet(userId, userType); + return payWalletTransactionMapper.selectPage(wallet.getId(), pageVO.getType(), pageVO, pageVO.getCreateTime()); + } + + @Override + public PageResult getWalletTransactionPage(PayWalletTransactionPageReqVO pageVO) { + // 基于 userId + userType 查询钱包 + if (pageVO.getWalletId() == null + && ObjectUtil.isAllNotEmpty(pageVO.getUserId(), pageVO.getUserType())) { + PayWalletDO wallet = payWalletService.getOrCreateWallet(pageVO.getUserId(), pageVO.getUserType()); + if (wallet != null) { + pageVO.setWalletId(wallet.getId()); + } + } + + // 查询分页 + return payWalletTransactionMapper.selectPage(pageVO.getWalletId(), null, pageVO, null); + } + + @Override + public PayWalletTransactionDO createWalletTransaction(WalletTransactionCreateReqBO bo) { + PayWalletTransactionDO transaction = PayWalletTransactionConvert.INSTANCE.convert(bo) + .setNo(noRedisDAO.generate(WALLET_NO_PREFIX)); + payWalletTransactionMapper.insert(transaction); + return transaction; + } + + @Override + public PayWalletTransactionDO getWalletTransactionByNo(String no) { + return payWalletTransactionMapper.selectByNo(no); + } + + @Override + public PayWalletTransactionDO getWalletTransaction(String bizId, PayWalletBizTypeEnum type) { + return payWalletTransactionMapper.selectByBiz(bizId, type.getType()); + } + + @Override + public AppPayWalletTransactionSummaryRespVO getWalletTransactionSummary(Long userId, Integer userType, LocalDateTime[] createTime) { + PayWalletDO wallet = payWalletService.getOrCreateWallet(userId, userType); + return new AppPayWalletTransactionSummaryRespVO() + .setTotalExpense(payWalletTransactionMapper.selectPriceSum(wallet.getId(), TYPE_EXPENSE, createTime)) + .setTotalIncome(payWalletTransactionMapper.selectPriceSum(wallet.getId(), TYPE_INCOME, createTime)); + } + +} diff --git a/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/wallet/bo/WalletTransactionCreateReqBO.java b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/wallet/bo/WalletTransactionCreateReqBO.java new file mode 100644 index 0000000..db40b9b --- /dev/null +++ b/aagro-module-pay/src/main/java/cn/aagro/pp/module/pay/service/wallet/bo/WalletTransactionCreateReqBO.java @@ -0,0 +1,59 @@ +package cn.aagro.pp.module.pay.service.wallet.bo; + +import cn.aagro.pp.framework.common.validation.InEnum; +import cn.aagro.pp.module.pay.enums.wallet.PayWalletBizTypeEnum; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + * 创建钱包流水 BO + * + * @author jason + */ +@Data +public class WalletTransactionCreateReqBO { + + /** + * 钱包编号 + * + */ + @NotNull(message = "钱包编号不能为空") + private Long walletId; + + /** + * 交易金额,单位分 + * + * 正值表示余额增加,负值表示余额减少 + */ + @NotNull(message = "交易金额不能为空") + private Integer price; + + /** + * 交易后余额,单位分 + */ + @NotNull(message = "交易后余额不能为空") + private Integer balance; + + /** + * 关联业务分类 + * + * 枚举 {@link PayWalletBizTypeEnum#getType()} + */ + @NotNull(message = "关联业务分类不能为空") + @InEnum(PayWalletBizTypeEnum.class) + private Integer bizType; + + /** + * 关联业务编号 + */ + @NotEmpty(message = "关联业务编号不能为空") + private String bizId; + + /** + * 流水说明 + */ + @NotEmpty(message = "流水说明不能为空") + private String title; +} diff --git a/aagro-module-pay/src/test/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/PayClientFactoryImplIntegrationTest.java b/aagro-module-pay/src/test/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/PayClientFactoryImplIntegrationTest.java new file mode 100644 index 0000000..611f363 --- /dev/null +++ b/aagro-module-pay/src/test/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/PayClientFactoryImplIntegrationTest.java @@ -0,0 +1,134 @@ +package cn.aagro.pp.module.pay.framework.pay.core.client.impl; + +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.RandomUtil; +import cn.aagro.pp.module.pay.enums.PayChannelEnum; +import cn.aagro.pp.module.pay.framework.pay.core.client.PayClient; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.impl.PayClientFactoryImpl; +import cn.aagro.pp.module.pay.framework.pay.core.client.impl.alipay.AlipayPayClientConfig; +import cn.aagro.pp.module.pay.framework.pay.core.client.impl.alipay.AlipayQrPayClient; +import cn.aagro.pp.module.pay.framework.pay.core.client.impl.alipay.AlipayWapPayClient; +import cn.aagro.pp.module.pay.framework.pay.core.client.impl.weixin.WxPayClientConfig; +import cn.aagro.pp.module.pay.framework.pay.core.client.impl.weixin.WxPubPayClient; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; + +/** + * {@link PayClientFactoryImpl} 的集成测试 + * + * @author 芋道源码 + */ +@Disabled +public class PayClientFactoryImplIntegrationTest { + + private static final String SERVER_URL_SANDBOX = "https://openapi.alipaydev.com/gateway.do"; + + private final PayClientFactoryImpl payClientFactory = new PayClientFactoryImpl(); + + /** + * {@link WxPubPayClient} 的 V2 版本 + */ + @Test + public void testCreatePayClient_WX_PUB_V2() { + // 创建配置 + WxPayClientConfig config = new WxPayClientConfig(); + config.setAppId("wx041349c6f39b268b"); + config.setMchId("1545083881"); + config.setApiVersion(WxPayClientConfig.API_VERSION_V2); + config.setMchKey("0alL64UDQdlCwiKZ73ib7ypaIjMns06p"); + // 创建客户端 + Long channelId = RandomUtil.randomLong(); + payClientFactory.createOrUpdatePayClient(channelId, PayChannelEnum.WX_PUB.getCode(), config); + PayClient client = payClientFactory.getPayClient(channelId); + // 发起支付 + PayOrderUnifiedReqDTO reqDTO = buildPayOrderUnifiedReqDTO(); +// CommonResult result = client.unifiedOrder(reqDTO); +// System.out.println(result); + } + + /** + * {@link WxPubPayClient} 的 V3 版本 + */ + @Test + public void testCreatePayClient_WX_PUB_V3() throws FileNotFoundException { + // 创建配置 + WxPayClientConfig config = new WxPayClientConfig(); + config.setAppId("wx041349c6f39b268b"); + config.setMchId("1545083881"); + config.setApiVersion(WxPayClientConfig.API_VERSION_V3); + config.setPrivateKeyContent(IoUtil.readUtf8(new FileInputStream("/Users/yunai/Downloads/wx_pay/apiclient_key.pem"))); +// config.setPrivateCertContent(IoUtil.readUtf8(new FileInputStream("/Users/yunai/Downloads/wx_pay/apiclient_cert.pem"))); + config.setApiV3Key("joerVi8y5DJ3o4ttA0o1uH47Xz1u2Ase"); + // 创建客户端 + Long channelId = RandomUtil.randomLong(); + payClientFactory.createOrUpdatePayClient(channelId, PayChannelEnum.WX_PUB.getCode(), config); + PayClient client = payClientFactory.getPayClient(channelId); + // 发起支付 + PayOrderUnifiedReqDTO reqDTO = buildPayOrderUnifiedReqDTO(); +// CommonResult result = client.unifiedOrder(reqDTO); +// System.out.println(result); + } + + /** + * {@link AlipayQrPayClient} + */ + @Test + @SuppressWarnings("unchecked") + public void testCreatePayClient_ALIPAY_QR() { + // 创建配置 + AlipayPayClientConfig config = new AlipayPayClientConfig(); + config.setAppId("2021000118634035"); + config.setServerUrl(SERVER_URL_SANDBOX); + config.setSignType(AlipayPayClientConfig.SIGN_TYPE_DEFAULT); + config.setPrivateKey("MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCHsEV1cDupwJv890x84qbppUtRIfhaKSwSVN0thCcsDCaAsGR5MZslDkO8NCT9V4r2SVXjyY7eJUZlZd1M0C8T01Tg4UOx5LUbic0O3A1uJMy6V1n9IyYwbAW3AEZhBd5bSbPgrqvmv3NeWSTQT6Anxnllf+2iDH6zyA2fPl7cYyQtbZoDJQFGqr4F+cGh2R6akzRKNoBkAeMYwoY6es2lX8sJxCVPWUmxNUoL3tScwlSpd7Bxw0q9c/X01jMwuQ0+Va358zgFiGERTE6yD01eu40OBDXOYO3z++y+TAYHlQQ2toMO63trepo88X3xV3R44/1DH+k2pAm2IF5ixiLrAgMBAAECggEAPx3SoXcseaD7rmcGcE0p4SMfbsUDdkUSmBBbtfF0GzwnqNLkWa+mgE0rWt9SmXngTQH97vByAYmLPl1s3G82ht1V7Sk7yQMe74lhFllr8eEyTjeVx3dTK1EEM4TwN+936DTXdFsr4TELJEcJJdD0KaxcCcfBLRDs2wnitEFZ9N+GoZybVmY8w0e0MI7PLObUZ2l0X4RurQnfG9ZxjXjC7PkeMVv7cGGylpNFi3BbvkRhdhLPDC2E6wqnr9e7zk+hiENivAezXrtxtwKovzCtnWJ1r0IO14Rh47H509Ic0wFnj+o5YyUL4LdmpL7yaaH6fM7zcSLFjNZPHvZCKPwYcQKBgQDQFho98QvnL8ex4v6cry4VitGpjSXm1qP3vmMQk4rTsn8iPWtcxPjqGEqOQJjdi4Mi0VZKQOLFwlH0kl95wNrD/isJ4O1yeYfX7YAXApzHqYNINzM79HemO3Yx1qLMW3okRFJ9pPRzbQ9qkTpsaegsmyX316zOBhzGRYjKbutTYwKBgQCm7phr9XdFW5Vh+XR90mVs483nrLmMiDKg7YKxSLJ8amiDjzPejCn7i95Hah08P+2MIZLIPbh2VLacczR6ltRRzN5bg5etFuqSgfkuHyxpoDmpjbe08+Q2h8JBYqcC5Nhv1AKU4iOUhVLHo/FBAQliMcGc/J3eiYTFC7EsNx382QKBgClb20doe7cttgFTXswBvaUmfFm45kmla924B7SpvrQpDD/f+VDtDZRp05fGmxuduSjYdtA3aVtpLiTwWu22OUUvZZqHDGruYOO4Hvdz23mL5b4ayqImCwoNU4bAZIc9v18p/UNf3/55NNE3oGcf/bev9rH2OjCQ4nM+Ktwhg8CFAoGACSgvbkShzUkv0ZcIf9ppu+ZnJh1AdGgINvGwaJ8vQ0nm/8h8NOoFZ4oNoGc+wU5Ubops7dUM6FjPR5e+OjdJ4E7Xp7d5O4J1TaIZlCEbo5OpdhaTDDcQvrkFu+Z4eN0qzj+YAKjDAOOrXc4tbr5q0FsgXscwtcNfaBuzFVTUrUkCgYEAwzPnMNhWG3zOWLUs2QFA2GP4Y+J8cpUYfj6pbKKzeLwyG9qBwF1NJpN8m+q9q7V9P2LY+9Lp9e1mGsGeqt5HMEA3P6vIpcqLJLqE/4PBLLRzfccTcmqb1m71+erxTRhHBRkGS+I7dZEb3olQfnS1Y1tpMBxiwYwR3LW4oXuJwj8="); + config.setAlipayPublicKey("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnq90KnF4dTnlzzmxpujbI05OYqi5WxAS6cL0gnZFv2gK51HExF8v/BaP7P979PhFMgWTqmOOI+Dtno5s+yD09XTY1WkshbLk6i4g2Xlr8fyW9ODnkU88RI2w9UdPhQU4cPPwBNlrsYhKkVK2OxwM3kFqjoBBY0CZoZCsSQ3LDH5WeZqPArlsS6xa2zqJBuuoKjMrdpELl3eXSjP8K54eDJCbeetCZNKWLL3DPahTPB7LZikfYmslb0QUvCgGapD0xkS7eVq70NaL1G57MWABs4tbfWgxike4Daj3EfUrzIVspQxj7w8HEj9WozJPgL88kSJSits0pqD3n5r8HSuseQIDAQAB"); + // 创建客户端 + Long channelId = RandomUtil.randomLong(); + payClientFactory.createOrUpdatePayClient(channelId, PayChannelEnum.ALIPAY_QR.getCode(), config); + PayClient client = payClientFactory.getPayClient(channelId); + // 发起支付 + PayOrderUnifiedReqDTO reqDTO = buildPayOrderUnifiedReqDTO(); + reqDTO.setNotifyUrl("http://yunai.natapp1.cc/admin-api/pay/notify/callback/18"); // TODO @tina: 这里改成你的 natapp 回调地址 +// CommonResult result = (CommonResult) client.unifiedOrder(reqDTO); +// System.out.println(JsonUtils.toJsonString(result)); +// System.out.println(result.getData().getQrCode()); + } + + /** + * {@link AlipayWapPayClient} + */ + @Test + public void testCreatePayClient_ALIPAY_WAP() { + // 创建配置 + AlipayPayClientConfig config = new AlipayPayClientConfig(); + config.setAppId("2021000118634035"); + config.setServerUrl(SERVER_URL_SANDBOX); + config.setSignType(AlipayPayClientConfig.SIGN_TYPE_DEFAULT); + config.setPrivateKey("MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCHsEV1cDupwJv890x84qbppUtRIfhaKSwSVN0thCcsDCaAsGR5MZslDkO8NCT9V4r2SVXjyY7eJUZlZd1M0C8T01Tg4UOx5LUbic0O3A1uJMy6V1n9IyYwbAW3AEZhBd5bSbPgrqvmv3NeWSTQT6Anxnllf+2iDH6zyA2fPl7cYyQtbZoDJQFGqr4F+cGh2R6akzRKNoBkAeMYwoY6es2lX8sJxCVPWUmxNUoL3tScwlSpd7Bxw0q9c/X01jMwuQ0+Va358zgFiGERTE6yD01eu40OBDXOYO3z++y+TAYHlQQ2toMO63trepo88X3xV3R44/1DH+k2pAm2IF5ixiLrAgMBAAECggEAPx3SoXcseaD7rmcGcE0p4SMfbsUDdkUSmBBbtfF0GzwnqNLkWa+mgE0rWt9SmXngTQH97vByAYmLPl1s3G82ht1V7Sk7yQMe74lhFllr8eEyTjeVx3dTK1EEM4TwN+936DTXdFsr4TELJEcJJdD0KaxcCcfBLRDs2wnitEFZ9N+GoZybVmY8w0e0MI7PLObUZ2l0X4RurQnfG9ZxjXjC7PkeMVv7cGGylpNFi3BbvkRhdhLPDC2E6wqnr9e7zk+hiENivAezXrtxtwKovzCtnWJ1r0IO14Rh47H509Ic0wFnj+o5YyUL4LdmpL7yaaH6fM7zcSLFjNZPHvZCKPwYcQKBgQDQFho98QvnL8ex4v6cry4VitGpjSXm1qP3vmMQk4rTsn8iPWtcxPjqGEqOQJjdi4Mi0VZKQOLFwlH0kl95wNrD/isJ4O1yeYfX7YAXApzHqYNINzM79HemO3Yx1qLMW3okRFJ9pPRzbQ9qkTpsaegsmyX316zOBhzGRYjKbutTYwKBgQCm7phr9XdFW5Vh+XR90mVs483nrLmMiDKg7YKxSLJ8amiDjzPejCn7i95Hah08P+2MIZLIPbh2VLacczR6ltRRzN5bg5etFuqSgfkuHyxpoDmpjbe08+Q2h8JBYqcC5Nhv1AKU4iOUhVLHo/FBAQliMcGc/J3eiYTFC7EsNx382QKBgClb20doe7cttgFTXswBvaUmfFm45kmla924B7SpvrQpDD/f+VDtDZRp05fGmxuduSjYdtA3aVtpLiTwWu22OUUvZZqHDGruYOO4Hvdz23mL5b4ayqImCwoNU4bAZIc9v18p/UNf3/55NNE3oGcf/bev9rH2OjCQ4nM+Ktwhg8CFAoGACSgvbkShzUkv0ZcIf9ppu+ZnJh1AdGgINvGwaJ8vQ0nm/8h8NOoFZ4oNoGc+wU5Ubops7dUM6FjPR5e+OjdJ4E7Xp7d5O4J1TaIZlCEbo5OpdhaTDDcQvrkFu+Z4eN0qzj+YAKjDAOOrXc4tbr5q0FsgXscwtcNfaBuzFVTUrUkCgYEAwzPnMNhWG3zOWLUs2QFA2GP4Y+J8cpUYfj6pbKKzeLwyG9qBwF1NJpN8m+q9q7V9P2LY+9Lp9e1mGsGeqt5HMEA3P6vIpcqLJLqE/4PBLLRzfccTcmqb1m71+erxTRhHBRkGS+I7dZEb3olQfnS1Y1tpMBxiwYwR3LW4oXuJwj8="); + config.setAlipayPublicKey("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnq90KnF4dTnlzzmxpujbI05OYqi5WxAS6cL0gnZFv2gK51HExF8v/BaP7P979PhFMgWTqmOOI+Dtno5s+yD09XTY1WkshbLk6i4g2Xlr8fyW9ODnkU88RI2w9UdPhQU4cPPwBNlrsYhKkVK2OxwM3kFqjoBBY0CZoZCsSQ3LDH5WeZqPArlsS6xa2zqJBuuoKjMrdpELl3eXSjP8K54eDJCbeetCZNKWLL3DPahTPB7LZikfYmslb0QUvCgGapD0xkS7eVq70NaL1G57MWABs4tbfWgxike4Daj3EfUrzIVspQxj7w8HEj9WozJPgL88kSJSits0pqD3n5r8HSuseQIDAQAB"); + // 创建客户端 + Long channelId = RandomUtil.randomLong(); + payClientFactory.createOrUpdatePayClient(channelId, PayChannelEnum.ALIPAY_WAP.getCode(), config); + PayClient client = payClientFactory.getPayClient(channelId); + // 发起支付 + PayOrderUnifiedReqDTO reqDTO = buildPayOrderUnifiedReqDTO(); +// CommonResult result = client.unifiedOrder(reqDTO); +// System.out.println(JsonUtils.toJsonString(result)); + } + + private static PayOrderUnifiedReqDTO buildPayOrderUnifiedReqDTO() { + PayOrderUnifiedReqDTO reqDTO = new PayOrderUnifiedReqDTO(); + reqDTO.setPrice(123); + reqDTO.setSubject("IPhone 13"); + reqDTO.setBody("biubiubiu"); + reqDTO.setOutTradeNo(String.valueOf(System.currentTimeMillis())); + reqDTO.setUserIp("127.0.0.1"); + reqDTO.setNotifyUrl("http://127.0.0.1:8080"); + return reqDTO; + } + +} diff --git a/aagro-module-pay/src/test/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/alipay/AbstractAlipayClientTest.java b/aagro-module-pay/src/test/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/alipay/AbstractAlipayClientTest.java new file mode 100644 index 0000000..2e7a852 --- /dev/null +++ b/aagro-module-pay/src/test/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/alipay/AbstractAlipayClientTest.java @@ -0,0 +1,221 @@ +package cn.aagro.pp.module.pay.framework.pay.core.client.impl.alipay; + +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.util.RandomUtil; +import cn.aagro.pp.framework.common.exception.ServiceException; +import cn.aagro.pp.framework.common.exception.enums.GlobalErrorCodeConstants; +import cn.aagro.pp.framework.common.exception.util.ServiceExceptionUtil; +import cn.aagro.pp.framework.test.core.ut.BaseMockitoUnitTest; +import cn.aagro.pp.module.pay.enums.refund.PayRefundStatusEnum; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.refund.PayRefundRespDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.exception.PayClientException; +import com.alipay.api.AlipayApiException; +import com.alipay.api.DefaultAlipayClient; +import com.alipay.api.DefaultSigner; +import com.alipay.api.domain.AlipayTradeRefundModel; +import com.alipay.api.request.AlipayTradeRefundRequest; +import com.alipay.api.response.AlipayTradeRefundResponse; +import lombok.Setter; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatcher; +import org.mockito.Mock; + +import javax.validation.ConstraintViolationException; +import java.util.Date; + +import static cn.aagro.pp.framework.test.core.util.RandomUtils.*; +import static cn.aagro.pp.module.pay.framework.pay.core.client.impl.alipay.AlipayPayClientConfig.MODE_PUBLIC_KEY; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.when; + +/** + * 支付宝 Client 的测试基类 + * + * @author jason + */ +public abstract class AbstractAlipayClientTest extends BaseMockitoUnitTest { + + protected AlipayPayClientConfig config = randomPojo(AlipayPayClientConfig.class, o -> { + o.setServerUrl(randomURL()); + o.setPrivateKey(randomString()); + o.setMode(MODE_PUBLIC_KEY); + o.setSignType(AlipayPayClientConfig.SIGN_TYPE_DEFAULT); + o.setAppCertContent(""); + o.setAlipayPublicCertContent(""); + o.setRootCertContent(""); + }); + + @Mock + protected DefaultAlipayClient defaultAlipayClient; + + @Setter + private AbstractAlipayPayClient client; + + /** + * 子类需要实现该方法. 设置 client 的具体实现 + */ + @BeforeEach + public abstract void setUp(); + + @Test + @DisplayName("支付宝 Client 初始化") + public void testDoInit() { + // 调用 + client.doInit(); + // 断言 + DefaultAlipayClient realClient = client.getClient(); + assertNotSame(defaultAlipayClient, realClient); + assertInstanceOf(DefaultSigner.class, realClient.getSigner()); + assertEquals(config.getPrivateKey(), ((DefaultSigner) realClient.getSigner()).getPrivateKey()); + } + + @Test + @DisplayName("支付宝 Client 统一退款:成功") + public void testUnifiedRefund_success() throws AlipayApiException { + // mock 方法 + String notifyUrl = randomURL(); + Date refundTime = randomDate(); + String outRefundNo = randomString(); + String outTradeNo = randomString(); + Integer refundAmount = randomInteger(); + AlipayTradeRefundResponse response = randomPojo(AlipayTradeRefundResponse.class, o -> { + o.setSubCode(""); + o.setGmtRefundPay(refundTime); + }); + when(defaultAlipayClient.execute(argThat((ArgumentMatcher) request -> { + assertInstanceOf(AlipayTradeRefundModel.class, request.getBizModel()); + AlipayTradeRefundModel bizModel = (AlipayTradeRefundModel) request.getBizModel(); + assertEquals(outRefundNo, bizModel.getOutRequestNo()); + assertEquals(outTradeNo, bizModel.getOutTradeNo()); + assertEquals(String.valueOf(refundAmount / 100.0), bizModel.getRefundAmount()); + return true; + }))).thenReturn(response); + // 准备请求参数 + PayRefundUnifiedReqDTO refundReqDTO = randomPojo(PayRefundUnifiedReqDTO.class, o -> { + o.setOutRefundNo(outRefundNo); + o.setOutTradeNo(outTradeNo); + o.setNotifyUrl(notifyUrl); + o.setRefundPrice(refundAmount); + }); + + // 调用 + PayRefundRespDTO resp = client.unifiedRefund(refundReqDTO); + // 断言 + assertEquals(PayRefundStatusEnum.SUCCESS.getStatus(), resp.getStatus()); + assertEquals(outRefundNo, resp.getOutRefundNo()); + assertNull(resp.getChannelRefundNo()); + assertEquals(LocalDateTimeUtil.of(refundTime), resp.getSuccessTime()); + assertSame(response, resp.getRawData()); + assertNull(resp.getChannelErrorCode()); + assertNull(resp.getChannelErrorMsg()); + } + + @Test + @DisplayName("支付宝 Client 统一退款:渠道返回失败") + public void test_unified_refund_channel_failed() throws AlipayApiException { + // mock 方法 + String notifyUrl = randomURL(); + String subCode = randomString(); + String subMsg = randomString(); + AlipayTradeRefundResponse response = randomPojo(AlipayTradeRefundResponse.class, o -> { + o.setSubCode(subCode); + o.setSubMsg(subMsg); + }); + when(defaultAlipayClient.execute(argThat((ArgumentMatcher) request -> { + assertInstanceOf(AlipayTradeRefundModel.class, request.getBizModel()); + return true; + }))).thenReturn(response); + // 准备请求参数 + String outRefundNo = randomString(); + String outTradeNo = randomString(); + PayRefundUnifiedReqDTO refundReqDTO = randomPojo(PayRefundUnifiedReqDTO.class, o -> { + o.setOutRefundNo(outRefundNo); + o.setOutTradeNo(outTradeNo); + o.setNotifyUrl(notifyUrl); + }); + + // 调用 + PayRefundRespDTO resp = client.unifiedRefund(refundReqDTO); + // 断言 + assertEquals(PayRefundStatusEnum.FAILURE.getStatus(), resp.getStatus()); + assertEquals(outRefundNo, resp.getOutRefundNo()); + assertNull(resp.getChannelRefundNo()); + assertNull(resp.getSuccessTime()); + assertSame(response, resp.getRawData()); + assertEquals(subCode, resp.getChannelErrorCode()); + assertEquals(subMsg, resp.getChannelErrorMsg()); + } + + @Test + @DisplayName("支付宝 Client 统一退款:参数校验不通过") + public void testUnifiedRefund_paramInvalidate() { + // 准备请求参数 + String notifyUrl = randomURL(); + PayRefundUnifiedReqDTO refundReqDTO = randomPojo(PayRefundUnifiedReqDTO.class, o -> { + o.setOutTradeNo(""); + o.setNotifyUrl(notifyUrl); + }); + + // 调用,并断言 + assertThrows(ConstraintViolationException.class, () -> client.unifiedRefund(refundReqDTO)); + } + + @Test + @DisplayName("支付宝 Client 统一退款:抛出业务异常") + public void testUnifiedRefund_throwServiceException() throws AlipayApiException { + // mock 方法 + when(defaultAlipayClient.execute(argThat((ArgumentMatcher) request -> true))) + .thenThrow(ServiceExceptionUtil.exception(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR)); + // 准备请求参数 + String notifyUrl = randomURL(); + PayRefundUnifiedReqDTO refundReqDTO = randomPojo(PayRefundUnifiedReqDTO.class, o -> o.setNotifyUrl(notifyUrl)); + + // 调用,并断言 + assertThrows(ServiceException.class, () -> client.unifiedRefund(refundReqDTO)); + } + + @Test + @DisplayName("支付宝 Client 统一退款:抛出系统异常") + public void testUnifiedRefund_throwPayException() throws AlipayApiException { + // mock 方法 + when(defaultAlipayClient.execute(argThat((ArgumentMatcher) request -> true))) + .thenThrow(new RuntimeException("系统异常")); + // 准备请求参数 + String notifyUrl = randomURL(); + PayRefundUnifiedReqDTO refundReqDTO = randomPojo(PayRefundUnifiedReqDTO.class, o -> o.setNotifyUrl(notifyUrl)); + + // 调用,并断言 + assertThrows(PayClientException.class, () -> client.unifiedRefund(refundReqDTO)); + } + + @Test + @DisplayName("支付宝 Client 统一下单:参数校验不通过") + public void testUnifiedOrder_paramInvalidate() { + // 准备请求参数 + String outTradeNo = randomString(); + String notifyUrl = randomURL(); + PayOrderUnifiedReqDTO reqDTO = randomPojo(PayOrderUnifiedReqDTO.class, o -> { + o.setOutTradeNo(outTradeNo); + o.setNotifyUrl(notifyUrl); + }); + + // 调用,并断言 + assertThrows(ConstraintViolationException.class, () -> client.unifiedOrder(reqDTO)); + } + + protected PayOrderUnifiedReqDTO buildOrderUnifiedReqDTO(String notifyUrl, String outTradeNo, Integer price) { + return randomPojo(PayOrderUnifiedReqDTO.class, o -> { + o.setOutTradeNo(outTradeNo); + o.setNotifyUrl(notifyUrl); + o.setPrice(price); + o.setSubject(RandomUtil.randomString(32)); + o.setBody(RandomUtil.randomString(32)); + }); + } + +} diff --git a/aagro-module-pay/src/test/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/alipay/AlipayBarPayClientTest.java b/aagro-module-pay/src/test/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/alipay/AlipayBarPayClientTest.java new file mode 100644 index 0000000..1944a01 --- /dev/null +++ b/aagro-module-pay/src/test/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/alipay/AlipayBarPayClientTest.java @@ -0,0 +1,170 @@ +package cn.aagro.pp.module.pay.framework.pay.core.client.impl.alipay; + +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.aagro.pp.framework.common.exception.ServiceException; +import cn.aagro.pp.module.pay.enums.order.PayOrderStatusEnum; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import cn.aagro.pp.module.pay.framework.pay.core.enums.PayOrderDisplayModeEnum; +import com.alipay.api.AlipayApiException; +import com.alipay.api.domain.AlipayTradePayModel; +import com.alipay.api.request.AlipayTradePayRequest; +import com.alipay.api.response.AlipayTradePayResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatcher; +import org.mockito.InjectMocks; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import static cn.aagro.pp.module.pay.enums.order.PayOrderStatusEnum.CLOSED; +import static cn.aagro.pp.module.pay.enums.order.PayOrderStatusEnum.WAITING; +import static cn.aagro.pp.framework.test.core.util.RandomUtils.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.when; + +/** + * {@link AlipayBarPayClient} 单元测试 + * + * @author jason + */ +public class AlipayBarPayClientTest extends AbstractAlipayClientTest { + + @InjectMocks + private AlipayBarPayClient client = new AlipayBarPayClient(randomLongId(), config); + + @Override + @BeforeEach + public void setUp() { + setClient(client); + } + + @Test + @DisplayName("支付宝条码支付:非免密码支付下单成功") + public void testUnifiedOrder_success() throws AlipayApiException { + // mock 方法 + String outTradeNo = randomString(); + String notifyUrl = randomURL(); + Integer price = randomInteger(); + String authCode = randomString(); + AlipayTradePayResponse response = randomPojo(AlipayTradePayResponse.class, o -> o.setSubCode("")); + when(defaultAlipayClient.execute(argThat((ArgumentMatcher) request -> { + assertInstanceOf(AlipayTradePayModel.class, request.getBizModel()); + assertEquals(notifyUrl, request.getNotifyUrl()); + AlipayTradePayModel model = (AlipayTradePayModel) request.getBizModel(); + assertEquals(outTradeNo, model.getOutTradeNo()); + assertEquals(String.valueOf(price / 100.0), model.getTotalAmount()); + assertEquals(authCode, model.getAuthCode()); + return true; + }))).thenReturn(response); + // 准备请求参数 + PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(notifyUrl, outTradeNo, price); + Map extraParam = new HashMap<>(); + extraParam.put("auth_code", authCode); + reqDTO.setChannelExtras(extraParam); + + // 调用方法 + PayOrderRespDTO resp = client.unifiedOrder(reqDTO); + // 断言 + assertEquals(WAITING.getStatus(), resp.getStatus()); + assertEquals(outTradeNo, resp.getOutTradeNo()); + assertNull(resp.getChannelOrderNo()); + assertNull(resp.getChannelUserId()); + assertNull(resp.getSuccessTime()); + assertEquals(PayOrderDisplayModeEnum.BAR_CODE.getMode(), resp.getDisplayMode()); + assertEquals("", resp.getDisplayContent()); + assertSame(response, resp.getRawData()); + assertNull(resp.getChannelErrorCode()); + assertNull(resp.getChannelErrorMsg()); + } + + @Test + @DisplayName("支付宝条码支付:免密码支付下单成功") + public void testUnifiedOrder_code10000Success() throws AlipayApiException { + // mock 方法 + String outTradeNo = randomString(); + String channelNo = randomString(); + String channelUserId = randomString(); + Date payTime = randomDate(); + AlipayTradePayResponse response = randomPojo(AlipayTradePayResponse.class, o -> { + o.setSubCode(""); + o.setCode("10000"); + o.setOutTradeNo(outTradeNo); + o.setTradeNo(channelNo); + o.setBuyerUserId(channelUserId); + o.setGmtPayment(payTime); + }); + when(defaultAlipayClient.execute(argThat((ArgumentMatcher) request -> true))) + .thenReturn(response); + // 准备请求参数 + String authCode = randomString(); + PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(randomURL(), outTradeNo, randomInteger()); + Map extraParam = new HashMap<>(); + extraParam.put("auth_code", authCode); + reqDTO.setChannelExtras(extraParam); + + // 下单请求 + PayOrderRespDTO resp = client.unifiedOrder(reqDTO); + // 断言 + assertEquals(PayOrderStatusEnum.SUCCESS.getStatus(), resp.getStatus()); + assertEquals(outTradeNo, resp.getOutTradeNo()); + assertEquals(channelNo, resp.getChannelOrderNo()); + assertEquals(channelUserId, resp.getChannelUserId()); + assertEquals(LocalDateTimeUtil.of(payTime), resp.getSuccessTime()); + assertEquals(PayOrderDisplayModeEnum.BAR_CODE.getMode(), resp.getDisplayMode()); + assertEquals("", resp.getDisplayContent()); + assertSame(response, resp.getRawData()); + assertNull(resp.getChannelErrorCode()); + assertNull(resp.getChannelErrorMsg()); + } + + @Test + @DisplayName("支付宝条码支付:没有传条码") + public void testUnifiedOrder_emptyAuthCode() { + // 准备参数 + PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(randomURL(), randomString(), randomInteger()); + + // 调用,并断言 + assertThrows(ServiceException.class, () -> client.unifiedOrder(reqDTO)); + } + + @Test + @DisplayName("支付宝条码支付:渠道返回失败") + public void test_unified_order_channel_failed() throws AlipayApiException { + // mock 方法 + String subCode = randomString(); + String subMsg = randomString(); + AlipayTradePayResponse response = randomPojo(AlipayTradePayResponse.class, o -> { + o.setSubCode(subCode); + o.setSubMsg(subMsg); + }); + when(defaultAlipayClient.execute(argThat((ArgumentMatcher) request -> true))) + .thenReturn(response); + // 准备请求参数 + String authCode = randomString(); + String outTradeNo = randomString(); + PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(randomURL(), outTradeNo, randomInteger()); + Map extraParam = new HashMap<>(); + extraParam.put("auth_code", authCode); + reqDTO.setChannelExtras(extraParam); + + // 调用方法 + PayOrderRespDTO resp = client.unifiedOrder(reqDTO); + // 断言 + assertEquals(CLOSED.getStatus(), resp.getStatus()); + assertEquals(outTradeNo, resp.getOutTradeNo()); + assertNull(resp.getChannelOrderNo()); + assertNull(resp.getChannelUserId()); + assertNull(resp.getSuccessTime()); + assertNull(resp.getDisplayMode()); + assertNull(resp.getDisplayContent()); + assertSame(response, resp.getRawData()); + assertEquals(subCode, resp.getChannelErrorCode()); + assertEquals(subMsg, resp.getChannelErrorMsg()); + } + +} diff --git a/aagro-module-pay/src/test/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/alipay/AlipayPcPayClientTest.java b/aagro-module-pay/src/test/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/alipay/AlipayPcPayClientTest.java new file mode 100644 index 0000000..ecc472f --- /dev/null +++ b/aagro-module-pay/src/test/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/alipay/AlipayPcPayClientTest.java @@ -0,0 +1,131 @@ +package cn.aagro.pp.module.pay.framework.pay.core.client.impl.alipay; + +import cn.hutool.http.Method; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import cn.aagro.pp.module.pay.framework.pay.core.enums.PayOrderDisplayModeEnum; +import com.alipay.api.AlipayApiException; +import com.alipay.api.request.AlipayTradePagePayRequest; +import com.alipay.api.response.AlipayTradePagePayResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatcher; +import org.mockito.InjectMocks; + +import static cn.aagro.pp.module.pay.enums.order.PayOrderStatusEnum.CLOSED; +import static cn.aagro.pp.module.pay.enums.order.PayOrderStatusEnum.WAITING; +import static cn.aagro.pp.framework.test.core.util.RandomUtils.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +/** + * {@link AlipayPcPayClient} 单元测试 + * + * @author jason + */ +public class AlipayPcPayClientTest extends AbstractAlipayClientTest { + + @InjectMocks + private AlipayPcPayClient client = new AlipayPcPayClient(randomLongId(), config); + + @Override + @BeforeEach + public void setUp() { + setClient(client); + } + + @Test + @DisplayName("支付宝 PC 网站支付:URL Display Mode 下单成功") + public void testUnifiedOrder_urlSuccess() throws AlipayApiException { + // mock 方法 + String notifyUrl = randomURL(); + AlipayTradePagePayResponse response = randomPojo(AlipayTradePagePayResponse.class, o -> o.setSubCode("")); + when(defaultAlipayClient.pageExecute(argThat((ArgumentMatcher) request -> true), + eq(Method.GET.name()))).thenReturn(response); + // 准备请求参数 + String outTradeNo = randomString(); + Integer price = randomInteger(); + PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(notifyUrl, outTradeNo, price); + reqDTO.setDisplayMode(null); + + // 调用 + PayOrderRespDTO resp = client.unifiedOrder(reqDTO); + // 断言 + assertEquals(WAITING.getStatus(), resp.getStatus()); + assertEquals(outTradeNo, resp.getOutTradeNo()); + assertNull(resp.getChannelOrderNo()); + assertNull(resp.getChannelUserId()); + assertNull(resp.getSuccessTime()); + assertEquals(PayOrderDisplayModeEnum.URL.getMode(), resp.getDisplayMode()); + assertEquals(response.getBody(), resp.getDisplayContent()); + assertSame(response, resp.getRawData()); + assertNull(resp.getChannelErrorCode()); + assertNull(resp.getChannelErrorMsg()); + } + + @Test + @DisplayName("支付宝 PC 网站支付:Form Display Mode 下单成功") + public void testUnifiedOrder_formSuccess() throws AlipayApiException { + // mock 方法 + String notifyUrl = randomURL(); + AlipayTradePagePayResponse response = randomPojo(AlipayTradePagePayResponse.class, o -> o.setSubCode("")); + when(defaultAlipayClient.pageExecute(argThat((ArgumentMatcher) request -> true), + eq(Method.POST.name()))).thenReturn(response); + // 准备请求参数 + String outTradeNo = randomString(); + Integer price = randomInteger(); + PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(notifyUrl, outTradeNo, price); + reqDTO.setDisplayMode(PayOrderDisplayModeEnum.FORM.getMode()); + + // 调用 + PayOrderRespDTO resp = client.unifiedOrder(reqDTO); + // 断言 + assertEquals(WAITING.getStatus(), resp.getStatus()); + assertEquals(outTradeNo, resp.getOutTradeNo()); + assertNull(resp.getChannelOrderNo()); + assertNull(resp.getChannelUserId()); + assertNull(resp.getSuccessTime()); + assertEquals(PayOrderDisplayModeEnum.FORM.getMode(), resp.getDisplayMode()); + assertEquals(response.getBody(), resp.getDisplayContent()); + assertSame(response, resp.getRawData()); + assertNull(resp.getChannelErrorCode()); + assertNull(resp.getChannelErrorMsg()); + } + + @Test + @DisplayName("支付宝 PC 网站支付:渠道返回失败") + public void testUnifiedOrder_channelFailed() throws AlipayApiException { + // mock 方法 + String subCode = randomString(); + String subMsg = randomString(); + AlipayTradePagePayResponse response = randomPojo(AlipayTradePagePayResponse.class, o -> { + o.setSubCode(subCode); + o.setSubMsg(subMsg); + }); + when(defaultAlipayClient.pageExecute(argThat((ArgumentMatcher) request -> true), + eq(Method.GET.name()))).thenReturn(response); + // 准备请求参数 + String outTradeNo = randomString(); + PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(randomURL(), outTradeNo, randomInteger()); + reqDTO.setDisplayMode(PayOrderDisplayModeEnum.URL.getMode()); + + // 调用 + PayOrderRespDTO resp = client.unifiedOrder(reqDTO); + // 断言 + assertEquals(CLOSED.getStatus(), resp.getStatus()); + assertEquals(outTradeNo, resp.getOutTradeNo()); + assertNull(resp.getChannelOrderNo()); + assertNull(resp.getChannelUserId()); + assertNull(resp.getSuccessTime()); + assertNull(resp.getDisplayMode()); + assertNull(resp.getDisplayContent()); + assertSame(response, resp.getRawData()); + assertEquals(subCode, resp.getChannelErrorCode()); + assertEquals(subMsg, resp.getChannelErrorMsg()); + } + +} diff --git a/aagro-module-pay/src/test/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/alipay/AlipayQrPayClientTest.java b/aagro-module-pay/src/test/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/alipay/AlipayQrPayClientTest.java new file mode 100644 index 0000000..340e1b9 --- /dev/null +++ b/aagro-module-pay/src/test/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/alipay/AlipayQrPayClientTest.java @@ -0,0 +1,147 @@ +package cn.aagro.pp.module.pay.framework.pay.core.client.impl.alipay; + +import cn.aagro.pp.framework.common.exception.ServiceException; +import cn.aagro.pp.framework.common.exception.enums.GlobalErrorCodeConstants; +import cn.aagro.pp.framework.common.exception.util.ServiceExceptionUtil; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.exception.PayClientException; +import cn.aagro.pp.module.pay.framework.pay.core.enums.PayOrderDisplayModeEnum; +import com.alipay.api.AlipayApiException; +import com.alipay.api.request.AlipayTradePrecreateRequest; +import com.alipay.api.response.AlipayTradePrecreateResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatcher; +import org.mockito.InjectMocks; + +import static cn.aagro.pp.framework.test.core.util.RandomUtils.*; +import static cn.aagro.pp.module.pay.enums.order.PayOrderStatusEnum.CLOSED; +import static cn.aagro.pp.module.pay.enums.order.PayOrderStatusEnum.WAITING; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.when; + +/** + * {@link AlipayQrPayClient} 单元测试 + * + * @author jason + */ +public class AlipayQrPayClientTest extends AbstractAlipayClientTest { + + @InjectMocks + private AlipayQrPayClient client = new AlipayQrPayClient(randomLongId(), config); + + @BeforeEach + public void setUp() { + setClient(client); + } + + @Test + @DisplayName("支付宝扫描支付:下单成功") + public void testUnifiedOrder_success() throws AlipayApiException { + // mock 方法 + String notifyUrl = randomURL(); + String qrCode = randomString(); + Integer price = randomInteger(); + AlipayTradePrecreateResponse response = randomPojo(AlipayTradePrecreateResponse.class, o -> { + o.setQrCode(qrCode); + o.setSubCode(""); + }); + when(defaultAlipayClient.execute(argThat((ArgumentMatcher) request -> { + assertEquals(notifyUrl, request.getNotifyUrl()); + return true; + }))).thenReturn(response); + // 准备请求参数 + String outTradeNo = randomString(); + PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(notifyUrl, outTradeNo, price); + + // 调用 + PayOrderRespDTO resp = client.unifiedOrder(reqDTO); + // 断言 + assertEquals(WAITING.getStatus(), resp.getStatus()); + assertEquals(outTradeNo, resp.getOutTradeNo()); + assertNull(resp.getChannelOrderNo()); + assertNull(resp.getChannelUserId()); + assertNull(resp.getSuccessTime()); + assertEquals(PayOrderDisplayModeEnum.QR_CODE.getMode(), resp.getDisplayMode()); + assertEquals(response.getQrCode(), resp.getDisplayContent()); + assertSame(response, resp.getRawData()); + assertNull(resp.getChannelErrorCode()); + assertNull(resp.getChannelErrorMsg()); + } + + @Test + @DisplayName("支付宝扫描支付:渠道返回失败") + public void testUnifiedOrder_channelFailed() throws AlipayApiException { + // mock 方法 + String notifyUrl = randomURL(); + String subCode = randomString(); + String subMsg = randomString(); + Integer price = randomInteger(); + AlipayTradePrecreateResponse response = randomPojo(AlipayTradePrecreateResponse.class, o -> { + o.setSubCode(subCode); + o.setSubMsg(subMsg); + }); + // mock + when(defaultAlipayClient.execute(argThat((ArgumentMatcher) request -> { + assertEquals(notifyUrl, request.getNotifyUrl()); + return true; + }))).thenReturn(response); + // 准备请求参数 + String outTradeNo = randomString(); + PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(notifyUrl, outTradeNo, price); + + // 调用 + PayOrderRespDTO resp = client.unifiedOrder(reqDTO); + // 断言 + assertEquals(CLOSED.getStatus(), resp.getStatus()); + assertEquals(outTradeNo, resp.getOutTradeNo()); + assertNull(resp.getChannelOrderNo()); + assertNull(resp.getChannelUserId()); + assertNull(resp.getSuccessTime()); + assertNull(resp.getDisplayMode()); + assertNull(resp.getDisplayContent()); + assertSame(response, resp.getRawData()); + assertEquals(subCode, resp.getChannelErrorCode()); + assertEquals(subMsg, resp.getChannelErrorMsg()); + } + + @Test + @DisplayName("支付宝扫描支付, 抛出系统异常") + public void testUnifiedOrder_throwPayException() throws AlipayApiException { + // mock 方法 + String outTradeNo = randomString(); + String notifyUrl = randomURL(); + Integer price = randomInteger(); + when(defaultAlipayClient.execute(argThat((ArgumentMatcher) request -> { + assertEquals(notifyUrl, request.getNotifyUrl()); + return true; + }))).thenThrow(new RuntimeException("系统异常")); + // 准备请求参数 + PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(notifyUrl, outTradeNo,price); + + // 调用,并断言 + assertThrows(PayClientException.class, () -> client.unifiedOrder(reqDTO)); + } + + @Test + @DisplayName("支付宝 Client 统一下单:抛出业务异常") + public void testUnifiedOrder_throwServiceException() throws AlipayApiException { + // mock 方法 + String outTradeNo = randomString(); + String notifyUrl = randomURL(); + Integer price = randomInteger(); + when(defaultAlipayClient.execute(argThat((ArgumentMatcher) request -> { + assertEquals(notifyUrl, request.getNotifyUrl()); + return true; + }))).thenThrow(ServiceExceptionUtil.exception(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR)); + // 准备请求参数 + PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(notifyUrl, outTradeNo, price); + + // 调用,并断言 + assertThrows(ServiceException.class, () -> client.unifiedOrder(reqDTO)); + } + +} diff --git a/aagro-module-pay/src/test/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/alipay/AlipayWapPayClientTest.java b/aagro-module-pay/src/test/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/alipay/AlipayWapPayClientTest.java new file mode 100644 index 0000000..a35856d --- /dev/null +++ b/aagro-module-pay/src/test/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/alipay/AlipayWapPayClientTest.java @@ -0,0 +1,111 @@ +package cn.aagro.pp.module.pay.framework.pay.core.client.impl.alipay; + +import cn.hutool.http.Method; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import cn.aagro.pp.module.pay.framework.pay.core.enums.PayOrderDisplayModeEnum; +import com.alipay.api.AlipayApiException; +import com.alipay.api.domain.AlipayTradeWapPayModel; +import com.alipay.api.request.AlipayTradeWapPayRequest; +import com.alipay.api.response.AlipayTradeWapPayResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatcher; +import org.mockito.InjectMocks; + +import static cn.aagro.pp.framework.test.core.util.RandomUtils.*; +import static cn.aagro.pp.module.pay.enums.order.PayOrderStatusEnum.CLOSED; +import static cn.aagro.pp.module.pay.enums.order.PayOrderStatusEnum.WAITING; +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +/** + * {@link AlipayWapPayClient} 单元测试 + * + * @author jason + */ +public class AlipayWapPayClientTest extends AbstractAlipayClientTest { + + /** + * 支付宝 H5 支付 Client + */ + @InjectMocks + private AlipayWapPayClient client = new AlipayWapPayClient(randomLongId(), config); + + @BeforeEach + public void setUp() { + setClient(client); + } + + @Test + @DisplayName("支付宝 H5 支付:下单成功") + public void testUnifiedOrder_success() throws AlipayApiException { + // mock 方法 + String h5Body = randomString(); + Integer price = randomInteger(); + AlipayTradeWapPayResponse response = randomPojo(AlipayTradeWapPayResponse.class, o -> { + o.setSubCode(""); + o.setBody(h5Body); + }); + String notifyUrl = randomURL(); + when(defaultAlipayClient.pageExecute(argThat((ArgumentMatcher) request -> { + assertInstanceOf(AlipayTradeWapPayModel.class, request.getBizModel()); + AlipayTradeWapPayModel bizModel = (AlipayTradeWapPayModel) request.getBizModel(); + assertEquals(String.valueOf(price / 100.0), bizModel.getTotalAmount()); + assertEquals(notifyUrl, request.getNotifyUrl()); + return true; + }), eq(Method.GET.name()))).thenReturn(response); + // 准备请求参数 + String outTradeNo = randomString(); + PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(notifyUrl, outTradeNo, price); + + // 调用 + PayOrderRespDTO resp = client.unifiedOrder(reqDTO); + // 断言 + assertEquals(WAITING.getStatus(), resp.getStatus()); + assertEquals(outTradeNo, resp.getOutTradeNo()); + assertNull(resp.getChannelOrderNo()); + assertNull(resp.getChannelUserId()); + assertNull(resp.getSuccessTime()); + assertEquals(PayOrderDisplayModeEnum.URL.getMode(), resp.getDisplayMode()); + assertEquals(response.getBody(), resp.getDisplayContent()); + assertSame(response, resp.getRawData()); + assertNull(resp.getChannelErrorCode()); + assertNull(resp.getChannelErrorMsg()); + } + + @Test + @DisplayName("支付宝 H5 支付:渠道返回失败") + public void test_unified_order_channel_failed() throws AlipayApiException { + // mock 方法 + String subCode = randomString(); + String subMsg = randomString(); + AlipayTradeWapPayResponse response = randomPojo(AlipayTradeWapPayResponse.class, o -> { + o.setSubCode(subCode); + o.setSubMsg(subMsg); + }); + when(defaultAlipayClient.pageExecute(argThat((ArgumentMatcher) request -> true), + eq(Method.GET.name()))).thenReturn(response); + String outTradeNo = randomString(); + PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(randomURL(), outTradeNo, randomInteger()); + + // 调用 + PayOrderRespDTO resp = client.unifiedOrder(reqDTO); + // 断言 + assertEquals(CLOSED.getStatus(), resp.getStatus()); + assertEquals(outTradeNo, resp.getOutTradeNo()); + assertNull(resp.getChannelOrderNo()); + assertNull(resp.getChannelUserId()); + assertNull(resp.getSuccessTime()); + assertNull(resp.getDisplayMode()); + assertNull(resp.getDisplayContent()); + assertSame(response, resp.getRawData()); + assertEquals(subCode, resp.getChannelErrorCode()); + assertEquals(subMsg, resp.getChannelErrorMsg()); + } + +} diff --git a/aagro-module-pay/src/test/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/weixin/WxBarPayClientIntegrationTest.java b/aagro-module-pay/src/test/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/weixin/WxBarPayClientIntegrationTest.java new file mode 100644 index 0000000..3593d9c --- /dev/null +++ b/aagro-module-pay/src/test/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/weixin/WxBarPayClientIntegrationTest.java @@ -0,0 +1,123 @@ +package cn.aagro.pp.module.pay.framework.pay.core.client.impl.weixin; + +import cn.aagro.pp.framework.common.util.date.LocalDateTimeUtils; +import cn.aagro.pp.framework.common.util.json.JsonUtils; +import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyResult; +import com.github.binarywang.wxpay.bean.request.WxPayMicropayRequest; +import com.github.binarywang.wxpay.bean.request.WxPayRefundRequest; +import com.github.binarywang.wxpay.bean.request.WxPayRefundV3Request; +import com.github.binarywang.wxpay.bean.result.WxPayMicropayResult; +import com.github.binarywang.wxpay.bean.result.WxPayRefundResult; +import com.github.binarywang.wxpay.bean.result.WxPayRefundV3Result; +import com.github.binarywang.wxpay.config.WxPayConfig; +import com.github.binarywang.wxpay.exception.WxPayException; +import com.github.binarywang.wxpay.service.WxPayService; +import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.time.Duration; + +import static cn.aagro.pp.module.pay.framework.pay.core.client.impl.weixin.AbstractWxPayClient.formatDateV2; + +/** + * {@link WxBarPayClient} 的集成测试,用于快速调试微信条码支付 + * + * @author 芋道源码 + */ +@Disabled +public class WxBarPayClientIntegrationTest { + + @Test + public void testPayV2() throws WxPayException { + // 创建 config 配置 + WxPayConfig config = buildWxPayConfigV2(); + // 创建 WxPayService 客户端 + WxPayService client = new WxPayServiceImpl(); + client.setConfig(config); + + // 执行发起支付 + WxPayMicropayRequest request = WxPayMicropayRequest.newBuilder() + .outTradeNo(String.valueOf(System.currentTimeMillis())) + .body("测试支付-body") + .detail("测试支付-detail") + .totalFee(1) // 单位分 + .timeExpire(formatDateV2(LocalDateTimeUtils.addTime(Duration.ofMinutes(2)))) + .spbillCreateIp("127.0.0.1") + .authCode("134298744426278497") + .build(); + System.out.println("========= request =========="); + System.out.println(JsonUtils.toJsonPrettyString(request)); + WxPayMicropayResult response = client.micropay(request); + System.out.println("========= response =========="); + System.out.println(JsonUtils.toJsonPrettyString(response)); + } + + @Test + public void testParseRefundNotifyV2() throws WxPayException { + // 创建 config 配置 + WxPayConfig config = buildWxPayConfigV2(); + // 创建 WxPayService 客户端 + WxPayService client = new WxPayServiceImpl(); + client.setConfig(config); + + // 执行解析 + String xml = "SUCCESS"; + WxPayRefundNotifyResult response = client.parseRefundNotifyResult(xml); + System.out.println(response.getReqInfo()); + } + + @Test + public void testRefundV2() throws WxPayException { + // 创建 config 配置 + WxPayConfig config = buildWxPayConfigV2(); + // 创建 WxPayService 客户端 + WxPayService client = new WxPayServiceImpl(); + client.setConfig(config); + + // 执行发起退款 + WxPayRefundRequest request = new WxPayRefundRequest() + .setOutTradeNo("1689545667276") + .setOutRefundNo(String.valueOf(System.currentTimeMillis())) + .setRefundFee(1) + .setRefundDesc("就是想退了") + .setTotalFee(1); + System.out.println("========= request =========="); + System.out.println(JsonUtils.toJsonPrettyString(request)); + WxPayRefundResult response = client.refund(request); + System.out.println("========= response =========="); + System.out.println(JsonUtils.toJsonPrettyString(response)); + } + + @Test + public void testRefundV3() throws WxPayException { + // 创建 config 配置 + WxPayConfig config = buildWxPayConfigV2(); + // 创建 WxPayService 客户端 + WxPayService client = new WxPayServiceImpl(); + client.setConfig(config); + + // 执行发起退款 + WxPayRefundV3Request request = new WxPayRefundV3Request() + .setOutTradeNo("1689506325635") + .setOutRefundNo(String.valueOf(System.currentTimeMillis())) + .setAmount(new WxPayRefundV3Request.Amount().setTotal(1).setRefund(1).setCurrency("CNY")) + .setReason("就是想退了"); + System.out.println("========= request =========="); + System.out.println(JsonUtils.toJsonPrettyString(request)); + WxPayRefundV3Result response = client.refundV3(request); + System.out.println("========= response =========="); + System.out.println(JsonUtils.toJsonPrettyString(response)); + } + + private WxPayConfig buildWxPayConfigV2() { + WxPayConfig config = new WxPayConfig(); + config.setAppId("wx62056c0d5e8db250"); + config.setMchId("1545083881"); + config.setMchKey("dS1ngeN63JLr3NRbvPH9AJy3MyUxZdim"); +// config.setSignType(WxPayConstants.SignType.MD5); + config.setKeyPath("/Users/yunai/Downloads/wx_pay/apiclient_cert.p12"); + return config; + } + +} diff --git a/aagro-module-pay/src/test/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/weixin/WxNativePayClientIntegrationTest.java b/aagro-module-pay/src/test/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/weixin/WxNativePayClientIntegrationTest.java new file mode 100644 index 0000000..01bc4de --- /dev/null +++ b/aagro-module-pay/src/test/java/cn/aagro/pp/module/pay/framework/pay/core/client/impl/weixin/WxNativePayClientIntegrationTest.java @@ -0,0 +1,84 @@ +package cn.aagro.pp.module.pay.framework.pay.core.client.impl.weixin; + +import cn.aagro.pp.framework.common.util.date.LocalDateTimeUtils; +import cn.aagro.pp.framework.common.util.json.JsonUtils; +import com.github.binarywang.wxpay.bean.request.WxPayRefundV3Request; +import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request; +import com.github.binarywang.wxpay.bean.result.WxPayRefundV3Result; +import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum; +import com.github.binarywang.wxpay.config.WxPayConfig; +import com.github.binarywang.wxpay.exception.WxPayException; +import com.github.binarywang.wxpay.service.WxPayService; +import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.time.Duration; + +import static cn.aagro.pp.module.pay.framework.pay.core.client.impl.weixin.AbstractWxPayClient.formatDateV3; + + +/** + * {@link WxNativePayClient} 的集成测试,用于快速调试微信扫码支付 + * + * @author 芋道源码 + */ +@Disabled +public class WxNativePayClientIntegrationTest { + + @Test + public void testPayV3() throws WxPayException { + // 创建 config 配置 + WxPayConfig config = buildWxPayConfigV3(); + // 创建 WxPayService 客户端 + WxPayService client = new WxPayServiceImpl(); + client.setConfig(config); + + // 执行发起支付 + WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request() + .setOutTradeNo(String.valueOf(System.currentTimeMillis())) + .setDescription("测试支付-body") + .setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(1)) // 单位分 + .setTimeExpire(formatDateV3(LocalDateTimeUtils.addTime(Duration.ofMinutes(2)))) + .setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp("127.0.0.1")) + .setNotifyUrl("http://127.0.0.1:48080"); + System.out.println("========= request =========="); + System.out.println(JsonUtils.toJsonPrettyString(request)); + String response = client.createOrderV3(TradeTypeEnum.NATIVE, request); + System.out.println("========= response =========="); + System.out.println(JsonUtils.toJsonPrettyString(response)); + } + + @Test + public void testRefundV3() throws WxPayException { + // 创建 config 配置 + WxPayConfig config = buildWxPayConfigV3(); + // 创建 WxPayService 客户端 + WxPayService client = new WxPayServiceImpl(); + client.setConfig(config); + + // 执行发起退款 + WxPayRefundV3Request request = new WxPayRefundV3Request() + .setOutTradeNo("1689545729695") + .setOutRefundNo(String.valueOf(System.currentTimeMillis())) + .setAmount(new WxPayRefundV3Request.Amount().setTotal(1).setRefund(1).setCurrency("CNY")) + .setReason("就是想退了"); + System.out.println("========= request =========="); + System.out.println(JsonUtils.toJsonPrettyString(request)); + WxPayRefundV3Result response = client.refundV3(request); + System.out.println("========= response =========="); + System.out.println(JsonUtils.toJsonPrettyString(response)); + } + + private WxPayConfig buildWxPayConfigV3() { + WxPayConfig config = new WxPayConfig(); + config.setAppId("wx62056c0d5e8db250"); + config.setMchId("1545083881"); + config.setApiV3Key("459arNsYHl1mgkiO6H9ZH5KkhFXSxaA4"); +// config.setCertSerialNo(serialNo); + config.setPrivateCertPath("/Users/yunai/Downloads/wx_pay/apiclient_cert.pem"); + config.setPrivateKeyPath("/Users/yunai/Downloads/wx_pay/apiclient_key.pem"); + return config; + } + +} diff --git a/aagro-module-pay/src/test/java/cn/aagro/pp/module/pay/service/app/PayAppServiceTest.java b/aagro-module-pay/src/test/java/cn/aagro/pp/module/pay/service/app/PayAppServiceTest.java new file mode 100644 index 0000000..969bbff --- /dev/null +++ b/aagro-module-pay/src/test/java/cn/aagro/pp/module/pay/service/app/PayAppServiceTest.java @@ -0,0 +1,260 @@ +package cn.aagro.pp.module.pay.service.app; + +import cn.hutool.core.util.RandomUtil; +import cn.aagro.pp.framework.common.enums.CommonStatusEnum; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.test.core.ut.BaseDbUnitTest; +import cn.aagro.pp.module.pay.controller.admin.app.vo.PayAppCreateReqVO; +import cn.aagro.pp.module.pay.controller.admin.app.vo.PayAppPageReqVO; +import cn.aagro.pp.module.pay.controller.admin.app.vo.PayAppUpdateReqVO; +import cn.aagro.pp.module.pay.dal.dataobject.app.PayAppDO; +import cn.aagro.pp.module.pay.dal.mysql.app.PayAppMapper; +import cn.aagro.pp.module.pay.service.order.PayOrderService; +import cn.aagro.pp.module.pay.service.refund.PayRefundService; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.Map; + +import static cn.aagro.pp.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime; +import static cn.aagro.pp.framework.common.util.date.LocalDateTimeUtils.buildTime; +import static cn.aagro.pp.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static cn.aagro.pp.framework.test.core.util.AssertUtils.assertPojoEquals; +import static cn.aagro.pp.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.aagro.pp.framework.test.core.util.RandomUtils.*; +import static cn.aagro.pp.module.pay.enums.ErrorCodeConstants.*; +import static java.util.Collections.singleton; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +/** + * {@link PayAppServiceImpl} 的单元测试 + * + * @author aquan + */ +@Disabled // TODO 芋艿:后续 fix 补充的单测 +@Import(PayAppServiceImpl.class) +public class PayAppServiceTest extends BaseDbUnitTest { + + @Resource + private PayAppServiceImpl appService; + + @Resource + private PayAppMapper appMapper; + + @MockBean + private PayOrderService orderService; + @MockBean + private PayRefundService refundService; + + @Test + public void testCreateApp_success() { + // 准备参数 + PayAppCreateReqVO reqVO = randomPojo(PayAppCreateReqVO.class, o -> + o.setStatus((RandomUtil.randomEle(CommonStatusEnum.values()).getStatus())) + .setOrderNotifyUrl(randomURL()) + .setRefundNotifyUrl(randomURL())); + + // 调用 + Long appId = appService.createApp(reqVO); + // 断言 + assertNotNull(appId); + PayAppDO app = appMapper.selectById(appId); + assertPojoEquals(reqVO, app); + } + + @Test + public void testUpdateApp_success() { + // mock 数据 + PayAppDO dbApp = randomPojo(PayAppDO.class); + appMapper.insert(dbApp);// @Sql: 先插入出一条存在的数据 + // 准备参数 + PayAppUpdateReqVO reqVO = randomPojo(PayAppUpdateReqVO.class, o -> { + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setOrderNotifyUrl(randomURL()).setRefundNotifyUrl(randomURL()); + o.setId(dbApp.getId()); // 设置更新的 ID + }); + + // 调用 + appService.updateApp(reqVO); + // 校验是否更新正确 + PayAppDO app = appMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, app); + } + + @Test + public void testUpdateApp_notExists() { + // 准备参数 + PayAppUpdateReqVO reqVO = randomPojo(PayAppUpdateReqVO.class, o -> + o.setStatus((RandomUtil.randomEle(CommonStatusEnum.values()).getStatus()))); + // 调用, 并断言异常 + assertServiceException(() -> appService.updateApp(reqVO), APP_NOT_FOUND); + } + + @Test + public void testUpdateAppStatus() { + // mock 数据 + PayAppDO dbApp = randomPojo(PayAppDO.class, o -> + o.setStatus(CommonStatusEnum.DISABLE.getStatus())); + appMapper.insert(dbApp);// @Sql: 先插入出一条存在的数据 + + // 准备参数 + Long id = dbApp.getId(); + Integer status = CommonStatusEnum.ENABLE.getStatus(); + // 调用 + appService.updateAppStatus(id, status); + // 断言 + PayAppDO app = appMapper.selectById(id); // 获取最新的 + assertEquals(status, app.getStatus()); + } + + @Test + public void testDeleteApp_success() { + // mock 数据 + PayAppDO dbApp = randomPojo(PayAppDO.class); + appMapper.insert(dbApp);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbApp.getId(); + + // 调用 + appService.deleteApp(id); + // 校验数据不存在了 + assertNull(appMapper.selectById(id)); + } + + @Test + public void testDeleteApp_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> appService.deleteApp(id), APP_NOT_FOUND); + } + + @Test + public void testDeleteApp_existOrder() { + // mock 数据 + PayAppDO dbApp = randomPojo(PayAppDO.class); + appMapper.insert(dbApp);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbApp.getId(); + // mock 订单有订单 + when(orderService.getOrderCountByAppId(eq(id))).thenReturn(10L); + + // 调用, 并断言异常 + assertServiceException(() -> appService.deleteApp(id), APP_EXIST_ORDER_CANT_DELETE); + } + + @Test + public void testDeleteApp_existRefund() { + // mock 数据 + PayAppDO dbApp = randomPojo(PayAppDO.class); + appMapper.insert(dbApp);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbApp.getId(); + // mock 订单有订单 + when(refundService.getRefundCountByAppId(eq(id))).thenReturn(10L); + + // 调用, 并断言异常 + assertServiceException(() -> appService.deleteApp(id), APP_EXIST_REFUND_CANT_DELETE); + } + + @Test + public void testApp() { + // mock 数据 + PayAppDO dbApp = randomPojo(PayAppDO.class); + appMapper.insert(dbApp);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbApp.getId(); + + // 调用 + PayAppDO app = appService.getApp(id); + // 校验数据一致 + assertPojoEquals(app, dbApp); + } + + @Test + public void testAppMap() { + // mock 数据 + PayAppDO dbApp01 = randomPojo(PayAppDO.class); + appMapper.insert(dbApp01);// @Sql: 先插入出一条存在的数据 + PayAppDO dbApp02 = randomPojo(PayAppDO.class); + appMapper.insert(dbApp02);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbApp01.getId(); + + // 调用 + Map appMap = appService.getAppMap(singleton(id)); + // 校验数据一致 + assertEquals(1, appMap.size()); + assertPojoEquals(dbApp01, appMap.get(id)); + } + + @Test + public void testGetAppPage() { + // mock 数据 + PayAppDO dbApp = randomPojo(PayAppDO.class, o -> { // 等会查询到 + o.setName("灿灿姐的杂货铺"); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setCreateTime(buildTime(2021,11,20)); + }); + + appMapper.insert(dbApp); + // 测试 name 不匹配 + appMapper.insert(cloneIgnoreId(dbApp, o -> o.setName("敏敏姐的杂货铺"))); + // 测试 status 不匹配 + appMapper.insert(cloneIgnoreId(dbApp, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + // 测试 createTime 不匹配 + appMapper.insert(cloneIgnoreId(dbApp, o -> o.setCreateTime(buildTime(2021,12,21)))); + // 准备参数 + PayAppPageReqVO reqVO = new PayAppPageReqVO(); + reqVO.setName("灿灿姐的杂货铺"); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + reqVO.setCreateTime(buildBetweenTime(2021, 11, 19, 2021, 11, 21)); + + // 调用 + PageResult pageResult = appService.getAppPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbApp, pageResult.getList().get(0)); + } + + @Test + public void testValidPayApp_success() { + // mock 数据 + PayAppDO dbApp = randomPojo(PayAppDO.class, + o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())); + appMapper.insert(dbApp);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbApp.getId(); + + // 调用 + PayAppDO app = appService.validPayApp(id); + // 校验数据一致 + assertPojoEquals(app, dbApp); + } + + @Test + public void testValidPayApp_notFound() { + assertServiceException(() -> appService.validPayApp(randomLongId()), APP_NOT_FOUND); + } + + @Test + public void testValidPayApp_disable() { + // mock 数据 + PayAppDO dbApp = randomPojo(PayAppDO.class, + o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus())); + appMapper.insert(dbApp);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbApp.getId(); + + // 调用,并断言异常 + assertServiceException(() -> appService.validPayApp(id), APP_IS_DISABLE); + } + +} diff --git a/aagro-module-pay/src/test/java/cn/aagro/pp/module/pay/service/channel/PayChannelServiceTest.java b/aagro-module-pay/src/test/java/cn/aagro/pp/module/pay/service/channel/PayChannelServiceTest.java new file mode 100644 index 0000000..501fd5c --- /dev/null +++ b/aagro-module-pay/src/test/java/cn/aagro/pp/module/pay/service/channel/PayChannelServiceTest.java @@ -0,0 +1,338 @@ +package cn.aagro.pp.module.pay.service.channel; + +import cn.aagro.pp.framework.common.enums.CommonStatusEnum; +import cn.aagro.pp.framework.common.util.json.JsonUtils; +import cn.aagro.pp.framework.test.core.ut.BaseDbUnitTest; +import cn.aagro.pp.module.pay.controller.admin.channel.vo.PayChannelCreateReqVO; +import cn.aagro.pp.module.pay.controller.admin.channel.vo.PayChannelUpdateReqVO; +import cn.aagro.pp.module.pay.dal.dataobject.channel.PayChannelDO; +import cn.aagro.pp.module.pay.dal.mysql.channel.PayChannelMapper; +import cn.aagro.pp.module.pay.enums.PayChannelEnum; +import cn.aagro.pp.module.pay.framework.pay.core.client.PayClient; +import cn.aagro.pp.module.pay.framework.pay.core.client.PayClientFactory; +import cn.aagro.pp.module.pay.framework.pay.core.client.impl.alipay.AlipayPayClientConfig; +import cn.aagro.pp.module.pay.framework.pay.core.client.impl.weixin.WxPayClientConfig; +import com.alibaba.fastjson.JSON; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import javax.validation.Validator; +import java.util.Collections; +import java.util.List; + +import static cn.aagro.pp.framework.test.core.util.AssertUtils.assertPojoEquals; +import static cn.aagro.pp.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.aagro.pp.framework.test.core.util.RandomUtils.*; +import static cn.aagro.pp.module.pay.enums.ErrorCodeConstants.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@Import({PayChannelServiceImpl.class}) +public class PayChannelServiceTest extends BaseDbUnitTest { + + @Resource + private PayChannelServiceImpl channelService; + + @Resource + private PayChannelMapper channelMapper; + + @MockBean + private PayClientFactory payClientFactory; + @MockBean + private Validator validator; + + @Test + public void testCreateChannel_success() { + // 准备参数 + WxPayClientConfig config = randomWxPayClientConfig(); + PayChannelCreateReqVO reqVO = randomPojo(PayChannelCreateReqVO.class, o -> { + o.setStatus(randomCommonStatus()); + o.setCode(PayChannelEnum.WX_PUB.getCode()); + o.setConfig(JsonUtils.toJsonString(config)); + }); + + // 调用 + Long channelId = channelService.createChannel(reqVO); + // 校验记录的属性是否正确 + PayChannelDO channel = channelMapper.selectById(channelId); + assertPojoEquals(reqVO, channel, "config"); + assertPojoEquals(config, channel.getConfig()); + } + + @Test + public void testCreateChannel_exists() { + // mock 数据 + PayChannelDO dbChannel = randomPojo(PayChannelDO.class, + o -> o.setConfig(randomWxPayClientConfig())); + channelMapper.insert(dbChannel);// @Sql: 先插入出一条存在的数据 + // 准备参数 + PayChannelCreateReqVO reqVO = randomPojo(PayChannelCreateReqVO.class, o -> { + o.setAppId(dbChannel.getAppId()); + o.setCode(dbChannel.getCode()); + }); + + // 调用, 并断言异常 + assertServiceException(() -> channelService.createChannel(reqVO), CHANNEL_EXIST_SAME_CHANNEL_ERROR); + } + + @Test + public void testUpdateChannel_success() { + // mock 数据 + PayChannelDO dbChannel = randomPojo(PayChannelDO.class, o -> { + o.setCode(PayChannelEnum.ALIPAY_APP.getCode()); + o.setConfig(randomAlipayPayClientConfig()); + }); + channelMapper.insert(dbChannel);// @Sql: 先插入出一条存在的数据 + // 准备参数 + AlipayPayClientConfig config = randomAlipayPayClientConfig(); + PayChannelUpdateReqVO reqVO = randomPojo(PayChannelUpdateReqVO.class, o -> { + o.setId(dbChannel.getId()); // 设置更新的 ID + o.setStatus(randomCommonStatus()); + o.setConfig(JsonUtils.toJsonString(config)); + }); + + // 调用 + channelService.updateChannel(reqVO); + // 校验是否更新正确 + PayChannelDO channel = channelMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, channel, "config"); + assertPojoEquals(config, channel.getConfig()); + } + + @Test + public void testUpdateChannel_notExists() { + // 准备参数 + AlipayPayClientConfig payClientPublicKeyConfig = randomAlipayPayClientConfig(); + PayChannelUpdateReqVO reqVO = randomPojo(PayChannelUpdateReqVO.class, o -> { + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setConfig(JSON.toJSONString(payClientPublicKeyConfig)); + }); + + // 调用, 并断言异常 + assertServiceException(() -> channelService.updateChannel(reqVO), CHANNEL_NOT_FOUND); + } + + @Test + public void testDeleteChannel_success() { + // mock 数据 + PayChannelDO dbChannel = randomPojo(PayChannelDO.class, o -> { + o.setCode(PayChannelEnum.ALIPAY_APP.getCode()); + o.setConfig(randomAlipayPayClientConfig()); + }); + channelMapper.insert(dbChannel);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbChannel.getId(); + + // 调用 + channelService.deleteChannel(id); + // 校验数据不存在了 + assertNull(channelMapper.selectById(id)); + } + + @Test + public void testDeleteChannel_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> channelService.deleteChannel(id), CHANNEL_NOT_FOUND); + } + + @Test + public void testGetChannel() { + // mock 数据 + PayChannelDO dbChannel = randomPojo(PayChannelDO.class, o -> { + o.setCode(PayChannelEnum.ALIPAY_APP.getCode()); + o.setConfig(randomAlipayPayClientConfig()); + }); + channelMapper.insert(dbChannel);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbChannel.getId(); + + // 调用 + PayChannelDO channel = channelService.getChannel(id); + // 校验是否更新正确 + assertPojoEquals(dbChannel, channel); + } + + @Test + public void testGetChannelListByAppIds() { + // mock 数据 + PayChannelDO dbChannel01 = randomPojo(PayChannelDO.class, o -> { + o.setCode(PayChannelEnum.ALIPAY_APP.getCode()); + o.setConfig(randomAlipayPayClientConfig()); + }); + channelMapper.insert(dbChannel01);// @Sql: 先插入出一条存在的数据 + PayChannelDO dbChannel02 = randomPojo(PayChannelDO.class, o -> { + o.setCode(PayChannelEnum.WX_PUB.getCode()); + o.setConfig(randomWxPayClientConfig()); + }); + channelMapper.insert(dbChannel02);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long appId = dbChannel01.getAppId(); + + // 调用 + List channels = channelService.getChannelListByAppIds(Collections.singleton(appId)); + // 校验是否更新正确 + assertEquals(1, channels.size()); + assertPojoEquals(dbChannel01, channels.get(0)); + } + + @Test + public void testGetChannelByAppIdAndCode() { + // mock 数据 + PayChannelDO dbChannel = randomPojo(PayChannelDO.class, o -> { + o.setCode(PayChannelEnum.ALIPAY_APP.getCode()); + o.setConfig(randomAlipayPayClientConfig()); + }); + channelMapper.insert(dbChannel);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long appId = dbChannel.getAppId(); + String code = dbChannel.getCode(); + + // 调用 + PayChannelDO channel = channelService.getChannelByAppIdAndCode(appId, code); + // 断言 + assertPojoEquals(channel, dbChannel); + } + + @Test + public void testValidPayChannel_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> channelService.validPayChannel(id), CHANNEL_NOT_FOUND); + } + + @Test + public void testValidPayChannel_isDisable() { + // mock 数据 + PayChannelDO dbChannel = randomPojo(PayChannelDO.class, o -> { + o.setCode(PayChannelEnum.ALIPAY_APP.getCode()); + o.setConfig(randomAlipayPayClientConfig()); + o.setStatus(CommonStatusEnum.DISABLE.getStatus()); + }); + channelMapper.insert(dbChannel);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbChannel.getId(); + + // 调用, 并断言异常 + assertServiceException(() -> channelService.validPayChannel(id), CHANNEL_IS_DISABLE); + } + + @Test + public void testValidPayChannel_success() { + // mock 数据 + PayChannelDO dbChannel = randomPojo(PayChannelDO.class, o -> { + o.setCode(PayChannelEnum.ALIPAY_APP.getCode()); + o.setConfig(randomAlipayPayClientConfig()); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + }); + channelMapper.insert(dbChannel);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbChannel.getId(); + + // 调用 + PayChannelDO channel = channelService.validPayChannel(id); + // 断言异常 + assertPojoEquals(channel, dbChannel); + } + + @Test + public void testValidPayChannel_appIdAndCode() { + // mock 数据 + PayChannelDO dbChannel = randomPojo(PayChannelDO.class, o -> { + o.setCode(PayChannelEnum.ALIPAY_APP.getCode()); + o.setConfig(randomAlipayPayClientConfig()); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + }); + channelMapper.insert(dbChannel);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long appId = dbChannel.getAppId(); + String code = dbChannel.getCode(); + + // 调用 + PayChannelDO channel = channelService.validPayChannel(appId, code); + // 断言异常 + assertPojoEquals(channel, dbChannel); + } + + @Test + public void testGetEnableChannelList() { + // 准备参数 + Long appId = randomLongId(); + // mock 数据 01(enable 不匹配) + PayChannelDO dbChannel01 = randomPojo(PayChannelDO.class, o -> { + o.setCode(PayChannelEnum.ALIPAY_APP.getCode()); + o.setConfig(randomAlipayPayClientConfig()); + o.setStatus(CommonStatusEnum.DISABLE.getStatus()); + }); + channelMapper.insert(dbChannel01);// @Sql: 先插入出一条存在的数据 + // mock 数据 02(appId 不匹配) + PayChannelDO dbChannel02 = randomPojo(PayChannelDO.class, o -> { + o.setCode(PayChannelEnum.ALIPAY_APP.getCode()); + o.setConfig(randomAlipayPayClientConfig()); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + }); + channelMapper.insert(dbChannel02);// @Sql: 先插入出一条存在的数据 + // mock 数据 03 + PayChannelDO dbChannel03 = randomPojo(PayChannelDO.class, o -> { + o.setCode(PayChannelEnum.ALIPAY_APP.getCode()); + o.setConfig(randomAlipayPayClientConfig()); + o.setAppId(appId); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + }); + channelMapper.insert(dbChannel03);// @Sql: 先插入出一条存在的数据 + + // 调用 + List channel = channelService.getEnableChannelList(appId); + // 断言异常 + assertPojoEquals(channel, dbChannel03); + } + + @Test + public void testGetPayClient() { + // mock 数据 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> { + o.setCode(PayChannelEnum.ALIPAY_APP.getCode()); + o.setConfig(randomAlipayPayClientConfig()); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + }); + channelMapper.insert(channel); + // mock 参数 + Long id = channel.getId(); + // mock 方法 + PayClient mockClient = mock(PayClient.class); + when(payClientFactory.createOrUpdatePayClient(eq(id), eq(channel.getCode()), eq(channel.getConfig()))) + .thenReturn(mockClient); + + // 调用 + PayClient client = channelService.getPayClient(id); + // 断言 + assertSame(client, mockClient); + } + + public WxPayClientConfig randomWxPayClientConfig() { + return new WxPayClientConfig() + .setAppId(randomString()) + .setMchId(randomString()) + .setApiVersion(WxPayClientConfig.API_VERSION_V2) + .setMchKey(randomString()); + } + + public AlipayPayClientConfig randomAlipayPayClientConfig() { + return new AlipayPayClientConfig() + .setServerUrl(randomURL()) + .setAppId(randomString()) + .setSignType(AlipayPayClientConfig.SIGN_TYPE_DEFAULT) + .setMode(AlipayPayClientConfig.MODE_PUBLIC_KEY) + .setPrivateKey(randomString()) + .setAlipayPublicKey(randomString()); + } + +} diff --git a/aagro-module-pay/src/test/java/cn/aagro/pp/module/pay/service/notify/PayNotifyServiceTest.java b/aagro-module-pay/src/test/java/cn/aagro/pp/module/pay/service/notify/PayNotifyServiceTest.java new file mode 100644 index 0000000..fca7b10 --- /dev/null +++ b/aagro-module-pay/src/test/java/cn/aagro/pp/module/pay/service/notify/PayNotifyServiceTest.java @@ -0,0 +1,353 @@ +package cn.aagro.pp.module.pay.service.notify; + +import cn.hutool.extra.spring.SpringUtil; +import cn.aagro.pp.framework.common.pojo.CommonResult; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.test.core.ut.BaseDbUnitTest; +import cn.aagro.pp.module.pay.controller.admin.notify.vo.PayNotifyTaskPageReqVO; +import cn.aagro.pp.module.pay.dal.dataobject.notify.PayNotifyLogDO; +import cn.aagro.pp.module.pay.dal.dataobject.notify.PayNotifyTaskDO; +import cn.aagro.pp.module.pay.dal.dataobject.order.PayOrderDO; +import cn.aagro.pp.module.pay.dal.dataobject.refund.PayRefundDO; +import cn.aagro.pp.module.pay.dal.mysql.notify.PayNotifyLogMapper; +import cn.aagro.pp.module.pay.dal.mysql.notify.PayNotifyTaskMapper; +import cn.aagro.pp.module.pay.dal.redis.notify.PayNotifyLockRedisDAO; +import cn.aagro.pp.module.pay.enums.notify.PayNotifyStatusEnum; +import cn.aagro.pp.module.pay.enums.notify.PayNotifyTypeEnum; +import cn.aagro.pp.module.pay.framework.job.config.PayJobConfiguration; +import cn.aagro.pp.module.pay.service.order.PayOrderService; +import cn.aagro.pp.module.pay.service.refund.PayRefundService; +import cn.aagro.pp.module.pay.service.refund.PayRefundServiceImpl; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.time.Duration; +import java.util.List; + +import static cn.aagro.pp.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; +import static cn.aagro.pp.framework.common.util.date.LocalDateTimeUtils.*; +import static cn.aagro.pp.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static cn.aagro.pp.framework.test.core.util.AssertUtils.assertPojoEquals; +import static cn.aagro.pp.framework.test.core.util.RandomUtils.randomPojo; +import static cn.aagro.pp.framework.test.core.util.RandomUtils.randomString; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +/** + * {@link PayRefundServiceImpl} 的单元测试类 + * + * @author 芋艿 + */ +@Disabled // TODO 芋艿:后续 fix 补充的单测 +@Import({PayJobConfiguration.class, PayNotifyServiceImpl.class, PayNotifyLockRedisDAO.class}) +public class PayNotifyServiceTest extends BaseDbUnitTest { + + @Resource + private PayNotifyServiceImpl notifyService; + + @MockBean + private PayOrderService orderService; + @MockBean + private PayRefundService refundService; + + @Resource + private PayNotifyTaskMapper notifyTaskMapper; + @Resource + private PayNotifyLogMapper notifyLogMapper; + + @MockBean + private RedissonClient redissonClient; + + @Test + public void testCreatePayNotifyTask_order() { + PayNotifyServiceImpl payNotifyService = mock(PayNotifyServiceImpl.class); + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PayNotifyServiceImpl.class))) + .thenReturn(payNotifyService); + + // 准备参数 + Integer type = PayNotifyTypeEnum.ORDER.getType(); + Long dataId = 1L; + // mock 方法(order) + PayOrderDO order = randomPojo(PayOrderDO.class); + when(orderService.getOrder(eq(1L))).thenReturn(order); + // mock 方法(lock) + mockLock(null); // null 的原因,是咱没办法拿到 taskId 新增 + + // 调用 + notifyService.createPayNotifyTask(type, dataId); + // 断言,task + PayNotifyTaskDO dbTask = notifyTaskMapper.selectOne(null); + assertNotNull(dbTask.getNextNotifyTime()); + assertThat(dbTask) + .extracting("type", "dataId", "status", "notifyTimes", "maxNotifyTimes", + "appId", "merchantOrderId", "notifyUrl") + .containsExactly(type, dataId, PayNotifyStatusEnum.WAITING.getStatus(), 0, 9, + order.getAppId(), order.getMerchantOrderId(), order.getNotifyUrl()); + // 断言,调用 + verify(payNotifyService).executeNotify0(eq(dbTask)); + } + } + + @Test + public void testCreatePayNotifyTask_refund() { + PayNotifyServiceImpl payNotifyService = mock(PayNotifyServiceImpl.class); + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PayNotifyServiceImpl.class))) + .thenReturn(payNotifyService); + + // 准备参数 + Integer type = PayNotifyTypeEnum.REFUND.getType(); + Long dataId = 1L; + // mock 方法(refund) + PayRefundDO refund = randomPojo(PayRefundDO.class); + when(refundService.getRefund(eq(1L))).thenReturn(refund); + // mock 方法(lock) + mockLock(null); // null 的原因,是咱没办法拿到 taskId 新增 + + // 调用 + notifyService.createPayNotifyTask(type, dataId); + // 断言,task + PayNotifyTaskDO dbTask = notifyTaskMapper.selectOne(null); + assertNotNull(dbTask.getNextNotifyTime()); + assertThat(dbTask) + .extracting("type", "dataId", "status", "notifyTimes", "maxNotifyTimes", + "appId", "merchantOrderId", "notifyUrl") + .containsExactly(type, dataId, PayNotifyStatusEnum.WAITING.getStatus(), 0, 9, + refund.getAppId(), refund.getMerchantOrderId(), refund.getNotifyUrl()); + // 断言,调用 + verify(payNotifyService).executeNotify0(eq(dbTask)); + } + } + + @Test + public void testExecuteNotify() throws InterruptedException { + // mock 数据(notify) + PayNotifyTaskDO dbTask01 = randomPojo(PayNotifyTaskDO.class, + o -> o.setStatus(PayNotifyStatusEnum.WAITING.getStatus()) + .setNextNotifyTime(addTime(Duration.ofMinutes(-1)))); + notifyTaskMapper.insert(dbTask01); + PayNotifyTaskDO dbTask02 = randomPojo(PayNotifyTaskDO.class, + o -> o.setStatus(PayNotifyStatusEnum.REQUEST_SUCCESS.getStatus()) + .setNextNotifyTime(addTime(Duration.ofMinutes(-1)))); + notifyTaskMapper.insert(dbTask02); + PayNotifyTaskDO dbTask03 = randomPojo(PayNotifyTaskDO.class, + o -> o.setStatus(PayNotifyStatusEnum.REQUEST_FAILURE.getStatus()) + .setNextNotifyTime(addTime(Duration.ofMinutes(-1)))); + notifyTaskMapper.insert(dbTask03); + PayNotifyTaskDO dbTask04 = randomPojo(PayNotifyTaskDO.class, // 不满足状态 + o -> o.setStatus(PayNotifyStatusEnum.FAILURE.getStatus()) + .setNextNotifyTime(addTime(Duration.ofMinutes(-1)))); + notifyTaskMapper.insert(dbTask04); + PayNotifyTaskDO dbTask05 = randomPojo(PayNotifyTaskDO.class, // 不满足状态 + o -> o.setStatus(PayNotifyStatusEnum.SUCCESS.getStatus()) + .setNextNotifyTime(addTime(Duration.ofMinutes(-1)))); + notifyTaskMapper.insert(dbTask05); + PayNotifyTaskDO dbTask06 = randomPojo(PayNotifyTaskDO.class, // 不满足时间 + o -> o.setStatus(PayNotifyStatusEnum.SUCCESS.getStatus()) + .setNextNotifyTime(addTime(Duration.ofMinutes(1)))); + notifyTaskMapper.insert(dbTask06); + // mock 方法(lock) + mockLock(dbTask01.getId()); + mockLock(dbTask02.getId()); + mockLock(dbTask03.getId()); + + // 调用 + int count = notifyService.executeNotify(); + // 断言,数量 + assertEquals(count, 3); + } + + @Test // 由于 HttpUtil 不好 mock,所以只测试异常的情况 + public void testExecuteNotify0_exception() { + // mock 数据(task) + PayNotifyTaskDO task = randomPojo(PayNotifyTaskDO.class, o -> o.setType(-1) + .setNotifyTimes(0).setMaxNotifyTimes(9)); + notifyTaskMapper.insert(task); + + // 调用 + notifyService.executeNotify0(task); + // 断言,task + PayNotifyTaskDO dbTask = notifyTaskMapper.selectById(task.getId()); + assertNotEquals(task.getNextNotifyTime(), dbTask.getNextNotifyTime()); + assertNotEquals(task.getLastExecuteTime(), dbTask.getNextNotifyTime()); + assertEquals(dbTask.getNotifyTimes(), 1); + assertEquals(dbTask.getStatus(), PayNotifyStatusEnum.REQUEST_FAILURE.getStatus()); + // 断言,log + PayNotifyLogDO dbLog = notifyLogMapper.selectOne(null); + assertEquals(dbLog.getTaskId(), task.getId()); + assertEquals(dbLog.getNotifyTimes(), 1); + assertTrue(dbLog.getResponse().contains("未知的通知任务类型:")); + assertEquals(dbLog.getStatus(), PayNotifyStatusEnum.REQUEST_FAILURE.getStatus()); + } + + @Test + public void testProcessNotifyResult_success() { + // mock 数据(task) + PayNotifyTaskDO task = randomPojo(PayNotifyTaskDO.class, + o -> o.setNotifyTimes(0).setMaxNotifyTimes(9)); + notifyTaskMapper.insert(task); + // 准备参数 + CommonResult invokeResult = CommonResult.success(randomString()); + + // 调用 + notifyService.processNotifyResult(task, invokeResult, null); + // 断言 + PayNotifyTaskDO dbTask = notifyTaskMapper.selectById(task.getId()); + assertEquals(task.getNextNotifyTime(), dbTask.getNextNotifyTime()); + assertNotEquals(task.getLastExecuteTime(), dbTask.getNextNotifyTime()); + assertEquals(dbTask.getNotifyTimes(), 1); + assertEquals(dbTask.getStatus(), PayNotifyStatusEnum.SUCCESS.getStatus()); + } + + @Test + public void testProcessNotifyResult_failure() { + // mock 数据(task) + PayNotifyTaskDO task = randomPojo(PayNotifyTaskDO.class, + o -> o.setNotifyTimes(8).setMaxNotifyTimes(9)); + notifyTaskMapper.insert(task); + // 准备参数 + CommonResult invokeResult = CommonResult.error(BAD_REQUEST); + + // 调用 + notifyService.processNotifyResult(task, invokeResult, null); + // 断言 + PayNotifyTaskDO dbTask = notifyTaskMapper.selectById(task.getId()); + assertEquals(task.getNextNotifyTime(), dbTask.getNextNotifyTime()); + assertNotEquals(task.getLastExecuteTime(), dbTask.getNextNotifyTime()); + assertEquals(dbTask.getNotifyTimes(), 9); + assertEquals(dbTask.getStatus(), PayNotifyStatusEnum.FAILURE.getStatus()); + } + + @Test + public void testProcessNotifyResult_requestFailure() { + // mock 数据(task) + PayNotifyTaskDO task = randomPojo(PayNotifyTaskDO.class, + o -> o.setNotifyTimes(0).setMaxNotifyTimes(9)); + notifyTaskMapper.insert(task); + // 准备参数 + CommonResult invokeResult = CommonResult.error(BAD_REQUEST); + + // 调用 + notifyService.processNotifyResult(task, invokeResult, null); + // 断言 + PayNotifyTaskDO dbTask = notifyTaskMapper.selectById(task.getId()); + assertNotEquals(task.getNextNotifyTime(), dbTask.getNextNotifyTime()); + assertNotEquals(task.getLastExecuteTime(), dbTask.getNextNotifyTime()); + assertEquals(dbTask.getNotifyTimes(), 1); + assertEquals(dbTask.getStatus(), PayNotifyStatusEnum.REQUEST_SUCCESS.getStatus()); + } + + @Test + public void testProcessNotifyResult_requestSuccess() { + // mock 数据(task) + PayNotifyTaskDO task = randomPojo(PayNotifyTaskDO.class, + o -> o.setNotifyTimes(0).setMaxNotifyTimes(9)); + notifyTaskMapper.insert(task); + // 准备参数 + CommonResult invokeResult = CommonResult.error(BAD_REQUEST); + RuntimeException invokeException = new RuntimeException(); + + // 调用 + notifyService.processNotifyResult(task, invokeResult, invokeException); + // 断言 + PayNotifyTaskDO dbTask = notifyTaskMapper.selectById(task.getId()); + assertNotEquals(task.getNextNotifyTime(), dbTask.getNextNotifyTime()); + assertNotEquals(task.getLastExecuteTime(), dbTask.getNextNotifyTime()); + assertEquals(dbTask.getNotifyTimes(), 1); + assertEquals(dbTask.getStatus(), PayNotifyStatusEnum.REQUEST_FAILURE.getStatus()); + } + + @Test + public void testGetNotifyTask() { + // mock 数据(task) + PayNotifyTaskDO task = randomPojo(PayNotifyTaskDO.class); + notifyTaskMapper.insert(task); + // 准备参数 + Long id = task.getId(); + + // 调用 + PayNotifyTaskDO dbTask = notifyService.getNotifyTask(id); + // 断言 + assertPojoEquals(dbTask, task); + } + + @Test + public void testGetNotifyTaskPage() { + // mock 数据 + PayNotifyTaskDO dbTask = randomPojo(PayNotifyTaskDO.class, o -> { // 等会查询到 + o.setAppId(1L); + o.setType(PayNotifyTypeEnum.REFUND.getType()); + o.setDataId(100L); + o.setStatus(PayNotifyStatusEnum.SUCCESS.getStatus()); + o.setMerchantOrderId("P110"); + o.setCreateTime(buildTime(2023, 2, 3)); + }); + notifyTaskMapper.insert(dbTask); + // 测试 appId 不匹配 + notifyTaskMapper.insert(cloneIgnoreId(dbTask, o -> o.setAppId(2L))); + // 测试 type 不匹配 + notifyTaskMapper.insert(cloneIgnoreId(dbTask, o -> o.setType(PayNotifyTypeEnum.ORDER.getType()))); + // 测试 dataId 不匹配 + notifyTaskMapper.insert(cloneIgnoreId(dbTask, o -> o.setDataId(200L))); + // 测试 status 不匹配 + notifyTaskMapper.insert(cloneIgnoreId(dbTask, o -> o.setStatus(PayNotifyStatusEnum.FAILURE.getStatus()))); + // 测试 merchantOrderId 不匹配 + notifyTaskMapper.insert(cloneIgnoreId(dbTask, o -> o.setMerchantOrderId(randomString()))); + // 测试 createTime 不匹配 + notifyTaskMapper.insert(cloneIgnoreId(dbTask, o -> o.setCreateTime(buildTime(2023, 1, 1)))); + // 准备参数 + PayNotifyTaskPageReqVO reqVO = new PayNotifyTaskPageReqVO(); + reqVO.setAppId(1L); + reqVO.setType(PayNotifyTypeEnum.REFUND.getType()); + reqVO.setDataId(100L); + reqVO.setStatus(PayNotifyStatusEnum.SUCCESS.getStatus()); + reqVO.setMerchantOrderId("P110"); + reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28)); + + // 调用 + PageResult pageResult = notifyService.getNotifyTaskPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbTask, pageResult.getList().get(0)); + } + + @Test + public void testGetNotifyLogList() { + // mock 数据 + PayNotifyLogDO dbLog = randomPojo(PayNotifyLogDO.class); + notifyLogMapper.insert(dbLog); + PayNotifyLogDO dbLog02 = randomPojo(PayNotifyLogDO.class); + notifyLogMapper.insert(dbLog02); + // 准备参数 + Long taskId = dbLog.getTaskId(); + + // 调用 + List logList = notifyService.getNotifyLogList(taskId); + // 断言 + assertEquals(logList.size(), 1); + assertPojoEquals(dbLog, logList.get(0)); + } + + private void mockLock(Long id) { + RLock lock = mock(RLock.class); + if (id == null) { + when(redissonClient.getLock(anyString())) + .thenReturn(lock); + } else { + when(redissonClient.getLock(eq("pay_notify:lock:" + id))) + .thenReturn(lock); + } + } + +} diff --git a/aagro-module-pay/src/test/java/cn/aagro/pp/module/pay/service/order/PayOrderServiceTest.java b/aagro-module-pay/src/test/java/cn/aagro/pp/module/pay/service/order/PayOrderServiceTest.java new file mode 100644 index 0000000..a7819d7 --- /dev/null +++ b/aagro-module-pay/src/test/java/cn/aagro/pp/module/pay/service/order/PayOrderServiceTest.java @@ -0,0 +1,1108 @@ +package cn.aagro.pp.module.pay.service.order; + +import cn.hutool.extra.spring.SpringUtil; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.test.core.ut.BaseDbAndRedisUnitTest; +import cn.aagro.pp.module.pay.api.order.dto.PayOrderCreateReqDTO; +import cn.aagro.pp.module.pay.controller.admin.order.vo.PayOrderExportReqVO; +import cn.aagro.pp.module.pay.controller.admin.order.vo.PayOrderPageReqVO; +import cn.aagro.pp.module.pay.controller.admin.order.vo.PayOrderSubmitReqVO; +import cn.aagro.pp.module.pay.controller.admin.order.vo.PayOrderSubmitRespVO; +import cn.aagro.pp.module.pay.dal.dataobject.app.PayAppDO; +import cn.aagro.pp.module.pay.dal.dataobject.channel.PayChannelDO; +import cn.aagro.pp.module.pay.dal.dataobject.order.PayOrderDO; +import cn.aagro.pp.module.pay.dal.dataobject.order.PayOrderExtensionDO; +import cn.aagro.pp.module.pay.dal.mysql.order.PayOrderExtensionMapper; +import cn.aagro.pp.module.pay.dal.mysql.order.PayOrderMapper; +import cn.aagro.pp.module.pay.dal.redis.no.PayNoRedisDAO; +import cn.aagro.pp.module.pay.enums.PayChannelEnum; +import cn.aagro.pp.module.pay.enums.notify.PayNotifyTypeEnum; +import cn.aagro.pp.module.pay.enums.order.PayOrderStatusEnum; +import cn.aagro.pp.module.pay.framework.pay.config.PayProperties; +import cn.aagro.pp.module.pay.framework.pay.core.client.PayClient; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO; +import cn.aagro.pp.module.pay.framework.pay.core.enums.PayOrderDisplayModeEnum; +import cn.aagro.pp.module.pay.service.app.PayAppService; +import cn.aagro.pp.module.pay.service.channel.PayChannelService; +import cn.aagro.pp.module.pay.service.notify.PayNotifyService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.List; + +import static cn.aagro.pp.framework.common.util.date.LocalDateTimeUtils.*; +import static cn.aagro.pp.framework.common.util.json.JsonUtils.toJsonString; +import static cn.aagro.pp.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static cn.aagro.pp.framework.test.core.util.AssertUtils.assertPojoEquals; +import static cn.aagro.pp.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.aagro.pp.framework.test.core.util.RandomUtils.*; +import static cn.aagro.pp.module.pay.enums.ErrorCodeConstants.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +/** + * {@link PayOrderServiceImpl} 的单元测试类 + * + * @author 芋艿 + */ +@Import({PayOrderServiceImpl.class, PayNoRedisDAO.class}) +public class PayOrderServiceTest extends BaseDbAndRedisUnitTest { + + @Resource + private PayOrderServiceImpl orderService; + + @Resource + private PayOrderMapper orderMapper; + @Resource + private PayOrderExtensionMapper orderExtensionMapper; + + @MockBean + private PayProperties properties; + @MockBean + private PayAppService appService; + @MockBean + private PayChannelService channelService; + @MockBean + private PayNotifyService notifyService; + + @BeforeEach + public void setUp() { + when(properties.getOrderNotifyUrl()).thenReturn("http://127.0.0.1"); + } + + @Test + public void testGetOrder_id() { + // mock 数据(PayOrderDO) + PayOrderDO order = randomPojo(PayOrderDO.class); + orderMapper.insert(order); + // 准备参数 + Long id = order.getId(); + + // 调用 + PayOrderDO dbOrder = orderService.getOrder(id); + // 断言 + assertPojoEquals(dbOrder, order); + } + + @Test + public void testGetOrder_appIdAndMerchantOrderId() { + // mock 数据(PayOrderDO) + PayOrderDO order = randomPojo(PayOrderDO.class); + orderMapper.insert(order); + // 准备参数 + Long appId = order.getAppId(); + String merchantOrderId = order.getMerchantOrderId(); + + // 调用 + PayOrderDO dbOrder = orderService.getOrder(appId, merchantOrderId); + // 断言 + assertPojoEquals(dbOrder, order); + } + + @Test + public void testGetOrderCountByAppId() { + // mock 数据(PayOrderDO) + PayOrderDO order01 = randomPojo(PayOrderDO.class); + orderMapper.insert(order01); + PayOrderDO order02 = randomPojo(PayOrderDO.class); + orderMapper.insert(order02); + // 准备参数 + Long appId = order01.getAppId(); + + // 调用 + Long count = orderService.getOrderCountByAppId(appId); + // 断言 + assertEquals(count, 1L); + } + + @Test + public void testGetOrderPage() { + // mock 数据 + PayOrderDO dbOrder = randomPojo(PayOrderDO.class, o -> { // 等会查询到 + o.setAppId(1L); + o.setChannelCode(PayChannelEnum.WX_PUB.getCode()); + o.setMerchantOrderId("110"); + o.setChannelOrderNo("220"); + o.setNo("330"); + o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()); + o.setCreateTime(buildTime(2018, 1, 15)); + }); + orderMapper.insert(dbOrder); + // 测试 appId 不匹配 + orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setAppId(2L))); + // 测试 channelCode 不匹配 + orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setChannelCode(PayChannelEnum.ALIPAY_APP.getCode()))); + // 测试 merchantOrderId 不匹配 + orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setMerchantOrderId(randomString()))); + // 测试 channelOrderNo 不匹配 + orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setChannelOrderNo(randomString()))); + // 测试 no 不匹配 + orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setNo(randomString()))); + // 测试 status 不匹配 + orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setStatus(PayOrderStatusEnum.CLOSED.getStatus()))); + // 测试 createTime 不匹配 + orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setCreateTime(buildTime(2019, 1, 1)))); + // 准备参数 + PayOrderPageReqVO reqVO = new PayOrderPageReqVO(); + reqVO.setAppId(1L); + reqVO.setChannelCode(PayChannelEnum.WX_PUB.getCode()); + reqVO.setMerchantOrderId("11"); + reqVO.setChannelOrderNo("22"); + reqVO.setNo("33"); + reqVO.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()); + reqVO.setCreateTime(buildBetweenTime(2018, 1, 10, 2018, 1, 30)); + + // 调用 + PageResult pageResult = orderService.getOrderPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbOrder, pageResult.getList().get(0)); + } + + @Test + public void testGetOrderList() { + // mock 数据 + PayOrderDO dbOrder = randomPojo(PayOrderDO.class, o -> { // 等会查询到 + o.setAppId(1L); + o.setChannelCode(PayChannelEnum.WX_PUB.getCode()); + o.setMerchantOrderId("110"); + o.setChannelOrderNo("220"); + o.setNo("330"); + o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()); + o.setCreateTime(buildTime(2018, 1, 15)); + }); + orderMapper.insert(dbOrder); + // 测试 appId 不匹配 + orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setAppId(2L))); + // 测试 channelCode 不匹配 + orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setChannelCode(PayChannelEnum.ALIPAY_APP.getCode()))); + // 测试 merchantOrderId 不匹配 + orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setMerchantOrderId(randomString()))); + // 测试 channelOrderNo 不匹配 + orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setChannelOrderNo(randomString()))); + // 测试 no 不匹配 + orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setNo(randomString()))); + // 测试 status 不匹配 + orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setStatus(PayOrderStatusEnum.CLOSED.getStatus()))); + // 测试 createTime 不匹配 + orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setCreateTime(buildTime(2019, 1, 1)))); + // 准备参数 + PayOrderExportReqVO reqVO = new PayOrderExportReqVO(); + reqVO.setAppId(1L); + reqVO.setChannelCode(PayChannelEnum.WX_PUB.getCode()); + reqVO.setMerchantOrderId("11"); + reqVO.setChannelOrderNo("22"); + reqVO.setNo("33"); + reqVO.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()); + reqVO.setCreateTime(buildBetweenTime(2018, 1, 10, 2018, 1, 30)); + + // 调用 + List list = orderService.getOrderList(reqVO); + // 断言 + assertEquals(1, list.size()); + assertPojoEquals(dbOrder, list.get(0)); + } + + @Test + public void testCreateOrder_success() { + // mock 参数 + PayOrderCreateReqDTO reqDTO = randomPojo(PayOrderCreateReqDTO.class, + o -> o.setAppKey("demo").setMerchantOrderId("10") + .setSubject(randomString()).setBody(randomString())); + // mock 方法 + PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L).setOrderNotifyUrl("http://127.0.0.1")); + when(appService.validPayApp(eq(reqDTO.getAppKey()))).thenReturn(app); + + // 调用 + Long orderId = orderService.createOrder(reqDTO); + // 断言 + PayOrderDO order = orderMapper.selectById(orderId); + assertPojoEquals(order, reqDTO); + assertEquals(order.getAppId(), 1L); + assertEquals(order.getNotifyUrl(), "http://127.0.0.1"); + assertEquals(order.getStatus(), PayOrderStatusEnum.WAITING.getStatus()); + assertEquals(order.getRefundPrice(), 0); + } + + @Test + public void testCreateOrder_exists() { + // mock 参数 + PayOrderCreateReqDTO reqDTO = randomPojo(PayOrderCreateReqDTO.class, + o -> o.setAppKey("demo").setMerchantOrderId("10")); + // mock 数据 + PayOrderDO dbOrder = randomPojo(PayOrderDO.class, o -> o.setAppId(1L).setMerchantOrderId("10")); + orderMapper.insert(dbOrder); + // mock 方法 + PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L).setOrderNotifyUrl("http://127.0.0.1")); + when(appService.validPayApp(eq(reqDTO.getAppKey()))).thenReturn(app); + + // 调用 + Long orderId = orderService.createOrder(reqDTO); + // 断言 + PayOrderDO order = orderMapper.selectById(orderId); + assertPojoEquals(dbOrder, order); + } + + @Test + public void testSubmitOrder_notFound() { + // 准备参数 + PayOrderSubmitReqVO reqVO = randomPojo(PayOrderSubmitReqVO.class); + String userIp = randomString(); + + // 调用, 并断言异常 + assertServiceException(() -> orderService.submitOrder(reqVO, userIp), PAY_ORDER_NOT_FOUND); + } + + @Test + public void testSubmitOrder_notWaiting() { + // mock 数据(order) + PayOrderDO order = randomPojo(PayOrderDO.class, o -> o.setStatus(PayOrderStatusEnum.REFUND.getStatus())); + orderMapper.insert(order); + // 准备参数 + PayOrderSubmitReqVO reqVO = randomPojo(PayOrderSubmitReqVO.class, o -> o.setId(order.getId())); + String userIp = randomString(); + + // 调用, 并断言异常 + assertServiceException(() -> orderService.submitOrder(reqVO, userIp), PAY_ORDER_STATUS_IS_NOT_WAITING); + } + + @Test + public void testSubmitOrder_isSuccess() { + // mock 数据(order) + PayOrderDO order = randomPojo(PayOrderDO.class, o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus())); + orderMapper.insert(order); + // 准备参数 + PayOrderSubmitReqVO reqVO = randomPojo(PayOrderSubmitReqVO.class, o -> o.setId(order.getId())); + String userIp = randomString(); + + // 调用, 并断言异常 + assertServiceException(() -> orderService.submitOrder(reqVO, userIp), PAY_ORDER_STATUS_IS_SUCCESS); + } + + @Test + public void testSubmitOrder_expired() { + // mock 数据(order) + PayOrderDO order = randomPojo(PayOrderDO.class, o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setExpireTime(addTime(Duration.ofDays(-1)))); + orderMapper.insert(order); + // 准备参数 + PayOrderSubmitReqVO reqVO = randomPojo(PayOrderSubmitReqVO.class, o -> o.setId(order.getId())); + String userIp = randomString(); + + // 调用, 并断言异常 + assertServiceException(() -> orderService.submitOrder(reqVO, userIp), PAY_ORDER_IS_EXPIRED); + } + + @Test + public void testSubmitOrder_channelNotFound() { + // mock 数据(order) + PayOrderDO order = randomPojo(PayOrderDO.class, o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setAppId(1L).setExpireTime(addTime(Duration.ofDays(1)))); + orderMapper.insert(order); + // 准备参数 + PayOrderSubmitReqVO reqVO = randomPojo(PayOrderSubmitReqVO.class, o -> o.setId(order.getId()) + .setChannelCode(PayChannelEnum.ALIPAY_APP.getCode())); + String userIp = randomString(); + // mock 方法(app) + PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L)); + when(appService.validPayApp(eq(1L))).thenReturn(app); + // mock 方法(channel) + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setCode(PayChannelEnum.ALIPAY_APP.getCode())); + when(channelService.validPayChannel(eq(1L), eq(PayChannelEnum.ALIPAY_APP.getCode()))) + .thenReturn(channel); + + // 调用, 并断言异常 + assertServiceException(() -> orderService.submitOrder(reqVO, userIp), CHANNEL_NOT_FOUND); + } + + @Test // 调用 unifiedOrder 接口,返回存在渠道错误 + public void testSubmitOrder_channelError() { + PayOrderServiceImpl payOrderServiceImpl = mock(PayOrderServiceImpl.class); + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PayOrderServiceImpl.class))) + .thenReturn(payOrderServiceImpl); + + // mock 数据(order) + PayOrderDO order = randomPojo(PayOrderDO.class, o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setAppId(1L).setExpireTime(addTime(Duration.ofDays(1)))); + orderMapper.insert(order); + // 准备参数 + PayOrderSubmitReqVO reqVO = randomPojo(PayOrderSubmitReqVO.class, o -> o.setId(order.getId()) + .setChannelCode(PayChannelEnum.ALIPAY_APP.getCode())); + String userIp = randomString(); + // mock 方法(app) + PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L)); + when(appService.validPayApp(eq(1L))).thenReturn(app); + // mock 方法(channel) + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L) + .setCode(PayChannelEnum.ALIPAY_APP.getCode())); + when(channelService.validPayChannel(eq(1L), eq(PayChannelEnum.ALIPAY_APP.getCode()))) + .thenReturn(channel); + // mock 方法(client) + PayClient client = mock(PayClient.class); + when(channelService.getPayClient(eq(10L))).thenReturn(client); + // mock 方法() + PayOrderRespDTO unifiedOrderResp = randomPojo(PayOrderRespDTO.class, o -> + o.setChannelErrorCode("001").setChannelErrorMsg("模拟异常")); + when(client.unifiedOrder(argThat(payOrderUnifiedReqDTO -> { + assertNotNull(payOrderUnifiedReqDTO.getOutTradeNo()); + assertThat(payOrderUnifiedReqDTO) +// .extracting("subject", "body", "notifyUrl", "returnUrl", "price", "expireTime") // TODO @芋艿:win11 下,时间不太准 + .extracting("subject", "body", "notifyUrl", "returnUrl", "price") + .containsExactly(order.getSubject(), order.getBody(), "http://127.0.0.1/10", +// reqVO.getReturnUrl(), order.getPrice(), order.getExpireTime()); + reqVO.getReturnUrl(), order.getPrice()); + return true; + }))).thenReturn(unifiedOrderResp); + + // 调用,并断言异常 + assertServiceException(() -> orderService.submitOrder(reqVO, userIp), + PAY_ORDER_SUBMIT_CHANNEL_ERROR, "001", "模拟异常"); + // 断言,数据记录(PayOrderExtensionDO) + PayOrderExtensionDO orderExtension = orderExtensionMapper.selectOne(null); + assertNotNull(orderExtension); + assertThat(orderExtension).extracting("no", "orderId").isNotNull(); + assertThat(orderExtension) + .extracting("channelId", "channelCode","userIp" ,"status", "channelExtras", + "channelErrorCode", "channelErrorMsg", "channelNotifyData") + .containsExactly(10L, PayChannelEnum.ALIPAY_APP.getCode(), userIp, + PayOrderStatusEnum.WAITING.getStatus(), reqVO.getChannelExtras(), + null, null, null); + } + } + + @Test + public void testSubmitOrder_success() { + PayOrderServiceImpl payOrderServiceImpl = mock(PayOrderServiceImpl.class); + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PayOrderServiceImpl.class))) + .thenReturn(payOrderServiceImpl); + + // mock 数据(order) + PayOrderDO order = randomPojo(PayOrderDO.class, o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setAppId(1L).setExpireTime(addTime(Duration.ofDays(1)))); + orderMapper.insert(order); + // 准备参数 + PayOrderSubmitReqVO reqVO = randomPojo(PayOrderSubmitReqVO.class, o -> o.setId(order.getId()) + .setChannelCode(PayChannelEnum.ALIPAY_APP.getCode())); + String userIp = randomString(); + // mock 方法(app) + PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L)); + when(appService.validPayApp(eq(1L))).thenReturn(app); + // mock 方法(channel) + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L) + .setCode(PayChannelEnum.ALIPAY_APP.getCode())); + when(channelService.validPayChannel(eq(1L), eq(PayChannelEnum.ALIPAY_APP.getCode()))) + .thenReturn(channel); + // mock 方法(client) + PayClient client = mock(PayClient.class); + when(channelService.getPayClient(eq(10L))).thenReturn(client); + // mock 方法(支付渠道的调用) + PayOrderRespDTO unifiedOrderResp = randomPojo(PayOrderRespDTO.class, o -> o.setChannelErrorCode(null).setChannelErrorMsg(null) + .setDisplayMode(PayOrderDisplayModeEnum.URL.getMode()).setDisplayContent("tudou")); + when(client.unifiedOrder(argThat(payOrderUnifiedReqDTO -> { + assertNotNull(payOrderUnifiedReqDTO.getOutTradeNo()); + assertThat(payOrderUnifiedReqDTO) +// .extracting("subject", "body", "notifyUrl", "returnUrl", "price", "expireTime") // TODO @芋艿:win11 下,时间不太准 + .extracting("subject", "body", "notifyUrl", "returnUrl", "price") + .containsExactly(order.getSubject(), order.getBody(), "http://127.0.0.1/10", +// reqVO.getReturnUrl(), order.getPrice(), order.getExpireTime()); + reqVO.getReturnUrl(), order.getPrice()); + return true; + }))).thenReturn(unifiedOrderResp); + + // 调用 + PayOrderSubmitRespVO result = orderService.submitOrder(reqVO, userIp); + // 断言,数据记录(PayOrderExtensionDO) + PayOrderExtensionDO orderExtension = orderExtensionMapper.selectOne(null); + assertNotNull(orderExtension); + assertThat(orderExtension).extracting("no", "orderId").isNotNull(); + assertThat(orderExtension) + .extracting("channelId", "channelCode","userIp" ,"status", "channelExtras", + "channelErrorCode", "channelErrorMsg", "channelNotifyData") + .containsExactly(10L, PayChannelEnum.ALIPAY_APP.getCode(), userIp, + PayOrderStatusEnum.WAITING.getStatus(), reqVO.getChannelExtras(), + null, null, null); + // 断言,返回(PayOrderSubmitRespVO) + assertThat(result) + .extracting("status", "displayMode", "displayContent") + .containsExactly(PayOrderStatusEnum.WAITING.getStatus(), PayOrderDisplayModeEnum.URL.getMode(), "tudou"); + // 断言,调用 + verify(payOrderServiceImpl).notifyOrder(same(channel), same(unifiedOrderResp)); + } + } + + @Test + public void testValidateOrderActuallyPaid_dbPaid() { + // 准备参数 + Long id = randomLongId(); + // mock 方法(OrderExtension 已支付) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setOrderId(id).setStatus(PayOrderStatusEnum.SUCCESS.getStatus())); + orderExtensionMapper.insert(orderExtension); + + // 调用,并断言异常 + assertServiceException(() -> orderService.validateOrderActuallyPaid(id), + PAY_ORDER_EXTENSION_IS_PAID); + } + + @Test + public void testValidateOrderActuallyPaid_remotePaid() { + // 准备参数 + Long id = randomLongId(); + // mock 方法(OrderExtension 已支付) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setOrderId(id).setStatus(PayOrderStatusEnum.WAITING.getStatus())); + orderExtensionMapper.insert(orderExtension); + // mock 方法(PayClient 已支付) + PayClient client = mock(PayClient.class); + when(channelService.getPayClient(eq(orderExtension.getChannelId()))).thenReturn(client); + when(client.getOrder(eq(orderExtension.getNo()))).thenReturn(randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()))); + + // 调用,并断言异常 + assertServiceException(() -> orderService.validateOrderActuallyPaid(id), + PAY_ORDER_EXTENSION_IS_PAID); + } + + @Test + public void testValidateOrderActuallyPaid_success() { + // 准备参数 + Long id = randomLongId(); + // mock 方法(OrderExtension 已支付) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setOrderId(id).setStatus(PayOrderStatusEnum.WAITING.getStatus())); + orderExtensionMapper.insert(orderExtension); + // mock 方法(PayClient 已支付) + PayClient client = mock(PayClient.class); + when(channelService.getPayClient(eq(orderExtension.getChannelId()))).thenReturn(client); + when(client.getOrder(eq(orderExtension.getNo()))).thenReturn(randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()))); + + // 调用,并断言异常 + orderService.validateOrderActuallyPaid(id); + } + + @Test + public void testNotifyOrder_channelId() { + PayOrderServiceImpl payOrderServiceImpl = mock(PayOrderServiceImpl.class); + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PayOrderServiceImpl.class))) + .thenReturn(payOrderServiceImpl); + // 准备参数 + Long channelId = 10L; + PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class); + // mock 方法(channel) + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)); + when(channelService.validPayChannel(eq(10L))).thenReturn(channel); + + // 调用 + orderService.notifyOrder(channelId, notify); + // 断言 + verify(payOrderServiceImpl).notifyOrder(same(channel), same(notify)); + } + } + + @Test + public void testNotifyOrderSuccess_orderExtension_notFound() { + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)); + PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus())); + + // 调用,并断言异常 + assertServiceException(() -> orderService.notifyOrder(channel, notify), + PAY_ORDER_EXTENSION_NOT_FOUND); + } + + @Test + public void testNotifyOrderSuccess_orderExtension_closed() { + // mock 数据(PayOrderExtensionDO) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.CLOSED.getStatus()) + .setNo("P110")); + orderExtensionMapper.insert(orderExtension); + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)); + PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()) + .setOutTradeNo("P110")); + + // 调用,并断言异常 + assertServiceException(() -> orderService.notifyOrder(channel, notify), + PAY_ORDER_EXTENSION_STATUS_IS_NOT_WAITING); + } + + @Test + public void testNotifyOrderSuccess_order_notFound() { + // mock 数据(PayOrderExtensionDO) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()) + .setNo("P110")); + orderExtensionMapper.insert(orderExtension); + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)); + PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()) + .setOutTradeNo("P110")); + + // 调用,并断言异常 + assertServiceException(() -> orderService.notifyOrder(channel, notify), + PAY_ORDER_NOT_FOUND); + // 断言 PayOrderExtensionDO :数据更新被回滚 + assertPojoEquals(orderExtension, orderExtensionMapper.selectOne(null)); + } + + @Test + public void testNotifyOrderSuccess_order_closed() { + testNotifyOrderSuccess_order_closedOrRefund(PayOrderStatusEnum.CLOSED.getStatus()); + } + + @Test + public void testNotifyOrderSuccess_order_refund() { + testNotifyOrderSuccess_order_closedOrRefund(PayOrderStatusEnum.REFUND.getStatus()); + } + + private void testNotifyOrderSuccess_order_closedOrRefund(Integer status) { + // mock 数据(PayOrderDO) + PayOrderDO order = randomPojo(PayOrderDO.class, o -> o.setStatus(status)); + orderMapper.insert(order); + // mock 数据(PayOrderExtensionDO) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()) + .setNo("P110") + .setOrderId(order.getId())); + orderExtensionMapper.insert(orderExtension); + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)); + PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()) + .setOutTradeNo("P110")); + + // 调用,并断言异常 + assertServiceException(() -> orderService.notifyOrder(channel, notify), + PAY_ORDER_STATUS_IS_NOT_WAITING); + // 断言 PayOrderExtensionDO :数据未更新,因为它是 SUCCESS + assertPojoEquals(orderExtension, orderExtensionMapper.selectOne(null)); + } + + @Test + public void testNotifyOrderSuccess_order_paid() { + // mock 数据(PayOrderDO) + PayOrderDO order = randomPojo(PayOrderDO.class, + o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus())); + orderMapper.insert(order); + // mock 数据(PayOrderExtensionDO) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()) + .setNo("P110") + .setOrderId(order.getId())); + orderExtensionMapper.insert(orderExtension); + // 重要:需要将 order 的 extensionId 更新下 + order.setExtensionId(orderExtension.getId()); + orderMapper.updateById(order); + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)); + PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()) + .setOutTradeNo("P110")); + + // 调用,并断言异常 + orderService.notifyOrder(channel, notify); + // 断言 PayOrderExtensionDO :数据未更新,因为它是 SUCCESS + assertPojoEquals(orderExtension, orderExtensionMapper.selectOne(null)); + // 断言 PayOrderDO :数据未更新,因为它是 SUCCESS + assertPojoEquals(order, orderMapper.selectOne(null), "updateTime", "updater"); + // 断言,调用 + verify(notifyService, never()).createPayNotifyTask(anyInt(), anyLong()); + } + + @Test + public void testNotifyOrderSuccess_order_waiting() { + // mock 数据(PayOrderDO) + PayOrderDO order = randomPojo(PayOrderDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setPrice(10)); + orderMapper.insert(order); + // mock 数据(PayOrderExtensionDO) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setNo("P110") + .setOrderId(order.getId())); + orderExtensionMapper.insert(orderExtension); + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L) + .setFeeRate(10D)); + PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()) + .setOutTradeNo("P110")); + + // 调用,并断言异常 + orderService.notifyOrder(channel, notify); + // 断言 PayOrderExtensionDO :数据未更新,因为它是 SUCCESS + orderExtension.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()) + .setChannelNotifyData(toJsonString(notify)); + assertPojoEquals(orderExtension, orderExtensionMapper.selectOne(null), + "updateTime", "updater"); + // 断言 PayOrderDO :数据未更新,因为它是 SUCCESS + order.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()) + .setChannelId(10L).setChannelCode(channel.getCode()) + .setSuccessTime(notify.getSuccessTime()).setExtensionId(orderExtension.getId()).setNo(orderExtension.getNo()) + .setChannelOrderNo(notify.getChannelOrderNo()).setChannelUserId(notify.getChannelUserId()) + .setChannelFeeRate(10D).setChannelFeePrice(1); + assertPojoEquals(order, orderMapper.selectOne(null), + "updateTime", "updater"); + // 断言,调用 + verify(notifyService).createPayNotifyTask(eq(PayNotifyTypeEnum.ORDER.getType()), + eq(orderExtension.getOrderId())); + } + + @Test + public void testNotifyOrderClosed_orderExtension_notFound() { + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)); + PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusEnum.CLOSED.getStatus())); + + // 调用,并断言异常 + assertServiceException(() -> orderService.notifyOrder(channel, notify), + PAY_ORDER_EXTENSION_NOT_FOUND); + } + + @Test + public void testNotifyOrderClosed_orderExtension_closed() { + // mock 数据(PayOrderExtensionDO) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.CLOSED.getStatus()) + .setNo("P110")); + orderExtensionMapper.insert(orderExtension); + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)); + PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusEnum.CLOSED.getStatus()) + .setOutTradeNo("P110")); + + // 调用,并断言 + orderService.notifyOrder(channel, notify); + // 断言 PayOrderExtensionDO :数据未更新,因为它是 CLOSED + assertPojoEquals(orderExtension, orderExtensionMapper.selectOne(null)); + } + + @Test + public void testNotifyOrderClosed_orderExtension_paid() { + // mock 数据(PayOrderExtensionDO) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()) + .setNo("P110")); + orderExtensionMapper.insert(orderExtension); + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)); + PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusEnum.CLOSED.getStatus()) + .setOutTradeNo("P110")); + + // 调用,并断言 + orderService.notifyOrder(channel, notify); + // 断言 PayOrderExtensionDO :数据未更新,因为它是 SUCCESS + assertPojoEquals(orderExtension, orderExtensionMapper.selectOne(null)); + } + + @Test + public void testNotifyOrderClosed_orderExtension_refund() { + // mock 数据(PayOrderExtensionDO) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.REFUND.getStatus()) + .setNo("P110")); + orderExtensionMapper.insert(orderExtension); + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)); + PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusEnum.CLOSED.getStatus()) + .setOutTradeNo("P110")); + + // 调用,并断言异常 + assertServiceException(() -> orderService.notifyOrder(channel, notify), + PAY_ORDER_EXTENSION_STATUS_IS_NOT_WAITING); + } + + @Test + public void testNotifyOrderClosed_orderExtension_waiting() { + // mock 数据(PayOrderExtensionDO) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setNo("P110")); + orderExtensionMapper.insert(orderExtension); + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)); + PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusEnum.CLOSED.getStatus()) + .setOutTradeNo("P110")); + + // 调用 + orderService.notifyOrder(channel, notify); + // 断言 PayOrderExtensionDO + orderExtension.setStatus(PayOrderStatusEnum.CLOSED.getStatus()).setChannelNotifyData(toJsonString(notify)) + .setChannelErrorCode(notify.getChannelErrorCode()).setChannelErrorMsg(notify.getChannelErrorMsg()); + assertPojoEquals(orderExtension, orderExtensionMapper.selectOne(null), + "updateTime", "updater"); + } + + @Test + public void testUpdateOrderRefundPrice_notFound() { + // 准备参数 + Long id = randomLongId(); + Integer incrRefundPrice = randomInteger(); + + // 调用,并断言异常 + assertServiceException(() -> orderService.updateOrderRefundPrice(id, incrRefundPrice), + PAY_ORDER_NOT_FOUND); + } + + @Test + public void testUpdateOrderRefundPrice_waiting() { + testUpdateOrderRefundPrice_waitingOrClosed(PayOrderStatusEnum.WAITING.getStatus()); + } + + @Test + public void testUpdateOrderRefundPrice_closed() { + testUpdateOrderRefundPrice_waitingOrClosed(PayOrderStatusEnum.CLOSED.getStatus()); + } + + private void testUpdateOrderRefundPrice_waitingOrClosed(Integer status) { + // mock 数据(PayOrderDO) + PayOrderDO order = randomPojo(PayOrderDO.class, + o -> o.setStatus(status)); + orderMapper.insert(order); + // 准备参数 + Long id = order.getId(); + Integer incrRefundPrice = randomInteger(); + + // 调用,并断言异常 + assertServiceException(() -> orderService.updateOrderRefundPrice(id, incrRefundPrice), + PAY_ORDER_REFUND_FAIL_STATUS_ERROR); + } + + @Test + public void testUpdateOrderRefundPrice_priceExceed() { + // mock 数据(PayOrderDO) + PayOrderDO order = randomPojo(PayOrderDO.class, + o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()) + .setRefundPrice(1).setPrice(10)); + orderMapper.insert(order); + // 准备参数 + Long id = order.getId(); + Integer incrRefundPrice = 10; + + // 调用,并断言异常 + assertServiceException(() -> orderService.updateOrderRefundPrice(id, incrRefundPrice), + REFUND_PRICE_EXCEED); + } + + @Test + public void testUpdateOrderRefundPrice_refund() { + testUpdateOrderRefundPrice_refundOrSuccess(PayOrderStatusEnum.REFUND.getStatus()); + } + + @Test + public void testUpdateOrderRefundPrice_success() { + testUpdateOrderRefundPrice_refundOrSuccess(PayOrderStatusEnum.SUCCESS.getStatus()); + } + + private void testUpdateOrderRefundPrice_refundOrSuccess(Integer status) { + // mock 数据(PayOrderDO) + PayOrderDO order = randomPojo(PayOrderDO.class, + o -> o.setStatus(status).setRefundPrice(1).setPrice(10)); + orderMapper.insert(order); + // 准备参数 + Long id = order.getId(); + Integer incrRefundPrice = 8; + + // 调用 + orderService.updateOrderRefundPrice(id, incrRefundPrice); + // 断言 + order.setRefundPrice(9).setStatus(PayOrderStatusEnum.REFUND.getStatus()); + assertPojoEquals(order, orderMapper.selectOne(null), + "updateTime", "updater"); + } + + @Test + public void testGetOrderExtension() { + // mock 数据(PayOrderExtensionDO) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class); + orderExtensionMapper.insert(orderExtension); + // 准备参数 + Long id = orderExtension.getId(); + + // 调用 + PayOrderExtensionDO dbOrderExtension = orderService.getOrderExtension(id); + // 断言 + assertPojoEquals(dbOrderExtension, orderExtension); + } + + @Test + public void testSyncOrder_payClientNotFound() { + // 准备参数 + LocalDateTime minCreateTime = LocalDateTime.now().minus(Duration.ofMinutes(10)); + // mock 数据(PayOrderExtensionDO) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setCreateTime(LocalDateTime.now())); + orderExtensionMapper.insert(orderExtension); + + // 调用 + int count = orderService.syncOrder(minCreateTime); + // 断言 + assertEquals(count, 0); + } + + @Test + public void testSyncOrder_exception() { + // 准备参数 + LocalDateTime minCreateTime = LocalDateTime.now().minus(Duration.ofMinutes(10)); + // mock 数据(PayOrderExtensionDO) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setChannelId(10L) + .setCreateTime(LocalDateTime.now())); + orderExtensionMapper.insert(orderExtension); + // mock 方法(PayClient) + PayClient client = mock(PayClient.class); + when(channelService.getPayClient(eq(10L))).thenReturn(client); + // mock 方法(PayClient 异常) + when(client.getOrder(any())).thenThrow(new RuntimeException()); + + // 调用 + int count = orderService.syncOrder(minCreateTime); + // 断言 + assertEquals(count, 0); + } + + @Test + public void testSyncOrder_orderSuccess() { + PayOrderServiceImpl payOrderServiceImpl = mock(PayOrderServiceImpl.class); + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PayOrderServiceImpl.class))) + .thenReturn(payOrderServiceImpl); + + // 准备参数 + LocalDateTime minCreateTime = LocalDateTime.now().minus(Duration.ofMinutes(10)); + // mock 数据(PayOrderExtensionDO) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setChannelId(10L).setNo("P110") + .setCreateTime(LocalDateTime.now())); + orderExtensionMapper.insert(orderExtension); + // mock 方法(PayClient) + PayClient client = mock(PayClient.class); + when(channelService.getPayClient(eq(10L))).thenReturn(client); + // mock 方法(PayClient 成功返回) + PayOrderRespDTO respDTO = randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus())); + when(client.getOrder(eq("P110"))).thenReturn(respDTO); + // mock 方法(PayChannelDO) + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)); + when(channelService.validPayChannel(eq(10L))).thenReturn(channel); + + // 调用 + int count = orderService.syncOrder(minCreateTime); + // 断言 + assertEquals(count, 1); + verify(payOrderServiceImpl).notifyOrder(same(channel), same(respDTO)); + } + } + + @Test + public void testSyncOrder_orderClosed() { + PayOrderServiceImpl payOrderServiceImpl = mock(PayOrderServiceImpl.class); + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PayOrderServiceImpl.class))) + .thenReturn(payOrderServiceImpl); + + // 准备参数 + LocalDateTime minCreateTime = LocalDateTime.now().minus(Duration.ofMinutes(10)); + // mock 数据(PayOrderExtensionDO) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setChannelId(10L).setNo("P110") + .setCreateTime(LocalDateTime.now())); + orderExtensionMapper.insert(orderExtension); + // mock 方法(PayClient) + PayClient client = mock(PayClient.class); + when(channelService.getPayClient(eq(10L))).thenReturn(client); + // mock 方法(PayClient 成功返回) + PayOrderRespDTO respDTO = randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusEnum.CLOSED.getStatus())); + when(client.getOrder(eq("P110"))).thenReturn(respDTO); + // mock 方法(PayChannelDO) + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)); + when(channelService.validPayChannel(eq(10L))).thenReturn(channel); + + // 调用 + int count = orderService.syncOrder(minCreateTime); + // 断言 + assertEquals(count, 0); + verify(payOrderServiceImpl, never()).notifyOrder(same(channel), same(respDTO)); + } + } + + @Test + public void testExpireOrder_orderExtension_isSuccess() { + // mock 数据(PayOrderDO) + PayOrderDO order = randomPojo(PayOrderDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setExpireTime(addTime(Duration.ofMinutes(-1)))); + orderMapper.insert(order); + // mock 数据(PayOrderExtensionDO 已支付) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()) + .setOrderId(order.getId())); + orderExtensionMapper.insert(orderExtension); + // mock 方法(PayClient) + PayClient client = mock(PayClient.class); + when(channelService.getPayClient(eq(10L))).thenReturn(client); + + // 调用 + int count = orderService.expireOrder(); + // 断言 + assertEquals(count, 0); + // 断言 order 没有变化,因为没更新 + assertPojoEquals(order, orderMapper.selectOne(null)); + } + + @Test + public void testExpireOrder_payClient_notFound() { + // mock 数据(PayOrderDO) + PayOrderDO order = randomPojo(PayOrderDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setExpireTime(addTime(Duration.ofMinutes(-1)))); + orderMapper.insert(order); + // mock 数据(PayOrderExtensionDO 等待中) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setOrderId(order.getId()) + .setChannelId(10L)); + orderExtensionMapper.insert(orderExtension); + + // 调用 + int count = orderService.expireOrder(); + // 断言 + assertEquals(count, 0); + // 断言 order 没有变化,因为没更新 + assertPojoEquals(order, orderMapper.selectOne(null)); + } + + @Test + public void testExpireOrder_getOrder_isRefund() { + // mock 数据(PayOrderDO) + PayOrderDO order = randomPojo(PayOrderDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setExpireTime(addTime(Duration.ofMinutes(-1)))); + orderMapper.insert(order); + // mock 数据(PayOrderExtensionDO 等待中) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setOrderId(order.getId()).setNo("P110") + .setChannelId(10L)); + orderExtensionMapper.insert(orderExtension); + // mock 方法(PayClient) + PayClient client = mock(PayClient.class); + when(channelService.getPayClient(eq(10L))).thenReturn(client); + // mock 方法(PayClient 退款返回) + PayOrderRespDTO respDTO = randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusEnum.REFUND.getStatus())); + when(client.getOrder(eq("P110"))).thenReturn(respDTO); + + // 调用 + int count = orderService.expireOrder(); + // 断言 + assertEquals(count, 0); + // 断言 order 没有变化,因为没更新 + assertPojoEquals(order, orderMapper.selectOne(null)); + } + + @Test + public void testExpireOrder_getOrder_isSuccess() { + PayOrderServiceImpl payOrderServiceImpl = mock(PayOrderServiceImpl.class); + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PayOrderServiceImpl.class))) + .thenReturn(payOrderServiceImpl); + + // mock 数据(PayOrderDO) + PayOrderDO order = randomPojo(PayOrderDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setExpireTime(addTime(Duration.ofMinutes(-1)))); + orderMapper.insert(order); + // mock 数据(PayOrderExtensionDO 等待中) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setOrderId(order.getId()).setNo("P110") + .setChannelId(10L)); + orderExtensionMapper.insert(orderExtension); + // mock 方法(PayClient) + PayClient client = mock(PayClient.class); + when(channelService.getPayClient(eq(10L))).thenReturn(client); + // mock 方法(PayClient 成功返回) + PayOrderRespDTO respDTO = randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus())); + when(client.getOrder(eq("P110"))).thenReturn(respDTO); + // mock 方法(PayChannelDO) + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)); + when(channelService.validPayChannel(eq(10L))).thenReturn(channel); + + // 调用 + int count = orderService.expireOrder(); + // 断言 + assertEquals(count, 0); + // 断言 order 没有变化,因为没更新 + assertPojoEquals(order, orderMapper.selectOne(null)); + verify(payOrderServiceImpl).notifyOrder(same(channel), same(respDTO)); + } + } + + @Test + public void testExpireOrder_success() { + // mock 数据(PayOrderDO) + PayOrderDO order = randomPojo(PayOrderDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setExpireTime(addTime(Duration.ofMinutes(-1)))); + orderMapper.insert(order); + // mock 数据(PayOrderExtensionDO 等待中) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setOrderId(order.getId()).setNo("P110") + .setChannelId(10L)); + orderExtensionMapper.insert(orderExtension); + // mock 方法(PayClient) + PayClient client = mock(PayClient.class); + when(channelService.getPayClient(eq(10L))).thenReturn(client); + // mock 方法(PayClient 关闭返回) + PayOrderRespDTO respDTO = randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusEnum.CLOSED.getStatus())); + when(client.getOrder(eq("P110"))).thenReturn(respDTO); + + // 调用 + int count = orderService.expireOrder(); + // 断言 + assertEquals(count, 1); + // 断言 extension 变化 + orderExtension.setStatus(PayOrderStatusEnum.CLOSED.getStatus()) + .setChannelNotifyData(toJsonString(respDTO)); + assertPojoEquals(orderExtension, orderExtensionMapper.selectOne(null), + "updateTime", "updater"); + // 断言 order 变化 + order.setStatus(PayOrderStatusEnum.CLOSED.getStatus()); + assertPojoEquals(order, orderMapper.selectOne(null), + "updateTime", "updater"); + } + +} diff --git a/aagro-module-pay/src/test/java/cn/aagro/pp/module/pay/service/refund/PayRefundServiceTest.java b/aagro-module-pay/src/test/java/cn/aagro/pp/module/pay/service/refund/PayRefundServiceTest.java new file mode 100644 index 0000000..42a9741 --- /dev/null +++ b/aagro-module-pay/src/test/java/cn/aagro/pp/module/pay/service/refund/PayRefundServiceTest.java @@ -0,0 +1,699 @@ +package cn.aagro.pp.module.pay.service.refund; + +import cn.hutool.extra.spring.SpringUtil; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.test.core.ut.BaseDbAndRedisUnitTest; +import cn.aagro.pp.module.pay.api.refund.dto.PayRefundCreateReqDTO; +import cn.aagro.pp.module.pay.controller.admin.refund.vo.PayRefundExportReqVO; +import cn.aagro.pp.module.pay.controller.admin.refund.vo.PayRefundPageReqVO; +import cn.aagro.pp.module.pay.dal.dataobject.app.PayAppDO; +import cn.aagro.pp.module.pay.dal.dataobject.channel.PayChannelDO; +import cn.aagro.pp.module.pay.dal.dataobject.order.PayOrderDO; +import cn.aagro.pp.module.pay.dal.dataobject.refund.PayRefundDO; +import cn.aagro.pp.module.pay.dal.mysql.refund.PayRefundMapper; +import cn.aagro.pp.module.pay.dal.redis.no.PayNoRedisDAO; +import cn.aagro.pp.module.pay.enums.PayChannelEnum; +import cn.aagro.pp.module.pay.enums.notify.PayNotifyTypeEnum; +import cn.aagro.pp.module.pay.enums.order.PayOrderStatusEnum; +import cn.aagro.pp.module.pay.enums.refund.PayRefundStatusEnum; +import cn.aagro.pp.module.pay.framework.pay.config.PayProperties; +import cn.aagro.pp.module.pay.framework.pay.core.client.PayClient; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.refund.PayRefundRespDTO; +import cn.aagro.pp.module.pay.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO; +import cn.aagro.pp.module.pay.service.app.PayAppService; +import cn.aagro.pp.module.pay.service.channel.PayChannelService; +import cn.aagro.pp.module.pay.service.notify.PayNotifyService; +import cn.aagro.pp.module.pay.service.order.PayOrderService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.List; + +import static cn.aagro.pp.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime; +import static cn.aagro.pp.framework.common.util.date.LocalDateTimeUtils.buildTime; +import static cn.aagro.pp.framework.common.util.json.JsonUtils.toJsonString; +import static cn.aagro.pp.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static cn.aagro.pp.framework.test.core.util.AssertUtils.assertPojoEquals; +import static cn.aagro.pp.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.aagro.pp.framework.test.core.util.RandomUtils.randomPojo; +import static cn.aagro.pp.framework.test.core.util.RandomUtils.randomString; +import static cn.aagro.pp.module.pay.enums.ErrorCodeConstants.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +/** + * {@link PayRefundServiceImpl} 的单元测试类 + * + * @author 芋艿 + */ +@Import({PayRefundServiceImpl.class, PayNoRedisDAO.class}) +public class PayRefundServiceTest extends BaseDbAndRedisUnitTest { + + @Resource + private PayRefundServiceImpl refundService; + + @Resource + private PayRefundMapper refundMapper; + + @MockBean + private PayProperties payProperties; + @MockBean + private PayOrderService orderService; + @MockBean + private PayAppService appService; + @MockBean + private PayChannelService channelService; + @MockBean + private PayNotifyService notifyService; + + @BeforeEach + public void setUp() { + when(payProperties.getRefundNotifyUrl()).thenReturn("http://127.0.0.1"); + } + + @Test + public void testGetRefund() { + // mock 数据 + PayRefundDO refund = randomPojo(PayRefundDO.class); + refundMapper.insert(refund); + // 准备参数 + Long id = refund.getId(); + + // 调用 + PayRefundDO dbRefund = refundService.getRefund(id); + // 断言 + assertPojoEquals(dbRefund, refund); + } + + @Test + public void testGetRefundCountByAppId() { + // mock 数据 + PayRefundDO refund01 = randomPojo(PayRefundDO.class); + refundMapper.insert(refund01); + PayRefundDO refund02 = randomPojo(PayRefundDO.class); + refundMapper.insert(refund02); + // 准备参数 + Long appId = refund01.getAppId(); + + // 调用 + Long count = refundService.getRefundCountByAppId(appId); + // 断言 + assertEquals(count, 1); + } + + @Test + public void testGetRefundPage() { + // mock 数据 + PayRefundDO dbRefund = randomPojo(PayRefundDO.class, o -> { // 等会查询到 + o.setAppId(1L); + o.setChannelCode(PayChannelEnum.WX_PUB.getCode()); + o.setMerchantOrderId("MOT0000001"); + o.setMerchantRefundId("MRF0000001"); + o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()); + o.setChannelOrderNo("CH0000001"); + o.setChannelRefundNo("CHR0000001"); + o.setCreateTime(buildTime(2021, 1, 10)); + }); + refundMapper.insert(dbRefund); + // 测试 appId 不匹配 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setAppId(2L))); + // 测试 channelCode 不匹配 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setChannelCode(PayChannelEnum.ALIPAY_APP.getCode()))); + // 测试 merchantOrderId 不匹配 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setMerchantOrderId(randomString()))); + // 测试 merchantRefundId 不匹配 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setMerchantRefundId(randomString()))); + // 测试 channelOrderNo 不匹配 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setChannelOrderNo(randomString()))); + // 测试 channelRefundNo 不匹配 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setChannelRefundNo(randomString()))); + // 测试 status 不匹配 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()))); + // 测试 createTime 不匹配 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setCreateTime(buildTime(2021, 1, 1)))); + // 准备参数 + PayRefundPageReqVO reqVO = new PayRefundPageReqVO(); + reqVO.setAppId(1L); + reqVO.setChannelCode(PayChannelEnum.WX_PUB.getCode()); + reqVO.setMerchantOrderId("MOT0000001"); + reqVO.setMerchantRefundId("MRF0000001"); + reqVO.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()); + reqVO.setChannelOrderNo("CH0000001"); + reqVO.setChannelRefundNo("CHR0000001"); + reqVO.setCreateTime(buildBetweenTime(2021, 1, 9, 2021, 1, 11)); + + // 调用 + PageResult pageResult = refundService.getRefundPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbRefund, pageResult.getList().get(0)); + } + + @Test + public void testGetRefundList() { + // mock 数据 + PayRefundDO dbRefund = randomPojo(PayRefundDO.class, o -> { // 等会查询到 + o.setAppId(1L); + o.setChannelCode(PayChannelEnum.WX_PUB.getCode()); + o.setMerchantOrderId("MOT0000001"); + o.setMerchantRefundId("MRF0000001"); + o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()); + o.setChannelOrderNo("CH0000001"); + o.setChannelRefundNo("CHR0000001"); + o.setCreateTime(buildTime(2021, 1, 10)); + }); + refundMapper.insert(dbRefund); + // 测试 appId 不匹配 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setAppId(2L))); + // 测试 channelCode 不匹配 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setChannelCode(PayChannelEnum.ALIPAY_APP.getCode()))); + // 测试 merchantOrderId 不匹配 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setMerchantOrderId(randomString()))); + // 测试 merchantRefundId 不匹配 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setMerchantRefundId(randomString()))); + // 测试 channelOrderNo 不匹配 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setChannelOrderNo(randomString()))); + // 测试 channelRefundNo 不匹配 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setChannelRefundNo(randomString()))); + // 测试 status 不匹配 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()))); + // 测试 createTime 不匹配 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setCreateTime(buildTime(2021, 1, 1)))); + // 准备参数 + PayRefundExportReqVO reqVO = new PayRefundExportReqVO(); + reqVO.setAppId(1L); + reqVO.setChannelCode(PayChannelEnum.WX_PUB.getCode()); + reqVO.setMerchantOrderId("MOT0000001"); + reqVO.setMerchantRefundId("MRF0000001"); + reqVO.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()); + reqVO.setChannelOrderNo("CH0000001"); + reqVO.setChannelRefundNo("CHR0000001"); + reqVO.setCreateTime(buildBetweenTime(2021, 1, 9, 2021, 1, 11)); + + // 调用 + List list = refundService.getRefundList(reqVO); + // 断言 + assertEquals(1, list.size()); + assertPojoEquals(dbRefund, list.get(0)); + } + + @Test + public void testCreateRefund_orderNotFound() { + PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class, + o -> o.setAppKey("demo")); + // mock 方法(app) + PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L)); + when(appService.validPayApp(eq("demo"))).thenReturn(app); + + // 调用,并断言异常 + assertServiceException(() -> refundService.createRefund(reqDTO), + PAY_ORDER_NOT_FOUND); + } + + @Test + public void testCreateRefund_orderWaiting() { + testCreateRefund_orderWaitingOrClosed(PayOrderStatusEnum.WAITING.getStatus()); + } + + @Test + public void testCreateRefund_orderClosed() { + testCreateRefund_orderWaitingOrClosed(PayOrderStatusEnum.CLOSED.getStatus()); + } + + private void testCreateRefund_orderWaitingOrClosed(Integer status) { + // 准备参数 + PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class, + o -> o.setAppKey("demo").setMerchantOrderId("100")); + // mock 方法(app) + PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L)); + when(appService.validPayApp(eq("demo"))).thenReturn(app); + // mock 数据(order) + PayOrderDO order = randomPojo(PayOrderDO.class, o -> o.setStatus(status)); + when(orderService.getOrder(eq(1L), eq("100"))).thenReturn(order); + + // 调用,并断言异常 + assertServiceException(() -> refundService.createRefund(reqDTO), + PAY_ORDER_REFUND_FAIL_STATUS_ERROR); + } + + @Test + public void testCreateRefund_refundPriceExceed() { + // 准备参数 + PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class, + o -> o.setAppKey("demo").setMerchantOrderId("100").setPrice(10)); + // mock 方法(app) + PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L)); + when(appService.validPayApp(eq("demo"))).thenReturn(app); + // mock 数据(order) + PayOrderDO order = randomPojo(PayOrderDO.class, o -> + o.setStatus(PayOrderStatusEnum.REFUND.getStatus()) + .setPrice(10).setRefundPrice(1)); + when(orderService.getOrder(eq(1L), eq("100"))).thenReturn(order); + + // 调用,并断言异常 + assertServiceException(() -> refundService.createRefund(reqDTO), + REFUND_PRICE_EXCEED); + } + + @Test + public void testCreateRefund_orderHasRefunding() { + // 准备参数 + PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class, + o -> o.setAppKey("demo").setMerchantOrderId("100").setPrice(10)); + // mock 方法(app) + PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L)); + when(appService.validPayApp(eq("demo"))).thenReturn(app); + // mock 数据(order) + PayOrderDO order = randomPojo(PayOrderDO.class, o -> + o.setStatus(PayOrderStatusEnum.REFUND.getStatus()) + .setPrice(10).setRefundPrice(1)); + when(orderService.getOrder(eq(1L), eq("100"))).thenReturn(order); + // mock 数据(refund 在退款中) + PayRefundDO refund = randomPojo(PayRefundDO.class, o -> + o.setOrderId(order.getId()).setStatus(PayOrderStatusEnum.WAITING.getStatus())); + refundMapper.insert(refund); + + // 调用,并断言异常 + assertServiceException(() -> refundService.createRefund(reqDTO), + REFUND_PRICE_EXCEED); + } + + @Test + public void testCreateRefund_channelNotFound() { + // 准备参数 + PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class, + o -> o.setAppKey("demo").setMerchantOrderId("100").setPrice(9)); + // mock 方法(app) + PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L)); + when(appService.validPayApp(eq("demo"))).thenReturn(app); + // mock 数据(order) + PayOrderDO order = randomPojo(PayOrderDO.class, o -> + o.setStatus(PayOrderStatusEnum.REFUND.getStatus()) + .setPrice(10).setRefundPrice(1) + .setChannelId(1L).setChannelCode(PayChannelEnum.ALIPAY_APP.getCode())); + when(orderService.getOrder(eq(1L), eq("100"))).thenReturn(order); + // mock 方法(channel) + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L) + .setCode(PayChannelEnum.ALIPAY_APP.getCode())); + when(channelService.validPayChannel(eq(1L))).thenReturn(channel); + + // 调用,并断言异常 + assertServiceException(() -> refundService.createRefund(reqDTO), + CHANNEL_NOT_FOUND); + } + + @Test + public void testCreateRefund_refundExists() { + // 准备参数 + PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class, + o -> o.setAppKey("demo").setMerchantOrderId("100").setPrice(9) + .setMerchantRefundId("200").setReason("测试退款")); + // mock 方法(app) + PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L)); + when(appService.validPayApp(eq("demo"))).thenReturn(app); + // mock 数据(order) + PayOrderDO order = randomPojo(PayOrderDO.class, o -> + o.setStatus(PayOrderStatusEnum.REFUND.getStatus()) + .setPrice(10).setRefundPrice(1) + .setChannelId(1L).setChannelCode(PayChannelEnum.ALIPAY_APP.getCode())); + when(orderService.getOrder(eq(1L), eq("100"))).thenReturn(order); + // mock 方法(channel) + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L) + .setCode(PayChannelEnum.ALIPAY_APP.getCode())); + when(channelService.validPayChannel(eq(1L))).thenReturn(channel); + // mock 方法(client) + PayClient client = mock(PayClient.class); + when(channelService.getPayClient(eq(10L))).thenReturn(client); + // mock 数据(refund 已存在) + PayRefundDO refund = randomPojo(PayRefundDO.class, o -> + o.setAppId(1L).setMerchantRefundId("200")); + refundMapper.insert(refund); + + // 调用,并断言异常 + assertServiceException(() -> refundService.createRefund(reqDTO), + REFUND_EXISTS); + } + + @Test + public void testCreateRefund_invokeException() { + // 准备参数 + PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class, + o -> o.setAppKey("demo").setMerchantOrderId("100").setPrice(9) + .setMerchantRefundId("200").setReason("测试退款")); + // mock 方法(app) + PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L)); + when(appService.validPayApp(eq("demo"))).thenReturn(app); + // mock 数据(order) + PayOrderDO order = randomPojo(PayOrderDO.class, o -> + o.setStatus(PayOrderStatusEnum.REFUND.getStatus()) + .setPrice(10).setRefundPrice(1) + .setChannelId(10L).setChannelCode(PayChannelEnum.ALIPAY_APP.getCode())); + when(orderService.getOrder(eq(1L), eq("100"))).thenReturn(order); + // mock 方法(channel) + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L) + .setCode(PayChannelEnum.ALIPAY_APP.getCode())); + when(channelService.validPayChannel(eq(10L))).thenReturn(channel); + // mock 方法(client) + PayClient client = mock(PayClient.class); + when(channelService.getPayClient(eq(10L))).thenReturn(client); + // mock 方法(client 调用发生异常) + when(client.unifiedRefund(any(PayRefundUnifiedReqDTO.class))).thenThrow(new RuntimeException()); + + // 调用 + Long refundId = refundService.createRefund(reqDTO); + // 断言 + PayRefundDO refundDO = refundMapper.selectById(refundId); + assertPojoEquals(reqDTO, refundDO); + assertNotNull(refundDO.getNo()); + assertThat(refundDO) + .extracting("orderId", "orderNo", "channelId", "channelCode", + "notifyUrl", "channelOrderNo", "status", "payPrice", "refundPrice") + .containsExactly(order.getId(), order.getNo(), channel.getId(), channel.getCode(), + app.getRefundNotifyUrl(), order.getChannelOrderNo(), PayRefundStatusEnum.WAITING.getStatus(), + order.getPrice(), reqDTO.getPrice()); + } + + @Test + public void testCreateRefund_invokeSuccess() { + PayRefundServiceImpl payRefundServiceImpl = mock(PayRefundServiceImpl.class); + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PayRefundServiceImpl.class))) + .thenReturn(payRefundServiceImpl); + + // 准备参数 + PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class, + o -> o.setAppKey("demo").setMerchantOrderId("100").setPrice(9) + .setMerchantRefundId("200").setReason("测试退款")); + // mock 方法(app) + PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L)); + when(appService.validPayApp(eq("demo"))).thenReturn(app); + // mock 数据(order) + PayOrderDO order = randomPojo(PayOrderDO.class, o -> + o.setStatus(PayOrderStatusEnum.REFUND.getStatus()) + .setPrice(10).setRefundPrice(1) + .setChannelId(10L).setChannelCode(PayChannelEnum.ALIPAY_APP.getCode())); + when(orderService.getOrder(eq(1L), eq("100"))).thenReturn(order); + // mock 方法(channel) + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L) + .setCode(PayChannelEnum.ALIPAY_APP.getCode())); + when(channelService.validPayChannel(eq(10L))).thenReturn(channel); + // mock 方法(client) + PayClient client = mock(PayClient.class); + when(channelService.getPayClient(eq(10L))).thenReturn(client); + // mock 方法(client 成功) + PayRefundRespDTO refundRespDTO = randomPojo(PayRefundRespDTO.class); + when(client.unifiedRefund(argThat(unifiedReqDTO -> { + assertNotNull(unifiedReqDTO.getOutRefundNo()); + assertThat(unifiedReqDTO) + .extracting("payPrice", "refundPrice", "outTradeNo", + "notifyUrl", "reason") + .containsExactly(order.getPrice(), reqDTO.getPrice(), order.getNo(), + "http://127.0.0.1/10", reqDTO.getReason()); + return true; + }))).thenReturn(refundRespDTO); + + // 调用 + Long refundId = refundService.createRefund(reqDTO); + // 断言 + PayRefundDO refundDO = refundMapper.selectById(refundId); + assertPojoEquals(reqDTO, refundDO); + assertNotNull(refundDO.getNo()); + assertThat(refundDO) + .extracting("orderId", "orderNo", "channelId", "channelCode", + "notifyUrl", "channelOrderNo", "status", "payPrice", "refundPrice") + .containsExactly(order.getId(), order.getNo(), channel.getId(), channel.getCode(), + app.getRefundNotifyUrl(), order.getChannelOrderNo(), PayRefundStatusEnum.WAITING.getStatus(), + order.getPrice(), reqDTO.getPrice()); + // 断言调用 + verify(payRefundServiceImpl).notifyRefund(same(channel), same(refundRespDTO)); + } + } + + @Test + public void testNotifyRefund() { + PayRefundServiceImpl payRefundServiceImpl = mock(PayRefundServiceImpl.class); + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PayRefundServiceImpl.class))) + .thenReturn(payRefundServiceImpl); + + // 准备参数 + Long channelId = 10L; + PayRefundRespDTO refundRespDTO = randomPojo(PayRefundRespDTO.class); + // mock 方法(channel) + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)); + when(channelService.validPayChannel(eq(10L))).thenReturn(channel); + + // 调用 + refundService.notifyRefund(channelId, refundRespDTO); + // 断言 + verify(payRefundServiceImpl).notifyRefund(same(channel), same(refundRespDTO)); + } + } + + @Test + public void testNotifyRefundSuccess_notFound() { + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L).setAppId(1L)); + PayRefundRespDTO refundRespDTO = randomPojo(PayRefundRespDTO.class, + o -> o.setStatus(PayRefundStatusEnum.SUCCESS.getStatus()).setOutRefundNo("R100")); + + // 调用,并断言异常 + assertServiceException(() -> refundService.notifyRefund(channel, refundRespDTO), + REFUND_NOT_FOUND); + } + + @Test + public void testNotifyRefundSuccess_isSuccess() { + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L).setAppId(1L)); + PayRefundRespDTO refundRespDTO = randomPojo(PayRefundRespDTO.class, + o -> o.setStatus(PayRefundStatusEnum.SUCCESS.getStatus()).setOutRefundNo("R100")); + // mock 数据(refund + 已支付) + PayRefundDO refund = randomPojo(PayRefundDO.class, o -> o.setAppId(1L).setNo("R100") + .setStatus(PayRefundStatusEnum.SUCCESS.getStatus())); + refundMapper.insert(refund); + + // 调用 + refundService.notifyRefund(channel, refundRespDTO); + // 断言,refund 没有更新,因为已经退款成功 + assertPojoEquals(refund, refundMapper.selectById(refund.getId())); + } + + @Test + public void testNotifyRefundSuccess_failure() { + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L).setAppId(1L)); + PayRefundRespDTO refundRespDTO = randomPojo(PayRefundRespDTO.class, + o -> o.setStatus(PayRefundStatusEnum.SUCCESS.getStatus()).setOutRefundNo("R100")); + // mock 数据(refund + 已支付) + PayRefundDO refund = randomPojo(PayRefundDO.class, o -> o.setAppId(1L).setNo("R100") + .setStatus(PayRefundStatusEnum.FAILURE.getStatus())); + refundMapper.insert(refund); + + // 调用,并断言异常 + assertServiceException(() -> refundService.notifyRefund(channel, refundRespDTO), + REFUND_STATUS_IS_NOT_WAITING); + } + + @Test + public void testNotifyRefundSuccess_updateOrderException() { + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L).setAppId(1L)); + PayRefundRespDTO refundRespDTO = randomPojo(PayRefundRespDTO.class, + o -> o.setStatus(PayRefundStatusEnum.SUCCESS.getStatus()).setOutRefundNo("R100")); + // mock 数据(refund + 已支付) + PayRefundDO refund = randomPojo(PayRefundDO.class, o -> o.setAppId(1L).setNo("R100") + .setStatus(PayRefundStatusEnum.WAITING.getStatus()) + .setOrderId(100L).setRefundPrice(23)); + refundMapper.insert(refund); + // mock 方法(order + 更新异常) + doThrow(new RuntimeException()).when(orderService) + .updateOrderRefundPrice(eq(100L), eq(23)); + + // 调用,并断言异常 + assertThrows(RuntimeException.class, () -> refundService.notifyRefund(channel, refundRespDTO)); + // 断言,refund 没有更新,因为事务回滚了 + assertPojoEquals(refund, refundMapper.selectById(refund.getId())); + } + + @Test + public void testNotifyRefundSuccess_success() { + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L).setAppId(1L)); + PayRefundRespDTO refundRespDTO = randomPojo(PayRefundRespDTO.class, + o -> o.setStatus(PayRefundStatusEnum.SUCCESS.getStatus()).setOutRefundNo("R100")); + // mock 数据(refund + 已支付) + PayRefundDO refund = randomPojo(PayRefundDO.class, o -> o.setAppId(1L).setNo("R100") + .setStatus(PayRefundStatusEnum.WAITING.getStatus()) + .setOrderId(100L).setRefundPrice(23)); + refundMapper.insert(refund); + + // 调用 + refundService.notifyRefund(channel, refundRespDTO); + // 断言,refund + refund.setSuccessTime(refundRespDTO.getSuccessTime()) + .setChannelRefundNo(refundRespDTO.getChannelRefundNo()) + .setStatus(PayRefundStatusEnum.SUCCESS.getStatus()) + .setChannelNotifyData(toJsonString(refundRespDTO)); + assertPojoEquals(refund, refundMapper.selectById(refund.getId()), + "updateTime", "updater"); + // 断言,调用 + verify(orderService).updateOrderRefundPrice(eq(100L), eq(23)); + verify(notifyService).createPayNotifyTask(eq(PayNotifyTypeEnum.REFUND.getType()), + eq(refund.getId())); + } + + @Test + public void testNotifyRefundFailure_notFound() { + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L).setAppId(1L)); + PayRefundRespDTO refundRespDTO = randomPojo(PayRefundRespDTO.class, + o -> o.setStatus(PayRefundStatusEnum.FAILURE.getStatus()).setOutRefundNo("R100")); + + // 调用,并断言异常 + assertServiceException(() -> refundService.notifyRefund(channel, refundRespDTO), + REFUND_NOT_FOUND); + } + + @Test + public void testNotifyRefundFailure_isFailure() { + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L).setAppId(1L)); + PayRefundRespDTO refundRespDTO = randomPojo(PayRefundRespDTO.class, + o -> o.setStatus(PayRefundStatusEnum.FAILURE.getStatus()).setOutRefundNo("R100")); + // mock 数据(refund + 退款失败) + PayRefundDO refund = randomPojo(PayRefundDO.class, o -> o.setAppId(1L).setNo("R100") + .setStatus(PayRefundStatusEnum.FAILURE.getStatus())); + refundMapper.insert(refund); + + // 调用 + refundService.notifyRefund(channel, refundRespDTO); + // 断言,refund 没有更新,因为已经退款失败 + assertPojoEquals(refund, refundMapper.selectById(refund.getId())); + } + + @Test + public void testNotifyRefundFailure_isSuccess() { + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L).setAppId(1L)); + PayRefundRespDTO refundRespDTO = randomPojo(PayRefundRespDTO.class, + o -> o.setStatus(PayRefundStatusEnum.FAILURE.getStatus()).setOutRefundNo("R100")); + // mock 数据(refund + 已支付) + PayRefundDO refund = randomPojo(PayRefundDO.class, o -> o.setAppId(1L).setNo("R100") + .setStatus(PayRefundStatusEnum.SUCCESS.getStatus())); + refundMapper.insert(refund); + + // 调用,并断言异常 + assertServiceException(() -> refundService.notifyRefund(channel, refundRespDTO), + REFUND_STATUS_IS_NOT_WAITING); + } + + @Test + public void testNotifyRefundFailure_success() { + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L).setAppId(1L)); + PayRefundRespDTO refundRespDTO = randomPojo(PayRefundRespDTO.class, + o -> o.setStatus(PayRefundStatusEnum.FAILURE.getStatus()).setOutRefundNo("R100")); + // mock 数据(refund + 已支付) + PayRefundDO refund = randomPojo(PayRefundDO.class, o -> o.setAppId(1L).setNo("R100") + .setStatus(PayRefundStatusEnum.WAITING.getStatus()) + .setOrderId(100L).setRefundPrice(23)); + refundMapper.insert(refund); + + // 调用 + refundService.notifyRefund(channel, refundRespDTO); + // 断言,refund + refund.setChannelRefundNo(refundRespDTO.getChannelRefundNo()) + .setStatus(PayRefundStatusEnum.FAILURE.getStatus()) + .setChannelNotifyData(toJsonString(refundRespDTO)) + .setChannelErrorCode(refundRespDTO.getChannelErrorCode()) + .setChannelErrorMsg(refundRespDTO.getChannelErrorMsg()); + assertPojoEquals(refund, refundMapper.selectById(refund.getId()), + "updateTime", "updater"); + // 断言,调用 + verify(notifyService).createPayNotifyTask(eq(PayNotifyTypeEnum.REFUND.getType()), + eq(refund.getId())); + } + + @Test + public void testSyncRefund_notFound() { + // 准备参数 + PayRefundDO refund = randomPojo(PayRefundDO.class, o -> o.setAppId(1L) + .setStatus(PayRefundStatusEnum.WAITING.getStatus())); + refundMapper.insert(refund); + + // 调用 + int count = refundService.syncRefund(); + // 断言 + assertEquals(count, 0); + } + + @Test + public void testSyncRefund_waiting() { + assertEquals(testSyncRefund_waitingOrSuccessOrFailure(PayRefundStatusEnum.WAITING.getStatus()), 0); + } + + @Test + public void testSyncRefund_success() { + assertEquals(testSyncRefund_waitingOrSuccessOrFailure(PayRefundStatusEnum.SUCCESS.getStatus()), 1); + } + + @Test + public void testSyncRefund_failure() { + assertEquals(testSyncRefund_waitingOrSuccessOrFailure(PayRefundStatusEnum.FAILURE.getStatus()), 1); + } + + private int testSyncRefund_waitingOrSuccessOrFailure(Integer status) { + PayRefundServiceImpl payRefundServiceImpl = mock(PayRefundServiceImpl.class); + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PayRefundServiceImpl.class))) + .thenReturn(payRefundServiceImpl); + + // 准备参数 + PayRefundDO refund = randomPojo(PayRefundDO.class, o -> o.setAppId(1L).setChannelId(10L) + .setStatus(PayRefundStatusEnum.WAITING.getStatus()) + .setOrderNo("P110").setNo("R220")); + refundMapper.insert(refund); + // mock 方法(client) + PayClient client = mock(PayClient.class); + when(channelService.getPayClient(eq(10L))).thenReturn(client); + // mock 方法(client 返回指定状态) + PayRefundRespDTO respDTO = randomPojo(PayRefundRespDTO.class, o -> o.setStatus(status)); + when(client.getRefund(eq("P110"), eq("R220"))).thenReturn(respDTO); + // mock 方法(channel) + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)); + when(channelService.validPayChannel(eq(10L))).thenReturn(channel); + + // 调用 + return refundService.syncRefund(); + } + } + + @Test + public void testSyncRefund_exception() { + // 准备参数 + PayRefundDO refund = randomPojo(PayRefundDO.class, o -> o.setAppId(1L).setChannelId(10L) + .setStatus(PayRefundStatusEnum.WAITING.getStatus()) + .setOrderNo("P110").setNo("R220")); + refundMapper.insert(refund); + // mock 方法(client) + PayClient client = mock(PayClient.class); + when(channelService.getPayClient(eq(10L))).thenReturn(client); + // mock 方法(client 抛出异常) + when(client.getRefund(eq("P110"), eq("R220"))).thenThrow(new RuntimeException()); + + // 调用 + int count = refundService.syncRefund(); + // 断言 + assertEquals(count, 0); + } + +} diff --git a/aagro-module-pay/src/test/resources/application-unit-test.yaml b/aagro-module-pay/src/test/resources/application-unit-test.yaml new file mode 100644 index 0000000..6dd1916 --- /dev/null +++ b/aagro-module-pay/src/test/resources/application-unit-test.yaml @@ -0,0 +1,48 @@ +spring: + main: + lazy-initialization: true # 开启懒加载,加快速度 + banner-mode: off # 单元测试,禁用 Banner + +--- #################### 数据库相关配置 #################### + +spring: + # 数据源配置项 + datasource: + name: ruoyi-vue-pro + url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false;NON_KEYWORDS=value; # MODE 使用 MySQL 模式;DATABASE_TO_UPPER 配置表和字段使用小写 + driver-class-name: org.h2.Driver + username: sa + password: + druid: + async-init: true # 单元测试,异步初始化 Druid 连接池,提升启动速度 + initial-size: 1 # 单元测试,配置为 1,提升启动速度 + sql: + init: + schema-locations: classpath:/sql/create_tables.sql + + # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 + redis: + host: 127.0.0.1 # 地址 + port: 16379 # 端口(单元测试,使用 16379 端口) + database: 0 # 数据库索引 + + +mybatis: + lazy-initialization: true # 单元测试,设置 MyBatis Mapper 延迟加载,加速每个单元测试 + +--- #################### 定时任务相关配置 #################### + +--- #################### 配置中心相关配置 #################### + +--- #################### 服务保障相关配置 #################### + +# Lock4j 配置项(单元测试,禁用 Lock4j) + +--- #################### 监控相关配置 #################### + +--- #################### 芋道相关配置 #################### + +# 芋道配置项,设置当前项目所有自定义的配置 +aagro: + info: + base-package: cn.aagro.pp.module diff --git a/aagro-module-pay/src/test/resources/logback.xml b/aagro-module-pay/src/test/resources/logback.xml new file mode 100644 index 0000000..daf756b --- /dev/null +++ b/aagro-module-pay/src/test/resources/logback.xml @@ -0,0 +1,4 @@ + + + + diff --git a/aagro-module-pay/src/test/resources/sql/clean.sql b/aagro-module-pay/src/test/resources/sql/clean.sql new file mode 100644 index 0000000..e7bf56b --- /dev/null +++ b/aagro-module-pay/src/test/resources/sql/clean.sql @@ -0,0 +1,8 @@ +DELETE FROM pay_app; +DELETE FROM pay_channel; +DELETE FROM pay_order; +DELETE FROM pay_order_extension; +DELETE FROM pay_refund; +DELETE FROM pay_transfer; +DELETE FROM pay_notify_task; +DELETE FROM pay_notify_log; diff --git a/aagro-module-pay/src/test/resources/sql/create_tables.sql b/aagro-module-pay/src/test/resources/sql/create_tables.sql new file mode 100644 index 0000000..6af612f --- /dev/null +++ b/aagro-module-pay/src/test/resources/sql/create_tables.sql @@ -0,0 +1,180 @@ +CREATE TABLE IF NOT EXISTS "pay_app" ( + "id" number NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "app_key" varchar(64) NOT NULL, + "name" varchar(64) NOT NULL, + "status" tinyint NOT NULL, + "remark" varchar(255) DEFAULT NULL, + `order_notify_url` varchar(1024) NOT NULL, + `refund_notify_url` varchar(1024) NOT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit(1) NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT = '支付应用'; + +CREATE TABLE IF NOT EXISTS "pay_channel" ( + "id" number NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "code" varchar(32) NOT NULL, + "status" tinyint(4) NOT NULL, + "remark" varchar(255) DEFAULT NULL, + "fee_rate" double NOT NULL DEFAULT 0, + "app_id" bigint(20) NOT NULL, + "config" varchar(10240) NOT NULL, + "creator" varchar(64) NULL DEFAULT '', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) NULL DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit(1) NOT NULL DEFAULT FALSE, + "tenant_id" bigint not null default '0', + PRIMARY KEY ("id") +) COMMENT = '支付渠道'; + +CREATE TABLE IF NOT EXISTS `pay_order` ( + "id" number NOT NULL GENERATED BY DEFAULT AS IDENTITY, + `app_id` bigint(20) NOT NULL, + `channel_id` bigint(20) DEFAULT NULL, + `channel_code` varchar(32) DEFAULT NULL, + `merchant_order_id` varchar(64) NOT NULL, + `subject` varchar(32) NOT NULL, + `body` varchar(128) NOT NULL, + `notify_url` varchar(1024) NOT NULL, + `price` bigint(20) NOT NULL, + `channel_fee_rate` double DEFAULT 0, + `channel_fee_price` bigint(20) DEFAULT 0, + `status` tinyint(4) NOT NULL, + `user_ip` varchar(50) NOT NULL, + `user_id` bigint(20) DEFAULT NULL, + `user_type` tinyint(4) DEFAULT NULL, + `expire_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `success_time` datetime(0) DEFAULT CURRENT_TIMESTAMP, + `notify_time` datetime(0) DEFAULT CURRENT_TIMESTAMP, + `extension_id` bigint(20) DEFAULT NULL, + `no` varchar(64) NULL, + `refund_price` bigint(20) NOT NULL, + `channel_user_id` varchar(255) DEFAULT NULL, + `channel_order_no` varchar(64) DEFAULT NULL, + `creator` varchar(64) DEFAULT '', + `create_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updater` varchar(64) DEFAULT '', + `update_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `deleted` bit(1) NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT = '支付订单'; + +CREATE TABLE IF NOT EXISTS `pay_order_extension` ( + "id" number NOT NULL GENERATED BY DEFAULT AS IDENTITY, + `no` varchar(64) NOT NULL, + `order_id` bigint(20) NOT NULL, + `channel_id` bigint(20) NOT NULL, + `channel_code` varchar(32) NOT NULL, + `user_ip` varchar(50) NULL DEFAULT NULL, + `status` tinyint(4) NOT NULL, + `channel_extras` varchar(1024) NULL DEFAULT NULL, + `channel_error_code` varchar(64) NULL, + `channel_error_msg` varchar(64) NULL, + `channel_notify_data` varchar(1024) NULL, + `creator` varchar(64) NULL DEFAULT '', + `create_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updater` varchar(64) NULL DEFAULT '', + `update_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `deleted` bit(1) NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT = '支付订单拓展'; + +CREATE TABLE IF NOT EXISTS `pay_refund` ( + "id" number NOT NULL GENERATED BY DEFAULT AS IDENTITY, + `no` varchar(64) NOT NULL, + `app_id` bigint(20) NOT NULL, + `channel_id` bigint(20) NOT NULL, + `channel_code` varchar(32) NOT NULL, + `order_id` bigint(20) NOT NULL, + `order_no` varchar(64) NOT NULL, + `merchant_order_id` varchar(64) NOT NULL, + `merchant_refund_id` varchar(64) NOT NULL, + `notify_url` varchar(1024) NOT NULL, + `status` tinyint(4) NOT NULL, + `pay_price` bigint(20) NOT NULL, + `refund_price` bigint(20) NOT NULL, + `reason` varchar(256) NOT NULL, + `user_ip` varchar(50) NULL DEFAULT NULL, + `user_id` bigint(20) NULL DEFAULT NULL, + `user_type` tinyint(4) NULL DEFAULT NULL, + `channel_order_no` varchar(64) NOT NULL, + `channel_refund_no` varchar(64) NULL DEFAULT NULL, + `success_time` datetime(0) NULL DEFAULT NULL, + `channel_error_code` varchar(128) NULL DEFAULT NULL, + `channel_error_msg` varchar(256) NULL DEFAULT NULL, + `channel_notify_data` varchar(1024) NULL, + `creator` varchar(64) NULL DEFAULT '', + `create_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updater` varchar(64) NULL DEFAULT '', + `update_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `deleted` bit(1) NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT = '退款订单'; + +CREATE TABLE IF NOT EXISTS `pay_notify_task` ( + "id" number NOT NULL GENERATED BY DEFAULT AS IDENTITY, + `app_id` bigint(20) NOT NULL, + `type` tinyint(4) NOT NULL, + `data_id` bigint(20) NOT NULL, + `merchant_order_id` varchar(64) NOT NULL, + `status` tinyint(4) NOT NULL, + `next_notify_time` datetime(0) NULL DEFAULT NULL, + `last_execute_time` datetime(0) NULL DEFAULT NULL, + `notify_times` int NOT NULL, + `max_notify_times` int NOT NULL, + `notify_url` varchar(1024) NOT NULL, + `creator` varchar(64) NULL DEFAULT '', + `create_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updater` varchar(64) NULL DEFAULT '', + `update_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `deleted` bit(1) NOT NULL DEFAULT FALSE, + `tenant_id` bigint(20) NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +) COMMENT = '支付通知任务'; + +CREATE TABLE IF NOT EXISTS `pay_notify_log` ( + "id" number NOT NULL GENERATED BY DEFAULT AS IDENTITY, + `task_id` bigint(20) NOT NULL, + `notify_times` int NOT NULL, + `response` varchar(1024) NOT NULL, + `status` tinyint(4) NOT NULL, + `creator` varchar(64) NULL DEFAULT '', + `create_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updater` varchar(64) NULL DEFAULT '', + `update_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `deleted` bit(1) NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT = '支付通知日志'; + +CREATE TABLE IF NOT EXISTS `pay_transfer` ( + "id" number NOT NULL GENERATED BY DEFAULT AS IDENTITY, + `no` varchar(64) NOT NULL, + `app_id` bigint(20) NOT NULL, + `channel_id` bigint(20) NOT NULL, + `channel_code` varchar(32) NOT NULL, + `user_id` bigint(20) NULL DEFAULT NULL, + `user_type` tinyint(4) NULL DEFAULT NULL, + `merchant_transfer_id` varchar(64) NOT NULL, + `price` bigint(20) NOT NULL, + `subject` varchar(256) NOT NULL, + `user_account` varchar(256) NOT NULL, + `user_name` varchar(64) NULL DEFAULT NULL, + `status` tinyint(4) NOT NULL, + `notify_url` varchar(1024) NULL DEFAULT NULL, + `channel_transfer_no` varchar(64) NULL DEFAULT NULL, + `success_time` datetime(0) NULL DEFAULT NULL, + `channel_error_code` varchar(128) NULL DEFAULT NULL, + `channel_error_msg` varchar(256) NULL DEFAULT NULL, + `channel_notify_data` varchar(1024) NULL DEFAULT NULL, + `channel_extras` varchar(1024) NULL DEFAULT NULL, + `creator` varchar(64) NULL DEFAULT '', + `create_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updater` varchar(64) NULL DEFAULT '', + `update_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `deleted` bit(1) NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT = '转账单'; diff --git a/aagro-module-report/pom.xml b/aagro-module-report/pom.xml new file mode 100644 index 0000000..ca58206 --- /dev/null +++ b/aagro-module-report/pom.xml @@ -0,0 +1,74 @@ + + + + cn.aagro.gg + aiot + ${revision} + ../pom.xml + + 4.0.0 + aagro-module-report + jar + + ${project.artifactId} + + report 模块,主要实现数据可视化报表等功能。 + 1. 基于「积木报表」实现,打印设计、报表设计、图形设计、大屏设计等。 + + + + + cn.aagro.gg + aagro-module-system + ${revision} + + + + + cn.aagro.gg + aagro-spring-boot-starter-biz-tenant + + + + + cn.aagro.gg + aagro-spring-boot-starter-web + + + + cn.aagro.gg + aagro-spring-boot-starter-security + + + + + cn.aagro.gg + aagro-spring-boot-starter-mybatis + + + + + cn.aagro.gg + aagro-spring-boot-starter-test + ${revision} + + + + + org.jeecgframework.jimureport + jimureport-spring-boot-starter + + + org.jeecgframework.jimureport + jimubi-spring-boot-starter + + + + cn.aagro.gg + aagro-spring-boot-starter-excel + + + + diff --git a/aagro-module-report/src/main/java/cn/aagro/pp/module/report/controller/admin/ajreport/package-info.java b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/controller/admin/ajreport/package-info.java new file mode 100644 index 0000000..b0117e8 --- /dev/null +++ b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/controller/admin/ajreport/package-info.java @@ -0,0 +1 @@ +package cn.aagro.pp.module.report.controller.admin.ajreport; diff --git a/aagro-module-report/src/main/java/cn/aagro/pp/module/report/controller/admin/goview/GoViewDataController.java b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/controller/admin/goview/GoViewDataController.java new file mode 100644 index 0000000..c9721eb --- /dev/null +++ b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/controller/admin/goview/GoViewDataController.java @@ -0,0 +1,63 @@ +package cn.aagro.pp.module.report.controller.admin.goview; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.RandomUtil; +import cn.aagro.pp.framework.common.pojo.CommonResult; +import cn.aagro.pp.module.report.controller.admin.goview.vo.data.GoViewDataGetBySqlReqVO; +import cn.aagro.pp.module.report.controller.admin.goview.vo.data.GoViewDataRespVO; +import cn.aagro.pp.module.report.service.goview.GoViewDataService; +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.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import javax.validation.Valid; + +import java.util.*; + +import static cn.aagro.pp.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - GoView 数据", description = "提供 SQL、HTTP 等数据查询的能力") +@RestController +@RequestMapping("/report/go-view/data") +@Validated +public class GoViewDataController { + + @Resource + private GoViewDataService goViewDataService; + + @RequestMapping("/get-by-sql") + @Operation(summary = "使用 SQL 查询数据") + @PreAuthorize("@ss.hasPermission('report:go-view-data:get-by-sql')") + public CommonResult getDataBySQL(@Valid @RequestBody GoViewDataGetBySqlReqVO reqVO) { + return success(goViewDataService.getDataBySQL(reqVO.getSql())); + } + + @RequestMapping("/get-by-http") + @Operation(summary = "使用 HTTP 查询数据", description = "这个只是示例接口,实际应该每个查询,都要写一个接口") + @PreAuthorize("@ss.hasPermission('report:go-view-data:get-by-http')") + public CommonResult getDataByHttp( + @RequestParam(required = false) Map params, + @RequestBody(required = false) String body) { // params、body 按照需要去接收,这里仅仅是示例 + GoViewDataRespVO respVO = new GoViewDataRespVO(); + // 1. 数据维度 + respVO.setDimensions(Arrays.asList("日期", "PV", "UV")); // PV 是每天访问次数;UV 是每天访问人数 + // 2. 明细数据列表 + // 目前通过随机的方式生成。一般来说,这里你可以写逻辑来实现数据的返回 + respVO.setSource(new LinkedList<>()); + for (int i = 1; i <= 12; i++) { + String date = "2021-" + (i < 10 ? "0" + i : i); + Integer pv = RandomUtil.randomInt(1000, 10000); + Integer uv = RandomUtil.randomInt(100, 1000); + respVO.getSource().add(MapUtil.builder().put("日期", date) + .put("PV", pv).put("UV", uv).build()); + } + return success(respVO); + } + +} diff --git a/aagro-module-report/src/main/java/cn/aagro/pp/module/report/controller/admin/goview/GoViewProjectController.java b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/controller/admin/goview/GoViewProjectController.java new file mode 100644 index 0000000..fe154f9 --- /dev/null +++ b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/controller/admin/goview/GoViewProjectController.java @@ -0,0 +1,77 @@ +package cn.aagro.pp.module.report.controller.admin.goview; + +import cn.aagro.pp.framework.common.pojo.CommonResult; +import cn.aagro.pp.framework.common.pojo.PageParam; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.report.controller.admin.goview.vo.project.GoViewProjectCreateReqVO; +import cn.aagro.pp.module.report.controller.admin.goview.vo.project.GoViewProjectRespVO; +import cn.aagro.pp.module.report.controller.admin.goview.vo.project.GoViewProjectUpdateReqVO; +import cn.aagro.pp.module.report.convert.goview.GoViewProjectConvert; +import cn.aagro.pp.module.report.dal.dataobject.goview.GoViewProjectDO; +import cn.aagro.pp.module.report.service.goview.GoViewProjectService; +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.jdbc.core.JdbcTemplate; +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.aagro.pp.framework.common.pojo.CommonResult.success; +import static cn.aagro.pp.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - GoView 项目") +@RestController +@RequestMapping("/report/go-view/project") +@Validated +public class GoViewProjectController { + + @Resource + private GoViewProjectService goViewProjectService; + + @PostMapping("/create") + @Operation(summary = "创建项目") + @PreAuthorize("@ss.hasPermission('report:go-view-project:create')") + public CommonResult createProject(@Valid @RequestBody GoViewProjectCreateReqVO createReqVO) { + return success(goViewProjectService.createProject(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新项目") + @PreAuthorize("@ss.hasPermission('report:go-view-project:update')") + public CommonResult updateProject(@Valid @RequestBody GoViewProjectUpdateReqVO updateReqVO) { + goViewProjectService.updateProject(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除 GoView 项目") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('report:go-view-project:delete')") + public CommonResult deleteProject(@RequestParam("id") Long id) { + goViewProjectService.deleteProject(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得项目") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('report:go-view-project:query')") + public CommonResult getProject(@RequestParam("id") Long id) { + GoViewProjectDO project = goViewProjectService.getProject(id); + return success(GoViewProjectConvert.INSTANCE.convert(project)); + } + + @GetMapping("/my-page") + @Operation(summary = "获得我的项目分页") + @PreAuthorize("@ss.hasPermission('report:go-view-project:query')") + public CommonResult> getMyProjectPage(@Valid PageParam pageVO) { + PageResult pageResult = goViewProjectService.getMyProjectPage( + pageVO, getLoginUserId()); + return success(GoViewProjectConvert.INSTANCE.convertPage(pageResult)); + } + +} diff --git a/aagro-module-report/src/main/java/cn/aagro/pp/module/report/controller/admin/goview/vo/data/GoViewDataGetBySqlReqVO.java b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/controller/admin/goview/vo/data/GoViewDataGetBySqlReqVO.java new file mode 100644 index 0000000..8b3ab84 --- /dev/null +++ b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/controller/admin/goview/vo/data/GoViewDataGetBySqlReqVO.java @@ -0,0 +1,16 @@ +package cn.aagro.pp.module.report.controller.admin.goview.vo.data; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; + +@Schema(description = "管理后台 - GoView 使用 SQL 查询数据 Request VO") +@Data +public class GoViewDataGetBySqlReqVO { + + @Schema(description = "SQL 语句", requiredMode = Schema.RequiredMode.REQUIRED, example = "SELECT * FROM user") + @NotEmpty(message = "SQL 语句不能为空") + private String sql; + +} diff --git a/aagro-module-report/src/main/java/cn/aagro/pp/module/report/controller/admin/goview/vo/data/GoViewDataRespVO.java b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/controller/admin/goview/vo/data/GoViewDataRespVO.java new file mode 100644 index 0000000..11b46b2 --- /dev/null +++ b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/controller/admin/goview/vo/data/GoViewDataRespVO.java @@ -0,0 +1,19 @@ +package cn.aagro.pp.module.report.controller.admin.goview.vo.data; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; +import java.util.Map; + +@Schema(description = "管理后台 - GoView 数据 Response VO") +@Data +public class GoViewDataRespVO { + + @Schema(description = "数据维度", requiredMode = Schema.RequiredMode.REQUIRED, example = "['product', 'data1', 'data2']") + private List dimensions; + + @Schema(description = "数据明细列表", requiredMode = Schema.RequiredMode.REQUIRED) + private List> source; + +} diff --git a/aagro-module-report/src/main/java/cn/aagro/pp/module/report/controller/admin/goview/vo/project/GoViewProjectCreateReqVO.java b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/controller/admin/goview/vo/project/GoViewProjectCreateReqVO.java new file mode 100644 index 0000000..9624554 --- /dev/null +++ b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/controller/admin/goview/vo/project/GoViewProjectCreateReqVO.java @@ -0,0 +1,15 @@ +package cn.aagro.pp.module.report.controller.admin.goview.vo.project; + +import lombok.*; +import io.swagger.v3.oas.annotations.media.Schema; +import javax.validation.constraints.*; + +@Schema(description = "管理后台 - GoView 项目创建 Request VO") +@Data +public class GoViewProjectCreateReqVO { + + @Schema(description = "项目名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "王五") + @NotEmpty(message = "项目名称不能为空") + private String name; + +} diff --git a/aagro-module-report/src/main/java/cn/aagro/pp/module/report/controller/admin/goview/vo/project/GoViewProjectRespVO.java b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/controller/admin/goview/vo/project/GoViewProjectRespVO.java new file mode 100644 index 0000000..6c5a831 --- /dev/null +++ b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/controller/admin/goview/vo/project/GoViewProjectRespVO.java @@ -0,0 +1,36 @@ +package cn.aagro.pp.module.report.controller.admin.goview.vo.project; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - GoView 项目 Response VO") +@Data +public class GoViewProjectRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "18993") + private Long id; + + @Schema(description = "项目名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "王五") + private String name; + + @Schema(description = "发布状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "报表内容") // JSON 格式 + private String content; + + @Schema(description = "预览图片 URL", example = "https://www.iocoder.cn") + private String picUrl; + + @Schema(description = "项目备注", example = "你猜") + private String remark; + + @Schema(description = "创建人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private String creator; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/aagro-module-report/src/main/java/cn/aagro/pp/module/report/controller/admin/goview/vo/project/GoViewProjectUpdateReqVO.java b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/controller/admin/goview/vo/project/GoViewProjectUpdateReqVO.java new file mode 100644 index 0000000..fa28d8e --- /dev/null +++ b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/controller/admin/goview/vo/project/GoViewProjectUpdateReqVO.java @@ -0,0 +1,34 @@ +package cn.aagro.pp.module.report.controller.admin.goview.vo.project; + +import cn.aagro.pp.framework.common.enums.CommonStatusEnum; +import cn.aagro.pp.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +import javax.validation.constraints.*; + +@Schema(description = "管理后台 - GoView 项目更新 Request VO") +@Data +public class GoViewProjectUpdateReqVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "18993") + @NotNull(message = "编号不能为空") + private Long id; + + @Schema(description = "项目名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "王五") + private String name; + + @Schema(description = "发布状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @InEnum(value = CommonStatusEnum.class, message = "发布状态必须是 {value}") + private Integer status; + + @Schema(description = "报表内容") // JSON 格式 + private String content; + + @Schema(description = "预览图片 URL", example = "https://www.iocoder.cn") + private String picUrl; + + @Schema(description = "项目备注", example = "你猜") + private String remark; + +} diff --git a/aagro-module-report/src/main/java/cn/aagro/pp/module/report/controller/package-info.java b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/controller/package-info.java new file mode 100644 index 0000000..464e9ef --- /dev/null +++ b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/controller/package-info.java @@ -0,0 +1,6 @@ +/** + * 提供 RESTful API 给前端: + * 1. admin 包:提供给管理后台 aagro-ui-admin 前端项目 + * 2. app 包:提供给用户 APP aagro-ui-app 前端项目,它的 Controller 和 VO 都要添加 App 前缀,用于和管理后台进行区分 + */ +package cn.aagro.pp.module.report.controller; diff --git a/aagro-module-report/src/main/java/cn/aagro/pp/module/report/convert/ajreport/package-info.java b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/convert/ajreport/package-info.java new file mode 100644 index 0000000..f54dd2c --- /dev/null +++ b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/convert/ajreport/package-info.java @@ -0,0 +1,4 @@ +/** + * TODO 占位,后续删除 + */ +package cn.aagro.pp.module.report.convert.ajreport; diff --git a/aagro-module-report/src/main/java/cn/aagro/pp/module/report/convert/goview/GoViewProjectConvert.java b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/convert/goview/GoViewProjectConvert.java new file mode 100644 index 0000000..e8938b1 --- /dev/null +++ b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/convert/goview/GoViewProjectConvert.java @@ -0,0 +1,24 @@ +package cn.aagro.pp.module.report.convert.goview; + +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.report.controller.admin.goview.vo.project.GoViewProjectCreateReqVO; +import cn.aagro.pp.module.report.controller.admin.goview.vo.project.GoViewProjectRespVO; +import cn.aagro.pp.module.report.controller.admin.goview.vo.project.GoViewProjectUpdateReqVO; +import cn.aagro.pp.module.report.dal.dataobject.goview.GoViewProjectDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface GoViewProjectConvert { + + GoViewProjectConvert INSTANCE = Mappers.getMapper(GoViewProjectConvert.class); + + GoViewProjectDO convert(GoViewProjectCreateReqVO bean); + + GoViewProjectDO convert(GoViewProjectUpdateReqVO bean); + + GoViewProjectRespVO convert(GoViewProjectDO bean); + + PageResult convertPage(PageResult page); + +} diff --git a/aagro-module-report/src/main/java/cn/aagro/pp/module/report/dal/dataobject/ajreport/package-info.java b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/dal/dataobject/ajreport/package-info.java new file mode 100644 index 0000000..fa2614b --- /dev/null +++ b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/dal/dataobject/ajreport/package-info.java @@ -0,0 +1,4 @@ +/** + * TODO 芋艿:占位,待删除 + */ +package cn.aagro.pp.module.report.dal.dataobject.ajreport; diff --git a/aagro-module-report/src/main/java/cn/aagro/pp/module/report/dal/dataobject/goview/GoViewProjectDO.java b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/dal/dataobject/goview/GoViewProjectDO.java new file mode 100644 index 0000000..c60a7d4 --- /dev/null +++ b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/dal/dataobject/goview/GoViewProjectDO.java @@ -0,0 +1,57 @@ +package cn.aagro.pp.module.report.dal.dataobject.goview; + +import cn.aagro.pp.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * GoView 项目表 + * + * 每个大屏图标,对应一个项目 + * + * @author 芋道源码 + */ +@TableName(value = "report_go_view_project", autoResultMap = true) // 由于 SQL Server 的 system_user 是关键字,所以使用 system_users +@KeySequence("report_go_view_project_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class GoViewProjectDO extends BaseDO { + + /** + * 编号,数据库自增 + */ + @TableId + private Long id; + /** + * 项目名称 + */ + private String name; + /** + * 预览图片 URL + */ + private String picUrl; + /** + * 报表内容 + * + * JSON 配置,使用字符串存储 + */ + private String content; + /** + * 发布状态 + * + * 0 - 已发布 + * 1 - 未发布 + * + * 枚举 {@link cn.aagro.pp.framework.common.enums.CommonStatusEnum} + */ + private Integer status; + /** + * 项目备注 + */ + private String remark; +} diff --git a/aagro-module-report/src/main/java/cn/aagro/pp/module/report/dal/mysql/ajreport/package-info.java b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/dal/mysql/ajreport/package-info.java new file mode 100644 index 0000000..e9135e9 --- /dev/null +++ b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/dal/mysql/ajreport/package-info.java @@ -0,0 +1,4 @@ +/** + * TODO 芋艿:占位,待删除 + */ +package cn.aagro.pp.module.report.dal.mysql.ajreport; diff --git a/aagro-module-report/src/main/java/cn/aagro/pp/module/report/dal/mysql/goview/GoViewProjectMapper.java b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/dal/mysql/goview/GoViewProjectMapper.java new file mode 100644 index 0000000..b89a1c4 --- /dev/null +++ b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/dal/mysql/goview/GoViewProjectMapper.java @@ -0,0 +1,19 @@ +package cn.aagro.pp.module.report.dal.mysql.goview; + +import cn.aagro.pp.framework.common.pojo.PageParam; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.mybatis.core.mapper.BaseMapperX; +import cn.aagro.pp.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.aagro.pp.module.report.dal.dataobject.goview.GoViewProjectDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface GoViewProjectMapper extends BaseMapperX { + + default PageResult selectPage(PageParam reqVO, Long userId) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eq(GoViewProjectDO::getCreator, userId) + .orderByDesc(GoViewProjectDO::getId)); + } + +} diff --git a/aagro-module-report/src/main/java/cn/aagro/pp/module/report/enums/ErrorCodeConstants.java b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/enums/ErrorCodeConstants.java new file mode 100644 index 0000000..17a8f39 --- /dev/null +++ b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/enums/ErrorCodeConstants.java @@ -0,0 +1,15 @@ +package cn.aagro.pp.module.report.enums; + +import cn.aagro.pp.framework.common.exception.ErrorCode; + +/** + * Report 错误码枚举类 + * + * report 系统,使用 1-003-000-000 段 + */ +public interface ErrorCodeConstants { + + // ========== GoView 模块 1-003-000-000 ========== + ErrorCode GO_VIEW_PROJECT_NOT_EXISTS = new ErrorCode(1_003_000_000, "GoView 项目不存在"); + +} diff --git a/aagro-module-report/src/main/java/cn/aagro/pp/module/report/framework/jmreport/config/JmReportConfiguration.java b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/framework/jmreport/config/JmReportConfiguration.java new file mode 100644 index 0000000..618107b --- /dev/null +++ b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/framework/jmreport/config/JmReportConfiguration.java @@ -0,0 +1,37 @@ +package cn.aagro.pp.module.report.framework.jmreport.config; + +import cn.aagro.pp.framework.common.biz.system.oauth2.OAuth2TokenCommonApi; +import cn.aagro.pp.framework.common.biz.system.permission.PermissionCommonApi; +import cn.aagro.pp.framework.security.config.SecurityProperties; +import cn.aagro.pp.module.report.framework.jmreport.core.service.JmOnlDragExternalServiceImpl; +import cn.aagro.pp.module.report.framework.jmreport.core.service.JmReportTokenServiceImpl; +import cn.aagro.pp.module.system.api.permission.PermissionApi; +import org.jeecg.modules.jmreport.api.JmReportTokenServiceI; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; + +/** + * 积木报表的配置类 + * + * @author 芋道源码 + */ +@Configuration(proxyBeanMethods = false) +@ComponentScan(basePackages = "org.jeecg.modules.jmreport") // 扫描积木报表的包 +public class JmReportConfiguration { + + @Bean + public JmReportTokenServiceI jmReportTokenService(OAuth2TokenCommonApi oAuth2TokenApi, + PermissionCommonApi permissionApi, + SecurityProperties securityProperties) { + return new JmReportTokenServiceImpl(oAuth2TokenApi, permissionApi, securityProperties); + } + + @Bean // 暂时注释:可以按需实现后打开 + @Primary + public JmOnlDragExternalServiceImpl jmOnlDragExternalService2() { + return new JmOnlDragExternalServiceImpl(); + } + +} diff --git a/aagro-module-report/src/main/java/cn/aagro/pp/module/report/framework/jmreport/core/service/JmOnlDragExternalServiceImpl.java b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/framework/jmreport/core/service/JmOnlDragExternalServiceImpl.java new file mode 100644 index 0000000..c4684f7 --- /dev/null +++ b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/framework/jmreport/core/service/JmOnlDragExternalServiceImpl.java @@ -0,0 +1,68 @@ +package cn.aagro.pp.module.report.framework.jmreport.core.service; + +import com.alibaba.fastjson.JSONObject; +import lombok.RequiredArgsConstructor; +import org.jeecg.modules.drag.service.IOnlDragExternalService; +import org.jeecg.modules.drag.vo.DragDictModel; +import org.jeecg.modules.drag.vo.DragLogDTO; + +import java.util.List; +import java.util.Map; + +/** + * {@link IOnlDragExternalService} 实现类,提供积木仪表盘的查询等功能 + * + * 实现可参考: + * 1. jimureport-example + * 2. JeecgBoot 集成 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +public class JmOnlDragExternalServiceImpl implements IOnlDragExternalService { + + // ========== DictItem 相关 ========== + + @Override + public Map> getManyDictItems(List codeList, List tableDictList) { + return IOnlDragExternalService.super.getManyDictItems(codeList, tableDictList); + } + + @Override + public List getDictItems(String dictCode) { + return IOnlDragExternalService.super.getDictItems(dictCode); + } + + @Override + public List getTableDictItems(String dictTable, String dictText, String dictCode) { + return IOnlDragExternalService.super.getTableDictItems(dictTable, dictText, dictCode); + } + + @Override + public List getCategoryTreeDictItems(List ids) { + return IOnlDragExternalService.super.getCategoryTreeDictItems(ids); + } + + @Override + public List getUserDictItems(List ids) { + return IOnlDragExternalService.super.getUserDictItems(ids); + } + + @Override + public List getDeptsDictItems(List ids) { + return IOnlDragExternalService.super.getDeptsDictItems(ids); + } + + // ========== Log 相关 ========== + + @Override + public void addLog(DragLogDTO dto) { + IOnlDragExternalService.super.addLog(dto); + } + + @Override + public void addLog(String logMsg, int logType, int operateType) { + IOnlDragExternalService.super.addLog(logMsg, logType, operateType); + } + +} diff --git a/aagro-module-report/src/main/java/cn/aagro/pp/module/report/framework/jmreport/core/service/JmReportTokenServiceImpl.java b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/framework/jmreport/core/service/JmReportTokenServiceImpl.java new file mode 100644 index 0000000..106c37b --- /dev/null +++ b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/framework/jmreport/core/service/JmReportTokenServiceImpl.java @@ -0,0 +1,161 @@ +package cn.aagro.pp.module.report.framework.jmreport.core.service; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.aagro.pp.framework.common.biz.system.permission.PermissionCommonApi; +import cn.aagro.pp.framework.common.exception.ServiceException; +import cn.aagro.pp.framework.common.util.servlet.ServletUtils; +import cn.aagro.pp.framework.security.config.SecurityProperties; +import cn.aagro.pp.framework.security.core.LoginUser; +import cn.aagro.pp.framework.security.core.util.SecurityFrameworkUtils; +import cn.aagro.pp.framework.tenant.core.context.TenantContextHolder; +import cn.aagro.pp.framework.web.core.util.WebFrameworkUtils; +import cn.aagro.pp.framework.common.biz.system.oauth2.OAuth2TokenCommonApi; +import cn.aagro.pp.framework.common.biz.system.oauth2.dto.OAuth2AccessTokenCheckRespDTO; +import cn.aagro.pp.module.system.api.permission.PermissionApi; +import cn.aagro.pp.module.system.enums.permission.RoleCodeEnum; +import lombok.RequiredArgsConstructor; +import org.jeecg.modules.jmreport.api.JmReportTokenServiceI; +import org.springframework.http.HttpHeaders; + +import javax.servlet.http.HttpServletRequest; +import java.util.Objects; + +/** + * {@link JmReportTokenServiceI} 实现类,提供积木报表的 Token 校验、用户信息的查询等功能 + * + * @author 随心 + */ +@RequiredArgsConstructor +public class JmReportTokenServiceImpl implements JmReportTokenServiceI { + + /** + * 积木 token head 头 + */ + private static final String JM_TOKEN_HEADER = "X-Access-Token"; + /** + * auth 相关格式 + */ + private static final String AUTHORIZATION_FORMAT = SecurityFrameworkUtils.AUTHORIZATION_BEARER + " %s"; + + private final OAuth2TokenCommonApi oauth2TokenApi; + private final PermissionCommonApi permissionApi; + + private final SecurityProperties securityProperties; + + /** + * 自定义 API 数据集appian自定义 Header,解决 Token 传递。 + * 参考 api数据集token机制详解 文档 + * + * @return 新 head + */ + @Override + public HttpHeaders customApiHeader() { + // 读取积木标标系统的 token + HttpServletRequest request = ServletUtils.getRequest(); + String token = request.getHeader(JM_TOKEN_HEADER); + + // 设置到 aagro 系统的 token + HttpHeaders headers = new HttpHeaders(); + headers.add(securityProperties.getTokenHeader(), String.format(AUTHORIZATION_FORMAT, token)); + return headers; + } + + /** + * 校验 Token 是否有效,即验证通过 + * + * @param token JmReport 前端传递的 token + * @return 是否认证通过 + */ + @Override + public Boolean verifyToken(String token) { + Long userId = SecurityFrameworkUtils.getLoginUserId(); + if (!Objects.isNull(userId)) { + return true; + } + return buildLoginUserByToken(token) != null; + } + + /** + * 获得用户编号 + *

+ * 虽然方法名获得的是 username,实际对应到项目中是用户编号 + * + * @param token JmReport 前端传递的 token + * @return 用户编号 + */ + @Override + public String getUsername(String token) { + Long userId = SecurityFrameworkUtils.getLoginUserId(); + if (ObjectUtil.isNotNull(userId)) { + return String.valueOf(userId); + } + LoginUser user = buildLoginUserByToken(token); + return user == null ? null : String.valueOf(user.getId()); + } + + /** + * 基于 token 构建登录用户 + * + * @param token token + * @return 返回 token 对应的用户信息 + */ + private LoginUser buildLoginUserByToken(String token) { + if (StrUtil.isEmpty(token)) { + return null; + } + // TODO 如下的实现不算特别优雅,主要咱是不想搞的太复杂,所以参考对应的 Filter 先实现了 + + // ① 参考 TokenAuthenticationFilter 的认证逻辑(Security 的上下文清理,交给 Spring Security 完成) + // 目的:实现基于 JmReport 前端传递的 token,实现认证 + TenantContextHolder.setIgnore(true); // 忽略租户,保证可查询到 token 信息 + LoginUser user = null; + try { + OAuth2AccessTokenCheckRespDTO accessToken = oauth2TokenApi.checkAccessToken(token); + if (accessToken == null) { + return null; + } + user = new LoginUser().setId(accessToken.getUserId()).setUserType(accessToken.getUserType()) + .setTenantId(accessToken.getTenantId()).setScopes(accessToken.getScopes()); + } catch (ServiceException ignored) { + // do nothing:如果报错,说明认证失败,则返回 false 即可 + } + if (user == null) { + return null; + } + SecurityFrameworkUtils.setLoginUser(user, WebFrameworkUtils.getRequest()); + + // ② 参考 TenantContextWebFilter 实现(Tenant 的上下文清理,交给 TenantContextWebFilter 完成) + // 目的:基于 LoginUser 获得到的租户编号,设置到 Tenant 上下文,避免查询数据库时的报错 + TenantContextHolder.setIgnore(false); + TenantContextHolder.setTenantId(user.getTenantId()); + return user; + } + + @Override + public String[] getRoles(String token) { + // 设置租户上下文。原因是:/jmreport/** 纯前端地址,不会走 buildLoginUserByToken 逻辑 + LoginUser loginUser = SecurityFrameworkUtils.getLoginUser(); + if (loginUser == null) { + return null; + } + TenantContextHolder.setTenantId(loginUser.getTenantId()); + + // 参见文档 https://help.jeecg.com/jimureport/prodSafe.html 文档 + // 适配:如果是本系统的管理员,则转换成 jimu 报表的管理员 + Long userId = SecurityFrameworkUtils.getLoginUserId(); + return permissionApi.hasAnyRoles(userId, RoleCodeEnum.SUPER_ADMIN.getCode()) + ? new String[]{"admin"} : null; + } + + @Override + public String getTenantId() { + // 补充说明:不能直接通过 TenantContext 获取,因为 jimu 报表前端请求时,没有带上 tenant-id Header + LoginUser loginUser = SecurityFrameworkUtils.getLoginUser(); + if (loginUser == null) { + return null; + } + return StrUtil.toStringOrNull(loginUser.getTenantId()); + } + +} diff --git a/aagro-module-report/src/main/java/cn/aagro/pp/module/report/framework/jmreport/core/web/package-info.java b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/framework/jmreport/core/web/package-info.java new file mode 100644 index 0000000..26ab077 --- /dev/null +++ b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/framework/jmreport/core/web/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位,后续会基于 Filter 实现积木报表的认证等功能,替代 {@link cn.aagro.pp.module.report.framework.jmreport.core.service.JmReportTokenServiceImpl} + */ +package cn.aagro.pp.module.report.framework.jmreport.core.web; diff --git a/aagro-module-report/src/main/java/cn/aagro/pp/module/report/framework/package-info.java b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/framework/package-info.java new file mode 100644 index 0000000..69f7548 --- /dev/null +++ b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/framework/package-info.java @@ -0,0 +1,6 @@ +/** + * 属于 report 模块的 framework 封装 + * + * @author 芋道源码 + */ +package cn.aagro.pp.module.report.framework; diff --git a/aagro-module-report/src/main/java/cn/aagro/pp/module/report/framework/security/config/SecurityConfiguration.java b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/framework/security/config/SecurityConfiguration.java new file mode 100644 index 0000000..d4aef1e --- /dev/null +++ b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/framework/security/config/SecurityConfiguration.java @@ -0,0 +1,32 @@ +package cn.aagro.pp.module.report.framework.security.config; + +import cn.aagro.pp.framework.security.config.AuthorizeRequestsCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer; +import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; + +/** + * Report 模块的 Security 配置 + */ +@Configuration("reportSecurityConfiguration") +public class SecurityConfiguration { + + @Bean("reportAuthorizeRequestsCustomizer") + public AuthorizeRequestsCustomizer authorizeRequestsCustomizer() { + return new AuthorizeRequestsCustomizer() { + + @Override + public void customize(AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry registry) { + // 积木报表 + registry.requestMatchers("/jmreport/**").permitAll(); + // 积木仪表盘 + registry.requestMatchers("/drag/**").permitAll(); + registry.requestMatchers("/jimubi/**").permitAll(); + } + + }; + } + +} diff --git a/aagro-module-report/src/main/java/cn/aagro/pp/module/report/framework/security/core/package-info.java b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/framework/security/core/package-info.java new file mode 100644 index 0000000..d9c67ec --- /dev/null +++ b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/framework/security/core/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位 + */ +package cn.aagro.pp.module.report.framework.security.core; diff --git a/aagro-module-report/src/main/java/cn/aagro/pp/module/report/package-info.java b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/package-info.java new file mode 100644 index 0000000..3f734ba --- /dev/null +++ b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/package-info.java @@ -0,0 +1,9 @@ +/** + * report 模块,主要实现数据可视化报表等功能: + * 1. 基于「积木报表」实现,打印设计、报表设计、图形设计、大屏设计等。URL 前缀是 /jmreport,表名前缀是 jimu_ + * + * 由于「积木报表」的大屏设计器需要收费,后续会自研,对应的是: + * 1. Controller URL:以 /report/ 开头,避免和其它 Module 冲突 + * 2. DataObject 表名:以 report_ 开头,方便在数据库中区分 + */ +package cn.aagro.pp.module.report; diff --git a/aagro-module-report/src/main/java/cn/aagro/pp/module/report/service/ajreport/package-info.java b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/service/ajreport/package-info.java new file mode 100644 index 0000000..4e0ec2f --- /dev/null +++ b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/service/ajreport/package-info.java @@ -0,0 +1,4 @@ +/** + * TODO 芋艿:占位,待删除 + */ +package cn.aagro.pp.module.report.service.ajreport; diff --git a/aagro-module-report/src/main/java/cn/aagro/pp/module/report/service/goview/GoViewDataService.java b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/service/goview/GoViewDataService.java new file mode 100644 index 0000000..9e601f7 --- /dev/null +++ b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/service/goview/GoViewDataService.java @@ -0,0 +1,20 @@ +package cn.aagro.pp.module.report.service.goview; + +import cn.aagro.pp.module.report.controller.admin.goview.vo.data.GoViewDataRespVO; + +/** + * GoView 数据 Service 接口 + * + * @author 芋道源码 + */ +public interface GoViewDataService { + + /** + * 使用 SQL 查询数据 + * + * @param sql SQL 语句 + * @return 数据 + */ + GoViewDataRespVO getDataBySQL(String sql); + +} diff --git a/aagro-module-report/src/main/java/cn/aagro/pp/module/report/service/goview/GoViewDataServiceImpl.java b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/service/goview/GoViewDataServiceImpl.java new file mode 100644 index 0000000..089b61b --- /dev/null +++ b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/service/goview/GoViewDataServiceImpl.java @@ -0,0 +1,55 @@ +package cn.aagro.pp.module.report.service.goview; + +import cn.aagro.pp.module.report.controller.admin.goview.vo.data.GoViewDataRespVO; +import com.google.common.collect.Maps; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.support.rowset.SqlRowSet; +import org.springframework.jdbc.support.rowset.SqlRowSetMetaData; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.Map; + +/** + * GoView 数据 Service 实现类 + * + * 补充说明: + * 1. 目前默认使用 jdbcTemplate 查询项目配置的数据源。如果你想查询其它数据源,可以新建对应数据源的 jdbcTemplate 来实现。 + * 2. 默认数据源是 MySQL 关系数据源,可能数据量比较大的情况下,会比较慢,可以考虑后续使用 Click House 等等。 + * + * @author 芋道源码 + */ +@Service +@Validated +public class GoViewDataServiceImpl implements GoViewDataService { + + @Resource + private JdbcTemplate jdbcTemplate; + + @Override + public GoViewDataRespVO getDataBySQL(String sql) { + // 1. 执行查询 + SqlRowSet sqlRowSet = jdbcTemplate.queryForRowSet(sql); + + // 2. 构建返回结果 + GoViewDataRespVO respVO = new GoViewDataRespVO(); + // 2.1 解析元数据 + SqlRowSetMetaData metaData = sqlRowSet.getMetaData(); + String[] columnNames = metaData.getColumnNames(); + respVO.setDimensions(Arrays.asList(columnNames)); + // 2.2 解析数据明细 + respVO.setSource(new LinkedList<>()); // 由于数据量不确认,使用 LinkedList 虽然内存占用大一点,但是不存在扩容复制的问题 + while (sqlRowSet.next()) { + Map data = Maps.newHashMapWithExpectedSize(columnNames.length); + for (String columnName : columnNames) { + data.put(columnName, sqlRowSet.getObject(columnName)); + } + respVO.getSource().add(data); + } + return respVO; + } + +} diff --git a/aagro-module-report/src/main/java/cn/aagro/pp/module/report/service/goview/GoViewProjectService.java b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/service/goview/GoViewProjectService.java new file mode 100644 index 0000000..0062923 --- /dev/null +++ b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/service/goview/GoViewProjectService.java @@ -0,0 +1,57 @@ +package cn.aagro.pp.module.report.service.goview; + +import cn.aagro.pp.framework.common.pojo.PageParam; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.report.controller.admin.goview.vo.project.GoViewProjectCreateReqVO; +import cn.aagro.pp.module.report.controller.admin.goview.vo.project.GoViewProjectUpdateReqVO; +import cn.aagro.pp.module.report.dal.dataobject.goview.GoViewProjectDO; + +import javax.validation.Valid; + +/** + * GoView 项目 Service 接口 + * + * @author 芋道源码 + */ +public interface GoViewProjectService { + + /** + * 创建项目 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createProject(@Valid GoViewProjectCreateReqVO createReqVO); + + /** + * 更新项目 + * + * @param updateReqVO 更新信息 + */ + void updateProject(@Valid GoViewProjectUpdateReqVO updateReqVO); + + /** + * 删除项目 + * + * @param id 编号 + */ + void deleteProject(Long id); + + /** + * 获得项目 + * + * @param id 编号 + * @return 项目 + */ + GoViewProjectDO getProject(Long id); + + /** + * 获得我的项目分页 + * + * @param pageReqVO 分页查询 + * @param userId 用户编号 + * @return GoView 项目分页 + */ + PageResult getMyProjectPage(PageParam pageReqVO, Long userId); + +} diff --git a/aagro-module-report/src/main/java/cn/aagro/pp/module/report/service/goview/GoViewProjectServiceImpl.java b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/service/goview/GoViewProjectServiceImpl.java new file mode 100644 index 0000000..af839b3 --- /dev/null +++ b/aagro-module-report/src/main/java/cn/aagro/pp/module/report/service/goview/GoViewProjectServiceImpl.java @@ -0,0 +1,74 @@ +package cn.aagro.pp.module.report.service.goview; + +import cn.aagro.pp.framework.common.enums.CommonStatusEnum; +import cn.aagro.pp.framework.common.pojo.PageParam; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.module.report.controller.admin.goview.vo.project.GoViewProjectCreateReqVO; +import cn.aagro.pp.module.report.controller.admin.goview.vo.project.GoViewProjectUpdateReqVO; +import cn.aagro.pp.module.report.convert.goview.GoViewProjectConvert; +import cn.aagro.pp.module.report.dal.dataobject.goview.GoViewProjectDO; +import cn.aagro.pp.module.report.dal.mysql.goview.GoViewProjectMapper; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; + +import static cn.aagro.pp.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.aagro.pp.module.report.enums.ErrorCodeConstants.GO_VIEW_PROJECT_NOT_EXISTS; + +/** + * GoView 项目 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class GoViewProjectServiceImpl implements GoViewProjectService { + + @Resource + private GoViewProjectMapper goViewProjectMapper; + + @Override + public Long createProject(GoViewProjectCreateReqVO createReqVO) { + // 插入 + GoViewProjectDO goViewProject = GoViewProjectConvert.INSTANCE.convert(createReqVO) + .setStatus(CommonStatusEnum.DISABLE.getStatus()); + goViewProjectMapper.insert(goViewProject); + // 返回 + return goViewProject.getId(); + } + + @Override + public void updateProject(GoViewProjectUpdateReqVO updateReqVO) { + // 校验存在 + validateProjectExists(updateReqVO.getId()); + // 更新 + GoViewProjectDO updateObj = GoViewProjectConvert.INSTANCE.convert(updateReqVO); + goViewProjectMapper.updateById(updateObj); + } + + @Override + public void deleteProject(Long id) { + // 校验存在 + validateProjectExists(id); + // 删除 + goViewProjectMapper.deleteById(id); + } + + private void validateProjectExists(Long id) { + if (goViewProjectMapper.selectById(id) == null) { + throw exception(GO_VIEW_PROJECT_NOT_EXISTS); + } + } + + @Override + public GoViewProjectDO getProject(Long id) { + return goViewProjectMapper.selectById(id); + } + + @Override + public PageResult getMyProjectPage(PageParam pageReqVO, Long userId) { + return goViewProjectMapper.selectPage(pageReqVO, userId); + } + +} diff --git a/aagro-module-report/src/test/java/cn/aagro/pp/module/report/service/goview/GoViewDataServiceImplTest.java b/aagro-module-report/src/test/java/cn/aagro/pp/module/report/service/goview/GoViewDataServiceImplTest.java new file mode 100644 index 0000000..a83a875 --- /dev/null +++ b/aagro-module-report/src/test/java/cn/aagro/pp/module/report/service/goview/GoViewDataServiceImplTest.java @@ -0,0 +1,58 @@ +package cn.aagro.pp.module.report.service.goview; + +import cn.aagro.pp.framework.test.core.ut.BaseDbUnitTest; +import cn.aagro.pp.module.report.controller.admin.goview.vo.data.GoViewDataRespVO; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.support.rowset.SqlRowSet; +import org.springframework.jdbc.support.rowset.SqlRowSetMetaData; + +import javax.annotation.Resource; +import java.util.Arrays; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@Import(GoViewDataServiceImpl.class) +public class GoViewDataServiceImplTest extends BaseDbUnitTest { + + @Resource + private GoViewDataServiceImpl goViewDataService; + + @MockBean + private JdbcTemplate jdbcTemplate; + + @Test + public void testGetDataBySQL() { + // 准备参数 + String sql = "SELECT id, name FROM system_users"; + // mock 方法 + SqlRowSet sqlRowSet = mock(SqlRowSet.class); + when(jdbcTemplate.queryForRowSet(eq(sql))).thenReturn(sqlRowSet); + // mock 元数据 + SqlRowSetMetaData metaData = mock(SqlRowSetMetaData.class); + when(sqlRowSet.getMetaData()).thenReturn(metaData); + when(metaData.getColumnNames()).thenReturn(new String[]{"id", "name"}); + // mock 数据明细 + when(sqlRowSet.next()).thenReturn(true).thenReturn(true).thenReturn(false); + when(sqlRowSet.getObject("id")).thenReturn(1L).thenReturn(2L); + when(sqlRowSet.getObject("name")).thenReturn("芋道源码").thenReturn("芋道"); + + // 调用 + GoViewDataRespVO dataBySQL = goViewDataService.getDataBySQL(sql); + // 断言 + assertEquals(Arrays.asList("id", "name"), dataBySQL.getDimensions()); + assertEquals(2, dataBySQL.getDimensions().size()); + assertEquals(2, dataBySQL.getSource().get(0).size()); + assertEquals(1L, dataBySQL.getSource().get(0).get("id")); + assertEquals("芋道源码", dataBySQL.getSource().get(0).get("name")); + assertEquals(2, dataBySQL.getSource().get(1).size()); + assertEquals(2L, dataBySQL.getSource().get(1).get("id")); + assertEquals("芋道", dataBySQL.getSource().get(1).get("name")); + } + +} diff --git a/aagro-module-report/src/test/java/cn/aagro/pp/module/report/service/goview/GoViewProjectServiceImplTest.java b/aagro-module-report/src/test/java/cn/aagro/pp/module/report/service/goview/GoViewProjectServiceImplTest.java new file mode 100644 index 0000000..1c60e53 --- /dev/null +++ b/aagro-module-report/src/test/java/cn/aagro/pp/module/report/service/goview/GoViewProjectServiceImplTest.java @@ -0,0 +1,135 @@ +package cn.aagro.pp.module.report.service.goview; + +import cn.aagro.pp.framework.common.pojo.PageParam; +import cn.aagro.pp.framework.common.pojo.PageResult; +import cn.aagro.pp.framework.test.core.ut.BaseDbUnitTest; +import cn.aagro.pp.module.report.controller.admin.goview.vo.project.GoViewProjectCreateReqVO; +import cn.aagro.pp.module.report.controller.admin.goview.vo.project.GoViewProjectUpdateReqVO; +import cn.aagro.pp.module.report.dal.dataobject.goview.GoViewProjectDO; +import cn.aagro.pp.module.report.dal.mysql.goview.GoViewProjectMapper; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; + +import static cn.aagro.pp.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static cn.aagro.pp.framework.test.core.util.AssertUtils.assertPojoEquals; +import static cn.aagro.pp.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.aagro.pp.framework.test.core.util.RandomUtils.*; +import static cn.aagro.pp.module.report.enums.ErrorCodeConstants.GO_VIEW_PROJECT_NOT_EXISTS; +import static org.junit.jupiter.api.Assertions.*; + +/** + * {@link GoViewProjectServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(GoViewProjectServiceImpl.class) +public class GoViewProjectServiceImplTest extends BaseDbUnitTest { + + @Resource + private GoViewProjectServiceImpl goViewProjectService; + + @Resource + private GoViewProjectMapper goViewProjectMapper; + + @Test + public void testCreateProject_success() { + // 准备参数 + GoViewProjectCreateReqVO reqVO = randomPojo(GoViewProjectCreateReqVO.class); + + // 调用 + Long goViewProjectId = goViewProjectService.createProject(reqVO); + // 断言 + assertNotNull(goViewProjectId); + // 校验记录的属性是否正确 + GoViewProjectDO goViewProject = goViewProjectMapper.selectById(goViewProjectId); + assertPojoEquals(reqVO, goViewProject); + } + + @Test + public void testUpdateProject_success() { + // mock 数据 + GoViewProjectDO dbGoViewProject = randomPojo(GoViewProjectDO.class); + goViewProjectMapper.insert(dbGoViewProject);// @Sql: 先插入出一条存在的数据 + // 准备参数 + GoViewProjectUpdateReqVO reqVO = randomPojo(GoViewProjectUpdateReqVO.class, o -> { + o.setId(dbGoViewProject.getId()); // 设置更新的 ID + o.setStatus(randomCommonStatus()); + }); + + // 调用 + goViewProjectService.updateProject(reqVO); + // 校验是否更新正确 + GoViewProjectDO goViewProject = goViewProjectMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, goViewProject); + } + + @Test + public void testUpdateProject_notExists() { + // 准备参数 + GoViewProjectUpdateReqVO reqVO = randomPojo(GoViewProjectUpdateReqVO.class); + + // 调用, 并断言异常 + assertServiceException(() -> goViewProjectService.updateProject(reqVO), GO_VIEW_PROJECT_NOT_EXISTS); + } + + @Test + public void testDeleteProject_success() { + // mock 数据 + GoViewProjectDO dbGoViewProject = randomPojo(GoViewProjectDO.class); + goViewProjectMapper.insert(dbGoViewProject);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbGoViewProject.getId(); + + // 调用 + goViewProjectService.deleteProject(id); + // 校验数据不存在了 + assertNull(goViewProjectMapper.selectById(id)); + } + + @Test + public void testDeleteProject_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> goViewProjectService.deleteProject(id), GO_VIEW_PROJECT_NOT_EXISTS); + } + + @Test + public void testGetProject() { + // mock 数据 + GoViewProjectDO dbGoViewProject = randomPojo(GoViewProjectDO.class); + goViewProjectMapper.insert(dbGoViewProject);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbGoViewProject.getId(); + + // 调用 + GoViewProjectDO goViewProject = goViewProjectService.getProject(id); + // 断言 + assertPojoEquals(dbGoViewProject, goViewProject); + } + + @Test + public void testGetMyGoViewProjectPage() { + // mock 数据 + GoViewProjectDO dbGoViewProject = randomPojo(GoViewProjectDO.class, o -> { // 等会查询到 + o.setCreator("1"); + }); + goViewProjectMapper.insert(dbGoViewProject); + // 测试 userId 不匹配 + goViewProjectMapper.insert(cloneIgnoreId(dbGoViewProject, o -> o.setCreator("2"))); + // 准备参数 + PageParam reqVO = new PageParam(); + Long userId = 1L; + + // 调用 + PageResult pageResult = goViewProjectService.getMyProjectPage(reqVO, userId); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbGoViewProject, pageResult.getList().get(0)); + } + +} diff --git a/aagro-module-report/src/test/resources/application-unit-test.yaml b/aagro-module-report/src/test/resources/application-unit-test.yaml new file mode 100644 index 0000000..b901462 --- /dev/null +++ b/aagro-module-report/src/test/resources/application-unit-test.yaml @@ -0,0 +1,47 @@ +spring: + main: + lazy-initialization: true # 开启懒加载,加快速度 + banner-mode: off # 单元测试,禁用 Banner + +--- #################### 数据库相关配置 #################### + +spring: + # 数据源配置项 + datasource: + name: ruoyi-vue-pro + url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false;NON_KEYWORDS=value; # MODE 使用 MySQL 模式;DATABASE_TO_UPPER 配置表和字段使用小写 + driver-class-name: org.h2.Driver + username: sa + password: + druid: + async-init: true # 单元测试,异步初始化 Druid 连接池,提升启动速度 + initial-size: 1 # 单元测试,配置为 1,提升启动速度 + sql: + init: + schema-locations: classpath:/sql/create_tables.sql + + # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 + redis: + host: 127.0.0.1 # 地址 + port: 16379 # 端口(单元测试,使用 16379 端口) + database: 0 # 数据库索引 + +mybatis: + lazy-initialization: true # 单元测试,设置 MyBatis Mapper 延迟加载,加速每个单元测试 + +--- #################### 定时任务相关配置 #################### + +--- #################### 配置中心相关配置 #################### + +--- #################### 服务保障相关配置 #################### + +# Lock4j 配置项(单元测试,禁用 Lock4j) + +--- #################### 监控相关配置 #################### + +--- #################### 芋道相关配置 #################### + +# 芋道配置项,设置当前项目所有自定义的配置 +aagro: + info: + base-package: cn.aagro.pp.module diff --git a/aagro-module-report/src/test/resources/logback.xml b/aagro-module-report/src/test/resources/logback.xml new file mode 100644 index 0000000..daf756b --- /dev/null +++ b/aagro-module-report/src/test/resources/logback.xml @@ -0,0 +1,4 @@ + + + + diff --git a/aagro-module-report/src/test/resources/sql/clean.sql b/aagro-module-report/src/test/resources/sql/clean.sql new file mode 100644 index 0000000..1529151 --- /dev/null +++ b/aagro-module-report/src/test/resources/sql/clean.sql @@ -0,0 +1 @@ +DELETE FROM "report_go_view_project"; \ No newline at end of file diff --git a/aagro-module-report/src/test/resources/sql/create_tables.sql b/aagro-module-report/src/test/resources/sql/create_tables.sql new file mode 100644 index 0000000..a77397f --- /dev/null +++ b/aagro-module-report/src/test/resources/sql/create_tables.sql @@ -0,0 +1,14 @@ +CREATE TABLE IF NOT EXISTS "report_go_view_project" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar NOT NULL, + "pic_url" varchar, + "content" varchar, + "status" varchar NOT NULL, + "remark" varchar, + "creator" varchar DEFAULT '', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT 'GoView 项目表'; diff --git a/aagro-server/pom.xml b/aagro-server/pom.xml index 0d36328..6d94906 100644 --- a/aagro-server/pom.xml +++ b/aagro-server/pom.xml @@ -33,18 +33,18 @@ - - - - - + + cn.aagro.gg + aagro-module-member + ${revision} + - - - - - + + cn.aagro.gg + aagro-module-report + ${revision} + @@ -52,18 +52,18 @@ - - - - - + + cn.aagro.gg + aagro-module-pay + ${revision} + - - - - - + + cn.aagro.gg + aagro-module-mp + ${revision} + diff --git a/aagro-server/src/main/resources/application-dev.yaml b/aagro-server/src/main/resources/application-dev.yaml index 5d1ddd3..6ca0ac0 100644 --- a/aagro-server/src/main/resources/application-dev.yaml +++ b/aagro-server/src/main/resources/application-dev.yaml @@ -1,5 +1,5 @@ server: - port: 48080 + port: 38080 --- #################### 数据库相关配置 #################### @@ -47,21 +47,21 @@ spring: primary: master datasource: master: - url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 + url: jdbc:mysql://111.3.47.177:13306/aiotmini?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 username: root - password: 123456 + password: aioitagro slave: # 模拟从库,可根据自己需要修改 # 模拟从库,可根据自己需要修改 lazy: true # 开启懒加载,保证启动速度 - url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 + url: jdbc:mysql://111.3.47.177:13306/aiotmini?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 username: root - password: 123456 + password: aioitagro # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 redis: - host: 400-infra.server.iocoder.cn # 地址 - port: 6379 # 端口 + host: 111.3.47.177 # 地址 + port: 16379 # 端口 database: 1 # 数据库索引 -# password: 123456 # 密码,建议生产环境开启 + password: aiotagro # 密码,建议生产环境开启 --- #################### 定时任务相关配置 #################### diff --git a/aagro-server/src/main/resources/application-local.yaml b/aagro-server/src/main/resources/application-local.yaml index 6b31aac..115a5ea 100644 --- a/aagro-server/src/main/resources/application-local.yaml +++ b/aagro-server/src/main/resources/application-local.yaml @@ -1,5 +1,5 @@ server: - port: 48080 + port: 38080 --- #################### 数据库相关配置 #################### spring: @@ -48,27 +48,14 @@ spring: primary: master datasource: master: - url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 - # url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=true&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true # MySQL Connector/J 5.X 连接的示例 - # url: jdbc:postgresql://127.0.0.1:5432/ruoyi-vue-pro # PostgreSQL 连接的示例 - # url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例 - # url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=ruoyi-vue-pro;SelectMethod=cursor;encrypt=false;rewriteBatchedStatements=true;useUnicode=true;characterEncoding=utf-8 # SQLServer 连接的示例 - # url: jdbc:dm://127.0.0.1:5236?schema=RUOYI_VUE_PRO # DM 连接的示例 - # url: jdbc:kingbase8://127.0.0.1:54321/test # 人大金仓 KingbaseES 连接的示例 - # url: jdbc:postgresql://127.0.0.1:5432/postgres # OpenGauss 连接的示例 + url: jdbc:mysql://111.3.47.177:13306/aiotmini?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 username: root - password: 123456 - # username: sa # SQL Server 连接的示例 - # password: Aagro@2024 # SQL Server 连接的示例 - # username: SYSDBA # DM 连接的示例 - # password: SYSDBA001 # DM 连接的示例 - # username: root # OpenGauss 连接的示例 - # password: Aagro@2024 # OpenGauss 连接的示例 + password: aioitagro slave: # 模拟从库,可根据自己需要修改 lazy: true # 开启懒加载,保证启动速度 - url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true&nullCatalogMeansCurrent=true + url: jdbc:mysql://111.3.47.177:13306/aiotmini?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true&nullCatalogMeansCurrent=true username: root - password: 123456 + password: aioitagro # tdengine: # IoT 数据库(需要 IoT 物联网再开启噢!) # url: jdbc:TAOS-RS://127.0.0.1:6041/ruoyi_vue_pro # driver-class-name: com.taosdata.jdbc.rs.RestfulDriver @@ -79,10 +66,10 @@ spring: # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 redis: - host: 127.0.0.1 # 地址 - port: 6379 # 端口 + host: 111.3.47.177 # 地址 + port: 16379 # 端口 database: 0 # 数据库索引 -# password: dev # 密码,建议生产环境开启 + password: aiotagro # 密码,建议生产环境开启 --- #################### 定时任务相关配置 #################### diff --git a/aagro-server/src/main/resources/application-prod.yaml b/aagro-server/src/main/resources/application-prod.yaml index 7921079..a876668 100644 --- a/aagro-server/src/main/resources/application-prod.yaml +++ b/aagro-server/src/main/resources/application-prod.yaml @@ -1,5 +1,5 @@ server: - port: 48080 + port: 38080 --- #################### 数据库相关配置 #################### @@ -47,12 +47,12 @@ spring: primary: master datasource: master: - url: jdbc:mysql://111.3.47.177:13306/vuepro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true + url: jdbc:mysql://111.3.47.177:13306/aiotmini?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true username: root password: aiotagro slave: # 模拟从库,可根据自己需要修改 lazy: true # 开启懒加载,保证启动速度 - url: jdbc:mysql://111.3.47.177:13306/vuepro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true + url: jdbc:mysql://111.3.47.177:13306/aiotmini?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true username: root password: aiotagro diff --git a/pom.xml b/pom.xml index beedc1f..c02d0d5 100644 --- a/pom.xml +++ b/pom.xml @@ -15,11 +15,11 @@ aagro-module-system aagro-module-infra - + aagro-module-member - - - + aagro-module-report + aagro-module-mp + aagro-module-pay