UnifiedUserCenterServiceImpl.java 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. package com.ylx.usercenter.service.impl;
  2. import cn.hutool.core.util.ObjUtil;
  3. import cn.hutool.core.util.StrUtil;
  4. import cn.hutool.http.HttpUtil;
  5. import com.alibaba.fastjson.JSON;
  6. import com.alibaba.fastjson.TypeReference;
  7. import com.ylx.common.core.domain.R;
  8. import com.ylx.common.exception.ServiceException;
  9. import com.ylx.lottery.service.LotteryCountService;
  10. import com.ylx.massage.domain.TWxUser;
  11. import com.ylx.massage.service.TWxUserService;
  12. import com.ylx.usercenter.domain.dto.UnifiedUserCenterDTO;
  13. import com.ylx.usercenter.domain.vo.UnifiedUserCenterResponseVO;
  14. import com.ylx.usercenter.domain.vo.UnifiedUserClientListVO;
  15. import com.ylx.usercenter.service.UnifiedUserCenterService;
  16. import lombok.extern.slf4j.Slf4j;
  17. import org.springframework.beans.factory.annotation.Value;
  18. import org.springframework.scheduling.annotation.Async;
  19. import org.springframework.stereotype.Service;
  20. import org.springframework.transaction.annotation.Propagation;
  21. import org.springframework.transaction.annotation.Transactional;
  22. import javax.annotation.Resource;
  23. @Slf4j
  24. @Service
  25. public class UnifiedUserCenterServiceImpl implements UnifiedUserCenterService {
  26. @Value("${remote.user-center.base-url}")
  27. private String userCenterBaseUrl;
  28. @Value("${remote.local-live.client-id}")
  29. private String clientId;
  30. // 定义接口路径常量
  31. private static final String QUERY_BIND_PATH = "/userApp/queryBind";
  32. private static final String BIND_PATH = "/userApp/bind";
  33. private static final String UNBIND_PATH = "/userApp/unbind";
  34. @Resource
  35. private TWxUserService wxUserService;
  36. @Resource
  37. private LotteryCountService lotteryCountService;
  38. @Override
  39. public R<?> queryBind(UnifiedUserCenterDTO dto) {
  40. validateDto(dto);
  41. // dto.setClientId(clientId);
  42. // 只需要关注业务路径和日志描述即可
  43. return executePost(QUERY_BIND_PATH, dto, "查询用户绑定信息");
  44. }
  45. @Override
  46. public R<?> bind(UnifiedUserCenterDTO dto) {
  47. try {
  48. // 1.检验参数信息
  49. validateDto(dto);
  50. // 2.获取用户信息
  51. TWxUser wxUser = this.wxUserService.getById(dto.getSourceUserId());
  52. if (ObjUtil.isEmpty(wxUser)) {
  53. log.error("用户信息不存在, userId: {}", dto.getSourceUserId());
  54. throw new ServiceException("用户信息不存在");
  55. }
  56. // 3.添加用户绑定信息
  57. R<?> remoteResult = executePost(BIND_PATH, dto, "添加用户绑定信息");
  58. // 4. 判断绑定成功
  59. if (!remoteResult.getData().equals(Boolean.TRUE)) {
  60. log.error("远程绑定失败, userId: {}, msg: {}", dto.getSourceUserId(), remoteResult.getMsg());
  61. return R.fail("用户绑定失败:" + remoteResult.getMsg());
  62. }
  63. // 5. 更新本地数据库(独立事务)
  64. updateUserBindStatus(dto);
  65. // 6. 同步抽奖次数(异步处理)
  66. syncLotteryCount(dto);
  67. return R.ok(remoteResult.getData(), "用户绑定成功");
  68. } catch (ServiceException e) {
  69. log.error("用户绑定业务异常, userId:{}", dto.getSourceUserId(), e);
  70. throw e;
  71. } catch (Exception e) {
  72. log.error("用户绑定系统异常, userId:{}", dto.getSourceUserId(), e);
  73. throw new ServiceException("系统异常,请稍后重试");
  74. }
  75. }
  76. @Override
  77. public R<?> unbind(UnifiedUserCenterDTO dto) {
  78. validateDto(dto);
  79. return executePost(UNBIND_PATH, dto, "解除用户绑定信息");
  80. }
  81. /**
  82. * 通用 HTTP POST 请求执行模板
  83. */
  84. private R<?> executePost(String path, UnifiedUserCenterDTO dto, String logDesc) {
  85. try {
  86. // 1. 构建 URL
  87. String url = userCenterBaseUrl + path;
  88. // 2. 转换参数
  89. String jsonParam = JSON.toJSONString(dto);
  90. // 3. 发送请求
  91. String result = HttpUtil.post(url, jsonParam);
  92. // 4. 校验结果
  93. if (StrUtil.isEmpty(result)) {
  94. throw new RuntimeException(logDesc + "失败:接口返回空结果");
  95. }
  96. // 5. 解析结果
  97. UnifiedUserCenterResponseVO response = JSON.parseObject(
  98. result,
  99. new TypeReference<UnifiedUserCenterResponseVO>() {
  100. }
  101. );
  102. // 4. 处理业务状态
  103. if (!response.isSuccess()) {
  104. log.warn("{}失败: data={},code={}, msg={}", logDesc, response.getData(), response.getCode(), response.getMessage());
  105. return R.fail(response.getData(), response.getMessage());
  106. }
  107. // 5. 处理 data 字段
  108. Object dataObj = response.getData();
  109. // 情况 A: data 是布尔值 false (虽然 code=0,但没数据)
  110. if (dataObj instanceof Boolean) {
  111. log.info("接口返回 data 为布尔值: {}", dataObj);
  112. return R.ok(response.getData());
  113. }
  114. // 情况 B: data 是包含列表的对象 { "userClients": [...] }
  115. if (dataObj instanceof UnifiedUserClientListVO) {
  116. UnifiedUserClientListVO wrapper = (UnifiedUserClientListVO) dataObj;
  117. return R.ok(wrapper.getUserClients(), response.getMessage());
  118. }
  119. return R.ok();
  120. } catch (Exception e) {
  121. log.error("{}发生异常", logDesc, e);
  122. throw new ServiceException(logDesc + "异常" + e);
  123. }
  124. }
  125. /**
  126. * 参数校验提取
  127. */
  128. private void validateDto(UnifiedUserCenterDTO dto) {
  129. if (StrUtil.isEmpty(dto.getSourceUserId())) {
  130. throw new IllegalArgumentException("源用户ID不能为空");
  131. }
  132. if (StrUtil.isEmpty(dto.getTargetUserId())) {
  133. throw new IllegalArgumentException("目标用户ID不能为空");
  134. }
  135. if (StrUtil.isEmpty(dto.getClientId())) {
  136. throw new IllegalArgumentException("客户端ID不能为空");
  137. }
  138. }
  139. // 独立事务方法
  140. @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
  141. public void updateUserBindStatus(UnifiedUserCenterDTO dto) {
  142. try {
  143. TWxUser wxUser = this.wxUserService.getById(dto.getSourceUserId());
  144. wxUser.setIsBind(1);
  145. boolean wxUserBool = this.wxUserService.updateById(wxUser);
  146. if (!wxUserBool) {
  147. log.error("更新用户绑定状态失败, userId: {}", dto.getSourceUserId());
  148. throw new ServiceException("更新用户绑定状态失败");
  149. }
  150. log.info("更新用户绑定一账通信息成功, userId:{}", dto.getSourceUserId());
  151. } catch (Exception e) {
  152. log.error("更新用户绑定状态异常, userId: {}", dto.getSourceUserId(), e);
  153. // 如果本地更新失败,异步调用远程解绑接口进行补偿
  154. asyncCompensateUnbind(dto);
  155. throw e;
  156. }
  157. }
  158. // 异步同步抽奖次数
  159. @Async
  160. public void syncLotteryCount(UnifiedUserCenterDTO dto) {
  161. try {
  162. boolean lotteryCountBool = this.lotteryCountService.batchAdd(dto);
  163. if (lotteryCountBool) {
  164. log.info("同步用户抽奖次数到本地生活服务成功, userId:{}", dto.getTargetUserId());
  165. } else {
  166. log.warn("同步用户抽奖次数到本地生活服务失败, userId:{}", dto.getTargetUserId());
  167. // 同步失败,记录告警日志
  168. log.error("【告警】同步用户抽奖次数到本地生活服务失败,需要人工处理, userId:{}", dto.getTargetUserId());
  169. }
  170. } catch (Exception e) {
  171. log.error("同步用户抽奖次数到本地生活服务异常, userId:{}", dto.getTargetUserId(), e);
  172. // 异常情况,记录告警日志
  173. log.error("【告警】同步用户抽奖次数到本地生活服务异常,需要人工处理, userId:{}", dto.getTargetUserId(), e);
  174. }
  175. }
  176. /**
  177. * 异步补偿:解除用户绑定
  178. * 带重试机制,最多重试3次
  179. */
  180. @Async
  181. public void asyncCompensateUnbind(UnifiedUserCenterDTO dto) {
  182. final int maxRetries = 3;
  183. int retryCount = 0;
  184. boolean success = false;
  185. while (retryCount < maxRetries && !success) {
  186. retryCount++;
  187. try {
  188. log.info("异步补偿:尝试解除用户绑定, userId: {}, 重试次数: {}", dto.getSourceUserId(), retryCount);
  189. executePost(UNBIND_PATH, dto, "补偿:解除用户绑定信息");
  190. log.info("异步补偿:成功解除用户绑定, userId: {}", dto.getSourceUserId());
  191. success = true;
  192. } catch (Exception e) {
  193. log.error("异步补偿:解除用户绑定失败, userId: {}, 重试次数: {}", dto.getSourceUserId(), retryCount, e);
  194. if (retryCount < maxRetries) {
  195. try {
  196. // 等待一段时间后重试
  197. Thread.sleep(1000 * retryCount);
  198. } catch (InterruptedException ie) {
  199. Thread.currentThread().interrupt();
  200. log.error("异步补偿:线程被中断, userId: {}", dto.getSourceUserId(), ie);
  201. break;
  202. }
  203. } else {
  204. // 重试次数用完,记录告警日志
  205. log.error("【告警】异步补偿:解除用户绑定失败,已达最大重试次数,需要人工处理, userId: {}", dto.getSourceUserId(), e);
  206. }
  207. }
  208. }
  209. }
  210. }