| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328 |
- package com.ylx.usercenter.service.impl;
- import cn.hutool.core.collection.CollUtil;
- import cn.hutool.core.util.ObjUtil;
- import cn.hutool.core.util.StrUtil;
- import cn.hutool.http.HttpUtil;
- import com.alibaba.fastjson.JSON;
- import com.alibaba.fastjson.JSONArray;
- import com.alibaba.fastjson.JSONObject;
- 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.OneAccountVO;
- import com.ylx.usercenter.domain.vo.UnifiedUserCenterResponseVO;
- 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;
- import java.util.Collections;
- import java.util.List;
- import java.util.Map;
- @Slf4j
- @Service
- public class UnifiedUserCenterServiceImpl implements UnifiedUserCenterService {
- @Value("${remote.user-center.base-url}")
- private String userCenterBaseUrl;
- @Value("${remote.night-fragrance.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";
- private static final String QUERY_CLIENTS_PATH = "/userApp/queryClients";
- private static final int DEFAULT_TIMEOUT = 5000; // 5秒超时
- @Resource
- private TWxUserService wxUserService;
- @Resource
- private LotteryCountService lotteryCountService;
- @Override
- public R<?> queryBind(UnifiedUserCenterDTO dto) {
- // 只需要关注业务路径和日志描述即可
- return executePost(QUERY_BIND_PATH, dto, "查询用户绑定信息");
- }
- @Override
- public R<?> bind(UnifiedUserCenterDTO dto) {
- try {
- // 1.检验参数信息
- validateDto(dto);
- // 2.获取用户信息
- TWxUser wxUser = this.wxUserService.getById(dto.getTargetUserId());
- if (ObjUtil.isEmpty(wxUser)) {
- log.error("用户信息不存在, userId: {}", dto.getTargetUserId());
- throw new ServiceException("用户信息不存在");
- }
- // 3.添加用户绑定信息
- R<?> remoteResult = executePost(BIND_PATH, dto, "添加用户绑定信息");
- // 4. 判断绑定成功
- if (ObjUtil.notEqual(remoteResult.getCode(), 200)) {
- log.error("远程绑定失败, userId: {}, msg: {}", dto.getTargetUserId(), remoteResult.getMsg());
- return R.fail("用户绑定失败:" + remoteResult.getMsg());
- }
- // 5. 更新本地数据库(独立事务)
- updateUserBindStatus(dto,wxUser);
- // 6. 同步抽奖次数(异步处理)
- syncLotteryCount(dto);
- return R.ok(remoteResult.getData(), "用户绑定成功");
- } catch (ServiceException e) {
- log.error("用户绑定业务异常, userId:{}", dto.getTargetUserId(), e);
- throw e;
- } catch (Exception e) {
- log.error("用户绑定系统异常, userId:{}", dto.getTargetUserId(), e);
- throw new ServiceException("系统异常,请稍后重试");
- }
- }
- @Override
- public R<?> unbind(UnifiedUserCenterDTO dto) {
- validateDto(dto);
- return executePost(UNBIND_PATH, dto, "解除用户绑定信息");
- }
- @Override
- public OneAccountVO queryClients(String uuid) {
- log.info("调用一账通查询平台绑定信息:{}", uuid);
- return this.queryClientsByUuid(uuid);
- }
- /**
- * 通用 HTTP POST 请求执行模板
- */
- private R<?> executePost(String path, UnifiedUserCenterDTO dto, String logDesc) {
- try {
- // 1. 构建 URL
- String url = userCenterBaseUrl + path;
- // 2. 转换参数
- String jsonParam = JSON.toJSONString(dto);
- // 3. 发送请求
- String result = HttpUtil.post(url, jsonParam);
- // 4. 校验结果
- if (StrUtil.isEmpty(result)) {
- throw new RuntimeException(logDesc + "失败:接口返回空结果");
- }
- // 5. 解析结果
- UnifiedUserCenterResponseVO response = JSON.parseObject(
- result,
- new TypeReference<UnifiedUserCenterResponseVO>() {
- }
- );
- // 4. 处理业务状态
- if (!response.isSuccess()) {
- log.warn("{}失败: data={},code={}, msg={}", logDesc, response.getData(), response.getCode(), response.getMessage());
- return R.fail(response.getData(), response.getMessage());
- }
- Object dataObj = response.getData();
- // 情况 A: data 是布尔值 false (虽然 code=0,但没数据)
- if (dataObj instanceof Boolean) {
- log.info("接口返回 data 为布尔值: {}", dataObj);
- return R.ok(response.getData());
- }
- // 情况 B: data 是 JSONObject (例如: { "userClients": [...] })
- if (dataObj instanceof JSONObject) {
- JSONObject jsonObject = (JSONObject) dataObj;
- // 优先尝试获取 "userClients" 字段 (针对当前截图的场景)
- Object userClientsObj = jsonObject.get("userClients");
- if (userClientsObj instanceof JSONArray) {
- JSONArray userClientsArray = (JSONArray) userClientsObj;
- log.info("获取到用户客户端数量: {}", userClientsArray.size());
- // 如果需要转为具体的 List<UserClientVO>,可以在这里做转换
- return R.ok(userClientsArray);
- }
- // 兜底逻辑:如果是其他类型的 JSONObject (例如查询详情),直接返回整个对象
- // 避免数据丢失
- return R.ok(jsonObject);
- }
- // 情况 C: data 是其他类型 (例如直接是一个 String 或 Number)
- return R.ok(dataObj);
- } catch (Exception e) {
- log.error("{}发生异常", logDesc, e);
- throw new ServiceException(logDesc + "异常" + e);
- }
- }
- /**
- * 参数校验提取
- */
- private void validateDto(UnifiedUserCenterDTO dto) {
- if (StrUtil.isEmpty(dto.getSourceUserId())) {
- throw new IllegalArgumentException("源用户ID不能为空");
- }
- if (StrUtil.isEmpty(dto.getTargetUserId())) {
- throw new IllegalArgumentException("目标用户ID不能为空");
- }
- if (StrUtil.isEmpty(dto.getSourceClientId())) {
- throw new IllegalArgumentException("源客户端ID不能为空");
- }
- if (StrUtil.isEmpty(dto.getTargetClientId())) {
- throw new IllegalArgumentException("目标客户端ID不能为空");
- }
- }
- // 独立事务方法
- @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
- public void updateUserBindStatus(UnifiedUserCenterDTO dto, TWxUser wxUser) {
- try {
- wxUser.setIsBind(1);
- wxUser.setLocalLiveUserId(dto.getSourceUserId());
- boolean wxUserBool = this.wxUserService.updateById(wxUser);
- if (!wxUserBool) {
- log.error("更新用户绑定状态失败, userId: {}", dto.getTargetUserId());
- throw new ServiceException("更新用户绑定状态失败");
- }
- log.info("更新用户绑定一账通信息成功, userId:{}", dto.getTargetUserId());
- } catch (Exception e) {
- log.error("更新用户绑定状态异常, userId: {}", dto.getTargetUserId(), 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.getTargetUserId(), retryCount);
- executePost(UNBIND_PATH, dto, "补偿:解除用户绑定信息");
- log.info("异步补偿:成功解除用户绑定, userId: {}", dto.getTargetUserId());
- success = true;
- } catch (Exception e) {
- log.error("异步补偿:解除用户绑定失败, userId: {}, 重试次数: {}", dto.getTargetUserId(), retryCount, e);
- if (retryCount < maxRetries) {
- try {
- // 等待一段时间后重试
- Thread.sleep(1000 * retryCount);
- } catch (InterruptedException ie) {
- Thread.currentThread().interrupt();
- log.error("异步补偿:线程被中断, userId: {}", dto.getTargetUserId(), ie);
- break;
- }
- } else {
- // 重试次数用完,记录告警日志
- log.error("【告警】异步补偿:解除用户绑定失败,已达最大重试次数,需要人工处理, userId: {}", dto.getSourceUserId(), e);
- }
- }
- }
- }
- private OneAccountVO queryClientsByUuid(String uuid) {
- try {
- // 1. 构建请求参数
- String url = userCenterBaseUrl + QUERY_CLIENTS_PATH;
- Map<String, Object> params = Collections.singletonMap("uuid", uuid);
- // 2. 发送请求
- String resultJson = HttpUtil.get(url, params, DEFAULT_TIMEOUT);
- // 3. 校验结果
- if (StrUtil.isEmpty(resultJson)) {
- throw new RuntimeException("调用一账通查询平台绑定信息接口失败:接口返回空结果");
- }
- // 4. 解析结果
- UnifiedUserCenterResponseVO<List<OneAccountVO>> response = JSON.parseObject(
- resultJson,
- new TypeReference<UnifiedUserCenterResponseVO<List<OneAccountVO>>>() {
- }
- );
- // 5. 处理业务状态
- if (ObjUtil.isNull(response) || !response.isSuccess()) {
- String msg = ObjUtil.isNotNull(response) ? response.getMessage() : "未知错误";
- Integer code = ObjUtil.isNotNull(response) ? response.getCode() : -1;
- log.warn("调用一账通查询平台绑定信息接口失败: code={}, msg={}", code, msg);
- throw new ServiceException("远程接口返回失败: " + msg);
- }
- // 6. 获取数据列表
- List<OneAccountVO> oneAccountVOs = response.getData();
- if (CollUtil.isEmpty(oneAccountVOs)) {
- log.info("未查询到相关活动数据");
- return null;
- }
- OneAccountVO oneAccount = oneAccountVOs.stream().filter(item -> item.getClientId().equals(clientId)).findFirst().orElse(null);
- log.info("成功获取调用一账通查询平台绑定信息: {}", JSON.toJSONString(oneAccount));
- return oneAccount;
- } catch (ServiceException e) {
- // 透传业务异常,不要重复包装
- throw e;
- } catch (Exception e) {
- log.error("调用一账通查询平台绑定信息接口发生系统异常", e);
- // 优化点:只抛出消息,不要直接把整个 Exception 对象 toString() 拼接到字符串里
- throw new ServiceException("调用一账通查询平台绑定信息接口异常: " + e.getMessage());
- }
- }
- }
|