package com.rj.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.rj.dto.hxr.HxrMoneyCouponApiContext; import com.rj.dto.hxr.HxrUserLoginApiContext; import com.rj.entity.LbThirdIntegrationConfig; import com.rj.entity.LbUser; import com.rj.entity.LbUserCoupon; import com.rj.mapper.LbUserCouponMapper; import com.rj.service.HxrAdminUserLoginService; import com.rj.service.ILbThirdIntegrationConfigService; import com.rj.service.ILbUserCouponService; import com.rj.service.ILbUserService; import com.rj.util.HxrGoodsSignUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import java.math.BigDecimal; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.security.SecureRandom; import java.time.Duration; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.UUID; @Slf4j @Service public class LbUserCouponServiceImpl extends ServiceImpl implements ILbUserCouponService { private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); private static final String DEFAULT_SIMULATE_LOGIN_PASSWORD = "123456"; private static final String USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36 Edg/148.0.0.0"; private static final String HEADER_TOKEN = "token"; private static final SecureRandom RANDOM = new SecureRandom(); private static final ObjectMapper JSON = new ObjectMapper() .setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE) .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); private final ILbUserService lbUserService; private final ILbThirdIntegrationConfigService lbThirdIntegrationConfigService; private final HxrAdminUserLoginService hxrAdminUserLoginService; public LbUserCouponServiceImpl( ILbUserService lbUserService, ILbThirdIntegrationConfigService lbThirdIntegrationConfigService, HxrAdminUserLoginService hxrAdminUserLoginService) { this.lbUserService = lbUserService; this.lbThirdIntegrationConfigService = lbThirdIntegrationConfigService; this.hxrAdminUserLoginService = hxrAdminUserLoginService; } @Override public Map syncUserCoupons(String tenantId, Integer recentDays) { Map result = new HashMap<>(); try { if (tenantId == null || tenantId.trim().isEmpty()) { result.put("success", false); result.put("message", "tenantId不能为空"); return result; } if (recentDays == null || recentDays < 1) { result.put("success", false); result.put("message", "recentDays必须大于0"); return result; } String tid = tenantId.trim(); // 获取第三方集成配置 LbThirdIntegrationConfig config = lbThirdIntegrationConfigService.getOne( new LambdaQueryWrapper() .eq(LbThirdIntegrationConfig::getTenantId, tid) .eq(LbThirdIntegrationConfig::getEnabled, 1) .last("LIMIT 1")); if (config == null) { result.put("success", false); result.put("message", "未找到该租户的第三方集成配置,或配置未启用"); return result; } // 获取登录上下文 Optional loginCtxOpt = lbThirdIntegrationConfigService.resolveUserLoginApiContext(tid); if (loginCtxOpt.isEmpty()) { result.put("success", false); result.put("message", "未找到登录API配置"); return result; } HxrUserLoginApiContext loginCtx = loginCtxOpt.get(); // 计算日期范围:现在 - recentDays 天 LocalDateTime startDate = LocalDateTime.now().minusDays(recentDays); // 查询用户列表,根据 updated_at 过滤,mobile 和 nickname 去重 List allUsers = lbUserService.list( new LambdaQueryWrapper() .eq(LbUser::getTenantId, tid) .ge(LbUser::getUpdatedAt, startDate) .ge(LbUser::getCoupon, 100) .isNotNull(LbUser::getMobile) .ne(LbUser::getMobile, "") .orderByDesc(LbUser::getUpdatedAt) .last("LIMIT 5000")); // 根据 mobile 和 nickname 去重 Map uniqueUserMap = new LinkedHashMap<>(); for (LbUser user : allUsers) { String key = user.getMobile() + "|" + (user.getNickname() != null ? user.getNickname() : ""); uniqueUserMap.put(key, user); } List userList = new ArrayList<>(uniqueUserMap.values()); log.info("同步用户优惠券:tenantId={}, recentDays={}, 过滤后用户数={}", tid, recentDays, userList.size()); int successCount = 0; int failCount = 0; for (LbUser user : userList) { String mobile = user.getMobile(); if (!StringUtils.hasText(mobile)) { continue; } try { // 调用登录接口 HxrAdminUserLoginService.LoginApiResult loginResult = hxrAdminUserLoginService.login(mobile, DEFAULT_SIMULATE_LOGIN_PASSWORD, loginCtx); if (!loginResult.success()) { log.warn("用户登录失败 mobile={} apiCode={} apiMsg={}", mobile, loginResult.apiCode(), loginResult.apiMsg()); failCount++; continue; } // 提取 token String token = HxrAdminUserLoginService.extractToken(loginResult.parsed()); if (!StringUtils.hasText(token)) { log.warn("登录成功但无法提取token mobile={}", mobile); failCount++; continue; } // 获取优惠券API上下文 Optional couponCtxOpt = lbThirdIntegrationConfigService.resolveMoneyCouponApiContext(tid, token); if (couponCtxOpt.isEmpty()) { log.warn("无法获取优惠券API上下文 mobile={}", mobile); failCount++; continue; } HxrMoneyCouponApiContext couponCtx = couponCtxOpt.get(); // 调用优惠券列表API String couponJson = fetchMoneyCouponList(couponCtx, recentDays); if (couponJson == null || couponJson.isBlank()) { log.warn("优惠券列表为空 mobile={}", mobile); continue; } // 解析并保存优惠券 int saved = parseAndSaveCoupons(couponJson, user, tid); successCount += saved; } catch (Exception e) { log.error("处理用户优惠券异常 mobile={}", mobile, e); failCount++; } } result.put("success", true); result.put("message", "同步完成"); result.put("totalUsers", userList.size()); result.put("successCount", successCount); result.put("failCount", failCount); return result; } catch (Exception e) { log.error("同步用户优惠券异常", e); result.put("success", false); result.put("message", "同步异常:" + e.getMessage()); return result; } } private String fetchMoneyCouponList(HxrMoneyCouponApiContext ctx, int recentDays) throws Exception { String couponApiBaseUrl = ctx.couponApiBaseUrl(); if (!StringUtils.hasText(couponApiBaseUrl)) { return null; } // 每页条数使用配置值 int pageSize = ctx.pageLimit() > 0 ? ctx.pageLimit() : 10; // 总共需要获取的条数 int totalNeeded = recentDays > 0 ? recentDays : 90; List allItems = new ArrayList<>(); int page = 1; int fetched = 0; HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(60)) .followRedirects(HttpClient.Redirect.NORMAL) .build(); while (fetched < totalNeeded) { int limit = Math.min(pageSize, totalNeeded - fetched); int cate = 2; int type = 1; String uri = couponApiBaseUrl + (couponApiBaseUrl.contains("?") ? "&" : "?") + "cate=" + cate + "&type=" + type + "&page=" + page + "&limit=" + limit; long timestamp = System.currentTimeMillis() / 1000; String noncestr = randomNoncestr(); Map signParams = new LinkedHashMap<>(); signParams.put("cate", cate); signParams.put("type", type); signParams.put("page", page); signParams.put("limit", limit); signParams.put("timestamp", timestamp); signParams.put("noncestr", noncestr); String sign = HxrGoodsSignUtil.computeSign(signParams, ctx.appStr().trim()); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(uri)) .timeout(Duration.ofSeconds(120)) .header("Accept", "application/json,*/*") .header("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8") .header("Origin", ctx.origin()) .header("Referer", ctx.referer()) .header("User-Agent", USER_AGENT) .header(HEADER_TOKEN, ctx.token()) .header("S", sign) .header("T", String.valueOf(timestamp)) .header("N", noncestr) .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); int status = response.statusCode(); if (status < 200 || status >= 300) { log.warn("优惠券API HTTP {} bodyPrefix={}", status, abbreviate(response.body(), 400)); break; } JsonNode root = JSON.readTree(response.body()); int code = root.path("code").asInt(-1); if (code != 0) { log.warn("优惠券API返回错误 code={} msg={}", code, root.path("msg").asText("")); break; } JsonNode listNode = root.path("data").path("list"); if (listNode.isMissingNode() || !listNode.isArray() || listNode.size() == 0) { log.info("优惠券API返回空列表 page={}", page); break; } for (JsonNode item : listNode) { if (fetched >= totalNeeded) { break; } allItems.add(item); fetched++; } // 如果返回的数据少于请求的limit,说明没有更多数据了 if (listNode.size() < limit) { break; } page++; } if (allItems.isEmpty()) { return null; } // 构建合并后的JSON ObjectNode resultNode = JSON.createObjectNode(); resultNode.put("code", 0); ObjectNode dataNode = resultNode.putObject("data"); ArrayNode listArray = dataNode.putArray("list"); for (JsonNode item : allItems) { listArray.add(item); } return JSON.writeValueAsString(resultNode); } private int parseAndSaveCoupons(String couponJson, LbUser user, String tenantId) throws Exception { JsonNode root = JSON.readTree(couponJson); int code = root.path("code").asInt(-1); if (code != 0) { log.warn("优惠券API返回错误 code={} msg={}", code, root.path("msg").asText("")); return 0; } JsonNode listNode = root.path("data").path("list"); if (listNode.isMissingNode() || !listNode.isArray()) { return 0; } int savedCount = 0; LocalDateTime now = LocalDateTime.now(); for (JsonNode item : listNode) { try { LbUserCoupon coupon = new LbUserCoupon(); coupon.setId(UUID.randomUUID().toString()); coupon.setTenantId(tenantId); coupon.setUserId(String.valueOf(user.getId())); coupon.setUserPhone(user.getMobile()); coupon.setUserNickname(user.getNickname()); coupon.setParentPhone(user.getPmobile()); coupon.setParentNickname(user.getPname()); // 从JSON中解析金额和日期 String moneyStr = item.path("money").asText("0"); coupon.setCouponAmount(new BigDecimal(moneyStr)); String createdAtStr = item.path("created_at").asText(null); if (createdAtStr != null && !createdAtStr.isBlank()) { try { LocalDate couponDate = LocalDate.parse(createdAtStr.substring(0, 10), DATE_FORMATTER); coupon.setCouponDate(couponDate); } catch (Exception e) { log.warn("日期解析失败 created_at={}", createdAtStr); } } coupon.setCreateTime(now); coupon.setUpdateTime(now); this.save(coupon); savedCount++; } catch (Exception e) { log.warn("保存优惠券记录异常 item={}", item, e); } } return savedCount; } private static String randomNoncestr() { String base36 = Long.toUnsignedString(Math.abs(RANDOM.nextLong()), 36); if (base36.length() >= 5) { return base36.substring(base36.length() - 5); } StringBuilder sb = new StringBuilder(base36); while (sb.length() < 5) { sb.append(Integer.toString(RANDOM.nextInt(36), 36)); } return sb.toString(); } private static String abbreviate(String s, int maxLen) { if (s == null) { return ""; } return s.length() <= maxLen ? s : s.substring(0, maxLen) + "..."; } private LocalDate parseDate(String dateStr) { if (dateStr == null || dateStr.trim().isEmpty()) { return null; } try { return LocalDate.parse(dateStr.trim(), DATE_FORMATTER); } catch (Exception e) { return null; } } @Override public Map add(LbUserCoupon entity) { Map result = new HashMap<>(); try { if (entity.getTenantId() == null || entity.getTenantId().trim().isEmpty()) { result.put("success", false); result.put("message", "tenantId不能为空"); return result; } entity.setTenantId(entity.getTenantId().trim()); if (entity.getUserPhone() != null) { entity.setUserPhone(entity.getUserPhone().trim()); } if (entity.getUserNickname() != null) { entity.setUserNickname(entity.getUserNickname().trim()); } if (entity.getParentPhone() != null) { entity.setParentPhone(entity.getParentPhone().trim()); } if (entity.getParentNickname() != null) { entity.setParentNickname(entity.getParentNickname().trim()); } if (entity.getCouponAmount() == null) { entity.setCouponAmount(BigDecimal.ZERO); } if (entity.getId() == null || entity.getId().trim().isEmpty()) { entity.setId(UUID.randomUUID().toString()); } LocalDateTime now = LocalDateTime.now(); if (entity.getCreateTime() == null) { entity.setCreateTime(now); } entity.setUpdateTime(now); boolean ok = this.save(entity); result.put("success", ok); result.put("message", ok ? "新增成功" : "新增失败"); if (ok) { result.put("data", entity); } return result; } catch (Exception e) { log.error("用户优惠券新增异常", e); result.put("success", false); result.put("message", "新增异常:" + e.getMessage()); return result; } } @Override public Map update(LbUserCoupon entity) { Map result = new HashMap<>(); try { if (entity.getId() == null || entity.getId().trim().isEmpty()) { result.put("success", false); result.put("message", "id不能为空"); return result; } if (entity.getTenantId() != null) { entity.setTenantId(entity.getTenantId().trim()); } if (entity.getUserPhone() != null) { entity.setUserPhone(entity.getUserPhone().trim()); } if (entity.getUserNickname() != null) { entity.setUserNickname(entity.getUserNickname().trim()); } if (entity.getParentPhone() != null) { entity.setParentPhone(entity.getParentPhone().trim()); } if (entity.getParentNickname() != null) { entity.setParentNickname(entity.getParentNickname().trim()); } entity.setUpdateTime(LocalDateTime.now()); boolean ok = this.updateById(entity); result.put("success", ok); result.put("message", ok ? "编辑成功" : "编辑失败"); if (ok) { result.put("data", this.getById(entity.getId())); } return result; } catch (Exception e) { log.error("用户优惠券编辑异常", e); result.put("success", false); result.put("message", "编辑异常:" + e.getMessage()); return result; } } @Override public Map deleteById(String id) { Map result = new HashMap<>(); try { if (id == null || id.trim().isEmpty()) { result.put("success", false); result.put("message", "id不能为空"); return result; } boolean ok = this.removeById(id.trim()); result.put("success", ok); result.put("message", ok ? "删除成功" : "删除失败"); return result; } catch (Exception e) { log.error("用户优惠券删除异常", e); result.put("success", false); result.put("message", "删除异常:" + e.getMessage()); return result; } } @Override public Map pageQuery(Integer current, Integer size, String tenantId, String userId, String userPhone, String userNickname, String parentPhone, String parentNickname, String couponDateStart, String couponDateEnd) { Map result = new HashMap<>(); try { if (current == null || current < 1) { current = 1; } if (size == null || size < 1) { size = 10; } LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); if (tenantId != null && !tenantId.trim().isEmpty()) { queryWrapper.eq(LbUserCoupon::getTenantId, tenantId.trim()); } if (userId != null && !userId.trim().isEmpty()) { queryWrapper.eq(LbUserCoupon::getUserId, userId.trim()); } if (userPhone != null && !userPhone.trim().isEmpty()) { queryWrapper.like(LbUserCoupon::getUserPhone, userPhone.trim()); } if (userNickname != null && !userNickname.trim().isEmpty()) { queryWrapper.like(LbUserCoupon::getUserNickname, userNickname.trim()); } if (parentPhone != null && !parentPhone.trim().isEmpty()) { queryWrapper.like(LbUserCoupon::getParentPhone, parentPhone.trim()); } if (parentNickname != null && !parentNickname.trim().isEmpty()) { queryWrapper.like(LbUserCoupon::getParentNickname, parentNickname.trim()); } LocalDate start = parseDate(couponDateStart); if (couponDateStart != null && !couponDateStart.trim().isEmpty() && start == null) { result.put("success", false); result.put("message", "couponDateStart 格式错误,请使用 yyyy-MM-dd"); return result; } LocalDate end = parseDate(couponDateEnd); if (couponDateEnd != null && !couponDateEnd.trim().isEmpty() && end == null) { result.put("success", false); result.put("message", "couponDateEnd 格式错误,请使用 yyyy-MM-dd"); return result; } if (start != null) { queryWrapper.ge(LbUserCoupon::getCouponDate, start); } if (end != null) { queryWrapper.le(LbUserCoupon::getCouponDate, end); } queryWrapper.orderByDesc(LbUserCoupon::getUpdateTime) .orderByDesc(LbUserCoupon::getCreateTime); Page page = this.page(new Page<>(current, size), queryWrapper); result.put("success", true); result.put("message", "查询成功"); result.put("data", page.getRecords()); result.put("total", page.getTotal()); result.put("current", page.getCurrent()); result.put("size", page.getSize()); result.put("pages", page.getPages()); return result; } catch (Exception e) { log.error("用户优惠券分页查询异常", e); result.put("success", false); result.put("message", "查询异常:" + e.getMessage()); return result; } } }