Quellcode durchsuchen

一账通绑定用户,将本地未同步的抽奖次数同步到本地生活

wangzhijun vor 1 Woche
Ursprung
Commit
cf7277888e

+ 0 - 1
nightFragrance-admin/src/main/java/com/ylx/web/controller/usercenter/UnifiedUserCenterController.java

@@ -3,7 +3,6 @@ package com.ylx.web.controller.usercenter;
 import com.ylx.common.core.controller.BaseController;
 import com.ylx.common.core.domain.R;
 import com.ylx.usercenter.domain.dto.UnifiedUserCenterDTO;
-import com.ylx.usercenter.domain.vo.UnifiedUserClientVO;
 import com.ylx.usercenter.service.UnifiedUserCenterService;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;

+ 46 - 0
nightFragrance-massage/src/main/java/com/ylx/lottery/domain/LotteryCountLog.java

@@ -0,0 +1,46 @@
+package com.ylx.lottery.domain;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.ylx.common.annotation.Excel;
+import com.ylx.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.Date;
+
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class LotteryCountLog extends BaseEntity {
+    private static final long serialVersionUID = -7870788541798529977L;
+
+    private Long id;
+
+    @Excel(name = "用户的openID")
+    private String openId;
+
+    @Excel(name = "用户的userID")
+    private String userId;
+
+    @Excel(name = "用户电话")
+    private String userPhone;
+
+    @Excel(name = "活动类型:1.推广拉新 2.任务奖励  3.消费奖励")
+    private Integer activityType;
+
+    @Excel(name = "本地活动表ID")
+    private String localActivityTableId;
+
+    @Excel(name = "获得抽奖次数")
+    private Integer lotteryNum;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @Excel(name = "领取时间", dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date receiveTime;
+
+    @Excel(name = "是否删除 (1:是, 0:否)")
+    private String isDelete;
+
+    @Excel(name = "同步状态 0-未同步到本地生活 1-已同步到本地生活")
+    private Integer status;
+
+}

+ 24 - 0
nightFragrance-massage/src/main/java/com/ylx/lottery/domain/dto/LotteryCountDTO.java

@@ -0,0 +1,24 @@
+package com.ylx.lottery.domain.dto;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 抽奖次数增加请求DTO
+ */
+@Data
+public class LotteryCountDTO implements Serializable {
+    private static final long serialVersionUID = -39721081196232178L;
+
+    @ApiModelProperty(value = "客户id")
+    private String appUserId;
+
+    @ApiModelProperty(value = "抽奖次数")
+    private Integer count;
+
+    @ApiModelProperty(value = "活动id")
+    private String activityId;
+
+}

+ 33 - 0
nightFragrance-massage/src/main/java/com/ylx/lottery/domain/vo/LotteryCountResponseVO.java

@@ -0,0 +1,33 @@
+package com.ylx.lottery.domain.vo;
+
+import lombok.Data;
+
+@Data
+public class LotteryCountResponseVO {
+
+    /**
+     * 请求是否成功
+     */
+    private boolean success;
+
+    /**
+     * 状态码 (例如: 200表示成功)
+     */
+    private Integer code;
+
+    /**
+     * 响应消息/提示信息
+     */
+    private String message;
+
+    /**
+     * 具体的业务结果数据
+     * 此处示例为String,实际开发中可根据需要改为泛型 <T>
+     */
+    private Object result;
+
+    /**
+     * 服务器响应时间戳
+     */
+    private Long timestamp;
+}

+ 11 - 0
nightFragrance-massage/src/main/java/com/ylx/lottery/domain/vo/LotteryStatVO.java

@@ -0,0 +1,11 @@
+package com.ylx.lottery.domain.vo;
+
+import lombok.Data;
+
+@Data
+public class LotteryStatVO {
+
+    private String localActivityTableId; // 对应分组的字段
+
+    private Integer totalNum;            // 对应求和的字段
+}

+ 20 - 0
nightFragrance-massage/src/main/java/com/ylx/lottery/mapper/LotteryCountLogMapper.java

@@ -0,0 +1,20 @@
+package com.ylx.lottery.mapper;
+
+import com.baomidou.mybatisplus.core.conditions.Wrapper;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.baomidou.mybatisplus.core.toolkit.Constants;
+import com.ylx.lottery.domain.LotteryCountLog;
+import com.ylx.lottery.domain.vo.LotteryStatVO;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+
+public interface LotteryCountLogMapper extends BaseMapper<LotteryCountLog> {
+
+    @Select("SELECT local_activity_table_id, SUM(lottery_num) as totalNum " +
+            "FROM lottery_count_log " +
+            "${ew.customSqlSegment} " +
+            "GROUP BY local_activity_table_id")
+    List<LotteryStatVO> selectSumByGroup(@Param(Constants.WRAPPER) Wrapper wrapper);
+}

+ 13 - 0
nightFragrance-massage/src/main/java/com/ylx/lottery/service/LotteryCountLogService.java

@@ -0,0 +1,13 @@
+package com.ylx.lottery.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.ylx.lottery.domain.LotteryCountLog;
+import com.ylx.lottery.domain.vo.LotteryStatVO;
+
+import java.util.List;
+
+public interface LotteryCountLogService extends IService<LotteryCountLog> {
+
+    List<LotteryStatVO> selectSumByGroup(LambdaQueryWrapper<LotteryCountLog> queryWrapper);
+}

+ 9 - 0
nightFragrance-massage/src/main/java/com/ylx/lottery/service/LotteryCountService.java

@@ -0,0 +1,9 @@
+package com.ylx.lottery.service;
+
+import com.ylx.usercenter.domain.dto.UnifiedUserCenterDTO;
+
+public interface LotteryCountService {
+
+    boolean batchAdd(UnifiedUserCenterDTO dto);
+
+}

+ 20 - 0
nightFragrance-massage/src/main/java/com/ylx/lottery/service/impl/LotteryCountLogServiceImpl.java

@@ -0,0 +1,20 @@
+package com.ylx.lottery.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.ylx.lottery.domain.LotteryCountLog;
+import com.ylx.lottery.domain.vo.LotteryStatVO;
+import com.ylx.lottery.mapper.LotteryCountLogMapper;
+import com.ylx.lottery.service.LotteryCountLogService;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+public class LotteryCountLogServiceImpl extends ServiceImpl<LotteryCountLogMapper, LotteryCountLog> implements LotteryCountLogService {
+
+    @Override
+    public List<LotteryStatVO> selectSumByGroup(LambdaQueryWrapper<LotteryCountLog> queryWrapper) {
+        return this.baseMapper.selectSumByGroup(queryWrapper);
+    }
+}

+ 119 - 0
nightFragrance-massage/src/main/java/com/ylx/lottery/service/impl/LotteryCountServiceImpl.java

@@ -0,0 +1,119 @@
+package com.ylx.lottery.service.impl;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.http.HttpUtil;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.TypeReference;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.ylx.common.exception.ServiceException;
+import com.ylx.lottery.domain.LotteryCountLog;
+import com.ylx.lottery.domain.dto.LotteryCountDTO;
+import com.ylx.lottery.domain.vo.LotteryCountResponseVO;
+import com.ylx.lottery.domain.vo.LotteryStatVO;
+import com.ylx.lottery.service.LotteryCountLogService;
+import com.ylx.lottery.service.LotteryCountService;
+import com.ylx.usercenter.domain.dto.UnifiedUserCenterDTO;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.ArrayList;
+import java.util.List;
+
+@Slf4j
+@Service
+public class LotteryCountServiceImpl implements LotteryCountService {
+
+    @Value("${remote.local-live.base-url}")
+    private String localLiveBaseUrl;
+
+    // 定义接口路径常量
+    private static final String LOTTERY_COUNT_ADD_PATH = "/lottery/lotteryUserLotteryCount/batchAdd";
+    @Resource
+    private LotteryCountLogService lotteryCountLogService;
+
+    @Override
+    public boolean batchAdd(UnifiedUserCenterDTO dto) {
+
+        List<LotteryCountDTO> dtoList = new ArrayList<>();
+        // 1. 获取广誉源用户本地未同步抽奖次数
+        LambdaQueryWrapper<LotteryCountLog> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(LotteryCountLog::getUserId, dto.getSourceUserId());
+        queryWrapper.eq(LotteryCountLog::getStatus, 0);
+        queryWrapper.eq(LotteryCountLog::getIsDelete, 0);
+        List<LotteryStatVO> resultList = lotteryCountLogService.selectSumByGroup(queryWrapper);
+
+        if (CollUtil.isEmpty(resultList)) {
+            log.info("用户[{}]没有待同步的抽奖记录", dto.getSourceUserId());
+            return true;
+        }
+
+        // 2. 构建请求参数
+        for (LotteryStatVO statVO : resultList) {
+            LotteryCountDTO countDTO = new LotteryCountDTO();
+            countDTO.setAppUserId(dto.getTargetUserId());
+            countDTO.setCount(statVO.getTotalNum());
+            countDTO.setActivityId(statVO.getLocalActivityTableId());
+            dtoList.add(countDTO);
+        }
+
+        // 3. 调用接口同步抽奖次数
+        try {
+            // 1. 构建 URL
+            String url = localLiveBaseUrl + LOTTERY_COUNT_ADD_PATH;
+
+            // 2. 转换参数
+            String jsonParam = JSON.toJSONString(dtoList);
+
+            // 3. 发送请求
+            String result = HttpUtil.post(url, jsonParam);
+
+            // 4. 校验结果
+            if (StrUtil.isEmpty(result)) {
+                throw new RuntimeException("抽奖次数批量添加接口失败:接口返回空结果");
+            }
+
+            // 5. 解析结果
+            LotteryCountResponseVO response = JSON.parseObject(
+                    result,
+                    new TypeReference<LotteryCountResponseVO>() {
+                    }
+            );
+
+            // 4. 处理业务状态
+            if (!response.isSuccess()) {
+                log.warn("抽奖次数批量添加接口失败失败: code={}, msg={}",  response.getCode(), response.getMessage());
+                throw new ServiceException("远程接口返回失败: " + response.getMessage());
+            }
+
+            // 5. 更新本地数据库状态
+            updateSyncStatus(dto.getSourceUserId());
+
+            return true;
+
+        } catch (Exception e) {
+            log.error("抽奖次数批量添加接口发生异常", e);
+            throw new ServiceException("抽奖次数批量添加接口异常" + e);
+        }
+
+    }
+
+    private void updateSyncStatus(String userId) {
+        LambdaUpdateWrapper<LotteryCountLog> updateWrapper = new LambdaUpdateWrapper<>();
+        updateWrapper.eq(LotteryCountLog::getUserId, userId)
+                .eq(LotteryCountLog::getStatus, 0) // 只更新未同步的
+                .eq(LotteryCountLog::getIsDelete, 0)
+                .set(LotteryCountLog::getStatus, 1); // 1-已同步
+
+        boolean updateResult = lotteryCountLogService.update(updateWrapper);
+        if (!updateResult) {
+            log.warn("用户[{}]抽奖记录状态更新失败", userId);
+        } else {
+            log.info("用户[{}]抽奖记录状态更新成功", userId);
+        }
+    }
+
+}

+ 137 - 9
nightFragrance-massage/src/main/java/com/ylx/usercenter/service/impl/UnifiedUserCenterServiceImpl.java

@@ -7,27 +7,39 @@ import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.TypeReference;
 import com.ylx.common.core.domain.R;
 import com.ylx.common.exception.ServiceException;
+import com.ylx.lottery.service.LotteryCountService;
+import com.ylx.massage.domain.TWxUser;
+import com.ylx.massage.service.TWxUserService;
 import com.ylx.usercenter.domain.dto.UnifiedUserCenterDTO;
 import com.ylx.usercenter.domain.vo.UnifiedUserCenterResponseVO;
 import com.ylx.usercenter.domain.vo.UnifiedUserClientListVO;
 import com.ylx.usercenter.service.UnifiedUserCenterService;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Value;
+import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Propagation;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
 
 @Slf4j
 @Service
 public class UnifiedUserCenterServiceImpl implements UnifiedUserCenterService {
 
     @Value("${remote.user-center.base-url}")
-    private String baseUrl;
-    @Value("${remote.user-center.client-id}")
+    private String userCenterBaseUrl;
+    @Value("${remote.local-live.client-id}")
     private String clientId;
 
     // 定义接口路径常量
     private static final String QUERY_BIND_PATH = "/userApp/queryBind";
     private static final String BIND_PATH = "/userApp/bind";
     private static final String UNBIND_PATH = "/userApp/unbind";
+    @Resource
+    private TWxUserService wxUserService;
+    @Resource
+    private LotteryCountService lotteryCountService;
 
     @Override
     public R<?> queryBind(UnifiedUserCenterDTO dto) {
@@ -39,8 +51,42 @@ public class UnifiedUserCenterServiceImpl implements UnifiedUserCenterService {
 
     @Override
     public R<?> bind(UnifiedUserCenterDTO dto) {
-        validateDto(dto);
-        return executePost(BIND_PATH, dto, "添加用户绑定信息");
+
+        try {
+            // 1.检验参数信息
+            validateDto(dto);
+
+            // 2.获取用户信息
+            TWxUser wxUser = this.wxUserService.getById(dto.getSourceUserId());
+            if (ObjUtil.isEmpty(wxUser)) {
+                log.error("用户信息不存在, userId: {}", dto.getSourceUserId());
+                throw new ServiceException("用户信息不存在");
+            }
+
+            // 3.添加用户绑定信息
+            R<?> remoteResult = executePost(BIND_PATH, dto, "添加用户绑定信息");
+
+            // 4. 判断绑定成功
+            if (!remoteResult.getData().equals(Boolean.TRUE)) {
+                log.error("远程绑定失败, userId: {}, msg: {}", dto.getSourceUserId(), remoteResult.getMsg());
+                return R.fail("用户绑定失败:" + remoteResult.getMsg());
+            }
+
+            // 5. 更新本地数据库(独立事务)
+            updateUserBindStatus(dto);
+
+            // 6. 同步抽奖次数(异步处理)
+            syncLotteryCount(dto);
+
+            return R.ok(remoteResult.getData(), "用户绑定成功");
+
+        } catch (ServiceException e) {
+            log.error("用户绑定业务异常, userId:{}", dto.getSourceUserId(), e);
+            throw e;
+        } catch (Exception e) {
+            log.error("用户绑定系统异常, userId:{}", dto.getSourceUserId(), e);
+            throw new ServiceException("系统异常,请稍后重试");
+        }
     }
 
     @Override
@@ -55,11 +101,10 @@ public class UnifiedUserCenterServiceImpl implements UnifiedUserCenterService {
     private R<?> executePost(String path, UnifiedUserCenterDTO dto, String logDesc) {
         try {
             // 1. 构建 URL
-            String url = baseUrl + path;
+            String url = userCenterBaseUrl + path;
 
             // 2. 转换参数
             String jsonParam = JSON.toJSONString(dto);
-            log.info("调用远程接口: {}, 参数: {}", url, jsonParam);
 
             // 3. 发送请求
             String result = HttpUtil.post(url, jsonParam);
@@ -78,7 +123,7 @@ public class UnifiedUserCenterServiceImpl implements UnifiedUserCenterService {
 
             // 4. 处理业务状态
             if (!response.isSuccess()) {
-                log.warn("查询绑定信息失败: data={},code={}, msg={}", response.getData(), response.getCode(), response.getMessage());
+                log.warn("{}失败: data={},code={}, msg={}", logDesc, response.getData(), response.getCode(), response.getMessage());
                 return R.fail(response.getData(), response.getMessage());
             }
 
@@ -109,8 +154,91 @@ public class UnifiedUserCenterServiceImpl implements UnifiedUserCenterService {
      * 参数校验提取
      */
     private void validateDto(UnifiedUserCenterDTO dto) {
-        if (ObjUtil.isNull(dto)) {
-            throw new IllegalArgumentException("请求参数不能为空");
+
+        if (StrUtil.isEmpty(dto.getSourceUserId())) {
+            throw new IllegalArgumentException("源用户ID不能为空");
+        }
+        if (StrUtil.isEmpty(dto.getTargetUserId())) {
+            throw new IllegalArgumentException("目标用户ID不能为空");
+        }
+        if (StrUtil.isEmpty(dto.getClientId())) {
+            throw new IllegalArgumentException("客户端ID不能为空");
+        }
+    }
+
+    // 独立事务方法
+    @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
+    public void updateUserBindStatus(UnifiedUserCenterDTO dto) {
+        try {
+            TWxUser wxUser = this.wxUserService.getById(dto.getSourceUserId());
+            wxUser.setIsBind(1);
+            boolean wxUserBool = this.wxUserService.updateById(wxUser);
+            if (!wxUserBool) {
+                log.error("更新用户绑定状态失败, userId: {}", dto.getSourceUserId());
+                throw new ServiceException("更新用户绑定状态失败");
+            }
+            log.info("更新用户绑定一账通信息成功, userId:{}", dto.getSourceUserId());
+        } catch (Exception e) {
+            log.error("更新用户绑定状态异常, userId: {}", dto.getSourceUserId(), e);
+            // 如果本地更新失败,异步调用远程解绑接口进行补偿
+            asyncCompensateUnbind(dto);
+            throw e;
+        }
+    }
+
+
+    // 异步同步抽奖次数
+    @Async
+    public void syncLotteryCount(UnifiedUserCenterDTO dto) {
+        try {
+            boolean lotteryCountBool = this.lotteryCountService.batchAdd(dto);
+            if (lotteryCountBool) {
+                log.info("同步用户抽奖次数到本地生活服务成功, userId:{}", dto.getTargetUserId());
+            } else {
+                log.warn("同步用户抽奖次数到本地生活服务失败, userId:{}", dto.getTargetUserId());
+                // 同步失败,记录告警日志
+                log.error("【告警】同步用户抽奖次数到本地生活服务失败,需要人工处理, userId:{}", dto.getTargetUserId());
+            }
+        } catch (Exception e) {
+            log.error("同步用户抽奖次数到本地生活服务异常, userId:{}", dto.getTargetUserId(), e);
+            // 异常情况,记录告警日志
+            log.error("【告警】同步用户抽奖次数到本地生活服务异常,需要人工处理, userId:{}", dto.getTargetUserId(), e);
+        }
+    }
+
+    /**
+     * 异步补偿:解除用户绑定
+     * 带重试机制,最多重试3次
+     */
+    @Async
+    public void asyncCompensateUnbind(UnifiedUserCenterDTO dto) {
+        final int maxRetries = 3;
+        int retryCount = 0;
+        boolean success = false;
+
+        while (retryCount < maxRetries && !success) {
+            retryCount++;
+            try {
+                log.info("异步补偿:尝试解除用户绑定, userId: {}, 重试次数: {}", dto.getSourceUserId(), retryCount);
+                executePost(UNBIND_PATH, dto, "补偿:解除用户绑定信息");
+                log.info("异步补偿:成功解除用户绑定, userId: {}", dto.getSourceUserId());
+                success = true;
+            } catch (Exception e) {
+                log.error("异步补偿:解除用户绑定失败, userId: {}, 重试次数: {}", dto.getSourceUserId(), retryCount, e);
+                if (retryCount < maxRetries) {
+                    try {
+                        // 等待一段时间后重试
+                        Thread.sleep(1000 * retryCount);
+                    } catch (InterruptedException ie) {
+                        Thread.currentThread().interrupt();
+                        log.error("异步补偿:线程被中断, userId: {}", dto.getSourceUserId(), ie);
+                        break;
+                    }
+                } else {
+                    // 重试次数用完,记录告警日志
+                    log.error("【告警】异步补偿:解除用户绑定失败,已达最大重试次数,需要人工处理, userId: {}", dto.getSourceUserId(), e);
+                }
+            }
         }
     }
 }