从第三方获取用户优惠券

This commit is contained in:
2026-06-20 08:42:34 +08:00
parent df823119ed
commit 9e4f32a47c
10 changed files with 818 additions and 0 deletions

View File

@@ -0,0 +1,526 @@
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.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<LbUserCouponMapper, LbUserCoupon>
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<String, Object> syncUserCoupons(String tenantId, Integer recentDays) {
Map<String, Object> 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<LbThirdIntegrationConfig>()
.eq(LbThirdIntegrationConfig::getTenantId, tid)
.eq(LbThirdIntegrationConfig::getEnabled, 1)
.last("LIMIT 1"));
if (config == null) {
result.put("success", false);
result.put("message", "未找到该租户的第三方集成配置,或配置未启用");
return result;
}
// 获取登录上下文
Optional<HxrUserLoginApiContext> 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<LbUser> allUsers = lbUserService.list(
new LambdaQueryWrapper<LbUser>()
.eq(LbUser::getTenantId, tid)
.ge(LbUser::getUpdatedAt, startDate)
.isNotNull(LbUser::getMobile)
.ne(LbUser::getMobile, "")
.orderByDesc(LbUser::getUpdatedAt)
.last("LIMIT 5000"));
// 根据 mobile 和 nickname 去重
Map<String, LbUser> uniqueUserMap = new LinkedHashMap<>();
for (LbUser user : allUsers) {
String key = user.getMobile() + "|" + (user.getNickname() != null ? user.getNickname() : "");
uniqueUserMap.put(key, user);
}
List<LbUser> 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<HxrMoneyCouponApiContext> 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 pageLimit = recentDays > 0 ? recentDays : 90;
String uri = couponApiBaseUrl + (couponApiBaseUrl.contains("?") ? "&" : "?") + "limit=" + pageLimit;
long timestamp = System.currentTimeMillis() / 1000;
String noncestr = randomNoncestr();
Map<String, Object> signParams = new LinkedHashMap<>();
signParams.put("timestamp", timestamp);
signParams.put("noncestr", noncestr);
String sign = HxrGoodsSignUtil.computeSign(signParams, ctx.appStr().trim());
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(60))
.followRedirects(HttpClient.Redirect.NORMAL)
.build();
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<String> 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));
return null;
}
return response.body();
}
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<String, Object> add(LbUserCoupon entity) {
Map<String, Object> 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<String, Object> update(LbUserCoupon entity) {
Map<String, Object> 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<String, Object> deleteById(String id) {
Map<String, Object> 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<String, Object> pageQuery(Integer current,
Integer size,
String tenantId,
String userId,
String userPhone,
String userNickname,
String parentPhone,
String parentNickname,
String couponDateStart,
String couponDateEnd) {
Map<String, Object> result = new HashMap<>();
try {
if (current == null || current < 1) {
current = 1;
}
if (size == null || size < 1) {
size = 10;
}
LambdaQueryWrapper<LbUserCoupon> 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<LbUserCoupon> 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;
}
}
}