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() { } ); // 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,可以在这里做转换 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 params = Collections.singletonMap("uuid", uuid); // 2. 发送请求 String resultJson = HttpUtil.get(url, params, DEFAULT_TIMEOUT); // 3. 校验结果 if (StrUtil.isEmpty(resultJson)) { throw new RuntimeException("调用一账通查询平台绑定信息接口失败:接口返回空结果"); } // 4. 解析结果 UnifiedUserCenterResponseVO> response = JSON.parseObject( resultJson, new TypeReference>>() { } ); // 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 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()); } } }