Browse Source

支付平台建设

wangqi49 2 weeks ago
parent
commit
f5408b9aef
95 changed files with 3857 additions and 205 deletions
  1. 42 43
      resources/database-sql/webchat-payment.sql
  2. 22 0
      resources/database-sql/webchat-pgc.sql
  3. 38 1
      resources/database-sql/webchat-ugc.sql
  4. 8 2
      resources/database-sql/webchat-user.sql
  5. 16 2
      webchat-admin/src/main/java/com/webchat/admin/controller/AccountManagementController.java
  6. 40 0
      webchat-admin/src/main/java/com/webchat/admin/controller/MessageCardTemplateController.java
  7. 15 2
      webchat-admin/src/main/java/com/webchat/admin/service/AccountManagementService.java
  8. 37 0
      webchat-admin/src/main/java/com/webchat/admin/service/MessageCardTemplateService.java
  9. 1 0
      webchat-aigc/src/main/java/com/webchat/aigc/WebchatAIGCApplication.java
  10. 2 0
      webchat-aigc/src/main/java/com/webchat/aigc/llm/GPTChatService.java
  11. 1 1
      webchat-client-chat/src/main/java/com/webchat/client/chat/controller/RedPacketController.java
  12. 10 0
      webchat-common/pom.xml
  13. 35 0
      webchat-common/src/main/java/com/webchat/common/config/configuration/RedissonConfig.java
  14. 38 0
      webchat-common/src/main/java/com/webchat/common/constants/RedPacketConstants.java
  15. 4 0
      webchat-common/src/main/java/com/webchat/common/constants/WebConstant.java
  16. 3 0
      webchat-common/src/main/java/com/webchat/common/enums/AccountRelationTypeEnum.java
  17. 2 1
      webchat-common/src/main/java/com/webchat/common/enums/ChatMessageTypeEnum.java
  18. 33 3
      webchat-common/src/main/java/com/webchat/common/enums/RedisKeyEnum.java
  19. 2 0
      webchat-common/src/main/java/com/webchat/common/enums/RoleCodeEnum.java
  20. 4 0
      webchat-common/src/main/java/com/webchat/common/enums/messagequeue/MessageBroadChannelEnum.java
  21. 25 0
      webchat-common/src/main/java/com/webchat/common/enums/payment/PaymentOrderDetailStatusEnum.java
  22. 26 0
      webchat-common/src/main/java/com/webchat/common/enums/payment/PaymentOrderStatusEnum.java
  23. 1 0
      webchat-common/src/main/java/com/webchat/common/service/FreeMarkEngineService.java
  24. 50 0
      webchat-common/src/main/java/com/webchat/common/service/RedisService.java
  25. 49 0
      webchat-common/src/main/java/com/webchat/common/service/SnowflakeIdGeneratorService.java
  26. 2 0
      webchat-common/src/main/java/com/webchat/common/service/messagequeue/producer/MessageQueueProducer.java
  27. 5 5
      webchat-common/src/main/java/com/webchat/common/util/SignUtil.java
  28. 87 0
      webchat-common/src/main/java/com/webchat/common/util/SnowflakeIdGenerator.java
  29. 8 1
      webchat-connect/src/main/java/com/webchat/connect/messagequeue/config/RedisConfig.java
  30. 31 0
      webchat-connect/src/main/java/com/webchat/connect/messagequeue/consumer/redis/MessageCardPushRedisQueueListener.java
  31. 30 0
      webchat-connect/src/main/java/com/webchat/connect/messagequeue/consumer/redis/RedPacketNotifyRedisQueueListener.java
  32. 27 0
      webchat-connect/src/main/java/com/webchat/connect/messagequeue/consumer/rocketmq/MessageCardPushRocketQueueConsumer.java
  33. 35 0
      webchat-connect/src/main/java/com/webchat/connect/messagequeue/consumer/rocketmq/RedPacketNotifyRocketQueueConsumer.java
  34. 5 4
      webchat-connect/src/main/java/com/webchat/connect/messagequeue/consumer/service/ArticlePushConsumeService.java
  35. 132 0
      webchat-connect/src/main/java/com/webchat/connect/messagequeue/consumer/service/MessageCardPushConsumeService.java
  36. 131 0
      webchat-connect/src/main/java/com/webchat/connect/messagequeue/consumer/service/RedPacketNotifyConsumeService.java
  37. 17 0
      webchat-connect/src/main/java/com/webchat/connect/service/AccountService.java
  38. 30 0
      webchat-connect/src/main/java/com/webchat/connect/service/MessageCardTemplateService.java
  39. 30 0
      webchat-domain/src/main/java/com/webchat/domain/dto/messagecard/MessageCardSendDTO.java
  40. 34 0
      webchat-domain/src/main/java/com/webchat/domain/dto/messagecard/MessageCardTemplate.java
  41. 50 0
      webchat-domain/src/main/java/com/webchat/domain/dto/payment/PaymentOrderBaseDTO.java
  42. 10 0
      webchat-domain/src/main/java/com/webchat/domain/dto/payment/PaymentOrderCreateDTO.java
  43. 21 0
      webchat-domain/src/main/java/com/webchat/domain/dto/payment/PaymentOrderDTO.java
  44. 2 0
      webchat-domain/src/main/java/com/webchat/domain/dto/payment/PaymentTransRequestDTO.java
  45. 24 0
      webchat-domain/src/main/java/com/webchat/domain/dto/payment/TransIdDTO.java
  46. 1 1
      webchat-domain/src/main/java/com/webchat/domain/vo/request/CreateAccountRequestVO.java
  47. 21 0
      webchat-domain/src/main/java/com/webchat/domain/vo/request/MessageCardTemplateRequestVO.java
  48. 8 3
      webchat-domain/src/main/java/com/webchat/domain/vo/request/SendRedPacketRequestVO.java
  49. 3 1
      webchat-domain/src/main/java/com/webchat/domain/vo/request/mess/MessageBaseVO.java
  50. 13 0
      webchat-domain/src/main/java/com/webchat/domain/vo/response/MessageCardTemplateResponseVO.java
  51. 65 0
      webchat-domain/src/main/java/com/webchat/domain/vo/response/RedPacketBaseVO.java
  52. 3 43
      webchat-domain/src/main/java/com/webchat/domain/vo/response/RedPacketDetailVO.java
  53. 5 2
      webchat-domain/src/main/java/com/webchat/domain/vo/response/mess/ChatMessageResponseVO.java
  54. 43 0
      webchat-pay/src/main/java/com/webchat/pay/config/constants/PaymentConstant.java
  55. 38 16
      webchat-pay/src/main/java/com/webchat/pay/controller/PaymentApiServiceController.java
  56. 15 0
      webchat-pay/src/main/java/com/webchat/pay/repository/dao/IPaymentOrderDAO.java
  57. 29 0
      webchat-pay/src/main/java/com/webchat/pay/repository/dao/IPaymentOrderDetailDAO.java
  58. 67 0
      webchat-pay/src/main/java/com/webchat/pay/repository/entity/PaymentOrderDetailEntity.java
  59. 84 0
      webchat-pay/src/main/java/com/webchat/pay/repository/entity/PaymentOrderEntity.java
  60. 115 17
      webchat-pay/src/main/java/com/webchat/pay/service/PaymentApiService.java
  61. 8 0
      webchat-pay/src/main/java/com/webchat/pay/service/PaymentOrderDetailService.java
  62. 310 0
      webchat-pay/src/main/java/com/webchat/pay/service/PaymentOrderService.java
  63. 49 0
      webchat-pgc/src/main/java/com/webchat/pgc/controller/MessageCardTemplateController.java
  64. 14 0
      webchat-pgc/src/main/java/com/webchat/pgc/repository/dao/IMessageCardTemplateDAO.java
  65. 66 0
      webchat-pgc/src/main/java/com/webchat/pgc/repository/entity/MessageCardTemplateEntity.java
  66. 128 0
      webchat-pgc/src/main/java/com/webchat/pgc/service/MessageCardTemplateService.java
  67. 29 14
      webchat-remote/src/main/java/com/webchat/rmi/pay/PaymentApiServiceClient.java
  68. 34 0
      webchat-remote/src/main/java/com/webchat/rmi/pgc/MessageCardTemplateClient.java
  69. 20 2
      webchat-remote/src/main/java/com/webchat/rmi/user/UserServiceClient.java
  70. 16 0
      webchat-ugc/src/main/java/com/webchat/ugc/config/MessageCardAccountConfig.java
  71. 16 0
      webchat-ugc/src/main/java/com/webchat/ugc/config/MessageCardTemplateConfig.java
  72. 1 1
      webchat-ugc/src/main/java/com/webchat/ugc/controller/RedPacketController.java
  73. 45 1
      webchat-ugc/src/main/java/com/webchat/ugc/messaegqueue/service/PersistentMessageService.java
  74. 13 0
      webchat-ugc/src/main/java/com/webchat/ugc/repository/dao/IRedPacketDAO.java
  75. 13 0
      webchat-ugc/src/main/java/com/webchat/ugc/repository/dao/IRedPacketRecordDAO.java
  76. 86 0
      webchat-ugc/src/main/java/com/webchat/ugc/repository/entity/RedPacketEntity.java
  77. 50 0
      webchat-ugc/src/main/java/com/webchat/ugc/repository/entity/RedPacketRecordEntity.java
  78. 18 0
      webchat-ugc/src/main/java/com/webchat/ugc/service/AccountService.java
  79. 26 0
      webchat-ugc/src/main/java/com/webchat/ugc/service/MessageCardTemplateService.java
  80. 123 7
      webchat-ugc/src/main/java/com/webchat/ugc/service/PaymentService.java
  81. 0 21
      webchat-ugc/src/main/java/com/webchat/ugc/service/RedPacketService.java
  82. 149 0
      webchat-ugc/src/main/java/com/webchat/ugc/service/redpacket/AbstractOpenRedPacketService.java
  83. 51 0
      webchat-ugc/src/main/java/com/webchat/ugc/service/redpacket/OpenRedPacketWithFactory.java
  84. 75 0
      webchat-ugc/src/main/java/com/webchat/ugc/service/redpacket/OpenRedPacketWithLuaService.java
  85. 85 0
      webchat-ugc/src/main/java/com/webchat/ugc/service/redpacket/OpenRedPacketWithQueueService.java
  86. 202 0
      webchat-ugc/src/main/java/com/webchat/ugc/service/redpacket/OpenRedPacketWithRedissonLockService.java
  87. 14 0
      webchat-ugc/src/main/java/com/webchat/ugc/service/redpacket/RedPacketOpenInter.java
  88. 344 0
      webchat-ugc/src/main/java/com/webchat/ugc/service/redpacket/RedPacketService.java
  89. 13 2
      webchat-user/src/main/java/com/webchat/user/controller/UserServiceController.java
  90. 76 4
      webchat-user/src/main/java/com/webchat/user/service/UserService.java
  91. 13 0
      webchat-user/src/main/java/com/webchat/user/service/relation/AbstractAccountRelationService.java
  92. 3 2
      webchat-user/src/main/java/com/webchat/user/service/relation/AccountRelationFactory.java
  93. 9 0
      webchat-user/src/main/java/com/webchat/user/service/relation/AccountRelationWrapper.java
  94. 0 3
      webchat-user/src/main/java/com/webchat/user/service/relation/User2OfficialAccountRelationService.java
  95. 33 0
      webchat-user/src/main/java/com/webchat/user/service/relation/User2ServerAccountRelationService.java

+ 42 - 43
resources/database-sql/webchat-payment.sql

@@ -20,48 +20,47 @@ CREATE TABLE webchat_payment.`web_chat_app` (
         KEY `INDEX_ADMIN` (`admin`)
 ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='接入支付平台的应用信息表';
 
-
--- 红包信息表
-CREATE TABLE webchat_payment.`web_chat_red_packet` (
-   `ID` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '自增ID',
-   `send_user_id` char(100) NOT NULL COMMENT '红包发送人',
-   `receiver_user_id` char(100) NOT NULL COMMENT '接受人',
-   `type` int(4) NOT NULL COMMENT '消息类型',
-   `count` int(4) NOT NULL COMMENT '红包个数',
-   `status` int(4) NOT NULL COMMENT '状态',
-   `total_money` DECIMAL(10, 2) default '0.00' COMMENT '金额',
-   `CREATE_BY` char(100) DEFAULT NULL COMMENT '创建人',
-   `CREATE_DATE` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
-   `expire_date` datetime NOT NULL COMMENT '过期时间',
-   `UPDATE_BY` char(100) DEFAULT NULL COMMENT '更新人',
-   `UPDATE_DATE` datetime DEFAULT NULL COMMENT '更新时间',
-   `VERSION` int DEFAULT '0' COMMENT '版本',
-   PRIMARY KEY (`ID`),
-   KEY `INDEX_SEND_USER_ID` (`send_user_id`),
-   KEY `INDEX_STATUS_EXPIRE_DATE` (`status`, `expire_date`)
-) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='红包信息表';
-
--- 红包拆分记录明细表
-CREATE TABLE webchat_payment.`web_chat_red_packet_record` (
-      `ID` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '自增ID',
-      `red_packet_id` bigint NOT NULL COMMENT '红包id',
-      `user_id` char(100) NOT NULL COMMENT '领取人',
-      `money` DECIMAL(10, 2) default '0.00' COMMENT '领取金额',
-      `CREATE_DATE` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
-      PRIMARY KEY (`ID`),
-      KEY `INDEX_RED_PACKET_ID` (`red_packet_id`),
-      KEY `INDEX_USER_ID` (`user_id`)
-) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='红包拆分记录明细表';
-
--- 用户钱包
-CREATE TABLE webchat_payment.`web_chat_user_wallet` (
+-- 支付订单表
+CREATE TABLE webchat_payment.`web_chat_payment_order`(
     `ID` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '自增ID',
-    `trans_event` int(4) NOT NULL COMMENT '事件类型',
-    `trans_type` int(4) NOT NULL COMMENT '收入/支出',
-    `user_id` char(100) NOT NULL COMMENT '用户id',
-    `target_user_id` char(100) NOT NULL COMMENT '目标用户',
-    `money` DECIMAL(10, 2) default '0.00' COMMENT '流转金额',
-    `trans_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '流转时间',
+    `order_id` char(64) NOT NULL COMMENT '交易订单ID',
+    `trans_id` char(64) NOT NULL COMMENT '交易订单对应分布式事务ID,用于订单回滚,包括订单明细的回滚',
+    `status` int(4) NOT NULL COMMENT '订单状态',
+    `app_id` bigint NOT NULL COMMENT '订单接入方(应用)',
+    `event_type` int(4) NOT NULL COMMENT '订单事件:出行、红包、餐饮……',
+    `trans_type` int(4) NOT NULL COMMENT '流转类型: 1:收入、-1:支出',
+    `amount` DECIMAL(10,2) NOT NULL COMMENT '订单总金额',
+    `source_account` char(64) NOT NULL COMMENT '交易订单发起账户',
+    `target_account` char(64) NOT NULL COMMENT '交易订单接收账户',
+    `description` varchar(100) DEFAULT NULL COMMENT '订单描述',
+    `expire_date` datetime DEFAULT NULL COMMENT '订单过期时间',
+    `CREATE_DATE` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `UPDATE_DATE` datetime DEFAULT NULL COMMENT '更新时间',
+    `VERSION` int DEFAULT '0' COMMENT '版本',
     PRIMARY KEY (`ID`),
-    KEY `INDEX_USER_ID` (`user_id`)
-) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='用户钱包';
+    UNIQUE KEY `INDEX_ORDER_ID` (`order_id`),
+    UNIQUE KEY `INDEX_TRANS_ID` (`trans_id`),
+    KEY `INDEX_STATUS` (`status`),
+    KEY `INDEX_SOURCE_ACCOUNT` (`source_account`),
+    KEY `INDEX_TARGET_ACCOUNT` (`target_account`)
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='支付订单表';
+
+
+-- 支付交易明细表
+CREATE TABLE webchat_payment.`web_chat_payment_order_detail`(
+     `ID` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '自增ID',
+     `order_id` char(64) NOT NULL COMMENT '关联交易订单ID',
+     `payment_id` char(64) NOT NULL COMMENT '交易ID',
+     `source_account` char(64) NOT NULL COMMENT '交易订单发起账户,一定是跟order中的发起人是同一个',
+     `target_account` char(64) NOT NULL COMMENT '交易订单目标账户,人/业务账户',
+     `amount` DECIMAL(10,2) NOT NULL COMMENT '交易总金额,收入类交易金额为正数、支出类为负数',
+     `status` int(4) NOT NULL COMMENT '交易明细状态',
+     `CREATE_DATE` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+     `UPDATE_DATE` datetime DEFAULT NULL COMMENT '更新时间',
+     `VERSION` int DEFAULT '0' COMMENT '版本',
+     PRIMARY KEY (`ID`),
+     KEY `INDEX_ORDER_ID` (`order_id`),
+     KEY `INDEX_STATUS` (`status`),
+     KEY `INDEX_SOURCE_ACCOUNT` (`source_account`),
+     KEY `INDEX_TARGET_ACCOUNT` (`target_account`)
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='支付订单明细表';

+ 22 - 0
resources/database-sql/webchat-pgc.sql

@@ -22,3 +22,25 @@ CREATE TABLE webchat_pgc.web_chat_article (
       KEY `INDEX_PLAN_PUSH_DATE` (`PLAN_PUSH_DATE`)
 ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='公众号文章表';
 
+-- 服务号消息卡片模版配置表
+CREATE TABLE webchat_pgc.web_chat_message_card_template (
+   `ID` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '自增ID',
+   `ACCOUNT` char(60) NOT NULL COMMENT '服务号',
+   `TEMPLATE_ID` varchar(60) NOT NULL COMMENT '模板ID',
+   `LOGO` varchar(400) DEFAULT NULL COMMENT 'logo',
+   `TITLE` varchar(100) DEFAULT NULL COMMENT '标题',
+   `CONTENT` text NOT NULL COMMENT '模版内容(richText html富文本模版)',
+   `REDIRECT_NAME` varchar(200) DEFAULT NULL COMMENT '链接名称',
+   `REDIRECT_URL` varchar(400) DEFAULT NULL COMMENT '卡片点击跳转链接',
+   `STATUS` tinyint(1) DEFAULT '1' COMMENT '状态',
+   `CREATE_BY` char(100) DEFAULT NULL COMMENT '创建人',
+   `CREATE_DATE` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+   `UPDATE_BY` char(100) DEFAULT NULL COMMENT '更新人',
+   `UPDATE_DATE` datetime DEFAULT NULL COMMENT '更新时间',
+   `VERSION` int DEFAULT '0' COMMENT '版本',
+   PRIMARY KEY (`ID`),
+   UNIQUE KEY `UK_TEMPLATE_ID` (`TEMPLATE_ID`),
+   KEY `INDEX_STATUS_ACCOUNT` (`STATUS`, `ACCOUNT`)
+) ENGINE=InnoDB AUTO_INCREMENT=1  DEFAULT CHARSET=utf8mb4 COMMENT='服务号消息卡片模版配置表';
+
+

+ 38 - 1
resources/database-sql/webchat-ugc.sql

@@ -14,4 +14,41 @@ CREATE TABLE webchat_ugc.`web_chat_message` (
      PRIMARY KEY (`ID`),
      KEY `INDEX_SENDER_PROXY_SENDER` (`sender`, `proxy_sender`),
      KEY `INDEX_RECEIVER` (`receiver`)
-) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='消息持久化数据表';
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='消息持久化数据表';
+
+
+-- 红包信息表
+CREATE TABLE webchat_ugc.`web_chat_red_packet` (
+       `ID` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '自增ID',
+       `order_id` char(64) NOT NULL COMMENT '支付平台交易订单id',
+       `sender` char(100) NOT NULL COMMENT '红包发送人',
+       `receiver` char(100) NOT NULL COMMENT '接受账户(人、群、企业账户)',
+       `type` int(4) NOT NULL COMMENT '红包类型 1 固定红包、2 拼手气',
+       `count` int(4) NOT NULL COMMENT '红包个数',
+       `cover` varchar(100) DEFAULT NULL COMMENT '红包封面',
+       `blessing` varchar(50) DEFAULT NULL COMMENT '祝福语',
+       `status` int(4) NOT NULL COMMENT '状态',
+       `total_money` DECIMAL(10, 2) default '0.00' COMMENT '金额',
+       `CREATE_BY` char(100) DEFAULT NULL COMMENT '创建人',
+       `CREATE_DATE` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+       `expire_date` datetime NOT NULL COMMENT '过期时间',
+       `UPDATE_BY` char(100) DEFAULT NULL COMMENT '更新人',
+       `UPDATE_DATE` datetime DEFAULT NULL COMMENT '更新时间',
+       `VERSION` int DEFAULT '0' COMMENT '版本',
+       PRIMARY KEY (`ID`),
+       KEY `INDEX_ORDER_ID` (`order_id`),
+       KEY `INDEX_SENDER` (`sender`),
+       KEY `INDEX_STATUS_EXPIRE_DATE` (`status`, `expire_date`)
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='红包信息表';
+
+-- 红包拆分记录明细表
+CREATE TABLE webchat_ugc.`web_chat_red_packet_record` (
+      `ID` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '自增ID',
+      `red_packet_id` bigint NOT NULL COMMENT '红包id',
+      `user_id` char(100) NOT NULL COMMENT '领取人',
+      `money` DECIMAL(10, 2) default '0.00' COMMENT '领取金额',
+      `CREATE_DATE` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+      PRIMARY KEY (`ID`),
+      KEY `INDEX_RED_PACKET_ID` (`red_packet_id`),
+      KEY `INDEX_USER_ID` (`user_id`)
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='红包拆分记录明细表';

+ 8 - 2
resources/database-sql/webchat-user.sql

@@ -26,12 +26,18 @@ INSERT INTO `webchat_user`.`web_chat_user` (`ID`, `USER_ID`, `USER_NAME`, `PHOTO
     (1, 'U_770cce9f632543588b4e8aa6ec43e6a2', '管理员', 'https://coderutil.oss-cn-beijing.aliyuncs.com/bbs-image/file_dd489633f1bb4513a9db81be6e9d692f.png',
      'admin', '06525f4969c6cf1886ee0db86bef82df', 1, 2, 'U_770cce9f632543588b4e8aa6ec43e6a2',
      '2022-03-12 05:55:26', NULL, '2022-03-22 10:28:38', 1);
--- 初始化文件传输助手
+-- 初始化我的AI助手
 INSERT INTO `webchat_user`.`web_chat_user` (`USER_ID`, `USER_NAME`, `PHOTO`, `MOBILE`, `PASSWORD`, `STATUS`, `ROLE_CODE`,
                                             `CREATE_BY`, `CREATE_DATE`, `UPDATE_BY`, `UPDATE_DATE`, `VERSION`) VALUES
-    ('AI_ef352f698ad7b60c1d8a3aaa4a948030', '文件传输助手', 'https://coderutil.oss-cn-beijing.aliyuncs.com/bbs-image/file_5a09444d0eb4431abfb01f4efb3afddc.png',
+    ('AI_ef352f698ad7b60c1d8a3aaa4a948030', '我的Ai助手', 'https://coderutil.oss-cn-beijing.aliyuncs.com/bbs-image/file_5a09444d0eb4431abfb01f4efb3afddc.png',
      'aiagent', 'AI_ef352f698ad7b60c1d8a3aaa4a948030', 1, 0, 'AI_ef352f698ad7b60c1d8a3aaa4a948030',
      '2022-03-12 05:55:26', NULL, '2022-03-22 10:28:38', 1);
+-- 初始化webchat支付服务号
+INSERT INTO `webchat_user`.`web_chat_user` (`USER_ID`, `USER_NAME`, `PHOTO`, `MOBILE`, `PASSWORD`, `STATUS`, `ROLE_CODE`,
+                                            `CREATE_BY`, `CREATE_DATE`, `UPDATE_BY`, `UPDATE_DATE`, `VERSION`) VALUES
+    ('S_51a7fbf50155c4b08c55fbbcfc5911db', 'WebChat支付', 'https://coderutil.oss-cn-beijing.aliyuncs.com/bbs-image/file_bf000793461b49ffa5ffe224611450ee.png',
+     'webchat_pay', 'S_51a7fbf50155c4b08c55fbbcfc5911db', 1, 7, 'S_51a7fbf50155c4b08c55fbbcfc5911db',
+     '2022-03-12 05:55:26', NULL, '2022-03-22 10:28:38', 1);
 
 -- 好友关系表
 CREATE TABLE webchat_user.`web_chat_friend` (

+ 16 - 2
webchat-admin/src/main/java/com/webchat/admin/controller/AccountManagementController.java

@@ -6,7 +6,7 @@ import com.webchat.common.bean.APIPageResponseBean;
 import com.webchat.common.bean.APIResponseBean;
 import com.webchat.common.bean.APIResponseBeanUtil;
 import com.webchat.common.helper.SessionHelper;
-import com.webchat.domain.vo.request.CreatePublicAccountRequestVO;
+import com.webchat.domain.vo.request.CreateAccountRequestVO;
 import com.webchat.domain.vo.request.CreateRobotRequestVO;
 import com.webchat.domain.vo.response.UserBaseResponseInfoVO;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -84,10 +84,24 @@ public class AccountManagementController {
      * @return
      */
     @PostMapping("/createPublicAccount")
-    public APIResponseBean createPublicAccount(@RequestBody CreatePublicAccountRequestVO requestPram) {
+    public APIResponseBean createPublicAccount(@RequestBody CreateAccountRequestVO requestPram) {
         String userId = SessionHelper.getCurrentUserId();
         requestPram.setCreateUserId(userId);
         accountManagementService.createPublicAccount(requestPram);
         return APIResponseBeanUtil.success("公众号创建成功");
     }
+
+    /**
+     * 创建服务号
+     * 默认只有管理员可以创建服务号
+     *
+     * @return
+     */
+    @PostMapping("/createServerAccount")
+    public APIResponseBean createServerAccount(@RequestBody CreateAccountRequestVO requestPram) {
+        String userId = SessionHelper.getCurrentUserId();
+        requestPram.setCreateUserId(userId);
+        accountManagementService.createServerAccount(requestPram);
+        return APIResponseBeanUtil.success("公众号创建成功");
+    }
 }

+ 40 - 0
webchat-admin/src/main/java/com/webchat/admin/controller/MessageCardTemplateController.java

@@ -0,0 +1,40 @@
+package com.webchat.admin.controller;
+
+
+import com.webchat.admin.service.MessageCardTemplateService;
+import com.webchat.common.bean.APIResponseBean;
+import com.webchat.common.bean.APIResponseBeanUtil;
+import com.webchat.common.helper.SessionHelper;
+import com.webchat.domain.vo.request.MessageCardTemplateRequestVO;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/admin-service/message-card/template")
+public class MessageCardTemplateController {
+
+
+    @Autowired
+    private MessageCardTemplateService messageCardTemplateService;
+
+    /**
+     * 创建或者更新服务号消息模版配置
+     *
+     * @param templateRequest
+     * @return
+     */
+    @PostMapping("/save")
+    public APIResponseBean<String> save(@RequestBody MessageCardTemplateRequestVO templateRequest) {
+
+        templateRequest.validateTemplateParam();
+
+        String userId = SessionHelper.getCurrentUserId();
+        templateRequest.setOperator(userId);
+        return APIResponseBeanUtil.success(messageCardTemplateService.save(templateRequest));
+    }
+
+
+}

+ 15 - 2
webchat-admin/src/main/java/com/webchat/admin/service/AccountManagementService.java

@@ -4,7 +4,7 @@ import com.webchat.common.bean.APIPageResponseBean;
 import com.webchat.common.bean.APIResponseBean;
 import com.webchat.common.bean.APIResponseBeanUtil;
 import com.webchat.common.exception.BusinessException;
-import com.webchat.domain.vo.request.CreatePublicAccountRequestVO;
+import com.webchat.domain.vo.request.CreateAccountRequestVO;
 import com.webchat.domain.vo.request.CreateRobotRequestVO;
 import com.webchat.domain.vo.response.UserBaseResponseInfoVO;
 import com.webchat.rmi.user.UserServiceClient;
@@ -68,7 +68,7 @@ public class AccountManagementService {
      *
      * 复用用户信息表,账号角色为 PUBLIC_ACCOUNT
      */
-    public void createPublicAccount(CreatePublicAccountRequestVO requestVO) {
+    public void createPublicAccount(CreateAccountRequestVO requestVO) {
         APIResponseBean apiResponseBean = userServiceClient.createPublicAccount(requestVO);
         if (APIResponseBeanUtil.isOk(apiResponseBean)) {
             return;
@@ -76,4 +76,17 @@ public class AccountManagementService {
         throw new BusinessException(apiResponseBean.getMsg());
     }
 
+    /**
+     * 创建服务号
+     *
+     * 复用用户信息表,账号角色为 PUBLIC_ACCOUNT
+     */
+    public void createServerAccount(CreateAccountRequestVO requestVO) {
+        APIResponseBean apiResponseBean = userServiceClient.createServerAccount(requestVO);
+        if (APIResponseBeanUtil.isOk(apiResponseBean)) {
+            return;
+        }
+        throw new BusinessException(apiResponseBean.getMsg());
+    }
+
 }

+ 37 - 0
webchat-admin/src/main/java/com/webchat/admin/service/MessageCardTemplateService.java

@@ -0,0 +1,37 @@
+package com.webchat.admin.service;
+
+
+import com.webchat.common.bean.APIResponseBean;
+import com.webchat.common.bean.APIResponseBeanUtil;
+import com.webchat.common.exception.BusinessException;
+import com.webchat.domain.vo.request.MessageCardTemplateRequestVO;
+import com.webchat.rmi.pgc.MessageCardTemplateClient;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.web.bind.annotation.RequestBody;
+
+@Service
+public class MessageCardTemplateService {
+
+
+    @Autowired
+    private MessageCardTemplateClient messageCardTemplateClient;
+
+    /**
+     * 创建或更新消息卡片模版配置
+     * @param templateRequest
+     * @return templateId
+     */
+    public String save(@RequestBody MessageCardTemplateRequestVO templateRequest) {
+
+        APIResponseBean<String> responseBean = messageCardTemplateClient.save(templateRequest);
+        if (APIResponseBeanUtil.isOk(responseBean)) {
+            return responseBean.getData();
+        }
+        String errorMessage = "模版创建失败";
+        if (responseBean != null) {
+            errorMessage = responseBean.getMsg();
+        }
+        throw new BusinessException(errorMessage);
+    }
+}

+ 1 - 0
webchat-aigc/src/main/java/com/webchat/aigc/WebchatAIGCApplication.java

@@ -13,6 +13,7 @@ import org.springframework.context.annotation.ComponentScan;
 public class WebchatAIGCApplication {
 
     public static void main(String[] args) {
+
         SpringApplication.run(WebchatAIGCApplication.class, args);
     }
 

+ 2 - 0
webchat-aigc/src/main/java/com/webchat/aigc/llm/GPTChatService.java

@@ -12,6 +12,7 @@ import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
+import org.springframework.cloud.context.config.annotation.RefreshScope;
 import org.springframework.stereotype.Service;
 import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
 
@@ -25,6 +26,7 @@ import java.util.Map;
  * @date 2024/10/29 13:42
  */
 @Slf4j
+@RefreshScope
 @Service
 public class GPTChatService {
 

+ 1 - 1
webchat-client-chat/src/main/java/com/webchat/client/chat/controller/RedPacketController.java

@@ -42,7 +42,7 @@ public class RedPacketController {
         sendRedPacketRequest.validateRequestParam();
         // 设置当前登录用户为红包发送人
         String userId = SessionHelper.getCurrentUserId();
-        sendRedPacketRequest.setSendUserId(userId);
+        sendRedPacketRequest.setSender(userId);
         Long redPacketId = redPacketService.sendRedPacket(sendRedPacketRequest);
         return APIResponseBeanUtil.success(redPacketId);
     }

+ 10 - 0
webchat-common/pom.xml

@@ -40,10 +40,20 @@
             <groupId>redis.clients</groupId>
             <artifactId>jedis</artifactId>
         </dependency>
+
+        <!-- spring boot starter data redis -->
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-data-redis</artifactId>
         </dependency>
+
+        <!-- redisson -->
+        <dependency>
+            <groupId>org.redisson</groupId>
+            <artifactId>redisson</artifactId>
+            <version>3.16.6</version>
+        </dependency>
+
         <dependency>
             <groupId>org.apache.rocketmq</groupId>
             <artifactId>rocketmq-spring-boot-starter</artifactId>

+ 35 - 0
webchat-common/src/main/java/com/webchat/common/config/configuration/RedissonConfig.java

@@ -0,0 +1,35 @@
+package com.webchat.common.config.configuration;
+
+import org.redisson.Redisson;
+import org.redisson.api.RedissonClient;
+import org.redisson.config.Config;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class RedissonConfig {
+
+    @Value("${spring.redis.host:localhost}")
+    private String host;
+
+    @Value("${spring.redis.port:6379}")
+    private String port;
+
+    /**
+     * 对 Redisson 的使用都是通过 RedissonClient 对象
+     * @return
+     */
+    @Bean(name = "redissonClient", destroyMethod = "shutdown") // 服务停止后调用 shutdown 方法
+    public RedissonClient redissonClient() {
+        // 1、创建配置
+        Config config = new Config();
+
+        // 2、集群模式
+        // config.useClusterServers().addNodeAddress("127.0.0.1:7004", "127.0.0.1:7001");
+        // 根据 Config 创建出 RedissonClient 示例
+        config.useSingleServer()
+                .setAddress(String.format("redis://%s:%s", host, port));
+        return Redisson.create(config);
+    }
+}

+ 38 - 0
webchat-common/src/main/java/com/webchat/common/constants/RedPacketConstants.java

@@ -0,0 +1,38 @@
+package com.webchat.common.constants;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+public class RedPacketConstants {
+
+
+    /**
+     * 二倍均值法,最小金额限制1
+     */
+    public static final int MIN_AMOUNT = 1;
+
+
+    @Getter
+    public enum OpenRedPacketWithEnum {
+
+        LUA, LOCK, QUEUE;
+    }
+
+
+    @Getter
+    @AllArgsConstructor
+    public enum RedPacketStatus {
+
+        RUNNING(1, "正常状态"),
+        EXPIRED(2, "已过期"),
+        END(3, "已抽完/结束");
+
+        private int status;
+        private String statusName;
+
+        public static boolean isRunning(int status) {
+            return RUNNING.status == status;
+        }
+    }
+
+}

+ 4 - 0
webchat-common/src/main/java/com/webchat/common/constants/WebConstant.java

@@ -24,6 +24,10 @@ public class WebConstant {
      * AI助手默认账号id
      */
     public static final String AI_AGENT_ID = "AI_ef352f698ad7b60c1d8a3aaa4a948030";
+    /**
+     * webchat支付-服务号
+     */
+    public static final String WEBCHAT_PAY_ID = "S_51a7fbf50155c4b08c55fbbcfc5911db";
 
     /**
      * 系统USERID

+ 3 - 0
webchat-common/src/main/java/com/webchat/common/enums/AccountRelationTypeEnum.java

@@ -19,6 +19,7 @@ public enum AccountRelationTypeEnum {
     USER_GROUP(2, "加入群聊"),
     USER_OFFICIAL(3, "订阅公众号"),
     USER_ROBOT(4, "添加机器人"),
+    USER_SERVER(5, "用户订阅服务号"),
     ;
 
     private Integer type;
@@ -38,6 +39,8 @@ public enum AccountRelationTypeEnum {
                 return AccountRelationTypeEnum.USER_ROBOT;
             case 6:
                 return AccountRelationTypeEnum.USER_OFFICIAL;
+            case 7:
+                return AccountRelationTypeEnum.USER_SERVER;
 
         }
         log.error("不支持的关系类型 =====> roleCode: {}", roleCode);

+ 2 - 1
webchat-common/src/main/java/com/webchat/common/enums/ChatMessageTypeEnum.java

@@ -18,7 +18,8 @@ public enum ChatMessageTypeEnum {
     CHATTING_REFRESH(7, "刷新对话列表"),
     VIDEO_CALL(8, "音视频呼叫"),
     GROUP_VIDEO_CALL(9, "群聊音视频呼叫"),
-    VIDEO_LEAVE(10, "一对一音视频对方挂断");
+    VIDEO_LEAVE(10, "一对一音视频对方挂断"),
+    SERVER_ACCOUNT_MESSAGE_CARD(11, "服务号消息卡");
 
     private Integer type;
     private String desc;

+ 33 - 3
webchat-common/src/main/java/com/webchat/common/enums/RedisKeyEnum.java

@@ -35,7 +35,7 @@ public enum RedisKeyEnum {
     /**
      * 创建公众号防重复触发,10S内一次
      */
-    CREATE_PUBLIC_ACCOUNT_LIMIT("CREATE_PUBLIC_ACCOUNT_LIMIT", 10L),
+    CREATE_ACCOUNT_LIMIT("CREATE_ACCOUNT_LIMIT", 10L),
 
     /**
      * 公众号列表缓存
@@ -177,7 +177,7 @@ public enum RedisKeyEnum {
     /**
      * 用户钱包余额
      */
-    USER_WALLET_BALANCE_CACHE("USER_WALLET_BALANCE_CACHE", 3* 24 * 60 * 60L),
+    USER_WALLET_BALANCE_CACHE("USER_WALLET_BALANCE_CACHE", 7 * 24 * 60 * 60L),
 
     /**
      * 用户钱包余额刷新 加锁
@@ -197,7 +197,7 @@ public enum RedisKeyEnum {
     /**
      * 红包领取的用户记录
      */
-    RED_PACKET_RECEIVER_COUNT("RED_PACKET_RECEIVER_COUNT", -1L),
+    RED_PACKET_OPEN_USERS("RED_PACKET_OPEN_USERS", 25 * 60 * 60L),
 
     /**
      * 红包拆分记录
@@ -311,6 +311,36 @@ public enum RedisKeyEnum {
      * 应用交易凭证缓存,2小时有效
      */
     PAYMENT_APP_ACCESS_TOKEN_CACHE("PAYMENT_APP_ACCESS_TOKEN_CACHE", 2 * 60 * 60L),
+
+    /**
+     * 交易分布式事务id缓存
+     */
+    PAYMENT_TRANS_ID_CACHE("PAYMENT_TRANS_ID_CACHE", 15 * 60L),
+
+    /**
+     * 消息卡片模版配详情置缓存
+     */
+    MESSAGE_CARD_TEMPLATE_CACHE("MESSAGE_CARD_TEMPLATE_CACHE", 24 * 60 * 60L),
+
+    /**
+     * 支付订单缓存
+     */
+    PAYMENT_ORDER_CACHE("PAYMENT_ORDER_CACHE",  60 * 60L),
+
+    /**
+     * 刷新红包详情缓存分布式锁
+     */
+    LOCK_REFRESH_RED_PACKET_CACHE("LOCK_REFRESH_RED_PACKET_CACHE",  60L),
+
+    /**
+     * 拆红包lock
+     */
+    LOCK_OPEN_RED_PACKET("LOCK_OPEN_RED_PACKET",  60L),
+
+    /**
+     * 红包金额预生成金额队列
+     */
+    QUEUE_RED_PACKET_AMOUNT("QUEUE_RED_PACKET_AMOUNT",  25 * 60 * 60L),
     ;
 
 

+ 2 - 0
webchat-common/src/main/java/com/webchat/common/enums/RoleCodeEnum.java

@@ -18,6 +18,8 @@ public enum RoleCodeEnum {
     ROBOT(5, "聊天机器人"),
 
     PUBLIC_ACCOUNT(6, "公众号"),
+
+    SERVER_ACCOUNT(7, "服务号"),
     ;
 
     private Integer code;

+ 4 - 0
webchat-common/src/main/java/com/webchat/common/enums/messagequeue/MessageBroadChannelEnum.java

@@ -26,6 +26,10 @@ public enum MessageBroadChannelEnum {
 
     QUEUE_CHAT_ROBOT("queue_chat_robot", "机器人对话消息队列"),
 
+    QUEUE_MESSAGE_CARD_PUSH("queue_message_card_push", "消息卡片推送频道"),
+
+    QUEUE_RED_PACKET_NOTIFY("queue_red_packet_notify", "红包来了~ 通知频道"),
+
     ;
 
     private String channel;

+ 25 - 0
webchat-common/src/main/java/com/webchat/common/enums/payment/PaymentOrderDetailStatusEnum.java

@@ -0,0 +1,25 @@
+package com.webchat.common.enums.payment;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+/**
+ * @author 程序员七七, https://www.coderutil.com网站作者
+ * @date 2024/11/9 03:53
+ */
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+public enum PaymentOrderDetailStatusEnum {
+
+    FINISHED(1, "完成"),
+
+    ROLLBACK(2, "回滚"),
+
+    HIDE(3, "隐藏");
+
+    private Integer status;
+
+    private String statusName;
+}

+ 26 - 0
webchat-common/src/main/java/com/webchat/common/enums/payment/PaymentOrderStatusEnum.java

@@ -0,0 +1,26 @@
+package com.webchat.common.enums.payment;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+/**
+ * @author 程序员七七, https://www.coderutil.com网站作者
+ * @date 2024/11/9 03:53
+ */
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+public enum PaymentOrderStatusEnum {
+
+    NEW(1, "新创建订单"),
+    RUNNING(2, "进行中订单"),
+    FINISHED(3, "正常结束的订单"),
+    STOP(4, "过期后平台强制结束的订单"),
+    ROLLBACK(5, "回滚类订单"),
+    DELETED(6, "已删除");
+
+    private Integer status;
+
+    private String statusName;
+}

+ 1 - 0
webchat-common/src/main/java/com/webchat/common/service/FreeMarkEngineService.java

@@ -13,6 +13,7 @@ import org.springframework.stereotype.Service;
 
 import java.io.IOException;
 import java.io.StringWriter;
+import java.util.HashMap;
 import java.util.Map;
 
 /**

+ 50 - 0
webchat-common/src/main/java/com/webchat/common/service/RedisService.java

@@ -8,8 +8,10 @@ import org.apache.commons.collections.CollectionUtils;
 import org.apache.commons.lang3.ObjectUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.connection.ReturnType;
 import org.springframework.data.redis.core.*;
 import org.springframework.data.redis.core.script.DefaultRedisScript;
+import org.springframework.data.redis.core.script.RedisScript;
 import org.springframework.data.redis.serializer.RedisSerializer;
 import org.springframework.stereotype.Service;
 import redis.clients.jedis.Protocol;
@@ -41,6 +43,25 @@ public class RedisService {
     }
 
     /**
+     * 执行LUA脚本
+     *
+     * @param script
+     * @param keys
+     * @param args
+     * @return
+     * @param <T>
+     */
+    public <T> T executeScript(RedisScript<T> script, List<String> keys, Object... args) {
+        try {
+            return redisTemplate.execute(script, keys, args);
+        } catch (Exception e) {
+            log.error("redis executeScript error! script:{}, keys:{}, args:{}",
+                    script.getScriptAsString(), keys, args, e);
+        }
+        return null;
+    }
+
+    /**
      * get取值
      *
      * @param key
@@ -368,6 +389,17 @@ public class RedisService {
         return result;
     }
 
+    public boolean zIsExist(String key, String value) {
+        try {
+            ZSetOperations<String, String> zset = redisTemplate.opsForZSet();
+            return zset.score(key, value) != null;
+        } catch (Exception e) {
+            log.error("redis zIsExist error! key:{}, value:{}", key, value, e);
+        }
+        return false;
+
+    }
+
     /**
      * @param key
      * @param start
@@ -736,6 +768,17 @@ public class RedisService {
         return result;
     }
 
+    public Long hmdel(String key, List<String> fields) {
+        if (StringUtils.isEmpty(key) || CollectionUtils.isEmpty(fields)) {
+            return null;
+        }
+        Object[] filteredFields = fields.stream()
+                .filter(Objects::nonNull)
+                .toArray();
+        long result = redisTemplate.opsForHash().delete(key, filteredFields);
+        return result;
+    }
+
     /**
      * @param key
      * @param queryFields
@@ -873,6 +916,13 @@ public class RedisService {
         return redisTemplate.opsForList().leftPop(key);
     }
 
+    public String lrightPop(String key) {
+        if (StringUtils.isEmpty(key)) {
+            return null;
+        }
+        return redisTemplate.opsForList().rightPop(key);
+    }
+
     /**
      * @param key
      * @param time

+ 49 - 0
webchat-common/src/main/java/com/webchat/common/service/SnowflakeIdGeneratorService.java

@@ -0,0 +1,49 @@
+package com.webchat.common.service;
+
+import com.webchat.common.util.DateUtils;
+import com.webchat.common.util.SnowflakeIdGenerator;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.util.Date;
+
+import static com.webchat.common.util.DateUtils.YYYYMMDD;
+
+@Component
+public class SnowflakeIdGeneratorService {
+
+    @Autowired
+    private RedisService redisService;
+
+    /**
+     * 基于雪花算法分布下唯一id生成器
+     *
+     * @return
+     */
+    public String generateId() {
+        SnowflakeIdGenerator snowflakeIdGenerator = new SnowflakeIdGenerator(incrementId());
+        return snowflakeIdGenerator.nextId() + "";
+    }
+
+
+    /**
+     * 生成带前缀的id
+     *
+     * @param prefix
+     * @return
+     */
+    public String generateId(String prefix) {
+
+        return prefix + "-" + this.generateId();
+    }
+
+    private Long incrementId() {
+        return redisService.increx("SnowflakeId", 1L);
+    }
+
+    public static void main(String[] args) {
+        SnowflakeIdGenerator snowflakeIdGenerator = new SnowflakeIdGenerator(1);
+        String prefix = DateUtils.getDate2String(YYYYMMDD, new Date());
+        System.out.println("orderId : " + prefix + snowflakeIdGenerator.nextId());
+    }
+}

+ 2 - 0
webchat-common/src/main/java/com/webchat/common/service/messagequeue/producer/MessageQueueProducer.java

@@ -5,8 +5,10 @@ import com.webchat.common.enums.messagequeue.MessageQueueEnum;
 import com.webchat.common.exception.BusinessException;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
+import org.springframework.cloud.context.config.annotation.RefreshScope;
 import org.springframework.stereotype.Service;
 
+@RefreshScope
 @Service
 public class MessageQueueProducer<T, P> {
 

+ 5 - 5
webchat-common/src/main/java/com/webchat/common/util/SignUtil.java

@@ -16,19 +16,19 @@ public class SignUtil {
      */
     public static String generateSignature(String sk, String ... params) {
         try {
-            String stringToSign = "";
+            StringBuffer sign = new StringBuffer();
             if (params != null && params.length > 0) {
                 for (int i = 0; i < params.length; i++) {
-                    stringToSign += params[i];
-                    if (i != params.length) {
-                        stringToSign += "-";
+                    sign.append(params[i]);
+                    if (i != params.length - 1) {
+                        sign.append("-");
                     }
                 }
             }
             Mac sha256HMAC = Mac.getInstance("HmacSHA256");
             SecretKeySpec secretKey = new SecretKeySpec(sk.getBytes(), "HmacSHA256");
             sha256HMAC.init(secretKey);
-            byte[] hashBytes = sha256HMAC.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8));
+            byte[] hashBytes = sha256HMAC.doFinal(sign.toString().getBytes(StandardCharsets.UTF_8));
             return Base64.getEncoder().encodeToString(hashBytes);
         } catch (Exception e) {
             throw new RuntimeException("签名生成失败", e);

+ 87 - 0
webchat-common/src/main/java/com/webchat/common/util/SnowflakeIdGenerator.java

@@ -0,0 +1,87 @@
+package com.webchat.common.util;
+
+public class SnowflakeIdGenerator {
+
+    // 起始时间戳(2023-01-01 00:00:00)
+    private final static long START_STAMP = 1672531200000L;
+
+    // 各部分位数分配
+    private final static long SEQUENCE_BIT = 12;  // 序列号位数
+    private final static long WORKER_BIT = 10;    // 工作节点位数
+
+    // 最大值计算(位运算优化)
+    private final static long MAX_SEQUENCE = ~(-1L << SEQUENCE_BIT);
+    private final static long MAX_WORKER_ID = ~(-1L << WORKER_BIT);
+
+    // 移位偏移量
+    private final static long WORKER_LEFT = SEQUENCE_BIT;
+    private final static long TIMESTAMP_LEFT = WORKER_LEFT + WORKER_BIT;
+
+    private final long workerId;     // 工作节点ID
+    private long sequence = 0L;     // 序列号
+    private long lastStamp = -1L;   // 上次生成时间
+
+    public SnowflakeIdGenerator(long workerId) {
+        if (workerId > MAX_WORKER_ID || workerId < 0) {
+            throw new IllegalArgumentException("Worker ID超出范围 [0, " + MAX_WORKER_ID + "]");
+        }
+        this.workerId = workerId;
+    }
+
+    /**
+     * 生成下一个ID(线程安全)
+     */
+    public synchronized long nextId() {
+        long currentStamp = getCurrentStamp();
+
+        // 时钟回拨检查
+        if (currentStamp < lastStamp) {
+            throw new RuntimeException("时钟回拨异常,拒绝生成ID。回拨时间:" + (lastStamp - currentStamp) + "ms");
+        }
+
+        if (currentStamp == lastStamp) {
+            // 同一毫秒内序列号自增
+            sequence = (sequence + 1) & MAX_SEQUENCE;
+            if (sequence == 0) { // 当前毫秒序列号用尽
+                currentStamp = waitNextMillis(lastStamp);
+            }
+        } else {
+            sequence = 0L; // 新毫秒重置序列号
+        }
+
+        lastStamp = currentStamp;
+
+        // 组合各部分生成最终ID
+        return (currentStamp - START_STAMP) << TIMESTAMP_LEFT
+                | workerId << WORKER_LEFT
+                | sequence;
+    }
+
+    /**
+     * 阻塞到下一毫秒
+     */
+    private long waitNextMillis(long lastStamp) {
+        long current = getCurrentStamp();
+        while (current <= lastStamp) {
+            current = getCurrentStamp();
+        }
+        return current;
+    }
+
+    /**
+     * 获取当前毫秒数
+     */
+    private long getCurrentStamp() {
+        return System.currentTimeMillis();
+    }
+
+    public static void main(String[] args) {
+        // 创建发号器实例(实际项目中workerId应从配置中心获取)
+        SnowflakeIdGenerator idGenerator = new SnowflakeIdGenerator(1);
+
+        // 连续生成10个ID
+        for (int i = 0; i < 10; i++) {
+            System.out.println(idGenerator.nextId());
+        }
+    }
+}

+ 8 - 1
webchat-connect/src/main/java/com/webchat/connect/messagequeue/config/RedisConfig.java

@@ -6,6 +6,8 @@ import com.webchat.connect.messagequeue.consumer.redis.ChatMessageRedisQueueList
 import com.webchat.connect.messagequeue.consumer.redis.ChatNotifyRedisQueueListener;
 import com.webchat.connect.messagequeue.consumer.redis.GroupVideoCallRedisQueueListener;
 import com.webchat.connect.messagequeue.consumer.redis.GroupVideoUserChangeRedisQueueListener;
+import com.webchat.connect.messagequeue.consumer.redis.MessageCardPushRedisQueueListener;
+import com.webchat.connect.messagequeue.consumer.redis.RedPacketNotifyRedisQueueListener;
 import com.webchat.connect.messagequeue.consumer.redis.WebRtcSDPRedisQueueListener;
 import jakarta.annotation.Resource;
 import org.springframework.context.annotation.Bean;
@@ -29,7 +31,10 @@ public class RedisConfig {
     private GroupVideoCallRedisQueueListener groupVideoCallRedisQueueListener;
     @Resource
     private GroupVideoUserChangeRedisQueueListener groupVideoUserChangeRedisQueueListener;
-
+    @Resource
+    private MessageCardPushRedisQueueListener messageCardPushRedisQueueListener;
+    @Resource
+    private RedPacketNotifyRedisQueueListener redPacketNotifyRedisQueueListener;
 
     @Bean
     public RedisMessageListenerContainer container(RedisConnectionFactory redisConnectionFactory) {
@@ -43,6 +48,8 @@ public class RedisConfig {
         container.addMessageListener(webRtcSDPRedisQueueListener, new ChannelTopic(MessageBroadChannelEnum.QUEUE_VIDEO_SDP.getChannel()));
         container.addMessageListener(groupVideoCallRedisQueueListener, new ChannelTopic(MessageBroadChannelEnum.QUEUE_GROUP_VIDEO_CALL.getChannel()));
         container.addMessageListener(groupVideoUserChangeRedisQueueListener, new ChannelTopic(MessageBroadChannelEnum.QUEUE_GROUP_VIDEO_USER_CHANGE.getChannel()));
+        container.addMessageListener(messageCardPushRedisQueueListener, new ChannelTopic(MessageBroadChannelEnum.QUEUE_MESSAGE_CARD_PUSH.getChannel()));
+        container.addMessageListener(redPacketNotifyRedisQueueListener, new ChannelTopic(MessageBroadChannelEnum.QUEUE_RED_PACKET_NOTIFY.getChannel()));
         return container;
     }
 }

+ 31 - 0
webchat-connect/src/main/java/com/webchat/connect/messagequeue/consumer/redis/MessageCardPushRedisQueueListener.java

@@ -0,0 +1,31 @@
+package com.webchat.connect.messagequeue.consumer.redis;
+
+import com.webchat.connect.messagequeue.consumer.service.MessageCardPushConsumeService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.connection.Message;
+import org.springframework.data.redis.connection.MessageListener;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Component;
+
+
+@Slf4j
+@Component
+public class MessageCardPushRedisQueueListener implements MessageListener {
+
+    @Autowired
+    private RedisTemplate redisTemplate;
+
+    @Autowired
+    private MessageCardPushConsumeService messageCardPushConsumeService;
+
+    @Override
+    public void onMessage(Message message, byte[] pattern) {
+
+        String channel = (String) redisTemplate.getStringSerializer().deserialize(message.getChannel());
+        String messageStr = (String) redisTemplate.getValueSerializer().deserialize(message.getBody());
+        log.info("MessageCardPushRedisQueueListener.onMessage =====> channel:{} messageStr:{}", channel, messageStr);
+
+        messageCardPushConsumeService.consume(messageStr);
+    }
+}

+ 30 - 0
webchat-connect/src/main/java/com/webchat/connect/messagequeue/consumer/redis/RedPacketNotifyRedisQueueListener.java

@@ -0,0 +1,30 @@
+package com.webchat.connect.messagequeue.consumer.redis;
+
+import com.webchat.connect.messagequeue.consumer.service.RedPacketNotifyConsumeService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.connection.Message;
+import org.springframework.data.redis.connection.MessageListener;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Component;
+
+@Slf4j
+@Component
+public class RedPacketNotifyRedisQueueListener implements MessageListener {
+
+
+    @Autowired
+    private RedisTemplate redisTemplate;
+
+    @Autowired
+    private RedPacketNotifyConsumeService redPacketNotifyConsumeService;
+
+    @Override
+    public void onMessage(Message message, byte[] pattern) {
+        String channel = (String) redisTemplate.getStringSerializer().deserialize(message.getChannel());
+        String messageStr = (String) redisTemplate.getValueSerializer().deserialize(message.getBody());
+        log.info("RedPacketNotifyRedisQueueListener.onMessage =====> channel:{} messageStr:{}", channel, messageStr);
+
+        redPacketNotifyConsumeService.consume(messageStr);
+    }
+}

+ 27 - 0
webchat-connect/src/main/java/com/webchat/connect/messagequeue/consumer/rocketmq/MessageCardPushRocketQueueConsumer.java

@@ -0,0 +1,27 @@
+package com.webchat.connect.messagequeue.consumer.rocketmq;
+
+import org.apache.rocketmq.spring.core.RocketMQListener;
+
+/**
+ * 当前对话消息队列,因为需要WebSocket或SSE服务端主动推送,为了解决分布式websocketsession及ssemetter共享问题,这里必须广播模式
+ */
+//@Component
+//@RocketMQMessageListener(consumerGroup = "web_chat",
+//                         topic = "queue_message_card_push",
+//                         messageModel = MessageModel.BROADCASTING)
+public class MessageCardPushRocketQueueConsumer implements RocketMQListener<String> {
+//
+//    @Autowired
+//    private MessageCardPushConsumeService messageCardPushConsumeService;
+//
+    /**
+     * 处理来自IM 对话相关消息
+     *
+     * @param message
+     */
+    @Override
+    public void onMessage(String message) {
+
+//        messageCardPushConsumeService.consume(message);
+    }
+}

+ 35 - 0
webchat-connect/src/main/java/com/webchat/connect/messagequeue/consumer/rocketmq/RedPacketNotifyRocketQueueConsumer.java

@@ -0,0 +1,35 @@
+package com.webchat.connect.messagequeue.consumer.rocketmq;
+
+import com.webchat.connect.messagequeue.consumer.service.ChatNotifyConsumeService;
+import com.webchat.connect.messagequeue.consumer.service.RedPacketNotifyConsumeService;
+import org.apache.rocketmq.spring.annotation.MessageModel;
+import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
+import org.apache.rocketmq.spring.core.RocketMQListener;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+/**
+ * 当前对话消息队列,因为需要WebSocket或SSE服务端主动推送,为了解决分布式websocketsession及ssemetter共享问题,这里必须广播模式
+ */
+//@Component
+//@RocketMQMessageListener(consumerGroup = "web_chat", topic = "queue_red_packet_notify", messageModel = MessageModel.BROADCASTING)
+public class RedPacketNotifyRocketQueueConsumer implements RocketMQListener<String> {
+
+
+//    @Autowired
+//    private RedPacketNotifyConsumeService redPacketNotifyConsumeService;
+
+
+    /**
+     * 处理来自IM 对话相关消息
+     *
+     * @param message
+     */
+    @Override
+    public void onMessage(String message) {
+
+        // TODO
+
+//        redPacketNotifyConsumeService.consume(message);
+    }
+}

+ 5 - 4
webchat-connect/src/main/java/com/webchat/connect/messagequeue/consumer/service/ArticlePushConsumeService.java

@@ -77,6 +77,11 @@ public class ArticlePushConsumeService {
         if (CollectionUtils.isEmpty(userIds)) {
             return;
         }
+        ChatMessageResponseVO chatMessageResponseVO = new ChatMessageResponseVO();
+        chatMessageResponseVO.setPublicAccountArticle(articleVO);
+        chatMessageResponseVO.setSenderId(publicAccount);
+        chatMessageResponseVO.setType(ChatMessageTypeEnum.PUBLIC_ACCOUNT_ARTICLE.getType());
+
         Set<String> bizCodes = ConnectConstants.ConnectBiz.getBizCode(ConnectConstants.BizEnum.CHAT);
         for (String bizCode : bizCodes) {
             Map<String, WebSocketSession> userWsMap = ChatWebSocketEndPointServletHandler.getSessions(bizCode, userIds);
@@ -85,11 +90,7 @@ public class ArticlePushConsumeService {
                 if (wsSession == null || !wsSession.isOpen()) {
                     continue;
                 }
-                ChatMessageResponseVO chatMessageResponseVO = new ChatMessageResponseVO();
-                chatMessageResponseVO.setPublicAccountArticle(articleVO);
                 chatMessageResponseVO.setReceiverId(userId);
-                chatMessageResponseVO.setSenderId(publicAccount);
-                chatMessageResponseVO.setType(ChatMessageTypeEnum.PUBLIC_ACCOUNT_ARTICLE.getType());
                 try {
                     /**
                      * 公众号文章推送

+ 132 - 0
webchat-connect/src/main/java/com/webchat/connect/messagequeue/consumer/service/MessageCardPushConsumeService.java

@@ -0,0 +1,132 @@
+package com.webchat.connect.messagequeue.consumer.service;
+
+
+import com.webchat.common.constants.ConnectConstants;
+import com.webchat.common.enums.AccountRelationTypeEnum;
+import com.webchat.common.enums.ChatMessageTypeEnum;
+import com.webchat.common.enums.messagequeue.MessageQueueEnum;
+import com.webchat.common.service.FreeMarkEngineService;
+import com.webchat.common.service.messagequeue.producer.MessageQueueProducer;
+import com.webchat.common.util.JsonUtil;
+import com.webchat.connect.service.AccountService;
+import com.webchat.connect.service.MessageCardTemplateService;
+import com.webchat.connect.websocket.handler.ChatWebSocketEndPointServletHandler;
+import com.webchat.domain.dto.messagecard.MessageCardSendDTO;
+import com.webchat.domain.vo.request.mess.ChatMessageRequestVO;
+import com.webchat.domain.vo.response.MessageCardTemplateResponseVO;
+import com.webchat.domain.vo.response.mess.ChatMessageResponseVO;
+import com.webchat.rmi.pgc.MessageCardTemplateClient;
+import freemarker.template.TemplateException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.web.socket.TextMessage;
+import org.springframework.web.socket.WebSocketSession;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.Set;
+
+import static com.webchat.common.enums.ChatMessageTypeEnum.SERVER_ACCOUNT_MESSAGE_CARD;
+
+@Service
+public class MessageCardPushConsumeService {
+
+
+    private static final Logger log = LoggerFactory.getLogger(MessageCardPushConsumeService.class);
+
+    @Autowired
+    private MessageCardTemplateService messageCardTemplateService;
+
+    @Autowired
+    private FreeMarkEngineService freeMarkEngineService;
+
+    @Autowired
+    private AccountService accountService;
+
+    @Autowired
+    private MessageQueueProducer<ChatMessageRequestVO, Long> messageQueueProducer;
+
+    /**
+     * 服务号消息卡片推送消费
+     *
+     * @param message
+     */
+    public void consume(String message) {
+
+        MessageCardSendDTO messageCard = JsonUtil.fromJson(message, MessageCardSendDTO.class);
+        String account = messageCard.getSender();
+        String receiver = messageCard.getReceiver();
+        Map<String, Object> vars = messageCard.getVars();
+        // 查询推送消息模版配置
+        String templateId = messageCard.getTemplateId();
+        MessageCardTemplateResponseVO template = messageCardTemplateService.get(templateId);
+        if (template == null) {
+            return;
+        }
+        // 判断是否订阅服务号
+        boolean isSub = accountService.isSubscribe(receiver, account, AccountRelationTypeEnum.USER_SERVER);
+        if (!isSub) {
+            return;
+        }
+
+        /**
+         * 渲染消息卡内容
+         */
+        String messageCardTemplateContent = null;
+        try {
+            messageCardTemplateContent = freeMarkEngineService.getContent(template.getContent(), vars);
+        } catch (Exception e) {
+            log.error("服务号消息卡模版内容引擎渲染异常 =====> template:{}, vars:{}",
+                    template.getContent(), JsonUtil.toJsonString(vars));
+            return;
+        }
+
+        MessageCardTemplateResponseVO messageExt = new MessageCardTemplateResponseVO();
+        messageExt.setLogo(messageCard.getLogo());
+        messageExt.setTitle(messageCard.getTitle());
+        messageExt.setContent(messageCardTemplateContent);
+        messageExt.setRedirectName(messageCard.getRedirectName());
+        messageExt.setRedirectUrl(messageCard.getRedirectUrl());
+
+        ChatMessageResponseVO<MessageCardTemplateResponseVO> chatMessageResponseVO = new ChatMessageResponseVO();
+        chatMessageResponseVO.setSenderId(account);
+        chatMessageResponseVO.setType(SERVER_ACCOUNT_MESSAGE_CARD.getType());
+        chatMessageResponseVO.setMessageExt(messageExt);
+        Set<String> bizCodes = ConnectConstants.ConnectBiz.getBizCode(ConnectConstants.BizEnum.CHAT);
+        for (String bizCode : bizCodes) {
+            WebSocketSession webSocketSession = ChatWebSocketEndPointServletHandler.getSession(bizCode, receiver);
+            if (webSocketSession == null || !webSocketSession.isOpen()) {
+                continue;
+            }
+            try {
+                webSocketSession.sendMessage(new TextMessage(JsonUtil.toJsonString(chatMessageResponseVO)));
+                /**
+                 *  持久化服务号消息卡数据
+                 */
+                this.persistentArticlePushMessage(account, receiver, message);
+            } catch (IOException e) {
+                // TODO
+            }
+        }
+    }
+
+    /**
+     * 公众号推送消息持久化
+     *
+     */
+    private void persistentArticlePushMessage(String serverAccount, String userId, String message) {
+
+        ChatMessageRequestVO persistentMessage = new ChatMessageRequestVO();
+        persistentMessage.setType(ChatMessageTypeEnum.SERVER_ACCOUNT_MESSAGE_CARD.getType());
+        persistentMessage.setSenderId(serverAccount);
+        persistentMessage.setReceiverId(userId);
+        persistentMessage.setMessage(message);
+        /**
+         * 走统一的消息持久化处理服务(服用了对话场景持久化服务)
+         *
+         */
+        messageQueueProducer.send(MessageQueueEnum.QUEUE_PERSISTENT_MESSAGE, persistentMessage);
+    }
+}

+ 131 - 0
webchat-connect/src/main/java/com/webchat/connect/messagequeue/consumer/service/RedPacketNotifyConsumeService.java

@@ -0,0 +1,131 @@
+package com.webchat.connect.messagequeue.consumer.service;
+
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.google.common.collect.Sets;
+import com.webchat.common.constants.ConnectConstants;
+import com.webchat.common.enums.AccountRelationTypeEnum;
+import com.webchat.common.enums.RoleCodeEnum;
+import com.webchat.common.enums.messagequeue.MessageQueueEnum;
+import com.webchat.common.service.messagequeue.producer.MessageQueueProducer;
+import com.webchat.common.util.JsonUtil;
+import com.webchat.connect.service.AccountService;
+import com.webchat.connect.websocket.handler.ChatWebSocketEndPointServletHandler;
+import com.webchat.domain.vo.request.mess.ChatMessageRequestVO;
+import com.webchat.domain.vo.request.mess.MessageBaseVO;
+import com.webchat.domain.vo.response.RedPacketBaseVO;
+import com.webchat.domain.vo.response.UserBaseResponseInfoVO;
+import org.apache.commons.collections.MapUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.web.socket.TextMessage;
+import org.springframework.web.socket.WebSocketSession;
+
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+@Service
+public class RedPacketNotifyConsumeService {
+
+
+    @Autowired
+    private AccountService accountService;
+
+    @Autowired
+    private MessageQueueProducer<ChatMessageRequestVO, Long> messageQueueProducer;
+
+
+    /**
+     * 红包通知消费(来自广播)
+     *
+     * @param messageJson
+     */
+    public void consume(String messageJson) {
+
+        if (StringUtils.isBlank(messageJson)) {
+            return;
+        }
+        MessageBaseVO<RedPacketBaseVO> message =
+                JsonUtil.fromJson(messageJson, new TypeReference<MessageBaseVO<RedPacketBaseVO>>() { });
+
+        // 判断红包接受账号是群聊还是机器人
+        String senderId = message.getSenderId();
+        String receiverId = message.getReceiverId();
+
+
+        Map<String, UserBaseResponseInfoVO> users = accountService.batchGet(Sets.newHashSet(receiverId, senderId));
+
+        UserBaseResponseInfoVO receiver = users.get(receiverId);
+        UserBaseResponseInfoVO sender = users.get(senderId);
+
+        Set<String> realReceiverIds = new HashSet<>();
+        realReceiverIds.add(senderId);
+        if (RoleCodeEnum.isUserRole(receiver.getRoleCode())) {
+            // 红包接受人是普通用户
+            realReceiverIds.add(receiverId);
+        } else if (RoleCodeEnum.GROUP.getCode().equals(receiver.getRoleCode())) {
+            // 红包接受人是群组
+            Set<String> groupUserIds = accountService.getAllSubscriberByAccount(receiverId, AccountRelationTypeEnum.USER_GROUP);
+            realReceiverIds.addAll(groupUserIds);
+            /**
+             * 消息代理人,由群聊代理实际红包发送人
+             */
+            // 群聊代理
+            message.setSenderId(receiverId);
+            message.setSender(receiver);
+            // 实际红包发送人
+            message.setProxySender(sender);
+            message.setProxySenderId(senderId);
+        } else {
+            return;
+        }
+        /**
+         * 通知接受人“红包来了”,并且前端渲染红包卡片效果
+         */
+        this.doNotifyByWebSocket(realReceiverIds, message);
+
+        /**
+         * 《离线场景》持久化消息队列,保存离线消息,同时会将数据同步到ES用于后续的RAG问答和消息搜索
+         *
+         *  Consumer在UGC服务:
+         *  com.webchat.ugc.messaegqueue.consumer.redis.PersistentMessageRedisMQConsumer
+         *  com.webchat.ugc.messaegqueue.consumer.rocketmq.PersistentMessageRocketMQConsumer
+         */
+        ChatMessageRequestVO chatMessageRequestVO = new ChatMessageRequestVO();
+        chatMessageRequestVO.setSenderId(senderId);
+        chatMessageRequestVO.setReceiverId(receiverId);
+        chatMessageRequestVO.setType(message.getType());
+        chatMessageRequestVO.setMessage(String.valueOf(message.getMessageExt().getId()));
+        messageQueueProducer.send(MessageQueueEnum.QUEUE_PERSISTENT_MESSAGE, chatMessageRequestVO);
+    }
+
+    private void doNotifyByWebSocket(Set<String> receiverUserIds, MessageBaseVO<RedPacketBaseVO> message) {
+
+        String messageJson = JsonUtil.toJsonString(message);
+        // 处理所有客户端消息推送,比如MAC、WIN、PC-WEB……
+        Set<String> bizCodes = ConnectConstants.ConnectBiz.getBizCode(ConnectConstants.BizEnum.CHAT);
+        for (String bizCode : bizCodes) {
+            // 批量获取接受人在当前节点的ws链接对象
+            Map<String, WebSocketSession> wsSessionMap = ChatWebSocketEndPointServletHandler.getSessions(bizCode, receiverUserIds);
+            if (MapUtils.isEmpty(wsSessionMap)) {
+                continue;
+            }
+            for (String receiverId : receiverUserIds) {
+                WebSocketSession session = wsSessionMap.get(receiverId);
+                if (session == null || !session.isOpen()) {
+                    continue;
+                }
+                try {
+                    session.sendMessage(new TextMessage(messageJson));
+                } catch (IOException e) {
+                    // TODO
+                    e.printStackTrace();
+                }
+            }
+        }
+
+    }
+}

+ 17 - 0
webchat-connect/src/main/java/com/webchat/connect/service/AccountService.java

@@ -8,6 +8,7 @@ import com.webchat.common.util.JsonUtil;
 import com.webchat.domain.vo.response.UserBaseResponseInfoVO;
 import com.webchat.rmi.user.UserServiceClient;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.ObjectUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
@@ -94,4 +95,20 @@ public class AccountService {
         }
         return null;
     }
+
+    /**
+     * 判断userAccount是否订阅account
+     * @param userAccount
+     * @param account
+     * @param accountRelationType
+     * @return
+     */
+    public boolean isSubscribe(String userAccount, String account, AccountRelationTypeEnum accountRelationType) {
+        APIResponseBean<Boolean> responseBean =
+                userServiceClient.isSubscribe(accountRelationType.getType(), userAccount, account);
+        if (APIResponseBeanUtil.isOk(responseBean)) {
+            return ObjectUtils.equals(responseBean.getData(), true);
+        }
+        return false;
+    }
 }

+ 30 - 0
webchat-connect/src/main/java/com/webchat/connect/service/MessageCardTemplateService.java

@@ -0,0 +1,30 @@
+package com.webchat.connect.service;
+
+import com.webchat.common.bean.APIResponseBean;
+import com.webchat.common.bean.APIResponseBeanUtil;
+import com.webchat.domain.vo.response.MessageCardTemplateResponseVO;
+import com.webchat.rmi.pgc.MessageCardTemplateClient;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+@Service
+public class MessageCardTemplateService {
+
+    @Autowired
+    private MessageCardTemplateClient messageCardTemplateClient;
+
+    /**
+     * 获取模版详情
+     *
+     * @param templateId
+     * @return
+     */
+    public MessageCardTemplateResponseVO get(String templateId) {
+        APIResponseBean<MessageCardTemplateResponseVO> responseBean =
+                messageCardTemplateClient.template(templateId);
+        if (APIResponseBeanUtil.isOk(responseBean)) {
+            return responseBean.getData();
+        }
+        return null;
+    }
+}

+ 30 - 0
webchat-domain/src/main/java/com/webchat/domain/dto/messagecard/MessageCardSendDTO.java

@@ -0,0 +1,30 @@
+package com.webchat.domain.dto.messagecard;
+
+
+import lombok.Data;
+
+import java.util.Map;
+
+@Data
+public class MessageCardSendDTO {
+
+    private String sender;
+
+    private String receiver;
+
+    private String templateId;
+
+    private String logo;
+
+    private String title;
+
+    /**
+     * 模版渲染参数变量
+     *
+     */
+    private Map<String, Object> vars;
+
+    private String redirectName;
+
+    private String redirectUrl;
+}

+ 34 - 0
webchat-domain/src/main/java/com/webchat/domain/dto/messagecard/MessageCardTemplate.java

@@ -0,0 +1,34 @@
+package com.webchat.domain.dto.messagecard;
+
+
+import lombok.Data;
+
+@Data
+public class MessageCardTemplate {
+
+    /**
+     * 服务号
+     */
+    private String account;
+
+    private String templateId;
+
+    private String logo;
+
+    private String title;
+
+    /**
+     * 模版内容(richText html富文本模版)
+     */
+    private String content;
+
+    /**
+     * 链接名称
+     */
+    private String redirectName;
+
+    /**
+     * 卡片点击跳转链接
+     */
+    private String redirectUrl;
+}

+ 50 - 0
webchat-domain/src/main/java/com/webchat/domain/dto/payment/PaymentOrderBaseDTO.java

@@ -0,0 +1,50 @@
+package com.webchat.domain.dto.payment;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+@Data
+public class PaymentOrderBaseDTO {
+
+    /**
+     * 订单事件:出行、红包、餐饮……
+     * @see PaymentTransEventEnum
+     */
+    private Integer eventType;
+
+    /**
+     * 账单类型: 1:收入、-1:支出
+     * @see PaymentTransTypeEnum
+     */
+    private Integer billType;
+
+    /**
+     * 订单总金额
+     */
+    private BigDecimal amount;
+
+    /**
+     * 交易订单发起账户
+     */
+    private String sourceAccount;
+
+    /**
+     * 交易订单接收账户
+     */
+    private String targetAccount;
+
+    /**
+     * 订单描述
+     */
+    private String description;
+
+    /**
+     * 订单过期时间
+     * --------------
+     * 比方说发起的订单属于过程型订单,即不是立刻结束的,比如发送红包,发完红包需要用户拆红包,这个是需要一个过程,不是立即结果的
+     * 当你指定了24小时候过期,支付平台24小时候会强制关闭订单
+     */
+    private Date expireDate;
+}

+ 10 - 0
webchat-domain/src/main/java/com/webchat/domain/dto/payment/PaymentOrderCreateDTO.java

@@ -0,0 +1,10 @@
+package com.webchat.domain.dto.payment;
+
+import lombok.Data;
+
+
+@Data
+public class PaymentOrderCreateDTO extends PaymentOrderBaseDTO {
+
+
+}

+ 21 - 0
webchat-domain/src/main/java/com/webchat/domain/dto/payment/PaymentOrderDTO.java

@@ -0,0 +1,21 @@
+package com.webchat.domain.dto.payment;
+
+import com.webchat.domain.vo.response.UserBaseResponseInfoVO;
+import lombok.Data;
+
+
+@Data
+public class PaymentOrderDTO extends PaymentOrderBaseDTO {
+
+    private String orderId;
+
+    private String transId;
+
+    private UserBaseResponseInfoVO sourceAccountInfo;
+
+    private UserBaseResponseInfoVO targetAccountInfo;
+
+    private Long createTime;
+
+    private Long updateTime;
+}

+ 2 - 0
webchat-domain/src/main/java/com/webchat/domain/dto/payment/PaymentTransRequestDTO.java

@@ -9,6 +9,8 @@ import java.math.BigDecimal;
 public class PaymentTransRequestDTO {
 
 
+    private String orderId;
+
     /**
      * 交易发起账户(我们默认使用用户id作为用户账户)
      */

+ 24 - 0
webchat-domain/src/main/java/com/webchat/domain/dto/payment/TransIdDTO.java

@@ -0,0 +1,24 @@
+package com.webchat.domain.dto.payment;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class TransIdDTO {
+
+    private String transId;
+
+    private Long appId;
+
+    /**
+     * 事务状态
+     */
+    private Integer status;
+
+    private String orderId;
+}

+ 1 - 1
webchat-domain/src/main/java/com/webchat/domain/vo/request/CreatePublicAccountRequestVO.java → webchat-domain/src/main/java/com/webchat/domain/vo/request/CreateAccountRequestVO.java

@@ -6,7 +6,7 @@ import lombok.Data;
  * 创建公众号
  */
 @Data
-public class CreatePublicAccountRequestVO {
+public class CreateAccountRequestVO {
 
     private Long id;
 

+ 21 - 0
webchat-domain/src/main/java/com/webchat/domain/vo/request/MessageCardTemplateRequestVO.java

@@ -0,0 +1,21 @@
+package com.webchat.domain.vo.request;
+
+
+import com.webchat.domain.dto.messagecard.MessageCardTemplate;
+import lombok.Data;
+
+@Data
+public class MessageCardTemplateRequestVO extends MessageCardTemplate {
+
+
+    /**
+     * 操作人
+     */
+    private String operator;
+
+
+    public void validateTemplateParam() {
+        // TODO
+    }
+
+}

+ 8 - 3
webchat-domain/src/main/java/com/webchat/domain/vo/request/SendRedPacketRequestVO.java

@@ -5,6 +5,7 @@ import org.apache.commons.lang3.StringUtils;
 import org.springframework.util.Assert;
 
 import java.math.BigDecimal;
+import java.util.Date;
 
 /**
  * @author 程序员七七, https://www.coderutil.com网站作者
@@ -16,18 +17,20 @@ public class SendRedPacketRequestVO {
     /**
      * 红包发送人
      */
-    private String sendUserId;
+    private String sender;
 
     /**
      * 接收人,可能是人 / 群
      */
-    private String receiverUserId;
+    private String receiver;
 
     /**
      * 红包封面图,不上传封面使用系统默认红包封面
      */
     private String cover;
 
+    private String blessing;
+
     /**
      * 拼手气、普通红包(平均分配)
      */
@@ -43,9 +46,11 @@ public class SendRedPacketRequestVO {
      */
     private Integer count;
 
+    private Date expireDate;
+
 
     public void validateRequestParam() {
-        Assert.isTrue(StringUtils.isNotBlank(receiverUserId), "红包接收人不能为空!");
+        Assert.isTrue(StringUtils.isNotBlank(receiver), "红包接收人不能为空!");
         Assert.isTrue(type != null, "红包类型不能为空!");
         Assert.isTrue(totalMoney != null &&
                                totalMoney.compareTo(new BigDecimal("200")) <= 0 &&

+ 3 - 1
webchat-domain/src/main/java/com/webchat/domain/vo/request/mess/MessageBaseVO.java

@@ -8,7 +8,7 @@ import lombok.Data;
  * @Date: 10.9.22 11:38 下午
  */
 @Data
-public class MessageBaseVO extends BaseQueueDTO {
+public class MessageBaseVO<T> extends BaseQueueDTO {
 
     /**
      * 当前用户ID
@@ -43,4 +43,6 @@ public class MessageBaseVO extends BaseQueueDTO {
      * 类型
      */
     private Integer type = 1;
+
+    private T messageExt;
 }

+ 13 - 0
webchat-domain/src/main/java/com/webchat/domain/vo/response/MessageCardTemplateResponseVO.java

@@ -0,0 +1,13 @@
+package com.webchat.domain.vo.response;
+
+import com.webchat.domain.dto.messagecard.MessageCardTemplate;
+import lombok.Data;
+
+@Data
+public class MessageCardTemplateResponseVO extends MessageCardTemplate {
+
+
+
+    private Long updateTime;
+
+}

+ 65 - 0
webchat-domain/src/main/java/com/webchat/domain/vo/response/RedPacketBaseVO.java

@@ -0,0 +1,65 @@
+package com.webchat.domain.vo.response;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * @author 程序员七七, https://www.coderutil.com网站作者
+ * @date 2024/11/9 04:43
+ */
+@Data
+public class RedPacketBaseVO {
+
+    /**
+     * 红包id
+     */
+    private Long id;
+
+    private String cover;
+
+    /**
+     * 祝福语
+     */
+    private String blessing;
+
+    /**
+     * 一共几个,发给个人只能一次拆一个,发给群聊可以配置多个
+     */
+    private Integer count;
+
+    /**
+     * 拼手气、普通红包(平均分配)
+     */
+    private Integer type;
+
+    /**
+     * 红包发送人信息
+     */
+    private String sender;
+
+    /**
+     * 红包接受人信息:普通用户或者群聊
+     */
+    private String receiver;
+
+    /**
+     * 红包状态
+     */
+    private Integer status;
+
+    /**
+     * 红包过期时间:默认24后过期
+     */
+    private Long expireTime;
+
+    /**
+     * 红包发送时间
+     */
+    private Long sendTime;
+
+    /**
+     * 总金额
+     */
+    private BigDecimal totalMoney;
+}

+ 3 - 43
webchat-domain/src/main/java/com/webchat/domain/vo/response/RedPacketDetailVO.java

@@ -9,52 +9,12 @@ import java.math.BigDecimal;
  * @date 2024/11/9 04:43
  */
 @Data
-public class RedPacketDetailVO {
+public class RedPacketDetailVO extends RedPacketBaseVO {
 
     /**
-     * 红包id
+     * 支付平台订单凭证id
      */
-    private Long id;
-
-    /**
-     * 红包发送人信息
-     */
-    private String sendUserId;
-
-    /**
-     * 红包接受人信息:普通用户或者群聊
-     */
-    private String receiverUserId;
-
-    /**
-     * 拼手气、普通红包(平均分配)
-     */
-    private Integer type;
-
-    /**
-     * 总金额
-     */
-    private BigDecimal totalMoney;
-
-    /**
-     * 一共几个,发给个人只能一次拆一个,发给群聊可以配置多个
-     */
-    private Integer count;
-
-    /**
-     * 状态
-     */
-    private Integer status;
-
-    /**
-     * 发送时间
-     */
-    private Long sendTime;
-
-    /**
-     * 失效时间
-     */
-    private Long expireTime;
+    private String orderId;
 
     /**
      * 是否有抢到

+ 5 - 2
webchat-domain/src/main/java/com/webchat/domain/vo/response/mess/ChatMessageResponseVO.java

@@ -1,6 +1,7 @@
 package com.webchat.domain.vo.response.mess;
 
 
+import com.webchat.domain.vo.response.RedPacketBaseVO;
 import com.webchat.domain.vo.response.RedPacketDetailVO;
 import com.webchat.domain.vo.response.UserBaseResponseInfoVO;
 import lombok.Data;
@@ -8,7 +9,7 @@ import org.apache.commons.lang3.ObjectUtils;
 import org.apache.commons.lang3.StringUtils;
 
 @Data
-public class ChatMessageResponseVO {
+public class ChatMessageResponseVO<T> {
 
 
     /**
@@ -71,13 +72,15 @@ public class ChatMessageResponseVO {
     /**
      * 红包信息
      */
-    private RedPacketDetailVO redPacketDetail;
+    private RedPacketBaseVO redPacketDetail;
 
     /**
      * 消息文章
      */
     private PublicAccountArticleMessageVO publicAccountArticle;
 
+    private T messageExt;
+
     /**
      * 是否来自音视频offer
      */

+ 43 - 0
webchat-pay/src/main/java/com/webchat/pay/config/constants/PaymentConstant.java

@@ -0,0 +1,43 @@
+package com.webchat.pay.config.constants;
+
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+public class PaymentConstant {
+
+    /**
+     * 托管账户
+     */
+    public static final String PAYMENT_SYSTEM_ID = "S_51a7fbf50155c4b08c55fbbcfc5911db";
+
+    @Getter
+    @AllArgsConstructor
+    public enum TrasnsIdStatusEnum {
+
+        INIT(1, "初始化"),
+
+        CREATE_ORDER(2, "已创建订单"),
+
+        ROLLBACK(3, "已回滚");
+
+        private int status;
+        private String statusName;
+
+    }
+
+    @Getter
+    @AllArgsConstructor
+    public enum TrasnsDetailStatusEnum {
+
+        FINISHED(1, "完成"),
+
+        ROLLBACK(2, "已回滚"),
+
+        DELETED(3, "删除");
+
+        private int status;
+        private String statusName;
+
+    }
+}

+ 38 - 16
webchat-pay/src/main/java/com/webchat/pay/controller/PaymentApiServiceController.java

@@ -2,9 +2,15 @@ package com.webchat.pay.controller;
 
 import com.webchat.common.bean.APIResponseBean;
 import com.webchat.common.bean.APIResponseBeanUtil;
+import com.webchat.common.enums.payment.PaymentTransTypeEnum;
+import com.webchat.common.service.SnowflakeIdGeneratorService;
+import com.webchat.domain.dto.payment.PaymentOrderCreateDTO;
 import com.webchat.domain.dto.payment.PaymentTransRequestDTO;
 import com.webchat.pay.config.annotation.ValidateAccessPaymentPermission;
+import com.webchat.pay.config.constants.PaymentConstant;
+import com.webchat.pay.repository.entity.PaymentOrderDetailEntity;
 import com.webchat.pay.service.PaymentApiService;
+import com.webchat.pay.service.PaymentOrderService;
 import com.webchat.rmi.pay.PaymentApiServiceClient;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.PathVariable;
@@ -20,6 +26,10 @@ public class PaymentApiServiceController implements PaymentApiServiceClient {
 
     @Autowired
     private PaymentApiService paymentApiService;
+    @Autowired
+    private PaymentOrderService paymentOrderService;
+    @Autowired
+    private SnowflakeIdGeneratorService snowflakeIdGeneratorService;
 
     /**
      * 获取所有交易请求鉴权token
@@ -49,7 +59,7 @@ public class PaymentApiServiceController implements PaymentApiServiceClient {
      *
      * 应用场景:如果上游接入方支付成功后业务异常,需要回滚交易数据
      *
-     * @param token
+     * @param accessToken
      * @param logId
      * @return
      */
@@ -58,29 +68,37 @@ public class PaymentApiServiceController implements PaymentApiServiceClient {
     public APIResponseBean<String> transId(@RequestHeader(name = "access-token") String accessToken,
                                            @RequestHeader(name = "log-id") String logId) {
 
-        return null;
+        String transId = paymentApiService.transId(accessToken);
+        return APIResponseBeanUtil.success(transId);
+    }
+
+    @Override
+    public APIResponseBean<String> orderId(@RequestHeader(name = "access-token") String token,
+                                           @RequestHeader(name = "trans-id") String transId,
+                                           @RequestHeader(name = "log-id") String logId,
+                                           @RequestBody PaymentOrderCreateDTO paymentOrderCreateDTO) {
+        // TODO 业务订单参数校验
+        String orderId = paymentOrderService.createOrder(transId, logId, paymentOrderCreateDTO);
+        return APIResponseBeanUtil.success(orderId);
     }
 
     /**
      * 真实交易
+     *
      * @param paymentTransRequest
-     * @param token
-     * @param transId
+     * @param accessToken
      * @param logId
-     * @return
+     *
+     * @return 返回交易成功订单id
      */
     @ValidateAccessPaymentPermission
     @Override
     public APIResponseBean<Boolean> doTrans(@RequestBody PaymentTransRequestDTO paymentTransRequest,
                                             @RequestHeader(name = "access-token") String accessToken,
-                                            @RequestHeader(name = "log-id") String logId,
-                                            @RequestHeader(name = "trans-id")  String transId) {
-
-
+                                            @RequestHeader(name = "log-id") String logId) {
 
-
-
-        return null;
+        boolean transResult = paymentOrderService.doTrans(paymentTransRequest, logId);
+        return APIResponseBeanUtil.success(transResult);
     }
 
 
@@ -97,21 +115,25 @@ public class PaymentApiServiceController implements PaymentApiServiceClient {
     public APIResponseBean<Boolean> rollback(@RequestHeader(name = "access-token") String accessToken,
                                              @RequestHeader(name = "log-id") String logId,
                                              @RequestHeader(name = "trans-id")  String transId) {
-        return null;
+
+        boolean rollbackResult = paymentOrderService.rollback(transId, logId);
+        return APIResponseBeanUtil.success(rollbackResult);
     }
 
     /**
      * 查询账户余额
      *
-     * @param userId
+     * @param account
      * @param logId
      * @return
      */
     @ValidateAccessPaymentPermission
     @Override
-    public APIResponseBean<BigDecimal> balance(@PathVariable String userId,
+    public APIResponseBean<BigDecimal> balance(@PathVariable String account,
                                                @RequestHeader(name = "access-token") String accessToken,
                                                @RequestHeader(name = "log-id") String logId) {
-        return null;
+
+        BigDecimal balance = paymentOrderService.getAccountBalanceFromCache(logId, account);
+        return APIResponseBeanUtil.success(new BigDecimal("100"));
     }
 }

+ 15 - 0
webchat-pay/src/main/java/com/webchat/pay/repository/dao/IPaymentOrderDAO.java

@@ -0,0 +1,15 @@
+package com.webchat.pay.repository.dao;
+
+import com.webchat.pay.repository.entity.PaymentOrderEntity;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface IPaymentOrderDAO extends
+        JpaSpecificationExecutor<PaymentOrderEntity>,
+        JpaRepository<PaymentOrderEntity, Long> {
+
+
+    PaymentOrderEntity findByOrderId(String orderId);
+}

+ 29 - 0
webchat-pay/src/main/java/com/webchat/pay/repository/dao/IPaymentOrderDetailDAO.java

@@ -0,0 +1,29 @@
+package com.webchat.pay.repository.dao;
+
+import com.webchat.pay.repository.entity.PaymentOrderDetailEntity;
+import com.webchat.pay.repository.entity.PaymentOrderEntity;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.stereotype.Repository;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+@Repository
+public interface IPaymentOrderDetailDAO extends
+        JpaSpecificationExecutor<PaymentOrderDetailEntity>,
+        JpaRepository<PaymentOrderDetailEntity, Long> {
+
+    /**
+     * 查询账户余额
+     *
+     * @param account
+     * @return
+     */
+    @Query(value = "select sum(pod.amount) from web_chat_payment_order_detail pod " +
+            "where pod.source_account = :account and pod.status = 1", nativeQuery = true)
+    BigDecimal getAccountBalance(String account);
+
+    List<PaymentOrderDetailEntity> findAllByOrderId(String orderId);
+}

+ 67 - 0
webchat-pay/src/main/java/com/webchat/pay/repository/entity/PaymentOrderDetailEntity.java

@@ -0,0 +1,67 @@
+package com.webchat.pay.repository.entity;
+
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.PrePersist;
+import jakarta.persistence.PreUpdate;
+import jakarta.persistence.Table;
+import jakarta.persistence.Version;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+@Data
+@Entity
+@Table(name = "web_chat_payment_order_detail")
+public class PaymentOrderDetailEntity {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private Long id;
+
+
+    @Column(name = "order_id", nullable = false, length = 64)
+    private String orderId;
+
+    @Column(name = "payment_id", nullable = false, length = 64)
+    private String paymentId;
+
+    @Column(name = "source_account", nullable = false, length = 64)
+    private String sourceAccount;
+
+    @Column(name = "target_account", nullable = false, length = 64)
+    private String targetAccount;
+
+    @Column(name = "amount", nullable = false, precision = 10, scale = 2)
+    private BigDecimal amount;
+
+    @Column(name = "status", nullable = false)
+    private Integer status;
+
+    @Column(name = "CREATE_DATE", updatable = false)
+    private Date createDate;
+
+    @Column(name = "UPDATE_DATE")
+    private Date updateDate;
+
+    @Version
+    @Column(name = "VERSION")
+    private Integer version;
+
+    @PrePersist
+    protected void onCreate() {
+        if (createDate == null) {
+            createDate = new Date();
+        }
+    }
+
+    @PreUpdate
+    protected void onUpdate() {
+        updateDate = new Date();
+    }
+}

+ 84 - 0
webchat-pay/src/main/java/com/webchat/pay/repository/entity/PaymentOrderEntity.java

@@ -0,0 +1,84 @@
+package com.webchat.pay.repository.entity;
+
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.PrePersist;
+import jakarta.persistence.PreUpdate;
+import jakarta.persistence.Table;
+import jakarta.persistence.Version;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+@Data
+@Entity
+@Table(name = "web_chat_payment_order")
+public class PaymentOrderEntity {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private Long id;
+
+    @Column(name = "order_id", nullable = false, length = 64)
+    private String orderId;
+
+    @Column(name = "trans_id", nullable = false, length = 64)
+    private String transId;
+
+    @Column(name = "status", nullable = false)
+    private Integer status;
+
+    @Column(name = "app_id", nullable = false)
+    private Long appId;
+
+    @Column(name = "event_type", nullable = false)
+    private Integer eventType;
+
+    @Column(name = "bill_type", nullable = false)
+    private Integer billType;
+
+    @Column(name = "amount", nullable = false, precision = 10, scale = 2)
+    private BigDecimal amount;
+
+    @Column(name = "source_account", nullable = false, length = 64)
+    private String sourceAccount;
+
+    @Column(name = "target_account", nullable = false, length = 64)
+    private String targetAccount;
+
+    @Column(name = "description", length = 100)
+    private String description;
+
+    /**
+     * 延迟队列根据过期时间处理过期订单
+     */
+    @Column(name = "expire_date")
+    private Date expireDate;
+
+    @Column(name = "CREATE_DATE", updatable = false)
+    private Date createDate;
+
+    @Column(name = "UPDATE_DATE")
+    private Date updateDate;
+
+    @Version
+    @Column(name = "VERSION")
+    private Integer version;
+
+    @PrePersist
+    protected void onCreate() {
+        if (createDate == null) {
+            createDate = new Date();
+        }
+    }
+
+    @PreUpdate
+    protected void onUpdate() {
+        updateDate = new Date();
+    }
+}

+ 115 - 17
webchat-pay/src/main/java/com/webchat/pay/service/PaymentApiService.java

@@ -4,11 +4,18 @@ package com.webchat.pay.service;
 import com.webchat.common.bean.APIResponseBean;
 import com.webchat.common.enums.RedisKeyEnum;
 import com.webchat.common.service.RedisService;
+import com.webchat.common.service.SnowflakeIdGeneratorService;
 import com.webchat.common.util.AkSkGenerator;
 import com.webchat.common.util.IDGenerateUtil;
+import com.webchat.common.util.JsonUtil;
 import com.webchat.common.util.SignUtil;
+import com.webchat.domain.dto.payment.PaymentOrderCreateDTO;
+import com.webchat.domain.dto.payment.TransIdDTO;
 import com.webchat.domain.vo.dto.payment.AppAkSkDTO;
+import com.webchat.domain.vo.response.payment.AppBaseResponseVO;
+import com.webchat.pay.config.constants.PaymentConstant;
 import com.webchat.pay.config.exception.PaymentAccessAuthException;
+import com.webchat.pay.repository.entity.PaymentOrderEntity;
 import org.apache.commons.lang3.ObjectUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -24,6 +31,9 @@ public class PaymentApiService {
     @Autowired
     private RedisService redisService;
 
+    @Autowired
+    private SnowflakeIdGeneratorService snowflakeIdGeneratorService;
+
 
     /**
      * 交易凭证获取签名的有效期:5分钟
@@ -87,6 +97,111 @@ public class PaymentApiService {
     }
 
     /**
+     * 校验支付交易接口请求token的有效性
+     *
+     * @param accessToken
+     * @return
+     */
+    public boolean validateAccessToken(String accessToken) {
+
+        /**
+         * token 由支付平台生成且未失效 ===> accToken是有效的
+         */
+        String cacheKey = this.accessTokenCacheKey(accessToken);
+        return redisService.exists(cacheKey);
+    }
+
+    /**
+     * 生成交易事务id
+     *
+     * @return
+     */
+    public String transId(String accessToken) {
+
+        Long appId = this.getAppIdFromAccessToken(accessToken);
+        Assert.notNull(appId, "交易凭证失效");
+        AppBaseResponseVO appBaseResponse = appService.appInfo(appId);
+        Assert.notNull(appBaseResponse, "应用不存在,联系客服");
+
+        /**
+         * 基于雪花算法生成分布式下唯一id,作为交易分布式事务id,用于后续异常交易快速回滚
+         */
+        String transId = snowflakeIdGeneratorService.generateId();
+
+        /**
+         * 初始化TransId缓存,用于后续事务类操作校验
+         */
+        this.setPaymentTransIdCache(transId, appId, PaymentConstant.TrasnsIdStatusEnum.INIT);
+
+        return transId;
+    }
+
+    /**
+     * 走缓存获取分布式事务id详情
+     *
+     * @param transId
+     * @return
+     */
+    public TransIdDTO getTransIdDTO(String transId) {
+
+        String cacheKey = this.transIdCacheKey(transId);
+        String cache = redisService.get(cacheKey);
+        if (StringUtils.isBlank(cache)) {
+            return null;
+        }
+        return JsonUtil.fromJson(cache, TransIdDTO.class);
+    }
+
+    /**
+     * 刷新事务id状态
+     *
+     * @param transId
+     * @param status
+     */
+    public void setPaymentTransIdCache(String transId, PaymentConstant.TrasnsIdStatusEnum status) {
+        TransIdDTO transIdDTO = this.getTransIdDTO(transId);
+        Assert.notNull(transIdDTO, "transId is null!");
+        this.setPaymentTransIdCache(transId, transIdDTO.getAppId(), status);
+    }
+
+    /**
+     * 分布式交易事务id添加orderId信息
+     *
+     * @param transId
+     * @param orderId
+     */
+    public void setPaymentTransOrderInfo(String transId, String orderId) {
+        TransIdDTO transIdDTO = this.getTransIdDTO(transId);
+        Assert.notNull(transIdDTO, "transId is null!");
+        transIdDTO.setOrderId(orderId);
+        String cacheKey = this.transIdCacheKey(transId);
+        redisService.set(cacheKey, JsonUtil.toJsonString(transIdDTO), RedisKeyEnum.PAYMENT_TRANS_ID_CACHE.getExpireTime());
+    }
+
+    /**
+     * 刷新分布式交易事务id缓存
+     *
+     * @param transId 事务id
+     * @param appId   应用id
+     * @param status  状态
+     */
+    private void setPaymentTransIdCache(String transId, Long appId, PaymentConstant.TrasnsIdStatusEnum status) {
+        String cacheKey = this.transIdCacheKey(transId);
+        TransIdDTO transIdDTO = TransIdDTO.builder()
+                                    .transId(transId)
+                                    .appId(appId)
+                                    .status(status.getStatus())
+                                    .build();
+        redisService.set(cacheKey, JsonUtil.toJsonString(transIdDTO), RedisKeyEnum.PAYMENT_TRANS_ID_CACHE.getExpireTime());
+    }
+
+
+    private String transIdCacheKey(String transId) {
+
+        return RedisKeyEnum.PAYMENT_TRANS_ID_CACHE.getKey(transId);
+    }
+
+    /**
      * 获取交易凭证背后的应用信息
      *
      * @param accessToken
@@ -116,21 +231,4 @@ public class PaymentApiService {
 
         return RedisKeyEnum.PAYMENT_APP_ACCESS_TOKEN_CACHE.getKey(accessToken);
     }
-
-
-    /**
-     * 校验支付交易接口请求token的有效性
-     *
-     * @param accessToken
-     * @return
-     */
-    public boolean validateAccessToken(String accessToken) {
-
-        /**
-         * token 由支付平台生成且未失效 ===> accToken是有效的
-         */
-        String cacheKey = this.accessTokenCacheKey(accessToken);
-        return redisService.exists(cacheKey);
-    }
-
 }

+ 8 - 0
webchat-pay/src/main/java/com/webchat/pay/service/PaymentOrderDetailService.java

@@ -0,0 +1,8 @@
+package com.webchat.pay.service;
+
+
+import org.springframework.stereotype.Service;
+
+@Service
+public class PaymentOrderDetailService {
+}

+ 310 - 0
webchat-pay/src/main/java/com/webchat/pay/service/PaymentOrderService.java

@@ -0,0 +1,310 @@
+package com.webchat.pay.service;
+
+
+import com.google.common.collect.Lists;
+import com.webchat.common.enums.RedisKeyEnum;
+import com.webchat.common.enums.payment.PaymentOrderDetailStatusEnum;
+import com.webchat.common.enums.payment.PaymentOrderStatusEnum;
+import com.webchat.common.service.RedisService;
+import com.webchat.common.service.SnowflakeIdGeneratorService;
+import com.webchat.common.util.JsonUtil;
+import com.webchat.common.util.TransactionSyncManagerUtil;
+import com.webchat.domain.dto.payment.PaymentOrderCreateDTO;
+import com.webchat.domain.dto.payment.PaymentOrderDTO;
+import com.webchat.domain.dto.payment.PaymentTransRequestDTO;
+import com.webchat.domain.dto.payment.TransIdDTO;
+import com.webchat.pay.config.constants.PaymentConstant;
+import com.webchat.pay.config.exception.PaymentAccessAuthException;
+import com.webchat.pay.repository.dao.IPaymentOrderDAO;
+import com.webchat.pay.repository.dao.IPaymentOrderDetailDAO;
+import com.webchat.pay.repository.entity.PaymentOrderDetailEntity;
+import com.webchat.pay.repository.entity.PaymentOrderEntity;
+import org.apache.commons.collections.CollectionUtils;
+import org.apache.commons.lang3.ObjectUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.Assert;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestHeader;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.locks.ReentrantLock;
+
+@Service
+public class PaymentOrderService {
+
+    @Autowired
+    private RedisService redisService;
+    @Autowired
+    private IPaymentOrderDAO paymentOrderDAO;
+    @Autowired
+    private IPaymentOrderDetailDAO paymentOrderDetailDAO;
+    @Autowired
+    private PaymentApiService paymentApiService;
+    @Autowired
+    private SnowflakeIdGeneratorService snowflakeIdGeneratorService;
+
+    private ReentrantLock lock = new ReentrantLock();
+
+    /**
+     * 创建交易订单
+     *
+     * @param token
+     * @param transId
+     * @param logId
+     * @param paymentOrderCreateDTO
+     * @return
+     */
+    @Transactional
+    public String createOrder(String transId,
+                              String logId,
+                              PaymentOrderCreateDTO paymentOrderCreateDTO) {
+        /**
+         * transId校验
+         */
+        TransIdDTO transIdDTO = paymentApiService.getTransIdDTO(transId);
+        if (transIdDTO == null ||
+                !ObjectUtils.equals(PaymentConstant.TrasnsIdStatusEnum.INIT.getStatus(), transIdDTO.getStatus())) {
+            throw new PaymentAccessAuthException("分布式交易事务ID无效!");
+        }
+        // 接入方应用ID
+        Long appId = transIdDTO.getAppId();
+        String orderId = this.doCreateOrder(transId, appId, paymentOrderCreateDTO);
+        /**
+         * 保证事务提交成功后,刷新transId状态 ---> 已经创建订单
+         */
+        TransactionSyncManagerUtil.registerSynchronization(() -> {
+            if (StringUtils.isNotBlank(orderId)) {
+                // 刷新事务id状态
+                paymentApiService.setPaymentTransIdCache(transId,  PaymentConstant.TrasnsIdStatusEnum.CREATE_ORDER);
+                paymentApiService.setPaymentTransOrderInfo(transId,  orderId);
+                // 刷新订单缓存
+                this.refreshPaymentOrderCache(orderId);
+            }
+        });
+        return orderId;
+    }
+
+    /**
+     * 实际交易流程
+     *
+     * @param paymentTransRequest
+     * @param logId
+     * @return
+     */
+    @Transactional
+    public boolean doTrans(PaymentTransRequestDTO paymentTransRequest,
+                          String logId) {
+        String orderId = paymentTransRequest.getOrderId();
+        PaymentOrderDTO paymentOrder = this.getOrder(orderId);
+        if (paymentOrder == null) {
+            throw new PaymentAccessAuthException("订单不存在!");
+        }
+        int transType = paymentTransRequest.getTransType();
+        BigDecimal amount = paymentTransRequest.getAmount();
+        // 交易账户明细
+        String paymentId = snowflakeIdGeneratorService.generateId();
+        PaymentOrderDetailEntity sourceOrderDetail = new PaymentOrderDetailEntity();
+        sourceOrderDetail.setAmount(amount.multiply(new BigDecimal(transType)));
+        sourceOrderDetail.setSourceAccount(paymentTransRequest.getSourceUserId());
+        sourceOrderDetail.setTargetAccount(PaymentConstant.PAYMENT_SYSTEM_ID);
+        sourceOrderDetail.setStatus(PaymentConstant.TrasnsDetailStatusEnum.FINISHED.getStatus());
+        sourceOrderDetail.setOrderId(orderId);
+        sourceOrderDetail.setPaymentId(paymentId);
+        // 托管账户明细
+        PaymentOrderDetailEntity targetOrderDetail = new PaymentOrderDetailEntity();
+        targetOrderDetail.setAmount(amount.multiply(new BigDecimal(transType * -1)));
+        targetOrderDetail.setSourceAccount(PaymentConstant.PAYMENT_SYSTEM_ID);
+        targetOrderDetail.setTargetAccount(paymentTransRequest.getSourceUserId());
+        targetOrderDetail.setStatus(PaymentConstant.TrasnsDetailStatusEnum.FINISHED.getStatus());
+        targetOrderDetail.setOrderId(orderId);
+        targetOrderDetail.setPaymentId(paymentId);
+        List<PaymentOrderDetailEntity> orderDetails = Lists.newArrayList(sourceOrderDetail, targetOrderDetail);
+        /**
+         * 交易明细持久化
+         */
+        paymentOrderDetailDAO.saveAll(orderDetails);
+        /**
+         * 交易变更,删除缓存+主动刷新保证数据一致性
+         */
+        this.deleteAccountBalanceCache(sourceOrderDetail.getSourceAccount());
+        this.deleteAccountBalanceCache(PaymentConstant.PAYMENT_SYSTEM_ID);
+        return true;
+    }
+
+    /**
+     * 查询账户余额
+     *
+     * @param account
+     * @return
+     */
+    public BigDecimal getAccountBalanceFromCache(String logId, String account) {
+        String cacheKey = RedisKeyEnum.USER_WALLET_BALANCE_CACHE.getKey();
+        String cache = redisService.hget(cacheKey, account);
+        if (StringUtils.isNotBlank(cache)) {
+            return new BigDecimal(cache);
+        }
+        return this.refreshAccountBalance(account);
+    }
+
+
+    /**
+     * 交易回滚
+     * @param transId
+     * @param logId
+     * @return
+     */
+    @Transactional
+    public boolean rollback(String transId, String logId) {
+
+        /**
+         * transId校验
+         */
+        TransIdDTO transIdDTO = paymentApiService.getTransIdDTO(transId);
+        Assert.notNull(transIdDTO, "分布式交易事务ID无效!");
+        if (ObjectUtils.equals(PaymentConstant.TrasnsIdStatusEnum.ROLLBACK.getStatus(), transIdDTO.getStatus())) {
+            throw new PaymentAccessAuthException("交易已回滚!");
+        }
+        String orderId = transIdDTO.getOrderId();
+        Assert.notNull(orderId, "未查找到订单信息");
+
+        // 回滚订单
+        PaymentOrderEntity paymentOrderEntity = paymentOrderDAO.findByOrderId(orderId);
+        paymentOrderEntity.setStatus(PaymentOrderStatusEnum.ROLLBACK.getStatus());
+        paymentOrderDAO.save(paymentOrderEntity);
+        // 回滚交易明细
+        List<PaymentOrderDetailEntity> details = paymentOrderDetailDAO.findAllByOrderId(orderId);
+        if (CollectionUtils.isEmpty(details)) {
+            return true;
+        }
+        Set<String> refreshAccounts = new HashSet<>();
+        for (PaymentOrderDetailEntity detail : details) {
+            detail.setStatus(PaymentOrderDetailStatusEnum.ROLLBACK.getStatus());
+            refreshAccounts.add(detail.getSourceAccount());
+            refreshAccounts.add(detail.getTargetAccount());
+        }
+        paymentOrderDetailDAO.saveAll(details);
+        // 刷新相关账户余额缓存
+        this.deleteAccountBalanceCache(new ArrayList<>(refreshAccounts));
+        return true;
+    }
+
+    /**
+     * 刷新账户余额缓存
+     *
+     * @param account
+     * @return
+     */
+    private BigDecimal refreshAccountBalance(String account) {
+
+        BigDecimal balance = paymentOrderDetailDAO.getAccountBalance(account);
+        String cacheKey = RedisKeyEnum.USER_WALLET_BALANCE_CACHE.getKey();
+        balance = balance == null ? new BigDecimal("0") : balance;
+        redisService.hset(cacheKey, account, balance.toString(), RedisKeyEnum.USER_WALLET_BALANCE_CACHE.getExpireTime());
+        return balance;
+    }
+
+    /**
+     * 账户账户余额缓存
+     *
+     * @param account
+     */
+    private void deleteAccountBalanceCache(String account) {
+        String cacheKey = RedisKeyEnum.USER_WALLET_BALANCE_CACHE.getKey();
+        redisService.hdel(cacheKey, account);
+    }
+
+    private void deleteAccountBalanceCache(List<String> accounts) {
+        String cacheKey = RedisKeyEnum.USER_WALLET_BALANCE_CACHE.getKey();
+        redisService.hmdel(cacheKey, accounts);
+    }
+
+    /**
+     * 根据订单id获取订单详情
+     *
+     * @param orderId
+     * @return
+     */
+    public PaymentOrderDTO getOrder(String orderId) {
+
+        String cacheKey = this.cacheKey(orderId);
+        String cache = redisService.get(cacheKey);
+        if (StringUtils.isNotBlank(cache)) {
+            return JsonUtil.fromJson(cache, PaymentOrderDTO.class);
+        }
+        lock.lock();
+        try {
+            if (StringUtils.isNotBlank(cache = redisService.get(cacheKey))) {
+                return JsonUtil.fromJson(cache, PaymentOrderDTO.class);
+            }
+            return this.refreshPaymentOrderCache(orderId);
+        } catch (Exception e) {
+            // TODO
+        } finally {
+            if (lock.isHeldByCurrentThread()) {
+                lock.unlock();
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 执行订单创建
+     * @param transId
+     * @param paymentOrderCreateDTO
+     * @return
+     */
+    public String doCreateOrder(String transId, Long appId, PaymentOrderCreateDTO paymentOrderCreateDTO) {
+
+        // 基于雪花算法+redis原子自增特性,生成分布式下全局唯一订单id
+        String orderId = snowflakeIdGeneratorService.generateId();
+        // 构造持久化订单entity
+        PaymentOrderEntity orderEntity = this.convert(orderId, transId, appId, paymentOrderCreateDTO);
+        orderEntity.setStatus(PaymentOrderStatusEnum.NEW.getStatus());
+        paymentOrderDAO.save(orderEntity);
+        return orderId;
+    }
+
+    private PaymentOrderDTO refreshPaymentOrderCache(String orderId) {
+        PaymentOrderEntity orderEntity = paymentOrderDAO.findByOrderId(orderId);
+        if (orderEntity == null) {
+            return null;
+        }
+        PaymentOrderDTO dto = this.convert(orderEntity);
+        String cacheKey = this.cacheKey(orderId);
+        redisService.set(cacheKey, JsonUtil.toJsonString(dto), RedisKeyEnum.PAYMENT_ORDER_CACHE.getExpireTime());
+        return dto;
+    }
+
+    private String cacheKey(String orderId) {
+        return  RedisKeyEnum.PAYMENT_ORDER_CACHE.getKey(orderId);
+    }
+
+    private PaymentOrderDTO convert(PaymentOrderEntity orderEntity) {
+        PaymentOrderDTO dto = new PaymentOrderDTO();
+        BeanUtils.copyProperties(orderEntity, dto);
+        dto.setCreateTime(orderEntity.getCreateDate().getTime());
+        if (orderEntity.getUpdateDate() != null) {
+            dto.setUpdateTime(orderEntity.getUpdateDate().getTime());
+        }
+        return dto;
+    }
+
+    private PaymentOrderEntity convert(String orderId, String transId, Long appId,
+                                       PaymentOrderCreateDTO paymentOrderCreateDTO) {
+        PaymentOrderEntity orderEntity = new PaymentOrderEntity();
+        BeanUtils.copyProperties(paymentOrderCreateDTO, orderEntity);
+        orderEntity.setOrderId(orderId);
+        orderEntity.setTransId(transId);
+        orderEntity.setAppId(appId);
+        return orderEntity;
+    }
+}

+ 49 - 0
webchat-pgc/src/main/java/com/webchat/pgc/controller/MessageCardTemplateController.java

@@ -0,0 +1,49 @@
+package com.webchat.pgc.controller;
+
+import com.webchat.common.bean.APIResponseBean;
+import com.webchat.common.bean.APIResponseBeanUtil;
+import com.webchat.domain.vo.request.MessageCardTemplateRequestVO;
+import com.webchat.domain.vo.response.MessageCardTemplateResponseVO;
+import com.webchat.pgc.service.MessageCardTemplateService;
+import com.webchat.rmi.pgc.MessageCardTemplateClient;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+public class MessageCardTemplateController implements MessageCardTemplateClient {
+
+
+    @Autowired
+    private MessageCardTemplateService messageCardTemplateService;
+
+
+    /**
+     * 创建或更新服务号消息卡片模版配置
+     *
+     * @param templateRequest
+     * @return
+     */
+    @Override
+    public APIResponseBean<String> save(@RequestBody MessageCardTemplateRequestVO templateRequest) {
+
+        templateRequest.validateTemplateParam();
+
+        String templateId = messageCardTemplateService.save(templateRequest);
+        return APIResponseBeanUtil.success(templateId);
+    }
+
+    /**
+     * 查询模版详情
+     *
+     * @param templateId
+     * @return
+     */
+    @Override
+    public APIResponseBean<MessageCardTemplateResponseVO> template(@PathVariable String templateId) {
+        MessageCardTemplateResponseVO messageCardTemplateResponse =
+                                      messageCardTemplateService.get(templateId);
+        return APIResponseBeanUtil.success(messageCardTemplateResponse);
+    }
+}

+ 14 - 0
webchat-pgc/src/main/java/com/webchat/pgc/repository/dao/IMessageCardTemplateDAO.java

@@ -0,0 +1,14 @@
+package com.webchat.pgc.repository.dao;
+
+import com.webchat.pgc.repository.entity.MessageCardTemplateEntity;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface IMessageCardTemplateDAO extends
+        JpaRepository<MessageCardTemplateEntity, Integer>,
+        JpaSpecificationExecutor<MessageCardTemplateEntity> {
+
+    MessageCardTemplateEntity findByTemplateId(String templateId);
+}

+ 66 - 0
webchat-pgc/src/main/java/com/webchat/pgc/repository/entity/MessageCardTemplateEntity.java

@@ -0,0 +1,66 @@
+package com.webchat.pgc.repository.entity;
+
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 公众号文章实体
+ */
+@Data
+@Entity
+@Table(name = "web_chat_message_card_template")
+public class MessageCardTemplateEntity extends BaseEntity {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    protected Long id;
+
+    /**
+     * 服务号
+     */
+    @Column(name = "account")
+    private String account;
+
+    @Column(name = "templateId")
+    private String templateId;
+
+    @Column(name = "logo")
+    private String logo;
+
+    @Column(name = "title")
+    private String title;
+
+    /**
+     * 模版内容(richText html富文本模版)
+     */
+    @Column(name = "content")
+    private String content;
+
+    /**
+     * 链接名称
+     */
+    @Column(name = "redirect_name")
+    private String redirectName;
+
+    /**
+     * 卡片点击跳转链接
+     */
+    @Column(name = "redirect_url")
+    private String redirectUrl;
+
+    /**
+     * 模版状态
+     *
+     * @see com.webchat.common.enums.CommonStatusEnum
+     */
+    @Column(name = "status")
+    private Integer status;
+}

+ 128 - 0
webchat-pgc/src/main/java/com/webchat/pgc/service/MessageCardTemplateService.java

@@ -0,0 +1,128 @@
+package com.webchat.pgc.service;
+
+import com.webchat.common.bean.APIPageResponseBean;
+import com.webchat.common.enums.RedisKeyEnum;
+import com.webchat.common.service.RedisService;
+import com.webchat.common.service.SnowflakeIdGeneratorService;
+import com.webchat.common.util.JsonUtil;
+import com.webchat.domain.vo.request.MessageCardTemplateRequestVO;
+import com.webchat.domain.vo.response.MessageCardTemplateResponseVO;
+import com.webchat.pgc.repository.dao.IMessageCardTemplateDAO;
+import com.webchat.pgc.repository.entity.MessageCardTemplateEntity;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.Assert;
+
+import java.util.Date;
+
+@Service
+public class MessageCardTemplateService {
+
+    @Autowired
+    private IMessageCardTemplateDAO messageCardTemplateDAO;
+    @Autowired
+    private RedisService redisService;
+    @Autowired
+    private SnowflakeIdGeneratorService snowflakeIdGeneratorService;
+
+
+    /**
+     * 创建或更新服务号消息卡片模版配置
+     *
+     * @param messageCardTemplateRequest
+     * @return
+     */
+    public String save(MessageCardTemplateRequestVO messageCardTemplateRequest) {
+
+        MessageCardTemplateEntity entity = this.convert(messageCardTemplateRequest);
+        messageCardTemplateDAO.save(entity);
+        /**
+         * 刷新模版配置缓存
+         */
+        this.refreshMessageCardTemplateCache(entity);
+        return entity.getTemplateId();
+    }
+
+    /**
+     * 根据模版id查询模版详情配置
+     *
+     * @param templateId
+     * @return
+     */
+    public MessageCardTemplateResponseVO get(String templateId) {
+        String cacheKey = RedisKeyEnum.MESSAGE_CARD_TEMPLATE_CACHE.getKey(templateId);
+        String cache = redisService.get(cacheKey);
+        if (StringUtils.isNotBlank(cache)) {
+            return JsonUtil.fromJson(cache, MessageCardTemplateResponseVO.class);
+        }
+        return this.refreshMessageCardTemplateCache(templateId);
+    }
+
+    /**
+     * 模版列表查询
+     *
+     * @return
+     */
+    public APIPageResponseBean<MessageCardTemplateResponseVO> page() {
+
+        return null;
+    }
+
+
+    /**
+     * 刷新模版配置缓存
+     *
+     * @param templateId
+     */
+    private MessageCardTemplateResponseVO refreshMessageCardTemplateCache(String templateId) {
+        MessageCardTemplateEntity entity = messageCardTemplateDAO.findByTemplateId(templateId);
+        return this.refreshMessageCardTemplateCache(entity);
+    }
+
+    private MessageCardTemplateResponseVO refreshMessageCardTemplateCache(MessageCardTemplateEntity entity) {
+        MessageCardTemplateResponseVO messageCardTemplate = this.convert(entity);
+        if (messageCardTemplate == null) {
+            return null;
+        }
+        String cacheKey = RedisKeyEnum.MESSAGE_CARD_TEMPLATE_CACHE.getKey(entity.getTemplateId());
+        redisService.set(cacheKey, JsonUtil.toJsonString(messageCardTemplate),
+                RedisKeyEnum.MESSAGE_CARD_TEMPLATE_CACHE.getExpireTime());
+        return messageCardTemplate;
+    }
+
+    private MessageCardTemplateEntity convert(MessageCardTemplateRequestVO requestVO) {
+        String templateId = requestVO.getTemplateId();
+        MessageCardTemplateEntity messageCardTemplate;
+        if (StringUtils.isNotBlank(templateId)) {
+            messageCardTemplate = messageCardTemplateDAO.findByTemplateId(templateId);
+            Assert.notNull(messageCardTemplate, "模版更新失败:模版不存在!");
+        } else {
+            messageCardTemplate = new MessageCardTemplateEntity();
+            templateId = snowflakeIdGeneratorService.generateId();
+            messageCardTemplate.setTemplateId(templateId);
+            messageCardTemplate.setAccount(requestVO.getAccount());
+            messageCardTemplate.setCreateBy(requestVO.getOperator());
+        }
+        messageCardTemplate.setLogo(requestVO.getLogo());
+        messageCardTemplate.setTitle(requestVO.getTitle());
+        messageCardTemplate.setContent(requestVO.getContent());
+        messageCardTemplate.setRedirectName(requestVO.getRedirectName());
+        messageCardTemplate.setRedirectUrl(requestVO.getRedirectUrl());
+        messageCardTemplate.setUpdateBy(requestVO.getOperator());
+        return messageCardTemplate;
+    }
+
+    private MessageCardTemplateResponseVO convert(MessageCardTemplateEntity entity) {
+        if (entity == null) {
+            return null;
+        }
+        MessageCardTemplateResponseVO messageCardTemplate = new MessageCardTemplateResponseVO();
+        BeanUtils.copyProperties(entity, messageCardTemplate);
+        Date updateDate = entity.getUpdateDate();
+        updateDate = updateDate == null ? entity.getCreateDate() : updateDate;
+        messageCardTemplate.setUpdateTime(updateDate.getTime());
+        return messageCardTemplate;
+    }
+}

+ 29 - 14
webchat-remote/src/main/java/com/webchat/rmi/pay/PaymentApiServiceClient.java

@@ -1,6 +1,7 @@
 package com.webchat.rmi.pay;
 
 import com.webchat.common.bean.APIResponseBean;
+import com.webchat.domain.dto.payment.PaymentOrderCreateDTO;
 import com.webchat.domain.dto.payment.PaymentTransRequestDTO;
 import org.springframework.cloud.openfeign.FeignClient;
 import org.springframework.web.bind.annotation.GetMapping;
@@ -17,8 +18,12 @@ public interface PaymentApiServiceClient {
     /**
      * 获取交易接入方凭证
      *
+     * @param appId
      * @param accessKey
-     * @param sign 签名
+     * @param secretKey
+     * @param timestamp
+     * @param signature
+     * @param logId
      * @return
      */
     @GetMapping("/pay-service/api/token")
@@ -36,8 +41,19 @@ public interface PaymentApiServiceClient {
      * @return
      */
     @GetMapping("/pay-service/api/transId")
-    APIResponseBean<String> transId(@RequestHeader String token,
-                                    @RequestHeader String logId);
+    APIResponseBean<String> transId(@RequestHeader(name = "access-token") String token,
+                                    @RequestHeader(name = "log-id") String logId);
+
+
+    /**
+     * 创建订单,获取订单id
+     * @return
+     */
+    @PostMapping("/pay-service/api/orderId")
+    APIResponseBean<String> orderId(@RequestHeader(name = "access-token") String token,
+                                    @RequestHeader(name = "trans-id") String transId,
+                                    @RequestHeader(name = "log-id") String logId,
+                                    @RequestBody PaymentOrderCreateDTO paymentOrderCreateDTO);
 
     /**
      * 真实交易接口
@@ -46,9 +62,8 @@ public interface PaymentApiServiceClient {
      */
     @PostMapping("/pay-service/api/doTrans")
     APIResponseBean<Boolean> doTrans(@RequestBody PaymentTransRequestDTO paymentTransRequest,
-                                     @RequestHeader String token,
-                                     @RequestHeader String transId,
-                                     @RequestHeader String logId);
+                                     @RequestHeader(name = "access-token") String token,
+                                     @RequestHeader(name = "log-id") String logId);
 
 
     /**
@@ -60,18 +75,18 @@ public interface PaymentApiServiceClient {
      * @return
      */
     @PostMapping("/pay-service/api/rollback")
-    APIResponseBean<Boolean> rollback(@RequestHeader String token,
-                                      @RequestHeader String transId,
-                                      @RequestHeader String logId);
+    APIResponseBean<Boolean> rollback(@RequestHeader(name = "access-token") String token,
+                                      @RequestHeader(name = "trans-id") String transId,
+                                      @RequestHeader(name = "log-id") String logId);
 
     /**
      * 查询账户余额
      *
-     * @param userId
+     * @param account
      * @return
      */
-    @GetMapping("/pay-service/api/balance/{userId}")
-    APIResponseBean<BigDecimal> balance(@PathVariable String userId,
-                                        @RequestHeader String token,
-                                        @RequestHeader String logId);
+    @GetMapping("/pay-service/api/balance/{account}")
+    APIResponseBean<BigDecimal> balance(@PathVariable String account,
+                                        @RequestHeader(name = "access-token") String token,
+                                        @RequestHeader(name = "log-id") String logId);
 }

+ 34 - 0
webchat-remote/src/main/java/com/webchat/rmi/pgc/MessageCardTemplateClient.java

@@ -0,0 +1,34 @@
+package com.webchat.rmi.pgc;
+
+import com.webchat.common.bean.APIResponseBean;
+import com.webchat.domain.vo.request.MessageCardTemplateRequestVO;
+import com.webchat.domain.vo.response.MessageCardTemplateResponseVO;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+
+
+@FeignClient(name = "webchat-pgc-service", contextId = "messageCardTemplateClient")
+public interface MessageCardTemplateClient {
+
+
+    /**
+     * 提供消息卡片模版配置服务
+     *
+     * @param templateRequest
+     * @return
+     */
+    @PostMapping("/pgc-service/message-card/template/save")
+    APIResponseBean<String> save(@RequestBody MessageCardTemplateRequestVO templateRequest);
+
+    /**
+     * 根据模版ID获取模版配置详情
+     *
+     * @param templateId
+     * @return
+     */
+    @GetMapping("/pgc-service/message-card/template/{templateId}")
+    APIResponseBean<MessageCardTemplateResponseVO> template(@PathVariable String templateId);
+}

+ 20 - 2
webchat-remote/src/main/java/com/webchat/rmi/user/UserServiceClient.java

@@ -2,7 +2,7 @@ package com.webchat.rmi.user;
 
 import com.webchat.common.bean.APIPageResponseBean;
 import com.webchat.common.bean.APIResponseBean;
-import com.webchat.domain.vo.request.CreatePublicAccountRequestVO;
+import com.webchat.domain.vo.request.CreateAccountRequestVO;
 import com.webchat.domain.vo.request.CreateRobotRequestVO;
 import com.webchat.domain.vo.request.UserRegistryInfoRequestVO;
 import com.webchat.domain.vo.response.UserBaseResponseInfoVO;
@@ -46,6 +46,15 @@ public interface UserServiceClient {
     APIResponseBean<Set<String>> getAllSubscriberByAccount(@PathVariable Integer relationType, @PathVariable String account);
 
     /**
+     * 获取群聊下所有用户id
+     *
+     * @param account
+     * @return
+     */
+    @GetMapping("/user-service/isSubscribe/{relationType}/{userAccount}/{account}")
+    APIResponseBean<Boolean> isSubscribe(@PathVariable Integer relationType, @PathVariable String userAccount, @PathVariable String account);
+
+    /**
      * 根据手机号查询用户基础信息
      *
      * @param mobile
@@ -98,7 +107,16 @@ public interface UserServiceClient {
      * @return
      */
     @PostMapping("/user-service/account/createPublicAccount")
-    APIResponseBean createPublicAccount(@RequestBody CreatePublicAccountRequestVO requestPram);
+    APIResponseBean createPublicAccount(@RequestBody CreateAccountRequestVO requestPram);
+
+    /**
+     * 创建服务号
+     * 默认只有管理员可以创建公众号
+     *
+     * @return
+     */
+    @PostMapping("/user-service/account/createServerAccount")
+    APIResponseBean createServerAccount(@RequestBody CreateAccountRequestVO requestPram);
 
 
     /**

+ 16 - 0
webchat-ugc/src/main/java/com/webchat/ugc/config/MessageCardAccountConfig.java

@@ -0,0 +1,16 @@
+package com.webchat.ugc.config;
+
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+@Data
+@Component
+@ConfigurationProperties(prefix = "message-card.account")
+public class MessageCardAccountConfig {
+
+
+    private String payment;
+
+}

+ 16 - 0
webchat-ugc/src/main/java/com/webchat/ugc/config/MessageCardTemplateConfig.java

@@ -0,0 +1,16 @@
+package com.webchat.ugc.config;
+
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+@Data
+@Component
+@ConfigurationProperties(prefix = "message-card.template")
+public class MessageCardTemplateConfig {
+
+    private String redPacketSend;
+
+    private String redPacketOpen;
+}

+ 1 - 1
webchat-ugc/src/main/java/com/webchat/ugc/controller/RedPacketController.java

@@ -4,7 +4,7 @@ import com.webchat.common.bean.APIResponseBean;
 import com.webchat.common.bean.APIResponseBeanUtil;
 import com.webchat.domain.vo.request.SendRedPacketRequestVO;
 import com.webchat.rmi.ugc.RedPacketClient;
-import com.webchat.ugc.service.RedPacketService;
+import com.webchat.ugc.service.redpacket.RedPacketService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.RequestBody;

+ 45 - 1
webchat-ugc/src/main/java/com/webchat/ugc/messaegqueue/service/PersistentMessageService.java

@@ -4,16 +4,20 @@ package com.webchat.ugc.messaegqueue.service;
 import com.webchat.common.bean.APIPageResponseBean;
 import com.webchat.common.bean.APIResponseBean;
 import com.webchat.common.bean.APIResponseBeanUtil;
+import com.webchat.common.enums.AccountRelationTypeEnum;
 import com.webchat.common.enums.ChatMessageTypeEnum;
 import com.webchat.common.enums.RedisKeyEnum;
 import com.webchat.common.enums.RoleCodeEnum;
 import com.webchat.common.enums.messagequeue.MessageQueueEnum;
+import com.webchat.common.service.FreeMarkEngineService;
 import com.webchat.common.service.RedisService;
 import com.webchat.common.service.messagequeue.producer.MessageQueueProducer;
 import com.webchat.common.util.DateUtils;
 import com.webchat.common.util.HtmlUtil;
 import com.webchat.common.util.JsonUtil;
+import com.webchat.domain.dto.messagecard.MessageCardSendDTO;
 import com.webchat.domain.vo.request.mess.ChatMessageRequestVO;
+import com.webchat.domain.vo.response.MessageCardTemplateResponseVO;
 import com.webchat.domain.vo.response.UserBaseResponseInfoVO;
 import com.webchat.domain.vo.response.mess.ChatMessageResponseVO;
 import com.webchat.domain.vo.response.mess.PublicAccountArticleMessageVO;
@@ -22,6 +26,8 @@ import com.webchat.rmi.pgc.OfficialArticleClient;
 import com.webchat.ugc.repository.dao.IChatMessageDAO;
 import com.webchat.ugc.repository.entity.ChatMessageEntity;
 import com.webchat.ugc.service.AccountService;
+import com.webchat.ugc.service.MessageCardTemplateService;
+import com.webchat.ugc.service.redpacket.RedPacketService;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.collections.CollectionUtils;
 import org.apache.commons.lang3.StringUtils;
@@ -64,6 +70,15 @@ public class PersistentMessageService {
     @Autowired
     private OfficialArticleClient officialArticleClient;
 
+    @Autowired
+    private RedPacketService redPacketService;
+
+    @Autowired
+    private MessageCardTemplateService messageCardTemplateService;
+
+    @Autowired
+    private FreeMarkEngineService freeMarkEngineService;
+
     /**
      * 《离线场景》持久化消息队列,保存离线消息,同时会将数据同步到ES用于后续的RAG问答和消息搜索
      *
@@ -296,14 +311,43 @@ public class PersistentMessageService {
         messageResponse.setIsRead(userMessEntity.getIsRead());
         messageResponse.setType(userMessEntity.getType());
         if (ChatMessageTypeEnum.RED_PACKET.getType().equals(userMessEntity.getType())) {
-//            messageResponse.setRedPacketDetail(redPacketService.getRedPacketDetailCache(Long.valueOf(userMessEntity.getMessage())));
+            messageResponse.setMessageExt(redPacketService.getRedPacket(Long.valueOf(userMessEntity.getMessage())));
         } else if (ChatMessageTypeEnum.PUBLIC_ACCOUNT_ARTICLE.getType().equals(userMessEntity.getType())) {
             messageResponse.setPublicAccountArticle(getArticleMessageVO(Long.valueOf(userMessEntity.getMessage())));
+        } else if (ChatMessageTypeEnum.SERVER_ACCOUNT_MESSAGE_CARD.getType().equals(userMessEntity.getType())) {
+            MessageCardSendDTO messageCard = JsonUtil.fromJson(userMessEntity.getMessage(), MessageCardSendDTO.class);
+            messageResponse.setMessageExt(getMessageCardSendDTO(messageCard));
         }
         messageResponse.setGroupMessage(StringUtils.isNotBlank(userMessEntity.getProxySender()));
         return messageResponse;
     }
 
+    private MessageCardTemplateResponseVO getMessageCardSendDTO(MessageCardSendDTO messageCard) {
+        Map<String, Object> vars = messageCard.getVars();
+        // 查询推送消息模版配置
+        String templateId = messageCard.getTemplateId();
+        MessageCardTemplateResponseVO template = messageCardTemplateService.get(templateId);
+        /**
+         * 渲染消息卡内容
+         */
+        String messageCardTemplateContent;
+        try {
+            messageCardTemplateContent = freeMarkEngineService.getContent(template.getContent(), vars);
+        } catch (Exception e) {
+            log.error("服务号消息卡模版内容引擎渲染异常 =====> template:{}, vars:{}",
+                    template.getContent(), JsonUtil.toJsonString(vars));
+            return null;
+        }
+
+        MessageCardTemplateResponseVO messageExt = new MessageCardTemplateResponseVO();
+        messageExt.setLogo(messageCard.getLogo());
+        messageExt.setTitle(messageCard.getTitle());
+        messageExt.setContent(messageCardTemplateContent);
+        messageExt.setRedirectName(messageCard.getRedirectName());
+        messageExt.setRedirectUrl(messageCard.getRedirectUrl());
+        return messageExt;
+    }
+
     private PublicAccountArticleMessageVO getArticleMessageVO(Long articleId) {
         APIResponseBean<ArticleBaseResponseVO> responseBean = officialArticleClient.detail(articleId, false);
         if (!APIResponseBeanUtil.isOk(responseBean)) {

+ 13 - 0
webchat-ugc/src/main/java/com/webchat/ugc/repository/dao/IRedPacketDAO.java

@@ -0,0 +1,13 @@
+package com.webchat.ugc.repository.dao;
+
+import com.webchat.ugc.repository.entity.RedPacketEntity;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.stereotype.Repository;
+
+
+@Repository
+public interface IRedPacketDAO extends JpaSpecificationExecutor<RedPacketEntity>,
+        JpaRepository<RedPacketEntity, Long> {
+
+}

+ 13 - 0
webchat-ugc/src/main/java/com/webchat/ugc/repository/dao/IRedPacketRecordDAO.java

@@ -0,0 +1,13 @@
+package com.webchat.ugc.repository.dao;
+
+import com.webchat.ugc.repository.entity.RedPacketRecordEntity;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.stereotype.Repository;
+
+
+@Repository
+public interface IRedPacketRecordDAO extends JpaSpecificationExecutor<RedPacketRecordEntity>,
+                                             JpaRepository<RedPacketRecordEntity, Long> {
+
+}

+ 86 - 0
webchat-ugc/src/main/java/com/webchat/ugc/repository/entity/RedPacketEntity.java

@@ -0,0 +1,86 @@
+package com.webchat.ugc.repository.entity;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * @author 程序员七七, https://www.coderutil.com网站作者
+ * @date 2024/11/8 23:05
+ */
+@Data
+@Entity
+@Table(name = "web_chat_red_packet")
+public class RedPacketEntity extends BaseEntity {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    protected Long id;
+
+    /**
+     * 订单id
+     */
+    @Column(name = "order_id")
+    private String orderId;
+
+    /**
+     * 红包封面
+     */
+    @Column(name = "cover")
+    private String cover;
+
+    /**
+     * 祝福语
+     */
+    @Column(name = "blessing")
+    private String blessing;
+
+    /**
+     * 红包发送人
+     */
+    @Column(name = "sender")
+    private String sender;
+
+    /**
+     * 可能是人 / 群
+     */
+    @Column(name = "receiver")
+    private String receiver;
+
+    /**
+     * 拼手气、普通红包(平均分配)
+     */
+    @Column(name = "type")
+    private Integer type;
+
+    /**
+     * 总金额
+     */
+    @Column(name = "total_money")
+    private BigDecimal totalMoney;
+
+    /**
+     * 一共几个,发给个人只能一次拆一个,发给群聊可以配置多个
+     */
+    @Column(name = "count")
+    private Integer count;
+
+    /**
+     * 红包状态:新建、过期、结束(提前抽完)
+     */
+    @Column(name = "status")
+    private Integer status;
+
+    /**
+     * 过期时间
+     */
+    @Column(name = "expire_date")
+    private Date expireDate;
+}

+ 50 - 0
webchat-ugc/src/main/java/com/webchat/ugc/repository/entity/RedPacketRecordEntity.java

@@ -0,0 +1,50 @@
+package com.webchat.ugc.repository.entity;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * @author 程序员七七, https://www.coderutil.com网站作者
+ * @date 2024/11/8 23:05
+ */
+@Data
+@Entity
+@Table(name = "web_chat_red_packet_record")
+public class RedPacketRecordEntity {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    protected Long id;
+
+    /**
+     * 红包id
+     */
+    @Column(name = "red_packet_id")
+    private Long redPacketId;
+
+    /**
+     * 领取人
+     */
+    @Column(name = "user_id")
+    private String userId;
+
+    /**
+     * 总金额
+     */
+    @Column(name = "money")
+    private BigDecimal money;
+
+    /**
+     * 过期时间
+     */
+    @Column(name = "create_date")
+    private Date createDate;
+}

+ 18 - 0
webchat-ugc/src/main/java/com/webchat/ugc/service/AccountService.java

@@ -6,6 +6,7 @@ import com.webchat.common.enums.AccountRelationTypeEnum;
 import com.webchat.domain.vo.response.UserBaseResponseInfoVO;
 import com.webchat.rmi.user.UserServiceClient;
 import org.apache.commons.collections.CollectionUtils;
+import org.apache.commons.lang3.ObjectUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
@@ -80,4 +81,21 @@ public class AccountService {
         }
         return null;
     }
+
+    /**
+     * 判断userAccount是否订阅account
+     *
+     * @param userAccount
+     * @param account
+     * @param accountRelationType
+     * @return
+     */
+    public boolean isSubscribe(String userAccount, String account, AccountRelationTypeEnum accountRelationType) {
+        APIResponseBean<Boolean> responseBean =
+                userServiceClient.isSubscribe(accountRelationType.getType(), userAccount, account);
+        if (APIResponseBeanUtil.isOk(responseBean)) {
+            return ObjectUtils.equals(responseBean.getData(), true);
+        }
+        return false;
+    }
 }

+ 26 - 0
webchat-ugc/src/main/java/com/webchat/ugc/service/MessageCardTemplateService.java

@@ -0,0 +1,26 @@
+package com.webchat.ugc.service;
+
+import com.webchat.common.bean.APIResponseBean;
+import com.webchat.common.bean.APIResponseBeanUtil;
+import com.webchat.domain.vo.response.MessageCardTemplateResponseVO;
+import com.webchat.rmi.pgc.MessageCardTemplateClient;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+@Service
+public class MessageCardTemplateService {
+
+
+    @Autowired
+    private MessageCardTemplateClient messageCardTemplateClient;
+
+
+    public MessageCardTemplateResponseVO get(String templateId) {
+        APIResponseBean<MessageCardTemplateResponseVO> responseBean =
+                messageCardTemplateClient.template(templateId);
+        if (APIResponseBeanUtil.isOk(responseBean)) {
+            return responseBean.getData();
+        }
+        return null;
+    }
+}

+ 123 - 7
webchat-ugc/src/main/java/com/webchat/ugc/service/PaymentService.java

@@ -8,17 +8,21 @@ import com.webchat.common.bean.APIResponseBeanUtil;
 import com.webchat.common.enums.RedisKeyEnum;
 import com.webchat.common.exception.BusinessException;
 import com.webchat.common.service.RedisService;
+import com.webchat.common.service.SnowflakeIdGeneratorService;
+import com.webchat.common.util.IDGenerateUtil;
 import com.webchat.common.util.JsonUtil;
 import com.webchat.common.util.SignUtil;
+import com.webchat.domain.dto.payment.PaymentOrderCreateDTO;
+import com.webchat.domain.dto.payment.PaymentTransRequestDTO;
 import com.webchat.rmi.pay.PaymentApiServiceClient;
 import com.webchat.ugc.config.PaymentAppConfig;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.ObjectUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
-import java.util.Date;
-import java.util.concurrent.TimeUnit;
+import java.math.BigDecimal;
 import java.util.concurrent.locks.ReentrantLock;
 
 
@@ -37,6 +41,11 @@ public class PaymentService {
 
     private ReentrantLock lock = new ReentrantLock();
 
+    /**
+     * 获取交易凭证
+     *
+     * @return
+     */
     public String token() {
         String key = this.getTokenCacheKey();
         String cache = redisService.get(key);
@@ -68,6 +77,7 @@ public class PaymentService {
      * @return
      */
     private String refreshAccessTokenCache() {
+
         String token = this.getTokenByClient();
         String key = this.getTokenCacheKey();
         redisService.set(key, token, RedisKeyEnum.PAYMENT_ACCESS_TOKEN_CACHE.getExpireTime());
@@ -79,6 +89,7 @@ public class PaymentService {
      * @return
      */
     private String getTokenCacheKey() {
+
         return RedisKeyEnum.PAYMENT_ACCESS_TOKEN_CACHE.getKey();
     }
 
@@ -113,9 +124,15 @@ public class PaymentService {
         throw new BusinessException("支付Token请求失败!");
     }
 
+    /**
+     * 获取交易id
+     *
+     * @return
+     */
     public String transId() {
-        String accessToken = token();
-        String logId = generateLogId();
+
+        String accessToken = this.token();
+        String logId = this.generateLogId();
         APIResponseBean<String> responseBean = paymentApiServiceClient.transId(accessToken, logId);
         if (APIResponseBeanUtil.isOk(responseBean)) {
             return responseBean.getData();
@@ -125,9 +142,108 @@ public class PaymentService {
         throw new BusinessException("支付TransId请求失败!");
     }
 
+    /**
+     * 申请创建交易订单,获取交易订单id
+     *
+     * @param transId
+     * @param paymentOrderCreateDTO
+     * @return
+     */
+    public String createOrder(String transId, PaymentOrderCreateDTO paymentOrderCreateDTO) {
+        String accessToken = this.token();
+        String logId = this.generateLogId();
+        APIResponseBean<String> responseBean = paymentApiServiceClient.orderId(accessToken, transId, logId, paymentOrderCreateDTO);
+        if (APIResponseBeanUtil.isOk(responseBean)) {
+            return responseBean.getData();
+        }
+        log.error("UGC 支付服务异常 =====> 交易订单创建失败, logId:{}, param:{}",
+                logId, JsonUtil.toJsonString(paymentOrderCreateDTO));
+        throw new BusinessException("交易订单创建失败!");
+    }
+
+    /**
+     * 请求支付平台,进行实际交易
+     *
+     * @param paymentTransRequest
+     * @return
+     */
+    public boolean doTrans(PaymentTransRequestDTO paymentTransRequest) {
+        String accessToken = this.token();
+        String logId = this.generateLogId();
+        APIResponseBean<Boolean> responseBean = paymentApiServiceClient.doTrans(paymentTransRequest, accessToken, logId);
+        if (APIResponseBeanUtil.isOk(responseBean)) {
+            return ObjectUtils.equals(responseBean.getData(), true);
+        }
+        log.error("UGC 支付服务异常 =====> 交易处理失败, logId:{}, param:{}",
+                logId, JsonUtil.toJsonString(paymentTransRequest));
+        return false;
+    }
+
+
+    /**
+     * 请求支付平台(wenchat-pay)回滚交易订单及交易明细
+     *
+     * @param transId
+     * @return
+     */
+    public void doRollBack(String transId) {
+        boolean rollbackResult = this.rollBack(transId);
+        if (!rollbackResult) {
+            // 回滚失败,补偿机制,MQ
+        }
+    }
+
+    /**
+     * 回滚交易事务id相关的所有订单及订单明细
+     *
+     * @param transId
+     * @return
+     */
+    public boolean rollBack(String transId) {
+
+        String accessToken = this.token();
+        String logId = this.generateLogId();
+        APIResponseBean<Boolean> responseBean = paymentApiServiceClient.rollback(accessToken, transId, logId);
+        if (APIResponseBeanUtil.isOk(responseBean)) {
+            return ObjectUtils.equals(responseBean, transId());
+        }
+        log.error("UGC 支付服务异常 =====> 交易回滚失败, logId:{}, transId:{}", logId, transId);
+        return false;
+    }
+
+    /**
+     * 校验账户余额
+     *
+     * @param account
+     * @param amount
+     * @return
+     */
+    public boolean validateAccountBalance(String account, BigDecimal amount) {
+        BigDecimal balance = this.getAccountBalance(account);
+        return balance.compareTo(amount) != -1;
+    }
+
+    public BigDecimal getAccountBalance(String account) {
+        /**
+         * 1. 获取access-token 交易凭证
+         */
+        String accessToken = this.token();
+        String logId = this.generateLogId();
+        /**
+         * RPC请求支付平台,获取账户余额
+         */
+        APIResponseBean<BigDecimal> responseBean = paymentApiServiceClient.balance(account, accessToken, logId);
+        if (APIResponseBeanUtil.isOk(responseBean)) {
+            return responseBean.getData();
+        }
+        log.error("UGC 支付服务异常 =====> 获取 {} 账户余额异常, 异常原因:{}, logId:{}",
+                account, responseBean.getMsg(), logId);
+        throw new BusinessException(responseBean.getMsg());
+    }
+
+
     private String generateLogId() {
-        // TODO 预留:
-        // 支持雪花算法ID生成
-        return null;
+
+        return IDGenerateUtil.uuid();
     }
 }

+ 0 - 21
webchat-ugc/src/main/java/com/webchat/ugc/service/RedPacketService.java

@@ -1,21 +0,0 @@
-package com.webchat.ugc.service;
-
-import com.webchat.domain.vo.request.SendRedPacketRequestVO;
-import org.springframework.stereotype.Service;
-
-@Service
-public class RedPacketService {
-
-
-    public Long send(SendRedPacketRequestVO sendRedPacketRequest) {
-
-
-        return null;
-    }
-
-    public String open(Long redPacketId,String userId) {
-
-
-        return null;
-    }
-}

+ 149 - 0
webchat-ugc/src/main/java/com/webchat/ugc/service/redpacket/AbstractOpenRedPacketService.java

@@ -0,0 +1,149 @@
+package com.webchat.ugc.service.redpacket;
+
+import com.webchat.common.constants.RedPacketConstants;
+import com.webchat.common.enums.AccountRelationTypeEnum;
+import com.webchat.common.enums.RedisKeyEnum;
+import com.webchat.common.enums.RoleCodeEnum;
+import com.webchat.common.exception.BusinessException;
+import com.webchat.common.service.RedisService;
+import com.webchat.domain.vo.response.RedPacketBaseVO;
+import com.webchat.domain.vo.response.UserBaseResponseInfoVO;
+import com.webchat.ugc.service.AccountService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.util.Assert;
+
+
+public abstract class AbstractOpenRedPacketService implements RedPacketOpenInter {
+
+    @Autowired
+    public RedisService redisService;
+    @Autowired
+    private RedPacketService redPacketService;
+    @Autowired
+    private AccountService accountService;
+
+    /**
+     * 实际红包拆分(保证并发数据安全)
+     * 具体逻辑由实现类实现:分布式锁实现、LUA脚本原子操作、预计算+队列
+     *
+     * @param redPacket
+     * @param userId
+     * @return
+     */
+    public abstract int openRedPacket(RedPacketBaseVO redPacket, String userId);
+
+    /**
+     * 拆包权限校验:
+     * 1. 红包是否有效(是否真实存在、未失效、结束)
+     * 2. 校验当前拆包人是否由拆包权限
+     *    2.1 一对一:当前人是否为红包的接收人
+     *    2.2 群聊多人:校验当前用户是否在群组内
+     * 3. 校验当前用户是否拆过红包(每个人只能拆一次)
+     *
+     * ===> 获得拆包资格
+     *
+     * @param redPacket
+     * @param userId
+     */
+    private void validateOpenPermission(RedPacketBaseVO redPacket, String userId) {
+
+        /**
+         * 红包是否有效(是否真实存在、未失效、结束)
+         */
+        Assert.notNull(redPacket, "红包不存在!");
+        int status = redPacket.getStatus();
+        if (RedPacketConstants.RedPacketStatus.EXPIRED.getStatus() == status) {
+            throw new BusinessException("红包已过期");
+        }
+        if (RedPacketConstants.RedPacketStatus.END.getStatus() == status) {
+            throw new BusinessException("来晚了~");
+        }
+        Assert.isTrue(RedPacketConstants.RedPacketStatus.RUNNING.getStatus() == status, "红包状态异常!");
+        Assert.isTrue(redPacket.getExpireTime() > System.currentTimeMillis(), "红包已过期");
+
+        /**
+         *  2. 校验当前拆包人是否由拆包权限
+         *    2.1 一对一:当前人是否为红包的接收人
+         *    2.2 群聊多人:校验当前用户是否在群组内
+         */
+        String receiverId = redPacket.getReceiver();
+        UserBaseResponseInfoVO receiver = accountService.accountInfo(receiverId);
+        Assert.notNull(receiver, "红包接受账号不存在");
+        if (RoleCodeEnum.isUserRole(receiver.getRoleCode())) {
+            Assert.isTrue(receiver.getUserId().equals(userId), "没有拆包权限");
+        } else if (RoleCodeEnum.GROUP.getCode().equals(receiver.getRoleCode())) {
+            boolean isGroupUser = accountService.isSubscribe(userId, receiverId, AccountRelationTypeEnum.USER_GROUP);
+            Assert.isTrue(isGroupUser, "没有拆包权限");
+        }
+
+        /**
+         * 3. 校验当前用户是否拆过红包(每个人只能拆一次)
+         */
+        boolean isOpen = this.isOpenRedPacket(redPacket.getId(), userId);
+        Assert.isTrue(!isOpen, "重复拆分");
+    }
+
+    @Override
+    public int open(Long redPacketId, String userId) {
+
+        RedPacketBaseVO redPacket = redPacketService.getRedPacket(redPacketId);
+        /**
+         * 1. 校验红包拆分权限
+         * 包含:红包有效、拆包权限 ……
+         */
+        this.validateOpenPermission(redPacket, userId);
+
+        /**
+         * 2. 获得拆包资格,拆红包
+         */
+        int amount = this.openRedPacket(redPacket, userId);
+
+        /**
+         * 3. 调用支付平台(webchat-pay 微服务,完成用户拆包金额入账)
+         */
+
+        /**
+         * 4. 业务侧持久化拆分数据
+         */
+
+        return amount;
+    }
+
+    public boolean updateRedPacketStatus(Long redPacketId, RedPacketConstants.RedPacketStatus status) {
+
+        return redPacketService.updateRedPacketStatus(redPacketId, status);
+    }
+
+    /**
+     * 添加{userId}加入到红包拆包用户redis缓存set集合
+     * @param redPacketId
+     * @param userId
+     */
+    public void addUser2OpenRedPacketUsersCache(Long redPacketId, String userId) {
+        String cacheKey = RedisKeyEnum.RED_PACKET_OPEN_USERS.getKey(String.valueOf(redPacketId));
+        redisService.sadd(cacheKey, userId);
+        redisService.expire(cacheKey, RedisKeyEnum.RED_PACKET_OPEN_USERS.getExpireTime());
+    }
+
+    /**
+     * 判断{userId}是否拆分过红包{redPacketId}
+     * @param redPacketId
+     * @param userId
+     * @return
+     */
+    private boolean isOpenRedPacket(Long redPacketId, String userId) {
+        String cacheKey = RedisKeyEnum.RED_PACKET_OPEN_USERS.getKey(String.valueOf(redPacketId));
+        return redisService.sIsMember(cacheKey, userId);
+    }
+
+    /**
+     * 刷新红包状态
+     *
+     * @param redPacketId
+     * @param status
+     */
+    protected void updateRedPacketStatusCache(Long redPacketId, RedPacketConstants.RedPacketStatus status) {
+
+        redPacketService.updateRedPacketStatus(redPacketId, status);
+    }
+}

+ 51 - 0
webchat-ugc/src/main/java/com/webchat/ugc/service/redpacket/OpenRedPacketWithFactory.java

@@ -0,0 +1,51 @@
+package com.webchat.ugc.service.redpacket;
+
+
+import com.webchat.common.constants.RedPacketConstants;
+import com.webchat.common.exception.BusinessException;
+import com.webchat.common.util.SpringContextUtil;
+import jakarta.annotation.PostConstruct;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 红包拆分实现方案服务工厂
+ */
+@Component
+public class OpenRedPacketWithFactory implements InitializingBean, ApplicationContextAware {
+
+    private ApplicationContext applicationContext;
+
+    public static Map<String, AbstractOpenRedPacketService> services = new ConcurrentHashMap<>();
+
+    @Override
+    public void afterPropertiesSet() throws Exception {
+        this.initServices();
+    }
+
+    @Override
+    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
+        this.applicationContext = applicationContext;
+    }
+
+    public void initServices() {
+
+        services.put(RedPacketConstants.OpenRedPacketWithEnum.LUA.name(), applicationContext.getBean(OpenRedPacketWithLuaService.class));
+        services.put(RedPacketConstants.OpenRedPacketWithEnum.LOCK.name(), applicationContext.getBean(OpenRedPacketWithRedissonLockService.class));
+        services.put(RedPacketConstants.OpenRedPacketWithEnum.QUEUE.name(), applicationContext.getBean(OpenRedPacketWithQueueService.class));
+    }
+
+    public static AbstractOpenRedPacketService getService(String type) {
+        AbstractOpenRedPacketService abstractOpenRedPacketService = services.get(type);
+        if (abstractOpenRedPacketService == null) {
+            throw new BusinessException("不支持的红包拆分服务");
+        }
+        return abstractOpenRedPacketService;
+    }
+}

+ 75 - 0
webchat-ugc/src/main/java/com/webchat/ugc/service/redpacket/OpenRedPacketWithLuaService.java

@@ -0,0 +1,75 @@
+package com.webchat.ugc.service.redpacket;
+
+import com.webchat.common.constants.RedPacketConstants;
+import com.webchat.common.enums.RedisKeyEnum;
+import com.webchat.common.exception.BusinessException;
+import com.webchat.domain.vo.response.RedPacketBaseVO;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.data.redis.core.script.RedisScript;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+/**
+ * 方案二:自定义lua脚本由redis底层命令单线程执行来保证并发安全
+ */
+@Service
+public class OpenRedPacketWithLuaService extends AbstractOpenRedPacketService {
+
+
+    private static final String OPEN_LUA_SCRIPT =
+            "-- 1. 接收execute参数" +
+            "local amountKey = KEYS[1] -- 红包余额缓存KEY" +
+            "local gradCountKey = KEYS[2] -- 红包剩余可以拆分份额缓存KEY" +
+            "local openUsersKey = KEYS[3] -- 红包余额缓存KEY" +
+            "local redPacketId = ARGV[1]  -- 红包ID" +
+            "local userId = ARGV[2]       -- 拆包ID" +
+            "-- 2. 校验是否由拆包资格" +
+            "local balanceAmount = tonumber(redis.call('HGET', amountKey, redPacketId)) or 0 " +
+            "if balanceAmount <= 0 " +
+            "   return '' " +
+            "local gradCount = tonumber(redis.call('HGET', gradCountKey, redPacketId)) or 0 " +
+            "if gradCount <= 0 " +
+            "   return '' " +
+            "-- 3. 当前剩余可拆分份额为1分,直接返回剩余金额" +
+            "if gradCount == 1 then "+
+            "   redis.call('HSET', amountKey, redPacketId, 0) "+
+            "   redis.call('HSET', gradCountKey, redPacketId, 0) "+
+            "   redis.call('SADD', openUsersKey, userId) "+
+            "   return tostring(balanceAmount) "+
+            "-- 4. 基于二倍均值红包算法,为当前拆包线程随机计算红包金额"+
+            "local minAmount = 1 " +
+            "local avgAmount = math.floor(balanceAmount / gradCount) " +
+            "local avgAmount2 = avgAmount * 2 " +
+            "local openAmount = math.random(minAmount, avgAmount2 + 1) " +
+            "   redis.call('HSET', amountKey, redPacketId, balanceAmount - openAmount ) "+
+            "   redis.call('HSET', gradCountKey, redPacketId, gradCount - 1) "+
+            "   redis.call('SADD', openUsersKey, userId) "+
+            "   return tostring(openAmount) "
+            ;
+
+    @Override
+    public int openRedPacket(RedPacketBaseVO redPacket, String userId) {
+        Long redPacketId = redPacket.getId();
+
+        List<String> keys = new ArrayList<>();
+        // KEY1:红包剩余可拆分金额
+        keys.add(RedisKeyEnum.RED_PACKET_BALANCE_COUNT.getKey());
+        // KEY2:红包剩余可拆分份额
+        keys.add(RedisKeyEnum.RED_PACKET_GRAD_COUNT.getKey());
+        // KEY3:红包对应拆分用户名单缓存
+        keys.add(RedisKeyEnum.RED_PACKET_OPEN_USERS.getKey(String.valueOf(redPacketId)));
+
+        String amount = redisService.executeScript(
+                RedisScript.of(OPEN_LUA_SCRIPT, String.class),
+                keys,
+                redPacketId, userId);
+        if (StringUtils.isBlank(amount)) {
+            super.updateRedPacketStatus(redPacketId, RedPacketConstants.RedPacketStatus.END);
+            throw new BusinessException("来晚了");
+        }
+       return Integer.valueOf(amount);
+    }
+}

+ 85 - 0
webchat-ugc/src/main/java/com/webchat/ugc/service/redpacket/OpenRedPacketWithQueueService.java

@@ -0,0 +1,85 @@
+package com.webchat.ugc.service.redpacket;
+
+import com.webchat.common.constants.RedPacketConstants;
+import com.webchat.common.enums.RedisKeyEnum;
+import com.webchat.common.exception.BusinessException;
+import com.webchat.domain.vo.response.RedPacketBaseVO;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ThreadLocalRandom;
+
+
+/**
+ * 方案三:预先计算好每分红包金额,走队列一次获取
+ */
+@Service
+public class OpenRedPacketWithQueueService extends AbstractOpenRedPacketService {
+
+
+    @Override
+    public int openRedPacket(RedPacketBaseVO redPacket, String userId) {
+
+        Long redPacketId = redPacket.getId();
+        String queueKey = this.redPacketAmountQueueCacheKey(redPacket.getId());
+        // 走队列获取预先计算好的红包金额,因为redis的单线程命令执行特性,所以这里是并发安全的
+        String amount = redisService.lrightPop(queueKey);
+        if (StringUtils.isBlank(amount)) {
+            // 红包被拆分完
+            super.updateRedPacketStatus(redPacketId, RedPacketConstants.RedPacketStatus.END);
+            throw new BusinessException("来晚了,红包已拆完");
+        }
+        int amountVal = Integer.valueOf(amount);
+        // 添加用户到红包拆分人员名单缓存
+        addUser2OpenRedPacketUsersCache(redPacketId, userId);
+        return amountVal;
+    }
+
+    public List<Integer> preGeneratedRedPackets(Long redPacketId, int totalMoneyFen, int gradCount) {
+        List<Integer> redPackets = new ArrayList<>();
+        // 递归生成红包金额
+        this.doRecursivePreGenerated(totalMoneyFen, gradCount, redPackets);
+        // 加入队列
+        String queueKey = this.redPacketAmountQueueCacheKey(redPacketId);
+        redisService.lleftPushAll(queueKey, redPackets);
+        return redPackets;
+    }
+
+    private String redPacketAmountQueueCacheKey(Long redPacketId) {
+
+        return RedisKeyEnum.QUEUE_RED_PACKET_AMOUNT.getKey(String.valueOf(redPacketId));
+    }
+
+
+    private void doRecursivePreGenerated(int balanceMoneyFen, int gradCountBalance, List<Integer> redPackets) {
+
+        if (gradCountBalance == 1) {
+            // 跳出条件
+            redPackets.add(balanceMoneyFen);
+            return;
+        }
+
+        // 二倍均值
+        BigDecimal avgAmount = new BigDecimal(balanceMoneyFen).divide(new BigDecimal(gradCountBalance), 0, BigDecimal.ROUND_HALF_UP);
+        BigDecimal avgAmount2 = avgAmount.multiply(new BigDecimal(2));
+        int maxVal = avgAmount2.intValue();
+        // 在[minVal, maxVal]之间随机一个红包金额:openAmount
+        int openAmount = ThreadLocalRandom.current().nextInt(RedPacketConstants.MIN_AMOUNT, maxVal + 1);
+        redPackets.add(openAmount);
+
+        doRecursivePreGenerated(balanceMoneyFen - openAmount, gradCountBalance - 1, redPackets);
+    }
+
+    public static void main(String[] args) {
+        List<Integer> redPackets = new ArrayList<>();
+        int gradCount = 10;
+        // 设置红包金额(分)
+        int totalMoneyFen = new BigDecimal("100").multiply(new BigDecimal(100)).intValue();
+        // 递归生成红包金额
+//        doRecursivePreGenerated(totalMoneyFen, gradCount, redPackets);
+        System.out.printf("预先计算好红包:" + redPackets);
+    }
+}

+ 202 - 0
webchat-ugc/src/main/java/com/webchat/ugc/service/redpacket/OpenRedPacketWithRedissonLockService.java

@@ -0,0 +1,202 @@
+package com.webchat.ugc.service.redpacket;
+
+import com.webchat.common.constants.RedPacketConstants;
+import com.webchat.common.enums.RedisKeyEnum;
+import com.webchat.domain.vo.response.RedPacketBaseVO;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.Assert;
+
+import java.math.BigDecimal;
+import java.util.concurrent.ThreadLocalRandom;
+
+
+/**
+ * 方案一:基于redisson分布式锁实现
+ *
+ * 注意:
+ * 这里红包剩余金额以及剩余可拆分份额,完全依赖redis(最好缓存失效可以查询保证一致性)
+ */
+@Service
+public class OpenRedPacketWithRedissonLockService extends AbstractOpenRedPacketService {
+
+    @Autowired
+    private RedissonClient redissonClient;
+
+    /**
+     * 采用实时计算红包金额方式
+     *
+     * @param redPacket
+     * @param userId
+     * @return
+     */
+    @Override
+    public int openRedPacket(RedPacketBaseVO redPacket, String userId) {
+
+        String lockKey = RedisKeyEnum.LOCK_OPEN_RED_PACKET.getKey(String.valueOf(redPacket.getId()));
+        RLock lock = redissonClient.getLock(lockKey);
+
+        try {
+            // 阻塞等待获取锁
+            lock.lock();
+            return doOpenRedPacket(redPacket, userId);
+        } catch (Exception e) {
+            throw e;
+        } finally {
+            if (lock.isLocked() && lock.isHeldByCurrentThread()) {
+                lock.unlock();
+            }
+        }
+    }
+
+    /**
+     * 红包拆分
+     *
+     * @param redPacket
+     * @param userId
+     * @return
+     */
+    private int doOpenRedPacket(RedPacketBaseVO redPacket, String userId) {
+        Long redPacketId = redPacket.getId();
+        /**
+         * 1. 读取当前红包剩余金额
+         */
+        int balanceAmount = this.getBalance(redPacketId);
+        Assert.isTrue(balanceAmount > 0, "红包被瓜分完了");
+
+        /**
+         * 2. 获取红包当前剩余可拆分份额
+         */
+        int gradCount = this.getGradCount(redPacketId);
+        Assert.isTrue(gradCount > 0, "来晚了");
+
+        /**
+         * 3. 判断红包类型(算法)
+         * 3.1 普通红包:非最后一个拆分,平均瓜分(总金额 / 总人数);最后一个拆分取剩余金额
+         * 3.2 拼手气:”二倍均值算法“ 为当前用户随机红包金额
+         *
+         * 二倍均值红包算法:
+         * 示例:100元红包、份10
+         * 1️⃣ 100元 ===> 10000分,保证所有金额为整数(因为我们小数点精确到后两位)
+         * 2️⃣ 第1份红包:
+         *    均值AVG = 余额 / 剩余份额
+         *    2倍均值= 2 * 均值AVG
+         *    用于随机生成红包金额范围:[0.01元/1分钱 ~ 2 * 均值AVG] --> randomAmount(1050分)
+         *    剩余:9份,余额 10000 - 1050 = 8950
+         * 3️⃣ 第2份红包:
+         *    均值AVG = 8950分 / 9
+         *    randomAmount(1566分)
+         *    剩余:8份,余额 8950分 - 1566
+         *
+         * ……
+         * 最后一次:
+         *    randomAmount = 10000分 - 历史总拆分金额
+         *    剩余:0份,余额 0
+         *
+         */
+        // 单位为分
+        int openAmount = 0;
+        if (gradCount == 1) {
+            // 最后一个可拆分名额
+            openAmount = balanceAmount;
+        } else {
+            // 二倍均值
+            BigDecimal avgAmount = new BigDecimal(balanceAmount).divide(new BigDecimal(gradCount), 0, BigDecimal.ROUND_HALF_UP);
+            BigDecimal avgAmount2 = avgAmount.multiply(new BigDecimal(2));
+            int maxVal = avgAmount2.intValue();
+            // 在[minVal, maxVal]之间随机一个红包金额:openAmount
+            openAmount = ThreadLocalRandom.current().nextInt(RedPacketConstants.MIN_AMOUNT, maxVal + 1);
+        }
+        /**
+         * 4. 写刷新红包剩余金额
+         */
+        balanceAmount = balanceAmount - openAmount;
+        // 刷新当前红包剩余可拆分金额
+        this.setBalance(redPacketId, balanceAmount);
+        // 红包剩余可拆分次数原子-1
+        this.increxGradCountCache(redPacketId, -1);
+        // 将当前用户加入红包拆分用户名单缓存
+        super.addUser2OpenRedPacketUsersCache(redPacketId, userId);
+        if (balanceAmount == 0) {
+            // 红包被拆分完
+            super.updateRedPacketStatus(redPacketId, RedPacketConstants.RedPacketStatus.END);
+        }
+        return openAmount;
+    }
+
+    /**
+     * 这里余额为啥是整数?
+     * 因为我们会讲所用 单位元 转化为 分,同时保证元的小数点精确到后两位。
+     *
+     * @return 单位:分,1块钱,这里返回100
+     */
+    private Integer getBalance(Long redPacketId) {
+        String balanceKey = RedisKeyEnum.RED_PACKET_BALANCE_COUNT.getKey();
+        String balanceCache = redisService.hget(balanceKey, String.valueOf(redPacketId));
+        return Integer.valueOf(balanceCache);
+    }
+
+
+    /**
+     * 刷新红包剩余可拆分金额
+     *
+     * @param redPacketId
+     * @param balance 单位需要转为分
+     */
+    private void setBalance(Long redPacketId, Integer balance) {
+        String balanceKey = RedisKeyEnum.RED_PACKET_BALANCE_COUNT.getKey();
+        redisService.hset(balanceKey, String.valueOf(redPacketId), String.valueOf(balance),
+                RedisKeyEnum.RED_PACKET_BALANCE_COUNT.getExpireTime());
+    }
+
+    /**
+     * 当前已瓜分数量
+     *
+     * @param redPacketId
+     * @return
+     */
+    private Integer getGradCount(Long redPacketId) {
+        String balanceKey = RedisKeyEnum.RED_PACKET_GRAD_COUNT.getKey();
+        String countCache = redisService.hget(balanceKey, String.valueOf(redPacketId));
+        return Integer.valueOf(countCache);
+    }
+
+    /**
+     *
+     *
+     *
+     * @param redPacketId
+     * @return
+     */
+    private void increxGradCountCache(Long redPacketId, long count) {
+        String balanceKey = RedisKeyEnum.RED_PACKET_GRAD_COUNT.getKey();
+        redisService.hIncrementVal(balanceKey, String.valueOf(redPacketId), count);
+    }
+
+    /**
+     * 初始化红包剩余可拆分金额
+     *
+     * @param redPacketId
+     * @param balance 单位需要转为分
+     */
+    public void initBalanceCache(Long redPacketId, Integer balance) {
+        String balanceKey = RedisKeyEnum.RED_PACKET_BALANCE_COUNT.getKey();
+        redisService.hset(balanceKey, String.valueOf(redPacketId), String.valueOf(balance),
+                RedisKeyEnum.RED_PACKET_BALANCE_COUNT.getExpireTime());
+    }
+
+    /**
+     *  初始化红包剩余可以瓜分数量
+     *
+     * @param redPacketId
+     * @return
+     */
+    public void initGradCountCache(Long redPacketId, int gradCount) {
+        String balanceKey = RedisKeyEnum.RED_PACKET_GRAD_COUNT.getKey();
+        redisService.hset(balanceKey, String.valueOf(redPacketId), gradCount + "",
+                RedisKeyEnum.RED_PACKET_GRAD_COUNT.getExpireTime());
+    }
+
+}

+ 14 - 0
webchat-ugc/src/main/java/com/webchat/ugc/service/redpacket/RedPacketOpenInter.java

@@ -0,0 +1,14 @@
+package com.webchat.ugc.service.redpacket;
+
+
+public interface RedPacketOpenInter {
+
+    /**
+     * 红包拆分
+     *
+     * @param redPacketId 红包ID
+     * @param userId      当前拆包人
+     * @return
+     */
+    int open(Long redPacketId, String userId);
+}

+ 344 - 0
webchat-ugc/src/main/java/com/webchat/ugc/service/redpacket/RedPacketService.java

@@ -0,0 +1,344 @@
+package com.webchat.ugc.service.redpacket;
+
+import com.webchat.common.constants.RedPacketConstants;
+import com.webchat.common.enums.ChatMessageTypeEnum;
+import com.webchat.common.enums.RedisKeyEnum;
+import com.webchat.common.enums.messagequeue.MessageBroadChannelEnum;
+import com.webchat.common.enums.payment.PaymentTransEventEnum;
+import com.webchat.common.enums.payment.PaymentTransTypeEnum;
+import com.webchat.common.exception.BusinessException;
+import com.webchat.common.service.RedisService;
+import com.webchat.common.service.messagequeue.producer.MessageQueueProducer;
+import com.webchat.common.util.DateUtils;
+import com.webchat.common.util.JsonUtil;
+import com.webchat.domain.dto.messagecard.MessageCardSendDTO;
+import com.webchat.domain.dto.payment.PaymentOrderCreateDTO;
+import com.webchat.domain.dto.payment.PaymentTransRequestDTO;
+import com.webchat.domain.vo.request.SendRedPacketRequestVO;
+import com.webchat.domain.vo.request.mess.ChatMessageRequestVO;
+import com.webchat.domain.vo.request.mess.MessageBaseVO;
+import com.webchat.domain.vo.response.RedPacketBaseVO;
+import com.webchat.domain.vo.response.RedPacketDetailVO;
+import com.webchat.ugc.config.MessageCardAccountConfig;
+import com.webchat.ugc.config.MessageCardTemplateConfig;
+import com.webchat.ugc.repository.dao.IRedPacketDAO;
+import com.webchat.ugc.repository.dao.IRedPacketRecordDAO;
+import com.webchat.ugc.repository.entity.RedPacketEntity;
+import com.webchat.ugc.service.PaymentService;
+import org.apache.commons.lang3.StringUtils;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.cloud.context.config.annotation.RefreshScope;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.Assert;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+@RefreshScope
+@Service
+public class RedPacketService {
+
+    @Autowired
+    private IRedPacketDAO redPacketDAO;
+    @Autowired
+    private IRedPacketRecordDAO redPacketRecordDAO;
+    @Autowired
+    private RedisService redisService;
+    @Autowired
+    private PaymentService paymentService;
+    @Autowired
+    private MessageQueueProducer<Object, Long> messageQueueProducer;
+    @Autowired
+    private MessageCardAccountConfig messageCardAccountConfig;
+    @Autowired
+    private MessageCardTemplateConfig messageCardTemplateConfig;
+    @Autowired
+    private RedissonClient redissonClient;
+
+    @Value("${red-packet.open-with}")
+    private String openWith;
+
+
+    /**
+     * 发送包
+     *
+     * @param sendRedPacketRequest
+     * @return
+     */
+    @Transactional
+    public Long send(SendRedPacketRequestVO sendRedPacketRequest) {
+
+        sendRedPacketRequest.setExpireDate(this.getRedPacketExpireDate());
+        String sendUserId = sendRedPacketRequest.getSender();
+        BigDecimal redPacketAmount = sendRedPacketRequest.getTotalMoney();
+        /**
+         * 1. 校验发包人账户余额
+         */
+        paymentService.validateAccountBalance(sendUserId, redPacketAmount);
+        /**
+         * 2. 申请分布式交易事务id, 申请创建一条交易订单
+         */
+        String transId = paymentService.transId();
+        // 构造支付平台创建订单所需业务参数
+        PaymentOrderCreateDTO paymentOrderCreateDTO = this.buildPaymentOrderCreateDTO(sendRedPacketRequest);
+        String orderId = paymentService.createOrder(transId, paymentOrderCreateDTO);
+        /**
+         * 3. 扣除发包人账户余额(实际交易)
+         */
+        // 构造实际交易请求参数
+        PaymentTransRequestDTO paymentTransRequest = this.buildPaymentTransRequestDTO(orderId, sendRedPacketRequest);
+        boolean transResult = paymentService.doTrans(paymentTransRequest);
+        if (!transResult) {
+            // 回滚交易订单
+            paymentService.doRollBack(transId);
+            throw new BusinessException("发包发送失败,稍后重试!");
+        }
+        try {
+            /**
+             * 4. 持久化红包发送记录
+             */
+            RedPacketEntity redPacketEntity = this.buildRedPacketEntity(orderId, sendRedPacketRequest);
+            redPacketEntity = redPacketDAO.save(redPacketEntity);
+            /**
+             * 5. 刷新红包详情redis缓存
+             * 后续红包的信息要在IM客户端展示,为了保证后续请求性能,我们现将红包详情信息做一层redis缓存
+             */
+            this.refreshRedPacketDetailCache(redPacketEntity);
+            this.initOpenRedPacketContextInfo(redPacketEntity);
+            /**
+             * 6. MQ广播通知websocket推送红包消息给接收人
+             */
+            MessageBaseVO<RedPacketBaseVO> chatMessage = this.buildMessageVOForSendRedPacket(redPacketEntity);
+            messageQueueProducer.broadSend(MessageBroadChannelEnum.QUEUE_RED_PACKET_NOTIFY, chatMessage);
+            /**
+             * 7. 走“WebChat支付这个服务号推送用户消息认证”
+             */
+            this.doPushRedPacketSendPaymentOrderMessage(sendRedPacketRequest);
+            return redPacketEntity.getId();
+        } catch (Exception e) {
+            // 交易成功,订单创建成功,但是业务侧处理失败,我们需要回滚用户交易订单
+            // 回滚交易订单
+            paymentService.doRollBack(transId);
+            // 回滚redis数据
+            // TODO
+        }
+        return null;
+    }
+
+
+    private void initOpenRedPacketContextInfo(RedPacketEntity redPacketEntity) {
+
+        Long redPacketId = redPacketEntity.getId();
+        int gradCount = redPacketEntity.getCount();
+        // 设置红包金额(元)
+        BigDecimal totalMoneyYun = redPacketEntity.getTotalMoney();
+        // 设置红包金额(分)
+        int totalMoneyFen = totalMoneyYun.multiply(new BigDecimal(100)).intValue();
+        if (RedPacketConstants.OpenRedPacketWithEnum.LOCK.name().equals(openWith) ||
+                RedPacketConstants.OpenRedPacketWithEnum.LUA.name().equals(openWith)) {
+            // 初始化红包金额
+            OpenRedPacketWithRedissonLockService openRedPacketService =
+                    (OpenRedPacketWithRedissonLockService) OpenRedPacketWithFactory.getService(
+                            RedPacketConstants.OpenRedPacketWithEnum.LOCK.name());
+            openRedPacketService.initBalanceCache(redPacketId, totalMoneyFen);
+            openRedPacketService.initGradCountCache(redPacketId, gradCount);
+        } else if (RedPacketConstants.OpenRedPacketWithEnum.QUEUE.name().equals(openWith)) {
+            // 预先生成好红包份额对应的金额
+            OpenRedPacketWithQueueService openRedPacketService =
+                    (OpenRedPacketWithQueueService) OpenRedPacketWithFactory.getService(
+                            RedPacketConstants.OpenRedPacketWithEnum.QUEUE.name());
+            openRedPacketService.preGeneratedRedPackets(redPacketId, totalMoneyFen, gradCount);
+        }
+    }
+
+    /**
+     * 更新红包的状态
+     *
+     * @param redPacketId
+     * @param redPacketStatus
+     * @return
+     */
+    @Transactional
+    public boolean updateRedPacketStatus(Long redPacketId, RedPacketConstants.RedPacketStatus redPacketStatus) {
+
+        RedPacketEntity entity = redPacketDAO.findById(redPacketId).orElse(null);
+        Assert.notNull(entity, "红包状态更新失败:红包不存在!");
+        entity.setStatus(redPacketStatus.getStatus());
+        try {
+            redPacketDAO.save(entity);
+            this.refreshRedPacketDetailCache(entity);
+        } catch (Exception e) {
+            throw new BusinessException("红包状态更新失败:redis缓存数据刷新异常!");
+        }
+        return true;
+    }
+
+    /**
+     * 红包拆分
+     *
+     * @param redPacketId
+     * @param userId
+     * @return
+     */
+    public String open(Long redPacketId, String userId) {
+        int amount = OpenRedPacketWithFactory.getService(openWith)
+                                .open(redPacketId, userId);
+        return new BigDecimal(amount).movePointLeft(2).setScale(2, RoundingMode.HALF_UP).toString();
+    }
+
+    /**
+     * 红包发送成功,走webchtaPay服务号推送交易凭证消息卡片
+     *------------------
+     * @param requestVO
+     */
+    private void doPushRedPacketSendPaymentOrderMessage(SendRedPacketRequestVO requestVO) {
+
+        MessageCardSendDTO messageCardSendDTO = new MessageCardSendDTO();
+        messageCardSendDTO.setSender(messageCardAccountConfig.getPayment());
+        messageCardSendDTO.setTemplateId(messageCardTemplateConfig.getRedPacketSend());
+        messageCardSendDTO.setReceiver(requestVO.getSender());
+        messageCardSendDTO.setTitle("扣费凭证");
+        messageCardSendDTO.setLogo("https://coderutil.oss-cn-beijing.aliyuncs.com/bbs-image/file_c3a791ab87c5419b980bc27d9791bab6.png");
+        messageCardSendDTO.setRedirectName("查看交易明细");
+        messageCardSendDTO.setRedirectUrl("https://www.coderutil.com");
+        Map<String, Object> vars = new HashMap<>();
+        vars.put("amountTitle", "扣费金额");
+        vars.put("amount", requestVO.getTotalMoney().toString());
+        vars.put("tip", "红包发送,账户变更");
+        vars.put("k1", "扣费服务");
+        vars.put("k2", "支付方式");
+        vars.put("v1", "红包服务");
+        vars.put("v2", "WebChatPay线上支付");
+        messageCardSendDTO.setVars(vars);
+        /**
+         * MQ广播通知connect服务,完成服务号消息卡内容websocket主动推送
+         */
+        messageQueueProducer.broadSend(MessageBroadChannelEnum.QUEUE_MESSAGE_CARD_PUSH, messageCardSendDTO);
+    }
+
+    private MessageBaseVO<RedPacketBaseVO> buildMessageVOForSendRedPacket(RedPacketEntity redPacketEntity) {
+        MessageBaseVO<RedPacketBaseVO> chatMessage = new ChatMessageRequestVO();
+        RedPacketBaseVO redPacketBaseVO = new RedPacketBaseVO();
+        chatMessage.setSenderId(redPacketEntity.getSender());
+        chatMessage.setReceiverId(redPacketEntity.getReceiver());
+        chatMessage.setType(ChatMessageTypeEnum.RED_PACKET.getType());
+        chatMessage.setMessageExt(redPacketBaseVO);
+        redPacketBaseVO.setId(redPacketEntity.getId());
+        redPacketBaseVO.setCover(redPacketEntity.getCover());
+        redPacketBaseVO.setBlessing(redPacketEntity.getBlessing());
+        return chatMessage;
+    }
+
+
+    private RedPacketDetailVO refreshRedPacketDetailCache(Long redPacketId) {
+        RedPacketEntity entity = redPacketDAO.findById(redPacketId).orElse(null);
+        return this.refreshRedPacketDetailCache(entity);
+    }
+
+    /**
+     * 刷新红包缓存
+     *
+     * @param redPacketEntity
+     * @return
+     */
+    private RedPacketDetailVO refreshRedPacketDetailCache(RedPacketEntity redPacketEntity) {
+        if (redPacketEntity == null) {
+            return null;
+        }
+        RedPacketDetailVO redPacketDetailVO = new RedPacketDetailVO();
+        BeanUtils.copyProperties(redPacketEntity, redPacketDetailVO);
+        redPacketDetailVO.setSendTime(redPacketEntity.getCreateDate().getTime());
+        redPacketDetailVO.setExpireTime(redPacketEntity.getExpireDate().getTime());
+        // 缓存红包详情
+        String cacheKey = this.redPacketCacheKey(redPacketEntity.getId());
+        redisService.set(cacheKey, JsonUtil.toJsonString(redPacketDetailVO),
+                RedisKeyEnum.RED_PACKET_DETAIL_CACHE.getExpireTime());
+        return redPacketDetailVO;
+    }
+
+
+    public RedPacketBaseVO getRedPacket(Long redPacketId) {
+
+        String cacheKey = this.redPacketCacheKey(redPacketId);
+        String cache = redisService.get(cacheKey);
+        if (StringUtils.isNotBlank(cache)) {
+            return JsonUtil.fromJson(cache, RedPacketBaseVO.class);
+        }
+        /**
+         * 虽然红包缓存设置的2 * 24小时,且红包24小时内有效,但是不能完全依赖redis
+         * 如果缓存失效,需要主动刷新
+         */
+        String refreshRedPacketCacheKey = RedisKeyEnum.LOCK_REFRESH_RED_PACKET_CACHE.getKey(String.valueOf(redPacketId));
+        RLock lock = redissonClient.getLock(refreshRedPacketCacheKey);
+        try {
+            lock.lock();
+            cache = redisService.get(cacheKey);
+            if (StringUtils.isNotBlank(cache)) {
+                return JsonUtil.fromJson(cache, RedPacketBaseVO.class);
+            }
+            return this.refreshRedPacketDetailCache(redPacketId);
+        } catch (Exception e) {
+            // TODO
+        } finally {
+            if (lock.isLocked() && lock.isHeldByCurrentThread()) {
+                lock.unlock();
+            }
+        }
+        return null;
+    }
+
+    private String redPacketCacheKey(Long id) {
+
+        return RedisKeyEnum.RED_PACKET_DETAIL_CACHE.getKey(String.valueOf(id));
+    }
+
+    private RedPacketEntity buildRedPacketEntity(String orderId, SendRedPacketRequestVO sendRedPacketRequestVO) {
+
+        RedPacketEntity entity = new RedPacketEntity();
+        BeanUtils.copyProperties(sendRedPacketRequestVO, entity);
+        entity.setOrderId(orderId);
+        entity.setStatus(RedPacketConstants.RedPacketStatus.RUNNING.getStatus());
+        return entity;
+    }
+
+    /**
+     * 构造创建交易订单业务参数
+     *
+     * @return
+     */
+    private PaymentOrderCreateDTO buildPaymentOrderCreateDTO(SendRedPacketRequestVO sendRedPacketRequest) {
+        PaymentOrderCreateDTO dto = new PaymentOrderCreateDTO();
+        dto.setAmount(sendRedPacketRequest.getTotalMoney());
+        dto.setSourceAccount(sendRedPacketRequest.getSender());
+        dto.setTargetAccount(sendRedPacketRequest.getReceiver());
+        dto.setEventType(PaymentTransEventEnum.RED_PACKET.getTransEvent());
+        dto.setExpireDate(sendRedPacketRequest.getExpireDate());
+        dto.setBillType(PaymentTransTypeEnum.EXPENSES.getTransType());
+        dto.setDescription("红包发送订单");
+        return dto;
+    }
+
+    private Date getRedPacketExpireDate() {
+        return DateUtils.addDays(new Date(), 1);
+    }
+
+    private PaymentTransRequestDTO buildPaymentTransRequestDTO(String orderId, SendRedPacketRequestVO sendRedPacketRequest) {
+        PaymentTransRequestDTO dto = new PaymentTransRequestDTO();
+        dto.setOrderId(orderId);
+        dto.setAmount(sendRedPacketRequest.getTotalMoney());
+        dto.setTransType(PaymentTransTypeEnum.EXPENSES.getTransType());
+        dto.setTransDetail("红包发送");
+        dto.setTransEvent(PaymentTransEventEnum.RED_PACKET.getTransEvent());
+        dto.setSourceUserId(sendRedPacketRequest.getSender());
+        dto.setTargetUserId(sendRedPacketRequest.getReceiver());
+        return dto;
+    }
+}

+ 13 - 2
webchat-user/src/main/java/com/webchat/user/controller/UserServiceController.java

@@ -3,7 +3,7 @@ package com.webchat.user.controller;
 import com.webchat.common.bean.APIPageResponseBean;
 import com.webchat.common.bean.APIResponseBean;
 import com.webchat.common.bean.APIResponseBeanUtil;
-import com.webchat.domain.vo.request.CreatePublicAccountRequestVO;
+import com.webchat.domain.vo.request.CreateAccountRequestVO;
 import com.webchat.domain.vo.request.CreateRobotRequestVO;
 import com.webchat.domain.vo.request.UserRegistryInfoRequestVO;
 import com.webchat.domain.vo.response.UserBaseResponseInfoVO;
@@ -43,6 +43,11 @@ public class UserServiceController implements UserServiceClient {
         return APIResponseBeanUtil.success(userService.getAllSubscriberByAccount(relationType, account));
     }
 
+    @Override
+    public APIResponseBean<Boolean> isSubscribe(Integer relationType, String userAccount, String account) {
+
+        return APIResponseBeanUtil.success(userService.isSubscribe(relationType, userAccount, account));
+    }
 
     @Override
     public APIResponseBean<UserBaseResponseVO> userBaseInfo(String mobile) {
@@ -69,12 +74,18 @@ public class UserServiceController implements UserServiceClient {
     }
 
     @Override
-    public APIResponseBean createPublicAccount(@RequestBody CreatePublicAccountRequestVO requestPram) {
+    public APIResponseBean createPublicAccount(@RequestBody CreateAccountRequestVO requestPram) {
         userService.createPublicAccount(requestPram);
         return APIResponseBeanUtil.success("创建成功");
     }
 
     @Override
+    public APIResponseBean createServerAccount(@RequestBody CreateAccountRequestVO requestPram) {
+        userService.createServerAccount(requestPram);
+        return APIResponseBeanUtil.success("创建成功");
+    }
+
+    @Override
     public APIResponseBean<Map<String, UserBaseResponseInfoVO>> batchGet(@RequestBody Set<String> userIds) {
         Map<String, UserBaseResponseInfoVO> map = userService.batchGet(userIds);
         return APIResponseBeanUtil.success(map);

+ 76 - 4
webchat-user/src/main/java/com/webchat/user/service/UserService.java

@@ -13,7 +13,7 @@ import com.webchat.common.helper.SessionHelper;
 import com.webchat.common.service.RedisService;
 import com.webchat.common.util.*;
 import com.webchat.domain.vo.request.CreateGroupRequestVO;
-import com.webchat.domain.vo.request.CreatePublicAccountRequestVO;
+import com.webchat.domain.vo.request.CreateAccountRequestVO;
 import com.webchat.domain.vo.request.CreateRobotRequestVO;
 import com.webchat.domain.vo.request.UpdateUserInfoRequestVO;
 import com.webchat.domain.vo.response.GroupResponseInfoVO;
@@ -27,6 +27,7 @@ import com.webchat.user.repository.entity.UserEntity;
 import com.webchat.user.service.relation.AccountRelationFactory;
 import com.webchat.user.service.relation.User2AIAgentSenderAccountRelationService;
 import com.webchat.user.service.relation.User2GroupAccountRelationService;
+import com.webchat.user.service.relation.User2ServerAccountRelationService;
 import jakarta.persistence.criteria.Predicate;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
@@ -77,6 +78,7 @@ public class UserService {
     private static final String ROBOT_ID_PREFIX = "R";
     private static final String GROUP_ID_PREFIX = "G";
     private static final String PUBLIC_ACCOUNT_ID_PREFIX = "P";
+    private static final String SERVER_ACCOUNT_ID_PREFIX = "S";
 
 
     public UserBaseResponseVO getUserBaseByMobile(String mobile) {
@@ -193,6 +195,7 @@ public class UserService {
         String uid = this.registryUser2DB(userName, photo, mobile, password);
         // 默认添加我的AI助手
         SpringContextUtil.getBean(User2AIAgentSenderAccountRelationService.class).subscribe(uid, WebConstant.AI_AGENT_ID);
+        SpringContextUtil.getBean(User2ServerAccountRelationService.class).subscribe(uid, WebConstant.WEBCHAT_PAY_ID);
         // 注册成功,事务结束后刷新用户缓存信息
         TransactionSyncManagerUtil.registerSynchronization(() -> {
             // 刷新用户缓存
@@ -272,7 +275,7 @@ public class UserService {
      * 复用用户信息表,账号角色为 PUBLIC_ACCOUNT
      */
     @Transactional
-    public void createPublicAccount(CreatePublicAccountRequestVO requestVO) {
+    public void createPublicAccount(CreateAccountRequestVO requestVO) {
         String account = requestVO.getAccount();
         String accountName = requestVO.getAccountName();
         String signature = requestVO.getSignature();
@@ -281,14 +284,35 @@ public class UserService {
         /**
          * 防快速点击
          */
-        String key = RedisKeyEnum.CREATE_PUBLIC_ACCOUNT_LIMIT.getKey();
-        boolean lockResult = redisService.installDistributedLock(key, WebConstant.CACHE_NONE, RedisKeyEnum.CREATE_ROBOT_LIMIT.getExpireTime());
+        String key = RedisKeyEnum.CREATE_ACCOUNT_LIMIT.getKey();
+        boolean lockResult = redisService.installDistributedLock(key, WebConstant.CACHE_NONE, RedisKeyEnum.CREATE_ACCOUNT_LIMIT.getExpireTime());
         Assert.isTrue(lockResult, "创建中请稍等!请勿频繁点击");
         // 创建机器人
         this.registryPublicAccount2DB(account, accountName, photo, signature, createUserId);
     }
 
     /**
+     * 创建服务号
+     *
+     * @param requestVO
+     */
+    @Transactional
+    public void createServerAccount(CreateAccountRequestVO requestVO) {
+        String account = requestVO.getAccount();
+        String accountName = requestVO.getAccountName();
+        String signature = requestVO.getSignature();
+        String photo = requestVO.getAccountPhoto();
+        String createUserId = requestVO.getCreateUserId();
+        /**
+         * 防快速点击
+         */
+        String key = RedisKeyEnum.CREATE_ACCOUNT_LIMIT.getKey();
+        boolean lockResult = redisService.installDistributedLock(key, WebConstant.CACHE_NONE, RedisKeyEnum.CREATE_ACCOUNT_LIMIT.getExpireTime());
+        Assert.isTrue(lockResult, "创建中请稍等!请勿频繁点击");
+        this.registryServerAccount2DB(account, accountName, photo, signature, createUserId);
+    }
+
+    /**
      * 查询用户加入的所有群聊
      *
      * @param userId
@@ -557,6 +581,41 @@ public class UserService {
     }
 
     /**
+     * 创建服务号
+     *
+     * @return
+     */
+    private String registryServerAccount2DB(String account, String accountName, String accountPhoto,
+                                            String accountSignature, String createUserId) {
+
+        UserEntity userEntity = userDAO.findByMobile(account);
+        Assert.isTrue(userEntity == null, "公众号已经存在");
+
+        userEntity = new UserEntity();
+        String userId = IDGenerateUtil.createId(SERVER_ACCOUNT_ID_PREFIX);
+        userEntity.setUserId(userId);
+        userEntity.setUserName(StringUtil.handleSpecialHtmlTag(accountName));
+        userEntity.setPhoto(accountPhoto);
+        userEntity.setMobile(account);
+        userEntity.setSignature(accountSignature);
+        userEntity.setStatus(UserStatusEnum.ENABLE.getStatus());
+        // 密码设置为用户ID, 本身也不支持登录
+        userEntity.setPassword(md5Pwd(account));
+        userEntity.setRoleCode(RoleCodeEnum.SERVER_ACCOUNT.getCode());
+        userEntity.setCreateBy(createUserId);
+        // 注册用户
+        userDAO.save(userEntity);
+        // 注册成功,事务结束后刷新用户缓存信息
+        TransactionSyncManagerUtil.registerSynchronization(() -> {
+            // 刷新用户缓存
+            this.refreshAndGetUserEntityFromCache(account);
+            // 刷新公众号文章列表
+            publicAccountService.refreshPublicAccountListCache();
+        });
+        return userId;
+    }
+
+    /**
      * 判断用户是否存在
      * @param mobile
      * @return
@@ -607,6 +666,19 @@ public class UserService {
     }
 
     /**
+     * 判断userAccount是否订阅account
+     *
+     * @param relationType
+     * @param userAccount
+     * @param account
+     * @return
+     */
+    public Boolean isSubscribe(Integer relationType, String userAccount, String account) {
+
+        return AccountRelationFactory.getServiceByType(relationType).isSubscribe(userAccount, account);
+    }
+
+    /**
      * 用户信息编辑
      *
      * @param updateUserInfoRequest

+ 13 - 0
webchat-user/src/main/java/com/webchat/user/service/relation/AbstractAccountRelationService.java

@@ -237,6 +237,19 @@ public abstract class AbstractAccountRelationService implements AccountRelationV
         return redisService.zreverseRange(cacheKey, 0, Integer.MAX_VALUE);
     }
 
+    @Override
+    public boolean isSubscribe(String userAccount, String account) {
+        Integer type = this.getRelationType().getType();
+        String prefix = "SAFE";
+        String cacheKey = this.getRelationListRedisKey(account, type);
+        String safeCacheKey = this.getRelationListRedisKey(prefix, account, type);
+        if (!redisService.exists(cacheKey) && !redisService.exists(safeCacheKey)) {
+            // 失效/没有好友关系, 刷新账号管理缓存列表
+            this.initAccountRelationListCache(account, type);
+            redisService.set(safeCacheKey, WebConstant.CACHE_NONE, 5 * 60);
+        }
+        return redisService.zIsExist(cacheKey, userAccount);
+    }
 
     /**
      * 添加targetAccount到sourceAccount的好友列表缓存

+ 3 - 2
webchat-user/src/main/java/com/webchat/user/service/relation/AccountRelationFactory.java

@@ -40,6 +40,7 @@ public class AccountRelationFactory implements InitializingBean, ApplicationCont
         services.put(RoleCodeEnum.GROUP.getCode(), applicationContext.getBean(User2GroupAccountRelationService.class));
         services.put(RoleCodeEnum.PUBLIC_ACCOUNT.getCode(), applicationContext.getBean(User2OfficialAccountRelationService.class));
         services.put(RoleCodeEnum.FILE_ACCOUNT.getCode(), applicationContext.getBean(User2AIAgentSenderAccountRelationService.class));
+        services.put(RoleCodeEnum.SERVER_ACCOUNT.getCode(), applicationContext.getBean(User2ServerAccountRelationService.class));
     }
 
     /**
@@ -55,8 +56,8 @@ public class AccountRelationFactory implements InitializingBean, ApplicationCont
                 applicationContext.getBean(User2OfficialAccountRelationService.class));
         serviceForType.put(AccountRelationTypeEnum.USER_ROBOT.getType(),
                 applicationContext.getBean(User2RobotAccountRelationService.class));
-        serviceForType.put(AccountRelationTypeEnum.USER_AI_AGENT.getType(),
-                applicationContext.getBean(User2AIAgentSenderAccountRelationService.class));
+        serviceForType.put(AccountRelationTypeEnum.USER_SERVER.getType(),
+                applicationContext.getBean(User2ServerAccountRelationService.class));
     }
 
     public static AbstractAccountRelationService getServiceByCode(Integer roleCode) {

+ 9 - 0
webchat-user/src/main/java/com/webchat/user/service/relation/AccountRelationWrapper.java

@@ -44,4 +44,13 @@ public interface AccountRelationWrapper {
      * @return
      */
     Set<String> getAllSubscriber(String account);
+
+    /**
+     * 判断用户是否订阅某个用户
+     *
+     * @param userAccount
+     * @param account
+     * @return
+     */
+    boolean isSubscribe(String userAccount, String account);
 }

+ 0 - 3
webchat-user/src/main/java/com/webchat/user/service/relation/User2OfficialAccountRelationService.java

@@ -23,10 +23,7 @@ public class User2OfficialAccountRelationService extends AbstractAccountRelation
 
     @Override
     protected void doAfterComplete(Long id, String sourceAccount, String targetAccount, boolean subscribe) {
-        // 添加群聊到用户账号关系列表
         if (subscribe) {
-            // 文件传输助手,用户注册后默认订阅,无需审核
-            // 添加文件传输助手到用户好友列表缓存
             super.init(targetAccount, sourceAccount);
             super.addTargetAccountRelationListCache(targetAccount, sourceAccount);
             super.init(sourceAccount, targetAccount);

+ 33 - 0
webchat-user/src/main/java/com/webchat/user/service/relation/User2ServerAccountRelationService.java

@@ -0,0 +1,33 @@
+package com.webchat.user.service.relation;
+
+import com.webchat.common.enums.AccountRelationTypeEnum;
+import org.springframework.stereotype.Service;
+
+
+@Service
+public class User2ServerAccountRelationService extends AbstractAccountRelationService {
+
+
+    @Override
+    protected AccountRelationTypeEnum getRelationType() {
+
+        return AccountRelationTypeEnum.USER_SERVER;
+    }
+
+    @Override
+    protected boolean isAsyncDoAfterComplete() {
+
+        return false;
+    }
+
+    @Override
+    protected void doAfterComplete(Long id, String sourceAccount, String targetAccount, boolean subscribe) {
+        if (subscribe) {
+            super.init(targetAccount, sourceAccount);
+            super.addTargetAccountRelationListCache(targetAccount, sourceAccount);
+            super.init(sourceAccount, targetAccount);
+            super.addTargetAccountRelationListCache(sourceAccount, targetAccount);
+            super.addTargetAccount2SourceLastChattingList(sourceAccount, targetAccount);
+        }
+    }
+}