Selaa lähdekoodia

支付平台应用创建、基于访问凭证和交易凭证鉴权

wangqi49 3 viikkoa sitten
vanhempi
commit
2721dcc312
48 muutettua tiedostoa jossa 1714 lisäystä ja 27 poistoa
  1. 23 0
      resources/database-sql/webchat-payment.sql
  2. 2 2
      resources/database-sql/webchat-user.sql
  3. 2 2
      webchat-admin/src/main/java/com/webchat/admin/service/WalletManagementService.java
  4. 27 0
      webchat-client-chat/src/main/java/com/webchat/client/chat/config/ClientWebMvcConfig.java
  5. 69 0
      webchat-client-chat/src/main/java/com/webchat/client/chat/config/interceptor/SafeClickInterceptor.java
  6. 64 0
      webchat-client-chat/src/main/java/com/webchat/client/chat/controller/RedPacketController.java
  7. 57 0
      webchat-client-chat/src/main/java/com/webchat/client/chat/service/RedPacketService.java
  8. 5 0
      webchat-common/pom.xml
  9. 2 2
      webchat-common/src/main/java/com/webchat/common/constants/WebConstant.java
  10. 2 2
      webchat-common/src/main/java/com/webchat/common/enums/AccountRelationTypeEnum.java
  11. 19 0
      webchat-common/src/main/java/com/webchat/common/enums/RedisKeyEnum.java
  12. 47 0
      webchat-common/src/main/java/com/webchat/common/enums/payment/PaymentTransEventEnum.java
  13. 36 0
      webchat-common/src/main/java/com/webchat/common/enums/payment/PaymentTransTypeEnum.java
  14. 93 0
      webchat-common/src/main/java/com/webchat/common/util/AkSkGenerator.java
  15. 37 0
      webchat-common/src/main/java/com/webchat/common/util/SignUtil.java
  16. 44 0
      webchat-domain/src/main/java/com/webchat/domain/dto/payment/PaymentTransRequestDTO.java
  17. 19 0
      webchat-domain/src/main/java/com/webchat/domain/vo/dto/payment/AppAkSkDTO.java
  18. 5 0
      webchat-domain/src/main/java/com/webchat/domain/vo/request/SendRedPacketRequestVO.java
  19. 33 0
      webchat-domain/src/main/java/com/webchat/domain/vo/request/payment/CreateAppRequestVO.java
  20. 32 0
      webchat-domain/src/main/java/com/webchat/domain/vo/response/payment/AppBaseResponseVO.java
  21. 14 0
      webchat-domain/src/main/java/com/webchat/domain/vo/response/payment/CreateAppResponseVO.java
  22. 2 0
      webchat-pay/src/main/java/com/webchat/pay/WebchatPayApplication.java
  23. 5 1
      webchat-pay/src/main/java/com/webchat/pay/advice/GlobalExceptionAdvice.java
  24. 29 0
      webchat-pay/src/main/java/com/webchat/pay/config/WebChatPaymentMvnConfigurer.java
  25. 15 0
      webchat-pay/src/main/java/com/webchat/pay/config/annotation/ValidateAccessPaymentPermission.java
  26. 19 0
      webchat-pay/src/main/java/com/webchat/pay/config/enums/PaymentErrorCodeEnum.java
  27. 24 0
      webchat-pay/src/main/java/com/webchat/pay/config/exception/PaymentAccessAuthException.java
  28. 59 0
      webchat-pay/src/main/java/com/webchat/pay/config/interceptor/ValidateAccessPaymentPermissionInterceptor.java
  29. 117 0
      webchat-pay/src/main/java/com/webchat/pay/controller/PaymentApiServiceController.java
  30. 27 0
      webchat-pay/src/main/java/com/webchat/pay/controller/PaymentAppServiceController.java
  31. 2 2
      webchat-pay/src/main/java/com/webchat/pay/controller/PaymentWalletServiceController.java
  32. 14 0
      webchat-pay/src/main/java/com/webchat/pay/repository/dao/IAppDAO.java
  33. 59 0
      webchat-pay/src/main/java/com/webchat/pay/repository/entity/AppEntity.java
  34. 11 2
      webchat-pay/src/main/java/com/webchat/pay/service/AccountService.java
  35. 136 0
      webchat-pay/src/main/java/com/webchat/pay/service/PaymentApiService.java
  36. 205 0
      webchat-pay/src/main/java/com/webchat/pay/service/PaymentAppService.java
  37. 2 2
      webchat-pay/src/main/java/com/webchat/pay/service/PaymentService.java
  38. 77 0
      webchat-remote/src/main/java/com/webchat/rmi/pay/PaymentApiServiceClient.java
  39. 22 0
      webchat-remote/src/main/java/com/webchat/rmi/pay/PaymentAppServiceClient.java
  40. 3 4
      webchat-remote/src/main/java/com/webchat/rmi/pay/PaymentWalletServiceClient.java
  41. 41 0
      webchat-remote/src/main/java/com/webchat/rmi/ugc/RedPacketClient.java
  42. 18 0
      webchat-ugc/src/main/java/com/webchat/ugc/config/PaymentAppConfig.java
  43. 33 0
      webchat-ugc/src/main/java/com/webchat/ugc/controller/RedPacketController.java
  44. 133 0
      webchat-ugc/src/main/java/com/webchat/ugc/service/PaymentService.java
  45. 21 0
      webchat-ugc/src/main/java/com/webchat/ugc/service/RedPacketService.java
  46. 3 3
      webchat-user/src/main/java/com/webchat/user/service/UserService.java
  47. 3 3
      webchat-user/src/main/java/com/webchat/user/service/relation/AccountRelationFactory.java
  48. 2 2
      webchat-user/src/main/java/com/webchat/user/service/relation/User2AIAgentSenderAccountRelationService.java

+ 23 - 0
resources/database-sql/webchat-payment.sql

@@ -1,3 +1,26 @@
+
+-- 接入支付平台的应用信息表
+CREATE TABLE webchat_payment.`web_chat_app` (
+        `ID` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '自增ID',
+        `name` char(20) NOT NULL COMMENT '应用名称 ',
+        `logo` varchar(300) NOT NULL COMMENT '应用Logo',
+        `description` varchar(200) NOT NULL COMMENT '应用描述',
+        `admin` char(100) NOT NULL COMMENT '创建人/管理员',
+        `status` int(4) NOT NULL COMMENT '状态',
+        `access_key` char(64) NOT NULL COMMENT 'access key 应用访问凭证',
+        `secret_hash_key` char(100) NOT NULL COMMENT 'secret key hash值 应用访问凭证秘钥',
+        `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 `INDEX_ACCESS_SECRET_KEY` (`ACCESS_KEY`, `SECRET_HASH_KEY`),
+        KEY `INDEX_STATUS` (`STATUS`),
+        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',

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

@@ -29,8 +29,8 @@ INSERT INTO `webchat_user`.`web_chat_user` (`ID`, `USER_ID`, `USER_NAME`, `PHOTO
 -- 初始化文件传输助手
 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
-    ('F_ef352f698ad7b60c1d8a3aaa4a948030', '文件传输助手', 'https://coderutil.oss-cn-beijing.aliyuncs.com/bbs-image/file_42a26236a73f4df8b742397ac9c46c4f.png',
-     'filesender', 'F_ef352f698ad7b60c1d8a3aaa4a948030', 1, 0, 'F_ef352f698ad7b60c1d8a3aaa4a948030',
+    ('AI_ef352f698ad7b60c1d8a3aaa4a948030', '文件传输助手', '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);
 
 -- 好友关系表

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

@@ -2,7 +2,7 @@ package com.webchat.admin.service;
 
 import com.webchat.common.bean.APIPageResponseBean;
 import com.webchat.domain.vo.response.UserWalletDetailResponseVO;
-import com.webchat.rmi.pay.PaymentServiceClient;
+import com.webchat.rmi.pay.PaymentWalletServiceClient;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
@@ -11,7 +11,7 @@ public class WalletManagementService {
 
 
     @Autowired
-    private PaymentServiceClient paymentServiceClient;
+    private PaymentWalletServiceClient paymentServiceClient;
 
     public APIPageResponseBean<UserWalletDetailResponseVO> page(Integer transType, Integer eventType, String userId,
                                                                 Integer pageNo, Integer pageSize) {

+ 27 - 0
webchat-client-chat/src/main/java/com/webchat/client/chat/config/ClientWebMvcConfig.java

@@ -0,0 +1,27 @@
+package com.webchat.client.chat.config;
+
+import com.webchat.client.chat.config.interceptor.SafeClickInterceptor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+@Configuration
+public class ClientWebMvcConfig implements WebMvcConfigurer {
+
+    private static final String CLINT_REQUEST_PATH_PREFIX = "/client-service/chat";
+
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+
+        /**
+         * 注册安全点击拦截器
+         */
+        registry.addInterceptor(safeClickInterceptor()).addPathPatterns(CLINT_REQUEST_PATH_PREFIX);
+    }
+
+    @Bean
+    public SafeClickInterceptor safeClickInterceptor() {
+        return new SafeClickInterceptor();
+    }
+}

+ 69 - 0
webchat-client-chat/src/main/java/com/webchat/client/chat/config/interceptor/SafeClickInterceptor.java

@@ -0,0 +1,69 @@
+package com.webchat.client.chat.config.interceptor;
+
+import com.webchat.common.config.annotation.SafeClick;
+import com.webchat.common.constants.WebConstant;
+import com.webchat.common.exception.AuthException;
+import com.webchat.common.exception.BusinessException;
+import com.webchat.common.helper.SessionHelper;
+import com.webchat.common.service.RedisService;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.method.HandlerMethod;
+import org.springframework.web.servlet.HandlerInterceptor;
+
+import java.lang.reflect.Method;
+
+/**
+ * 安全点击自定义注解实现,拦截器实现
+ *
+ * 实现原理:基于redis分布式实现限制当前登录用于在一定时间内只能触发一次特定事件,超过限制则报错提醒
+ */
+@Slf4j
+public class SafeClickInterceptor implements HandlerInterceptor {
+
+
+    @Autowired
+    private RedisService redisService;
+
+    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
+
+        // 检查 handler 是否为 HandlerMethod 类型
+        if (!(handler instanceof HandlerMethod)) {
+            // 不是 HandlerMethod 类型,可能是静态资源请求,直接放行
+            return true;
+        }
+        HandlerMethod handlerMethod = (HandlerMethod) handler;
+        Method method = handlerMethod.getMethod();
+        // 获取当前被拦截方法是否由SafeClick
+        SafeClick safeClick = method.getAnnotation(SafeClick.class);
+        if (safeClick == null) {
+            // 没有添加@SafeClick,则不需要校验安全点击
+            return true;
+        }
+        // 获取操作人(当前登录人) ====>
+        // 当前请求在主线程内(tomcat线程池分配主线程),所有可以从ThreadLocal中直接获取
+        String userId = SessionHelper.getCurrentUserId();
+        if (StringUtils.isBlank(userId)) {
+            // 当前用户未登录(实际上合理不会触发未登录,因为我们在filter中刚完整了SSO登录认证)
+            // PS:但是为保证逻辑的严谨性,这里还是做了判空
+            throw new AuthException("未登录");
+        }
+        // 获取用户安全点击事件类型
+        String action = safeClick.event().name();
+        // 允许多久内操作一次
+        Long time = safeClick.time();
+        // 构造redis分布式锁redis key【规则:事件名称-用户id】
+        String redisKey = action.concat("-").concat(userId);
+        boolean installLock = redisService.installLockForMS(redisKey, WebConstant.CACHE_NONE, time);
+        if (installLock) {
+            // 分布式加锁成功,说明当前用户在请求可以允许放行,反之则触发限流
+            return true;
+        }
+        log.error("<<< {} >>>, userId: {}, action:{}", safeClick.message(), userId, action);
+        // 触发限流,抛异常,异常提示来自用户注解自定义异常描述
+        throw new BusinessException(safeClick.message());
+    }
+}

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

@@ -0,0 +1,64 @@
+package com.webchat.client.chat.controller;
+
+
+import com.webchat.client.chat.service.RedPacketService;
+import com.webchat.common.bean.APIResponseBean;
+import com.webchat.common.bean.APIResponseBeanUtil;
+import com.webchat.common.config.annotation.SafeClick;
+import com.webchat.common.config.annotation.ValidateLogin;
+import com.webchat.common.enums.ClickEvent;
+import com.webchat.common.helper.SessionHelper;
+import com.webchat.domain.vo.request.SendRedPacketRequestVO;
+import org.springframework.beans.factory.annotation.Autowired;
+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;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/client-service/chat/red-packet")
+public class RedPacketController {
+
+
+    @Autowired
+    private RedPacketService redPacketService;
+
+    /**
+     * 发红包API
+     *
+     * @param sendRedPacketRequest
+     *
+     * @SafeClick 是我们的自定义注解,作用:安全点击,实现原理:基于redis分布式锁
+     * SafeClick具体实现详见拦截器:com.webchat.client.chat.config.interceptor.SafeClickInterceptor
+     * @return
+     */
+    @SafeClick(event = ClickEvent.SEND_RED_PACKET, time = 5000, message = "5秒内只能发一次红包")
+    @PostMapping("/send")
+    public APIResponseBean<Long> send(@RequestBody SendRedPacketRequestVO sendRedPacketRequest) {
+
+        // 发红包参数校验
+        sendRedPacketRequest.validateRequestParam();
+        // 设置当前登录用户为红包发送人
+        String userId = SessionHelper.getCurrentUserId();
+        sendRedPacketRequest.setSendUserId(userId);
+        Long redPacketId = redPacketService.sendRedPacket(sendRedPacketRequest);
+        return APIResponseBeanUtil.success(redPacketId);
+    }
+
+    /**
+     * 拆分红包
+     *
+     * 包含群聊多人随机红包拆分 + 一对一固定红包拆分
+     *
+     * @param redPacketId
+     * @return
+     */
+    @GetMapping("/open/{redPacketId}")
+    public APIResponseBean<String> open(@PathVariable Long redPacketId) {
+        String userId = SessionHelper.getCurrentUserId();
+        String money = redPacketService.openRedPacket(redPacketId, userId);
+        return APIResponseBeanUtil.success(money);
+    }
+}

+ 57 - 0
webchat-client-chat/src/main/java/com/webchat/client/chat/service/RedPacketService.java

@@ -0,0 +1,57 @@
+package com.webchat.client.chat.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.SendRedPacketRequestVO;
+import com.webchat.rmi.ugc.RedPacketClient;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+@Service
+public class RedPacketService {
+
+    /**
+     * 注入 UGC 远程服务的红包服务客户端
+     */
+    @Autowired
+    private RedPacketClient redPacketClient;
+
+    /**
+     * 发红包
+     *
+     * CLIENT -> UGC -> PAY 服务调用连
+     *
+     * @param sendRedPacketRequest
+     * @return
+     */
+    public Long sendRedPacket(SendRedPacketRequestVO sendRedPacketRequest) {
+
+        // RPC远程调用,红包实际的收发逻辑由UGC实现,底层钱包扣除由PAY服务支持
+        APIResponseBean<Long> responseBean = redPacketClient.send(sendRedPacketRequest);
+        if (APIResponseBeanUtil.isOk(responseBean)) {
+            return responseBean.getData();
+        }
+        throw new BusinessException(responseBean.getMsg());
+    }
+
+    /**
+     * 拆红包
+     *
+     * CLIENT -> UGC -> PAY 服务调用连
+     *
+     * @param redPacketId 红包id
+     * @param userId 拆包用户id
+     * @return
+     */
+    public String openRedPacket(Long redPacketId, String userId) {
+
+        // RPC远程调用,红包实际的收发逻辑由UGC实现,底层钱包扣除由PAY服务支持
+        APIResponseBean<String> responseBean = redPacketClient.open(redPacketId, userId);
+        if (APIResponseBeanUtil.isOk(responseBean)) {
+            return responseBean.getData();
+        }
+        throw new BusinessException(responseBean.getMsg());
+    }
+}

+ 5 - 0
webchat-common/pom.xml

@@ -22,6 +22,11 @@
 
     <dependencies>
         <dependency>
+            <groupId>com.auth0</groupId>
+            <artifactId>java-jwt</artifactId>
+            <version>3.8.2</version>
+        </dependency>
+        <dependency>
             <groupId>io.minio</groupId>
             <artifactId>minio</artifactId>
             <version>8.4.2</version>

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

@@ -21,9 +21,9 @@ public class WebConstant {
     public static final String SYSTEM_WALLET_ID = "U_770cce9f632543588b4e8aa6ec43e6a2";
 
     /**
-     * 文件传输助手默认账号id
+     * AI助手默认账号id
      */
-    public static final String FILE_SEND_ID = "F_ef352f698ad7b60c1d8a3aaa4a948030";
+    public static final String AI_AGENT_ID = "AI_ef352f698ad7b60c1d8a3aaa4a948030";
 
     /**
      * 系统USERID

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

@@ -14,7 +14,7 @@ import lombok.extern.slf4j.Slf4j;
 public enum AccountRelationTypeEnum {
 
 
-    USER_FILE_SENDER(0, "添加文件传输助手(默认添加)"),
+    USER_AI_AGENT(0, "我的AI助手(默认添加)"),
     USER_USER(1, "好友关注"),
     USER_GROUP(2, "加入群聊"),
     USER_OFFICIAL(3, "订阅公众号"),
@@ -27,7 +27,7 @@ public enum AccountRelationTypeEnum {
     public static AccountRelationTypeEnum getByTargetAccountRoleCode(int roleCode) {
         switch (roleCode) {
             case 0:
-                return AccountRelationTypeEnum.USER_FILE_SENDER;
+                return AccountRelationTypeEnum.USER_AI_AGENT;
             case 1:
             case 2:
             case 3:

+ 19 - 0
webchat-common/src/main/java/com/webchat/common/enums/RedisKeyEnum.java

@@ -292,6 +292,25 @@ public enum RedisKeyEnum {
      *
      */
     GROUP_VIDEO_ONLINE_USER_ZSET("GROUP_VIDEO_ONLINE_USER_ZSET", 24 * 60 * 60L),
+
+    PAYMENT_ACCESS_TOKEN_CACHE("PAYMENT_ACCESS_TOKEN_CACHE", 2 * 60 * 60L - 60L),
+
+    /**
+     * 支付平台注册应用信息缓存
+     */
+    PAYMENT_APP_INFO_CACHE("PAYMENT_APP_INFO_CACHE", 24 * 60 * 60L),
+
+    /**
+     * 支付平台注册应用信息访问凭证缓存
+     */
+    PAYMENT_APP_AK_SK_CACHE("PAYMENT_APP_AK_SK_CACHE", 3 * 24 * 60 * 60L),
+
+    PAYMENT_DEFAULT_APP_INFO_CACHE("PAYMENT_DEFAULT_APP_INFO_CACHE", 5 * 60 * 60L),
+
+    /**
+     * 应用交易凭证缓存,2小时有效
+     */
+    PAYMENT_APP_ACCESS_TOKEN_CACHE("PAYMENT_APP_ACCESS_TOKEN_CACHE", 2 * 60 * 60L),
     ;
 
 

+ 47 - 0
webchat-common/src/main/java/com/webchat/common/enums/payment/PaymentTransEventEnum.java

@@ -0,0 +1,47 @@
+package com.webchat.common.enums.payment;
+
+import com.webchat.common.enums.WalletTransTypeEnum;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+/**
+ * @author 程序员七七, https://www.coderutil.com网站作者
+ * @date 2024/11/9 03:53
+ *
+ *
+ * event设计目的:
+ *      将降级从事件维度划分为几大类,便于后续数据的统计分析 ===> 例如基于event可以实现用户账单(出行、购物……)
+ */
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+public enum PaymentTransEventEnum {
+
+    SYSTEM_GRANT(1, "系统发放"),
+
+    RED_PACKET(2, "红包"),
+
+    SHOPPING(3, "购物"),
+
+    TRAVEL(4, "出行"),
+
+    FOOD(5, "美食"),
+
+    OTHER(99, "其他"),
+    ;
+
+    private Integer transEvent;
+
+    private String transEventName;
+
+
+    public static String getEventName(Integer transEvent) {
+        for (PaymentTransEventEnum event : PaymentTransEventEnum.values()) {
+            if (event.transEvent.equals(transEvent)) {
+                return event.transEventName;
+            }
+        }
+        return "未知";
+    }
+}

+ 36 - 0
webchat-common/src/main/java/com/webchat/common/enums/payment/PaymentTransTypeEnum.java

@@ -0,0 +1,36 @@
+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 PaymentTransTypeEnum {
+
+    INCOME(1, "收入"),
+
+    EXPENSES(-1, "支出");
+
+    private Integer transType;
+
+    private String transTypeName;
+
+
+    public static String getTransTypeName(Integer transType) {
+        if (transType == null) {
+            return "未知";
+        }
+        for (PaymentTransTypeEnum walletTransTypeEnum : PaymentTransTypeEnum.values()) {
+            if (walletTransTypeEnum.getTransType().equals(transType)) {
+                return walletTransTypeEnum.getTransTypeName();
+            }
+        }
+        return "未知";
+    }
+}

+ 93 - 0
webchat-common/src/main/java/com/webchat/common/util/AkSkGenerator.java

@@ -0,0 +1,93 @@
+package com.webchat.common.util;
+
+import org.springframework.security.crypto.bcrypt.BCrypt;
+
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Base64;
+import java.util.UUID;
+
+public class AkSkGenerator {
+
+    /**
+     * AK前缀标识(企业规范)
+     */
+    private static final String AK_PREFIX = "ak-";
+
+    /**
+     * 生成32位AK(UUIDv4规范)
+     *
+     * @return
+     */
+    public static String generateAk() {
+
+        return AK_PREFIX + UUID.randomUUID().toString().replace("-", "").toLowerCase();
+    }
+
+    /**
+     * 生成32字节SK(加密级随机数)
+     *
+     * @return
+     * @throws NoSuchAlgorithmException
+     */
+    public static String generateSk() {
+
+        try {
+            SecureRandom secureRandom = SecureRandom.getInstanceStrong();
+            byte[] skBytes = new byte[32];
+            secureRandom.nextBytes(skBytes);
+            return Base64.getEncoder().encodeToString(skBytes);
+        } catch (Exception e) {
+            throw new RuntimeException("SK生成失败!");
+        }
+    }
+
+    /**
+     * HAHS算法,采用BCrypt实现
+     *
+     * SK加密存储, 使用BCrypt
+     * BCrypt哈希值的格式为:$2a$[cost]$[22位盐][31位哈希]
+     * 一般不会直接将key存储到数据库,会存储hashKey(随机加盐),更安全
+     *
+     * 常用对比,BCrypt优势
+     * ---------------------------------------------------------------------------------
+     * |    方法	           |          问题	                |       BCrypt优势         |
+     * ---------------------------------------------------------------------------------
+     * |明文存储SK	       | 数据库泄露导致SK直接暴露	        | 哈希不可逆,攻击者无法还原SK |
+     * |MD5/SHA-256哈希	   | 无盐易破解,无法防御彩虹表攻击	    | 内置随机盐,相同SK不同哈希   |
+     * |自定义加密算法	   |  实现漏洞风险高(如ECB模式)	    | 经过安全社区验证的标准实现   |
+     * ---------------------------------------------------------------------------------
+     *
+     * @param key
+     * @return
+     */
+    public static String hashKey(String key) {
+
+        return BCrypt.hashpw(key, BCrypt.gensalt());
+    }
+
+    /**
+     * 校验key与hashKey是否相同
+     *
+     * @param key
+     * @param hashKey
+     * @return
+     */
+    public static boolean checkKey(String key, String hashKey) {
+
+        return BCrypt.checkpw(key, hashKey);
+    }
+
+    public static void main(String[] args) {
+        String ak = generateAk();
+        String sk = generateSk();
+        System.out.println(ak);
+        System.out.println(sk);
+        // SK加密存储, 使用BCrypt
+        // BCrypt哈希值的格式为:$2a$[cost]$[22位盐][31位哈希]
+        String storedHashedSk = hashKey(sk);
+        System.out.println(storedHashedSk);
+        // 校验sk是否正确
+        System.out.println(checkKey(sk, storedHashedSk));
+    }
+}

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

@@ -0,0 +1,37 @@
+package com.webchat.common.util;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+public class SignUtil {
+
+    /**
+     * 签名生成(华为云规范)
+     *
+     * @param sk
+     * @param params
+     * @return
+     */
+    public static String generateSignature(String sk, String ... params) {
+        try {
+            String stringToSign = "";
+            if (params != null && params.length > 0) {
+                for (int i = 0; i < params.length; i++) {
+                    stringToSign += params[i];
+                    if (i != params.length) {
+                        stringToSign += "-";
+                    }
+                }
+            }
+            Mac sha256HMAC = Mac.getInstance("HmacSHA256");
+            SecretKeySpec secretKey = new SecretKeySpec(sk.getBytes(), "HmacSHA256");
+            sha256HMAC.init(secretKey);
+            byte[] hashBytes = sha256HMAC.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8));
+            return Base64.getEncoder().encodeToString(hashBytes);
+        } catch (Exception e) {
+            throw new RuntimeException("签名生成失败", e);
+        }
+    }
+}

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

@@ -0,0 +1,44 @@
+package com.webchat.domain.dto.payment;
+
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+public class PaymentTransRequestDTO {
+
+
+    /**
+     * 交易发起账户(我们默认使用用户id作为用户账户)
+     */
+    private String sourceUserId;
+
+    /**
+     * 交易目标账户
+     */
+    private String targetUserId;
+
+    /**
+     * 交易金额
+     */
+    private BigDecimal amount;
+
+    /**
+     * 交易类型
+     */
+    private Integer transType;
+
+    /**
+     * 交易事件
+     */
+    private Integer transEvent;
+
+    /**
+     * 交易明细
+     * 示例:
+     *  webchat发红包
+     *  webshop下单XXX商品
+     */
+    private String transDetail;
+}

+ 19 - 0
webchat-domain/src/main/java/com/webchat/domain/vo/dto/payment/AppAkSkDTO.java

@@ -0,0 +1,19 @@
+package com.webchat.domain.vo.dto.payment;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+
+/**
+ * 应用请求支付api 访问凭证信息
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class AppAkSkDTO {
+
+    private String accessKey;
+
+    private String secretHashKey;
+}

+ 5 - 0
webchat-domain/src/main/java/com/webchat/domain/vo/request/SendRedPacketRequestVO.java

@@ -24,6 +24,11 @@ public class SendRedPacketRequestVO {
     private String receiverUserId;
 
     /**
+     * 红包封面图,不上传封面使用系统默认红包封面
+     */
+    private String cover;
+
+    /**
      * 拼手气、普通红包(平均分配)
      */
     private Integer type;

+ 33 - 0
webchat-domain/src/main/java/com/webchat/domain/vo/request/payment/CreateAppRequestVO.java

@@ -0,0 +1,33 @@
+package com.webchat.domain.vo.request.payment;
+
+import lombok.Data;
+
+@Data
+public class CreateAppRequestVO {
+
+    /**
+     * 应用ID
+     *
+     */
+    private Long id;
+
+    /**
+     * 应用名称
+     */
+    private String name;
+
+    /**
+     * 应用logo
+     */
+    private String logo;
+
+    /**
+     * 应用描述
+     */
+    private String description;
+
+    /**
+     * 应用创建人,默认应用管理员
+     */
+    private String userId;
+}

+ 32 - 0
webchat-domain/src/main/java/com/webchat/domain/vo/response/payment/AppBaseResponseVO.java

@@ -0,0 +1,32 @@
+package com.webchat.domain.vo.response.payment;
+
+import lombok.Data;
+
+@Data
+public class AppBaseResponseVO {
+
+    private Long id;
+
+    private String logo;
+
+    private String name;
+
+    private String description;
+
+    private Integer status;
+
+    /**
+     * 接口访问凭证
+     */
+    private String accessKey;
+
+    /**
+     * 接口访问凭证秘钥
+     */
+    private String secretKey;
+
+    /**
+     * 应用管理员
+     */
+    private String admin;
+}

+ 14 - 0
webchat-domain/src/main/java/com/webchat/domain/vo/response/payment/CreateAppResponseVO.java

@@ -0,0 +1,14 @@
+package com.webchat.domain.vo.response.payment;
+
+import com.webchat.domain.vo.response.UserBaseResponseInfoVO;
+import lombok.Data;
+
+@Data
+public class CreateAppResponseVO extends AppBaseResponseVO {
+
+
+    /**
+     * 应用管理员
+     */
+    private UserBaseResponseInfoVO adminInfo;
+}

+ 2 - 0
webchat-pay/src/main/java/com/webchat/pay/WebchatPayApplication.java

@@ -4,7 +4,9 @@ import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
 import org.springframework.cloud.openfeign.EnableFeignClients;
+import org.springframework.context.annotation.ComponentScan;
 
+@ComponentScan("com.webchat")
 @SpringBootApplication
 @EnableDiscoveryClient
 @EnableFeignClients("com.webchat.rmi")

+ 5 - 1
webchat-pay/src/main/java/com/webchat/pay/advice/GlobalExceptionAdvice.java

@@ -6,6 +6,7 @@ import com.webchat.common.enums.APIErrorCommonEnum;
 import com.webchat.common.exception.AuthException;
 import com.webchat.common.exception.BusinessException;
 import com.webchat.common.exception.NotFoundException;
+import com.webchat.pay.config.exception.PaymentAccessAuthException;
 import jakarta.servlet.http.HttpServletRequest;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.http.HttpStatus;
@@ -30,7 +31,10 @@ public class GlobalExceptionAdvice {
     @ResponseBody
     public ResponseEntity<APIResponseBean> exceptionHandler(HttpServletRequest request, Exception e) {
         log.error("异常详情信息:", e);
-        if (e instanceof AuthException) {
+        if (e instanceof PaymentAccessAuthException) {
+            APIResponseBean apiResponseBean = APIResponseBeanUtil.error(((PaymentAccessAuthException) e).getCode(), e.getMessage());
+            return new ResponseEntity(apiResponseBean, HttpStatus.OK);
+        } else if (e instanceof AuthException) {
             APIResponseBean apiResponseBean = APIResponseBeanUtil.error(((AuthException) e).getCode(), e.getMessage());
             return new ResponseEntity(apiResponseBean, HttpStatus.OK);
         }

+ 29 - 0
webchat-pay/src/main/java/com/webchat/pay/config/WebChatPaymentMvnConfigurer.java

@@ -0,0 +1,29 @@
+package com.webchat.pay.config;
+
+import com.webchat.pay.config.interceptor.ValidateAccessPaymentPermissionInterceptor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+@Configuration
+public class WebChatPaymentMvnConfigurer implements WebMvcConfigurer {
+
+
+    /**
+     * 拦截所有三方支付接口请求
+     */
+    private static final String PAYMENT_API_PATH = "/pay-service/api/**";
+
+
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        registry.addInterceptor(validateAccessPaymentPermissionInterceptor())
+                .addPathPatterns(PAYMENT_API_PATH);
+    }
+
+    @Bean
+    public ValidateAccessPaymentPermissionInterceptor validateAccessPaymentPermissionInterceptor() {
+        return new ValidateAccessPaymentPermissionInterceptor();
+    }
+}

+ 15 - 0
webchat-pay/src/main/java/com/webchat/pay/config/annotation/ValidateAccessPaymentPermission.java

@@ -0,0 +1,15 @@
+package com.webchat.pay.config.annotation;
+
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD})
+@Documented
+public @interface ValidateAccessPaymentPermission {
+
+}

+ 19 - 0
webchat-pay/src/main/java/com/webchat/pay/config/enums/PaymentErrorCodeEnum.java

@@ -0,0 +1,19 @@
+package com.webchat.pay.config.enums;
+
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+public enum PaymentErrorCodeEnum {
+
+    NO_AUTH(10402, "支付接口请求无权限!");
+
+
+    private Integer errCode;
+
+    private String errDesc;
+}

+ 24 - 0
webchat-pay/src/main/java/com/webchat/pay/config/exception/PaymentAccessAuthException.java

@@ -0,0 +1,24 @@
+package com.webchat.pay.config.exception;
+
+import com.webchat.pay.config.enums.PaymentErrorCodeEnum;
+
+
+public class PaymentAccessAuthException extends RuntimeException {
+
+    /**
+     * 异常状态码
+     */
+    private int code = PaymentErrorCodeEnum.NO_AUTH.getErrCode();
+
+    public PaymentAccessAuthException() {
+        super(PaymentErrorCodeEnum.NO_AUTH.getErrDesc());
+    }
+
+    public PaymentAccessAuthException(String message) {
+        super(message);
+    }
+
+    public Integer getCode() {
+        return code;
+    }
+}

+ 59 - 0
webchat-pay/src/main/java/com/webchat/pay/config/interceptor/ValidateAccessPaymentPermissionInterceptor.java

@@ -0,0 +1,59 @@
+package com.webchat.pay.config.interceptor;
+
+import com.webchat.pay.config.annotation.ValidateAccessPaymentPermission;
+import com.webchat.pay.config.exception.PaymentAccessAuthException;
+import com.webchat.pay.service.PaymentApiService;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.method.HandlerMethod;
+import org.springframework.web.servlet.HandlerInterceptor;
+
+import java.lang.reflect.Method;
+
+/**
+ * 基于拦截器实现自定义注解 @ValidateAccessPaymentPermission
+ * 作用:完成支付对外三方API请求统一鉴权、优雅实现鉴权逻辑解耦
+ */
+@Slf4j
+public class ValidateAccessPaymentPermissionInterceptor implements HandlerInterceptor {
+
+    @Autowired
+    private PaymentApiService paymentApiService;
+
+    /**
+     * 鉴权逻辑:
+     *
+     * 1、从当前请求头中获取token参数
+     * 2、对token实现有效性校验
+     *
+     * @param request
+     * @param response
+     * @param handler
+     * @return
+     * @throws Exception
+     */
+    @Override
+    public boolean preHandle(HttpServletRequest request,
+                             HttpServletResponse response,
+                             Object handler) throws Exception {
+
+        HandlerMethod handlerMethod = (HandlerMethod) handler;
+        Method method = handlerMethod.getMethod();
+        ValidateAccessPaymentPermission validateAccessPaymentPermission =
+                method.getAnnotation(ValidateAccessPaymentPermission.class);
+        if (validateAccessPaymentPermission == null) {
+            // 当前请求不需要校验token
+            return true;
+        }
+        // 从当前请求http中获取token参数
+        String token = request.getHeader("access-token");
+        boolean validateResult = paymentApiService.validateAccessToken(token);
+        if (validateResult) {
+            return true;
+        }
+        // 支付服务请求token无权限
+        throw new PaymentAccessAuthException("交易凭证校验失败");
+    }
+}

+ 117 - 0
webchat-pay/src/main/java/com/webchat/pay/controller/PaymentApiServiceController.java

@@ -0,0 +1,117 @@
+package com.webchat.pay.controller;
+
+import com.webchat.common.bean.APIResponseBean;
+import com.webchat.common.bean.APIResponseBeanUtil;
+import com.webchat.domain.dto.payment.PaymentTransRequestDTO;
+import com.webchat.pay.config.annotation.ValidateAccessPaymentPermission;
+import com.webchat.pay.service.PaymentApiService;
+import com.webchat.rmi.pay.PaymentApiServiceClient;
+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.RequestHeader;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.math.BigDecimal;
+
+
+@RestController
+public class PaymentApiServiceController implements PaymentApiServiceClient {
+
+    @Autowired
+    private PaymentApiService paymentApiService;
+
+    /**
+     * 获取所有交易请求鉴权token
+     *
+     * @param appId
+     * @param accessKey
+     * @param secretKey
+     * @param timestamp
+     * @param signature
+     * @param logId
+     * @return
+     */
+    @Override
+    public APIResponseBean<String> accessToken(@RequestHeader(name = "app-id") Long appId,
+                                               @RequestHeader(name = "access-key") String accessKey,
+                                               @RequestHeader(name = "secret-key") String secretKey,
+                                               @RequestHeader Long timestamp,
+                                               @RequestHeader String signature,
+                                               @RequestHeader(name = "log-id") String logId) {
+
+        String accessToken = paymentApiService.token(appId, accessKey, secretKey, timestamp, signature, logId);
+        return APIResponseBeanUtil.success(accessToken);
+    }
+
+    /**
+     * 获取分布式交易事务id
+     *
+     * 应用场景:如果上游接入方支付成功后业务异常,需要回滚交易数据
+     *
+     * @param token
+     * @param logId
+     * @return
+     */
+    @ValidateAccessPaymentPermission
+    @Override
+    public APIResponseBean<String> transId(@RequestHeader(name = "access-token") String accessToken,
+                                           @RequestHeader(name = "log-id") String logId) {
+
+        return null;
+    }
+
+    /**
+     * 真实交易
+     * @param paymentTransRequest
+     * @param token
+     * @param transId
+     * @param logId
+     * @return
+     */
+    @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) {
+
+
+
+
+
+        return null;
+    }
+
+
+    /**
+     * 交易回滚
+     *
+     * @param token
+     * @param transId 回滚事务id
+     * @param logId
+     * @return
+     */
+    @ValidateAccessPaymentPermission
+    @Override
+    public APIResponseBean<Boolean> rollback(@RequestHeader(name = "access-token") String accessToken,
+                                             @RequestHeader(name = "log-id") String logId,
+                                             @RequestHeader(name = "trans-id")  String transId) {
+        return null;
+    }
+
+    /**
+     * 查询账户余额
+     *
+     * @param userId
+     * @param logId
+     * @return
+     */
+    @ValidateAccessPaymentPermission
+    @Override
+    public APIResponseBean<BigDecimal> balance(@PathVariable String userId,
+                                               @RequestHeader(name = "access-token") String accessToken,
+                                               @RequestHeader(name = "log-id") String logId) {
+        return null;
+    }
+}

+ 27 - 0
webchat-pay/src/main/java/com/webchat/pay/controller/PaymentAppServiceController.java

@@ -0,0 +1,27 @@
+package com.webchat.pay.controller;
+
+import com.webchat.common.bean.APIResponseBean;
+import com.webchat.common.bean.APIResponseBeanUtil;
+import com.webchat.domain.vo.request.payment.CreateAppRequestVO;
+import com.webchat.domain.vo.response.payment.CreateAppResponseVO;
+import com.webchat.pay.service.PaymentAppService;
+import com.webchat.rmi.pay.PaymentAppServiceClient;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+
+
+@RestController
+public class PaymentAppServiceController implements PaymentAppServiceClient {
+
+
+    @Autowired
+    private PaymentAppService paymentAppService;
+
+    @Override
+    public APIResponseBean<CreateAppResponseVO> createApp(@RequestBody CreateAppRequestVO createAppRequest) {
+        // TODO
+        CreateAppResponseVO app = paymentAppService.saveApp(createAppRequest);
+        return APIResponseBeanUtil.success(app);
+    }
+}

+ 2 - 2
webchat-pay/src/main/java/com/webchat/pay/controller/PaymentServiceController.java → webchat-pay/src/main/java/com/webchat/pay/controller/PaymentWalletServiceController.java

@@ -4,7 +4,7 @@ import com.webchat.common.bean.APIPageResponseBean;
 import com.webchat.common.bean.APIResponseBean;
 import com.webchat.domain.vo.response.UserWalletDetailResponseVO;
 import com.webchat.pay.service.PaymentService;
-import com.webchat.rmi.pay.PaymentServiceClient;
+import com.webchat.rmi.pay.PaymentWalletServiceClient;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.RestController;
 
@@ -12,7 +12,7 @@ import java.math.BigDecimal;
 
 
 @RestController
-public class PaymentServiceController implements PaymentServiceClient {
+public class PaymentWalletServiceController implements PaymentWalletServiceClient {
 
     @Autowired
     private PaymentService paymentService;

+ 14 - 0
webchat-pay/src/main/java/com/webchat/pay/repository/dao/IAppDAO.java

@@ -0,0 +1,14 @@
+package com.webchat.pay.repository.dao;
+
+import com.webchat.pay.repository.entity.AppEntity;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.stereotype.Repository;
+
+
+@Repository
+public interface IAppDAO extends JpaSpecificationExecutor<AppEntity>,
+        JpaRepository<AppEntity, Long> {
+
+
+}

+ 59 - 0
webchat-pay/src/main/java/com/webchat/pay/repository/entity/AppEntity.java

@@ -0,0 +1,59 @@
+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.Table;
+import lombok.Data;
+
+@Data
+@Entity
+@Table(name = "web_chat_app")
+public class AppEntity extends BaseEntity{
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private Long id;
+
+    /**
+     * 应用名称
+     */
+    @Column(name = "name")
+    private String name;
+    /**
+     * logo
+     */
+    @Column(name = "logo")
+    private String logo;
+    /**
+     * 应用描述
+     */
+    @Column(name = "description")
+    private String description;
+    /**
+     * 管理员、创建人
+     */
+    @Column(name = "admin")
+    private String admin;
+
+    /**
+     * 状态
+     */
+    @Column(name = "status")
+    private Integer status;
+
+    /**
+     * 应用访问凭证
+     */
+    @Column(name = "access_key")
+    private String accessKey;
+
+    /**
+     * 应用访问凭证秘钥
+     */
+    @Column(name = "secret_hash_key")
+    private String secretHashKey;
+}

+ 11 - 2
webchat-pay/src/main/java/com/webchat/pay/service/UserService.java → webchat-pay/src/main/java/com/webchat/pay/service/AccountService.java

@@ -14,12 +14,21 @@ import java.util.Set;
 
 @Slf4j
 @Service
-public class UserService {
-
+public class AccountService {
 
     @Autowired
     private UserServiceClient userServiceClient;
 
+    public UserBaseResponseInfoVO accountInfo(String account) {
+        APIResponseBean<UserBaseResponseInfoVO> responseBean = userServiceClient.userInfo(account);
+        if (APIResponseBeanUtil.isOk(responseBean)) {
+            return responseBean.getData();
+        }
+        /**
+         * 降级策略 TODO
+         */
+        return null;
+    }
 
     public Map<String, UserBaseResponseInfoVO> batchGet(Set<String> userIds) {
         APIResponseBean<Map<String, UserBaseResponseInfoVO>> responseBean = userServiceClient.batchGet(userIds);

+ 136 - 0
webchat-pay/src/main/java/com/webchat/pay/service/PaymentApiService.java

@@ -0,0 +1,136 @@
+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.util.AkSkGenerator;
+import com.webchat.common.util.IDGenerateUtil;
+import com.webchat.common.util.SignUtil;
+import com.webchat.domain.vo.dto.payment.AppAkSkDTO;
+import com.webchat.pay.config.exception.PaymentAccessAuthException;
+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 org.springframework.util.Assert;
+
+@Service
+public class PaymentApiService {
+
+    @Autowired
+    private PaymentAppService appService;
+
+    @Autowired
+    private RedisService redisService;
+
+
+    /**
+     * 交易凭证获取签名的有效期:5分钟
+     */
+    private static final Long SIGNATURE_EXPIRE_TIME = 5 * 60 * 1000L;
+
+    /**
+     * 生成交易凭证
+     *
+     * @param appId
+     * @param accessKey
+     * @param timestamp
+     * @param signature
+     * @param logId
+     * @return
+     */
+    public String token(Long appId,
+                        String accessKey,
+                        String secretKey,
+                        Long timestamp,
+                        String signature,
+                        String logId) {
+
+
+        /**
+         * 这里三重校验
+         * ------------------------
+         * 1. 校验访问凭证
+         * 2. 校验校验有效
+         * 3. 校验签名
+         */
+        // 1. 校验访问凭证
+        AppAkSkDTO akSkDTO = appService.getAppAkSk(appId);
+        if (akSkDTO == null) {
+            throw new PaymentAccessAuthException("应用不存在");
+        }
+        boolean checkAk = ObjectUtils.equals(akSkDTO.getAccessKey(), accessKey);
+        boolean checkSk = AkSkGenerator.checkKey(secretKey, akSkDTO.getSecretHashKey());
+        if (!(checkAk && checkSk)) {
+            throw new PaymentAccessAuthException("应用访问凭证校验失败");
+        }
+        // 2. 校验校验有效
+        Long diffTime = System.currentTimeMillis() - timestamp;
+        if (diffTime > SIGNATURE_EXPIRE_TIME) {
+            throw new PaymentAccessAuthException("签名已过期");
+        }
+        // 3. 校验签名
+        String validSignature = SignUtil.generateSignature(secretKey,
+                                                           String.valueOf(appId),
+                                                           accessKey,
+                                                           String.valueOf(timestamp));
+        if (!ObjectUtils.equals(signature, validSignature)) {
+            throw new PaymentAccessAuthException("签名校验失败");
+        }
+
+        /**
+         * 生成并返回接入方access-token(交易凭证,用于后续的交易校验)
+         */
+
+        return this.generateAccessToken(appId);
+    }
+
+    /**
+     * 获取交易凭证背后的应用信息
+     *
+     * @param accessToken
+     * @return
+     */
+    public Long getAppIdFromAccessToken(String accessToken) {
+        String accessTokenCacheKey = this.accessTokenCacheKey(accessToken);
+        String cache = redisService.get(accessTokenCacheKey);
+        if (StringUtils.isNotBlank(cache)) {
+            return Long.valueOf(cache);
+        }
+        return null;
+    }
+
+    private String generateAccessToken(Long appId) {
+        /**
+         * 生成交易凭证
+         */
+        String accessToken = IDGenerateUtil.uuid();
+        String accessTokenCacheKey = this.accessTokenCacheKey(accessToken);
+        redisService.set(accessTokenCacheKey, String.valueOf(appId),
+                RedisKeyEnum.PAYMENT_APP_ACCESS_TOKEN_CACHE.getExpireTime());
+        return accessToken;
+    }
+
+    private String accessTokenCacheKey(String accessToken) {
+
+        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);
+    }
+
+}

+ 205 - 0
webchat-pay/src/main/java/com/webchat/pay/service/PaymentAppService.java

@@ -0,0 +1,205 @@
+package com.webchat.pay.service;
+
+
+import com.webchat.common.constants.WebConstant;
+import com.webchat.common.enums.CommonStatusEnum;
+import com.webchat.common.enums.RedisKeyEnum;
+import com.webchat.common.service.RedisService;
+import com.webchat.common.util.AkSkGenerator;
+import com.webchat.common.util.JsonUtil;
+import com.webchat.domain.vo.dto.payment.AppAkSkDTO;
+import com.webchat.domain.vo.request.payment.CreateAppRequestVO;
+import com.webchat.domain.vo.response.payment.AppBaseResponseVO;
+import com.webchat.domain.vo.response.payment.CreateAppResponseVO;
+import com.webchat.pay.repository.dao.IAppDAO;
+import com.webchat.pay.repository.entity.AppEntity;
+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.util.Assert;
+
+@Service
+public class PaymentAppService {
+
+    @Autowired
+    private IAppDAO appDAO;
+
+    @Autowired
+    private RedisService redisService;
+
+    @Autowired
+    private AccountService accountService;
+
+    /**
+     * 获取接入方访问凭证,走redis
+     *
+     * @param appId
+     * @return
+     */
+    public AppAkSkDTO getAppAkSk(Long appId) {
+        String akskCacheKey = RedisKeyEnum.PAYMENT_APP_AK_SK_CACHE.getKey();
+        String cache = redisService.hget(akskCacheKey, String.valueOf(appId));
+        if (StringUtils.isNotBlank(cache)) {
+            return JsonUtil.fromJson(cache, AppAkSkDTO.class);
+        }
+        return this.refreshAppAkSkCache(appId);
+    }
+
+    /**
+     * 查询应用详情信息,走redis
+     *
+     * @param appId
+     * @return
+     */
+    public AppBaseResponseVO appInfo(Long appId) {
+        String appCacheKey = this.appCacheKey(appId);
+        String cache = redisService.get(appCacheKey);
+        if (StringUtils.isNotBlank(cache)) {
+            return JsonUtil.fromJson(cache, AppBaseResponseVO.class);
+        }
+        // 缓存防止击穿
+        String defaultCache = redisService.get(defaultAppCacheKey(appId));
+        if (ObjectUtils.equals(defaultCache, WebConstant.CACHE_NONE)) {
+            return null;
+        }
+        AppBaseResponseVO appBase = this.refreshAppInfoCache(appId);
+        if (appBase == null) {
+            this.setDefaultAppCache(appId);
+        }
+        return appBase;
+    }
+
+    /**
+     * 创建应用,接入支付平台
+     *
+     * @param createAppRequest
+     * @return
+     */
+    public CreateAppResponseVO saveApp(CreateAppRequestVO createAppRequest) {
+        /**
+         * 1. 持久化:创建 Or 更新
+         */
+        boolean isFirstCreate = createAppRequest.getId() == null;
+        AppEntity appEntity = convert(createAppRequest);
+        String sk = null;
+        if (isFirstCreate) {
+            /**
+             * 首次创建为应用生成支付凭证
+             */
+            String ak = AkSkGenerator.generateAk();
+            sk = AkSkGenerator.generateSk();
+            String secretHashKey = AkSkGenerator.hashKey(sk);
+            appEntity.setAccessKey(ak);
+            appEntity.setSecretHashKey(secretHashKey);
+        }
+        appEntity = appDAO.save(appEntity);
+        /**
+         * 2. 创建缓存
+         */
+        this.createOrUpdateRedisCache(appEntity);
+        /**
+         * 3. 构造应用信息页面响应VO
+         */
+        AppBaseResponseVO appBaseResponseVO = covertResponseVo(appEntity);;
+        CreateAppResponseVO createAppResponseVO = new CreateAppResponseVO();
+        BeanUtils.copyProperties(appBaseResponseVO, createAppResponseVO);
+        createAppResponseVO.setSecretKey(sk);
+        createAppResponseVO.setAdminInfo(accountService.accountInfo(appEntity.getAdmin()));
+        return createAppResponseVO;
+    }
+
+    private AppBaseResponseVO covertResponseVo(AppEntity appEntity) {
+        AppBaseResponseVO response = new AppBaseResponseVO();
+        BeanUtils.copyProperties(appEntity, response);
+        return response;
+    }
+
+    /**
+     * 创建或者更新应用信息Redis缓存
+     */
+    private void createOrUpdateRedisCache(AppEntity appEntity) {
+
+        // 1. 刷新应用最新详情缓存
+        this.refreshAppInfoCache(appEntity);
+
+        // 2. 创建或更新AK-SK缓存
+        this.refreshAppAkSkCache(appEntity.getId(), appEntity.getAccessKey(), appEntity.getSecretHashKey());
+    }
+
+
+    /**
+     * 刷新应用访问支付平台凭证缓存
+     *
+     * @param appId
+     * @return
+     */
+    private AppAkSkDTO refreshAppAkSkCache(Long appId) {
+
+        AppEntity app = appDAO.findById(appId).orElse(null);
+        return this.refreshAppAkSkCache(app.getId(), app.getAccessKey(), app.getSecretHashKey());
+    }
+
+    private AppAkSkDTO refreshAppAkSkCache(Long appId, String accessKey, String secretHashKey) {
+        String akskCacheKey = RedisKeyEnum.PAYMENT_APP_AK_SK_CACHE.getKey();
+        AppAkSkDTO appAkSkDTO = new AppAkSkDTO(accessKey, secretHashKey);
+        redisService.hset(akskCacheKey,
+                String.valueOf(appId), JsonUtil.toJsonString(appAkSkDTO),
+                RedisKeyEnum.PAYMENT_APP_AK_SK_CACHE.getExpireTime());
+        return appAkSkDTO;
+    }
+
+    private void setDefaultAppCache(Long appId) {
+        String defaultKey = this.defaultAppCacheKey(appId);
+        redisService.set(defaultKey, WebConstant.CACHE_NONE, RedisKeyEnum.PAYMENT_DEFAULT_APP_INFO_CACHE.getExpireTime());
+    }
+
+    private AppBaseResponseVO refreshAppInfoCache(Long appId) {
+        AppEntity appEntity = appDAO.findById(appId).orElse(null);
+        return this.refreshAppInfoCache(appEntity);
+    }
+
+    /**
+     * 刷新应用信息详情缓存
+     *
+     * @param appEntity
+     * @return
+     */
+    private AppBaseResponseVO refreshAppInfoCache(AppEntity appEntity) {
+        if (appEntity == null) {
+            return null;
+        }
+        AppBaseResponseVO appBaseResponseVO = this.covertResponseVo(appEntity);
+        String appCacheKey = this.appCacheKey(appEntity.getId());
+        redisService.set(appCacheKey, JsonUtil.toJsonString(appBaseResponseVO),
+                RedisKeyEnum.PAYMENT_APP_INFO_CACHE.getExpireTime());
+        return appBaseResponseVO;
+    }
+
+    private String defaultAppCacheKey(Long appId) {
+        return RedisKeyEnum.PAYMENT_DEFAULT_APP_INFO_CACHE.getKey(String.valueOf(appId));
+    }
+
+    private String appCacheKey(Long appId) {
+        return RedisKeyEnum.PAYMENT_APP_INFO_CACHE.getKey(String.valueOf(appId));
+    }
+
+    private AppEntity convert(CreateAppRequestVO createAppRequest) {
+        AppEntity appEntity;
+        if (createAppRequest.getId() != null) {
+            appEntity = appDAO.findById(createAppRequest.getId()).orElse(null);
+            Assert.notNull(appEntity, "应用信息更新失败:应用不存在!");
+        } else {
+            appEntity = new AppEntity();
+            appEntity.setAdmin(createAppRequest.getUserId());
+            appEntity.setCreateBy(createAppRequest.getUserId());
+            appEntity.setStatus(CommonStatusEnum.NEW.getStatusCode());
+        }
+        appEntity.setLogo(createAppRequest.getLogo());
+        appEntity.setName(createAppRequest.getName());
+        appEntity.setDescription(createAppRequest.getDescription());
+        appEntity.setUpdateBy(createAppRequest.getUserId());
+        return appEntity;
+    }
+}

+ 2 - 2
webchat-pay/src/main/java/com/webchat/pay/service/PaymentService.java

@@ -33,7 +33,7 @@ public class PaymentService {
     private IUserWalletDAO userWalletDAO;
 
     @Autowired
-    private UserService userService;
+    private AccountService accountService;
 
 
     public APIPageResponseBean<UserWalletDetailResponseVO> page(Integer transType, Integer eventType, String userId,
@@ -47,7 +47,7 @@ public class PaymentService {
         if (CollectionUtils.isNotEmpty(userWalletEntities)) {
             userWalletDetailResponseVOS = userWalletEntities.stream().map(this::convert).collect(Collectors.toList());
             Set<String> userIdSet = userWalletEntities.stream().map(UserWalletEntity::getUserId).collect(Collectors.toSet());
-            Map<String, UserBaseResponseInfoVO> userMap = userService.batchGet(userIdSet);
+            Map<String, UserBaseResponseInfoVO> userMap = accountService.batchGet(userIdSet);
             userWalletDetailResponseVOS.forEach(uw -> uw.setUser(userMap.get(uw.getUserId())));
         }
         return APIPageResponseBean.success(pageNo, pageSize, userWalletEntityPage.getTotalElements(), userWalletDetailResponseVOS);

+ 77 - 0
webchat-remote/src/main/java/com/webchat/rmi/pay/PaymentApiServiceClient.java

@@ -0,0 +1,77 @@
+package com.webchat.rmi.pay;
+
+import com.webchat.common.bean.APIResponseBean;
+import com.webchat.domain.dto.payment.PaymentTransRequestDTO;
+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;
+import org.springframework.web.bind.annotation.RequestHeader;
+
+import java.math.BigDecimal;
+
+@FeignClient(name = "webchat-pay-service", contextId = "paymentApiServiceClient")
+public interface PaymentApiServiceClient {
+
+    /**
+     * 获取交易接入方凭证
+     *
+     * @param accessKey
+     * @param sign 签名
+     * @return
+     */
+    @GetMapping("/pay-service/api/token")
+    APIResponseBean<String> accessToken(@RequestHeader(name = "app-id") Long appId,
+                                  @RequestHeader(name = "access-key") String accessKey,
+                                  @RequestHeader(name = "secret-key") String secretKey,
+                                  @RequestHeader Long timestamp,
+                                  @RequestHeader String signature,
+                                  @RequestHeader(name = "log-id") String logId);
+
+    /**
+     * 获取交易事务id(用于交易回滚,分布式事务一致性)
+     *
+     * @param token
+     * @return
+     */
+    @GetMapping("/pay-service/api/transId")
+    APIResponseBean<String> transId(@RequestHeader String token,
+                                    @RequestHeader String logId);
+
+    /**
+     * 真实交易接口
+     *
+     * @return
+     */
+    @PostMapping("/pay-service/api/doTrans")
+    APIResponseBean<Boolean> doTrans(@RequestBody PaymentTransRequestDTO paymentTransRequest,
+                                     @RequestHeader String token,
+                                     @RequestHeader String transId,
+                                     @RequestHeader String logId);
+
+
+    /**
+     * 交易回滚(保证分布式事务数据一致性)
+     *
+     * @param token
+     * @param transId
+     * @param logId
+     * @return
+     */
+    @PostMapping("/pay-service/api/rollback")
+    APIResponseBean<Boolean> rollback(@RequestHeader String token,
+                                      @RequestHeader String transId,
+                                      @RequestHeader String logId);
+
+    /**
+     * 查询账户余额
+     *
+     * @param userId
+     * @return
+     */
+    @GetMapping("/pay-service/api/balance/{userId}")
+    APIResponseBean<BigDecimal> balance(@PathVariable String userId,
+                                        @RequestHeader String token,
+                                        @RequestHeader String logId);
+}

+ 22 - 0
webchat-remote/src/main/java/com/webchat/rmi/pay/PaymentAppServiceClient.java

@@ -0,0 +1,22 @@
+package com.webchat.rmi.pay;
+
+import com.webchat.common.bean.APIResponseBean;
+import com.webchat.domain.vo.request.payment.CreateAppRequestVO;
+import com.webchat.domain.vo.response.payment.CreateAppResponseVO;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+
+@FeignClient(name = "webchat-pay-service", contextId = "paymentAppServiceClient")
+public interface PaymentAppServiceClient {
+
+
+    /**
+     * 支付平台创建应用(接入方)
+     *
+     * @param createAppRequest
+     * @return
+     */
+    @PostMapping("/pay-service/app/create")
+    APIResponseBean<CreateAppResponseVO> createApp(@RequestBody CreateAppRequestVO createAppRequest);
+}

+ 3 - 4
webchat-remote/src/main/java/com/webchat/rmi/pay/PaymentServiceClient.java → webchat-remote/src/main/java/com/webchat/rmi/pay/PaymentWalletServiceClient.java

@@ -7,18 +7,17 @@ import com.webchat.domain.vo.response.UserWalletDetailResponseVO;
 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.RequestParam;
 
 import java.math.BigDecimal;
 
-@FeignClient(name = "webchat-pay-service")
-public interface PaymentServiceClient {
-
+@FeignClient(name = "webchat-pay-service", contextId = "paymentWalletServiceClient")
+public interface PaymentWalletServiceClient {
 
     @GetMapping("/pay-service/wallet/balance/{userId}")
     APIResponseBean<BigDecimal> getBalance(@PathVariable String userId);
 
-
     @ValidatePermission
     @GetMapping("/pay-service/wallet/page")
     APIPageResponseBean<UserWalletDetailResponseVO> page(

+ 41 - 0
webchat-remote/src/main/java/com/webchat/rmi/ugc/RedPacketClient.java

@@ -0,0 +1,41 @@
+package com.webchat.rmi.ugc;
+
+
+import com.webchat.common.bean.APIResponseBean;
+import com.webchat.domain.vo.request.SendRedPacketRequestVO;
+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;
+
+/**
+ * webchat 红包服务远程RPC接口声明
+ *
+ * 问题:为什么红包收发RPC接口要声明在UGC服务,不应该是在PAY支付基础服务吗?
+ * 回答:红包收发归属业务场景(类如商城购买商品属于商城订单/下单业务)
+ * 而钱包扣除属于基础钱包交易应该由基于PAY服务支持。
+ */
+@FeignClient(name = "webchat-ugc-service", contextId = "redPacketClient")
+public interface RedPacketClient {
+
+    /**
+     * 红包发送 UGC 服务接口声明
+     *
+     * @param sendRedPacketRequest
+     * @return
+     */
+    @PostMapping("/ugc-service/chat/red-packet/send")
+    APIResponseBean<Long> send(@RequestBody SendRedPacketRequestVO sendRedPacketRequest);
+
+    /**
+     * 拆红包/抢红包
+     *
+     * @param redPacketId 红包id
+     * @param userId      拆包用户id
+     * @return
+     */
+    @GetMapping("/ugc-service/chat/red-packet/open/{redPacketId}/{userId}")
+    APIResponseBean<String> open(@PathVariable Long redPacketId, @PathVariable String userId);
+
+}

+ 18 - 0
webchat-ugc/src/main/java/com/webchat/ugc/config/PaymentAppConfig.java

@@ -0,0 +1,18 @@
+package com.webchat.ugc.config;
+
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+@Data
+@Component
+@ConfigurationProperties(prefix = "pay.config")
+public class PaymentAppConfig {
+
+    private Long appId;
+
+    private String accessKey;
+
+    private String secretKey;
+}

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

@@ -0,0 +1,33 @@
+package com.webchat.ugc.controller;
+
+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 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 RedPacketController implements RedPacketClient {
+
+    @Autowired
+    private RedPacketService redPacketService;
+
+    @Override
+    public APIResponseBean<Long> send(@RequestBody SendRedPacketRequestVO sendRedPacketRequest) {
+
+        Long redPacketId = redPacketService.send(sendRedPacketRequest);
+        return APIResponseBeanUtil.success(redPacketId);
+    }
+
+    @Override
+    public APIResponseBean<String> open(@PathVariable Long redPacketId, @PathVariable String userId) {
+
+        // 红包金额避免精度等引发的展示数据不准确问题,一般返回给VIEW层使用String类型较多
+        String money = redPacketService.open(redPacketId, userId);
+        return APIResponseBeanUtil.success(money);
+    }
+}

+ 133 - 0
webchat-ugc/src/main/java/com/webchat/ugc/service/PaymentService.java

@@ -0,0 +1,133 @@
+package com.webchat.ugc.service;
+
+
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.webchat.common.bean.APIResponseBean;
+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.util.JsonUtil;
+import com.webchat.common.util.SignUtil;
+import com.webchat.rmi.pay.PaymentApiServiceClient;
+import com.webchat.ugc.config.PaymentAppConfig;
+import lombok.extern.slf4j.Slf4j;
+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.util.concurrent.locks.ReentrantLock;
+
+
+@Slf4j
+@Service
+public class PaymentService {
+
+    @Autowired
+    private PaymentApiServiceClient paymentApiServiceClient;
+
+    @Autowired
+    private PaymentAppConfig paymentAppConfig;
+
+    @Autowired
+    private RedisService redisService;
+
+    private ReentrantLock lock = new ReentrantLock();
+
+    public String token() {
+        String key = this.getTokenCacheKey();
+        String cache = redisService.get(key);
+        if (StringUtils.isNotBlank(cache)) {
+            return cache;
+        }
+        String token;
+        try {
+            lock.lock();
+            // 双重检查锁
+            token = redisService.get(key);
+            if (StringUtils.isNotBlank(token)) {
+                return token;
+            }
+            token = refreshAccessTokenCache();
+        } catch (Exception e) {
+            throw new BusinessException("支付Token请求失败!");
+        } finally {
+            if (lock.isHeldByCurrentThread()) {
+                lock.unlock();
+            }
+        }
+        return token;
+    }
+
+    /**
+     * 刷新token redis缓存
+     *
+     * @return
+     */
+    private String refreshAccessTokenCache() {
+        String token = this.getTokenByClient();
+        String key = this.getTokenCacheKey();
+        redisService.set(key, token, RedisKeyEnum.PAYMENT_ACCESS_TOKEN_CACHE.getExpireTime());
+        return token;
+    }
+
+    /**
+     * 获取Token redis缓存
+     * @return
+     */
+    private String getTokenCacheKey() {
+        return RedisKeyEnum.PAYMENT_ACCESS_TOKEN_CACHE.getKey();
+    }
+
+    /**
+     * 获取token
+     *
+     * @return
+     */
+    public String getTokenByClient() {
+        String logId = generateLogId();
+        // 13位时间戳
+        Long timestamp = System.currentTimeMillis();
+        // 计算签名(5分钟内有效期)
+        // SHA256(sk, "appId - accessKey - timestamp")
+        String signature = SignUtil.generateSignature(paymentAppConfig.getSecretKey(),
+                                                      String.valueOf(paymentAppConfig.getAppId()),
+                                                      paymentAppConfig.getAccessKey(),
+                                                      String.valueOf(timestamp));
+
+
+        APIResponseBean<String> responseBean = paymentApiServiceClient.accessToken(paymentAppConfig.getAppId(),
+                                                                                   paymentAppConfig.getAccessKey(),
+                                                                                   paymentAppConfig.getSecretKey(),
+                                                                                   timestamp,
+                                                                                   signature,
+                                                                                   logId);
+        if (APIResponseBeanUtil.isOk(responseBean)) {
+            return responseBean.getData();
+        }
+        log.error("UGC 支付服务异常 =====> 获取Token异常!logId:{}, responseBean:{}",
+                logId, JsonUtil.toJsonString(responseBean));
+        throw new BusinessException("支付Token请求失败!");
+    }
+
+    public String transId() {
+        String accessToken = token();
+        String logId = generateLogId();
+        APIResponseBean<String> responseBean = paymentApiServiceClient.transId(accessToken, logId);
+        if (APIResponseBeanUtil.isOk(responseBean)) {
+            return responseBean.getData();
+        }
+        log.error("UGC 支付服务异常 =====> 获取TransId异常!logId:{}, responseBean:{}",
+                logId, JsonUtil.toJsonString(responseBean));
+        throw new BusinessException("支付TransId请求失败!");
+    }
+
+    private String generateLogId() {
+        // TODO 预留:
+        // 支持雪花算法ID生成
+        return null;
+    }
+}

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

@@ -0,0 +1,21 @@
+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;
+    }
+}

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

@@ -25,7 +25,7 @@ import com.webchat.user.repository.dao.IUserDAO;
 import com.webchat.user.repository.entity.GroupUserEntity;
 import com.webchat.user.repository.entity.UserEntity;
 import com.webchat.user.service.relation.AccountRelationFactory;
-import com.webchat.user.service.relation.User2FileSenderAccountRelationService;
+import com.webchat.user.service.relation.User2AIAgentSenderAccountRelationService;
 import com.webchat.user.service.relation.User2GroupAccountRelationService;
 import jakarta.persistence.criteria.Predicate;
 import jakarta.servlet.http.HttpServletRequest;
@@ -191,8 +191,8 @@ public class UserService {
         this.validateRegistryParam(userName, mobile);
         // 注册信息入库
         String uid = this.registryUser2DB(userName, photo, mobile, password);
-        // 默认添加文件传输助手
-        SpringContextUtil.getBean(User2FileSenderAccountRelationService.class).subscribe(uid, WebConstant.FILE_SEND_ID);
+        // 默认添加我的AI助手
+        SpringContextUtil.getBean(User2AIAgentSenderAccountRelationService.class).subscribe(uid, WebConstant.AI_AGENT_ID);
         // 注册成功,事务结束后刷新用户缓存信息
         TransactionSyncManagerUtil.registerSynchronization(() -> {
             // 刷新用户缓存

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

@@ -39,7 +39,7 @@ public class AccountRelationFactory implements InitializingBean, ApplicationCont
         services.put(RoleCodeEnum.ROBOT.getCode(), applicationContext.getBean(User2RobotAccountRelationService.class));
         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(User2FileSenderAccountRelationService.class));
+        services.put(RoleCodeEnum.FILE_ACCOUNT.getCode(), applicationContext.getBean(User2AIAgentSenderAccountRelationService.class));
     }
 
     /**
@@ -55,8 +55,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_FILE_SENDER.getType(),
-                applicationContext.getBean(User2FileSenderAccountRelationService.class));
+        serviceForType.put(AccountRelationTypeEnum.USER_AI_AGENT.getType(),
+                applicationContext.getBean(User2AIAgentSenderAccountRelationService.class));
     }
 
     public static AbstractAccountRelationService getServiceByCode(Integer roleCode) {

+ 2 - 2
webchat-user/src/main/java/com/webchat/user/service/relation/User2FileSenderAccountRelationService.java → webchat-user/src/main/java/com/webchat/user/service/relation/User2AIAgentSenderAccountRelationService.java

@@ -5,13 +5,13 @@ import org.springframework.stereotype.Service;
 
 
 @Service
-public class User2FileSenderAccountRelationService extends AbstractAccountRelationService{
+public class User2AIAgentSenderAccountRelationService extends AbstractAccountRelationService{
 
 
     @Override
     protected AccountRelationTypeEnum getRelationType() {
 
-        return AccountRelationTypeEnum.USER_FILE_SENDER;
+        return AccountRelationTypeEnum.USER_AI_AGENT;
     }
 
     @Override