diff --git a/src/main/java/com/rj/common/LbThirdIntegrationConstants.java b/src/main/java/com/rj/common/LbThirdIntegrationConstants.java index 4085519..0584c88 100644 --- a/src/main/java/com/rj/common/LbThirdIntegrationConstants.java +++ b/src/main/java/com/rj/common/LbThirdIntegrationConstants.java @@ -16,10 +16,12 @@ public final class LbThirdIntegrationConstants { public static final String DEFAULT_USER_UPDATE_PATH = "/app/admin/user/update"; public static final String DEFAULT_GOODS_API_PATH = "/api/order/goods"; public static final String DEFAULT_BUY_API_PATH = "/api/order/buy"; + public static final String DEFAULT_BUYER_ORDER_LIST_PATH = "/app/admin/order/buy/select"; public static final String DEFAULT_FANS_API_PATH = "/api/share/select"; public static final String DEFAULT_LOGIN_API_PATH = "/api/user/login"; public static final int DEFAULT_ORDER_PAGE_LIMIT = 90; + public static final int DEFAULT_BUYER_ORDER_LIST_LIMIT = 90; public static final int DEFAULT_USER_PAGE_LIMIT = 90; public static final int DEFAULT_GOODS_PAGE_LIMIT = 20; public static final int DEFAULT_FANS_PAGE_LIMIT = 10; diff --git a/src/main/java/com/rj/controller/LbBuyerShoppingController.java b/src/main/java/com/rj/controller/LbBuyerShoppingController.java index aaea2a6..1e0814e 100644 --- a/src/main/java/com/rj/controller/LbBuyerShoppingController.java +++ b/src/main/java/com/rj/controller/LbBuyerShoppingController.java @@ -1,5 +1,6 @@ package com.rj.controller; +import com.rj.dto.LbBuyerShoppingPullRequest; import com.rj.entity.LbBuyerShopping; import com.rj.service.ILbBuyerShoppingService; import io.swagger.v3.oas.annotations.Operation; @@ -48,6 +49,20 @@ public class LbBuyerShoppingController { return toResponse(result); } + @PostMapping("/pull-from-third") + @Operation( + summary = "从第三方拉取买方购物列表", + description = + "按 tenantId 从 lb_third_integration_config 读取 login_api_path、buyer_order_list_path 等配置," + + "对每个买方手机号模拟登录获取 token,再分页调用买方订单列表 API 并解析入库;" + + "未传 password 时默认密码为 123456") + public ResponseEntity> pullFromThird( + @Parameter(description = "租户 id 与买方手机号列表", required = true) + @RequestBody LbBuyerShoppingPullRequest request) { + Map result = lbBuyerShoppingService.pullFromThirdParty(request); + return toResponse(result); + } + @GetMapping("/list") @Operation(summary = "分页查询") public ResponseEntity> list( diff --git a/src/main/java/com/rj/dto/LbBuyerShoppingPullRequest.java b/src/main/java/com/rj/dto/LbBuyerShoppingPullRequest.java new file mode 100644 index 0000000..373bd24 --- /dev/null +++ b/src/main/java/com/rj/dto/LbBuyerShoppingPullRequest.java @@ -0,0 +1,23 @@ +package com.rj.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +/** + * 买方购物:按手机号批量模拟登录并拉取第三方订单列表。 + */ +@Data +@Schema(description = "买方购物第三方拉取请求") +public class LbBuyerShoppingPullRequest { + + @Schema(description = "租户 id,关联 lb_third_integration_config.tenant_id", requiredMode = Schema.RequiredMode.REQUIRED) + private String tenantId; + + @Schema(description = "买方手机号列表", requiredMode = Schema.RequiredMode.REQUIRED) + private List mobiles; + + @Schema(description = "登录密码;未传时默认为 123456") + private String password; +} diff --git a/src/main/java/com/rj/dto/hxr/HxrBuyerOrderApiContext.java b/src/main/java/com/rj/dto/hxr/HxrBuyerOrderApiContext.java new file mode 100644 index 0000000..4568ad4 --- /dev/null +++ b/src/main/java/com/rj/dto/hxr/HxrBuyerOrderApiContext.java @@ -0,0 +1,13 @@ +package com.rj.dto.hxr; + +/** + * 调用 hxrd 买方订单列表 API 所需的运行时配置(由 {@code lb_third_integration_config} 解析而来)。 + */ +public record HxrBuyerOrderApiContext( + String buyerOrderListBaseUrl, + int pageLimit, + String origin, + String referer, + String token, + String appStr) { +} diff --git a/src/main/java/com/rj/entity/LbThirdIntegrationConfig.java b/src/main/java/com/rj/entity/LbThirdIntegrationConfig.java index 26f389d..79431b1 100644 --- a/src/main/java/com/rj/entity/LbThirdIntegrationConfig.java +++ b/src/main/java/com/rj/entity/LbThirdIntegrationConfig.java @@ -68,6 +68,10 @@ public class LbThirdIntegrationConfig implements Serializable { @Schema(description = "抢购 API 路径") private String buyApiPath; + @TableField("buyer_order_list_path") + @Schema(description = "买方订单列表 API 路径") + private String buyerOrderListPath; + @TableField("fans_api_path") @Schema(description = "粉丝列表 API 路径") private String fansApiPath; @@ -80,6 +84,10 @@ public class LbThirdIntegrationConfig implements Serializable { @Schema(description = "订单分页 limit") private Integer orderPageLimit; + @TableField("buyer_order_list_limit") + @Schema(description = "买方订单列表分页 limit") + private Integer buyerOrderListLimit; + @TableField("user_page_limit") @Schema(description = "用户分页 limit") private Integer userPageLimit; diff --git a/src/main/java/com/rj/service/HxrAdminBuyerOrderSelectService.java b/src/main/java/com/rj/service/HxrAdminBuyerOrderSelectService.java new file mode 100644 index 0000000..3a6c108 --- /dev/null +++ b/src/main/java/com/rj/service/HxrAdminBuyerOrderSelectService.java @@ -0,0 +1,220 @@ +package com.rj.service; + +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.type.CollectionType; +import com.rj.dto.hxr.HxrBuyerOrderApiContext; +import com.rj.dto.hxr.HxrOrderRow; +import com.rj.util.HxrGoodsSignUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.web.util.UriComponentsBuilder; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.security.SecureRandom; +import java.time.Duration; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * 调用 hxrd 买方订单列表 API(请求头 {@code Token} + {@code S/T/N} 鉴权)并解析 JSON。 + */ +@Slf4j +@Service +public class HxrAdminBuyerOrderSelectService { + + private static final SecureRandom RANDOM = new SecureRandom(); + + 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 ObjectMapper JSON = new ObjectMapper() + .setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + /** + * 分页拉取买方订单列表。 + * + * @param page 页码,从 1 开始 + */ + public Optional fetchBuyerOrderPage(int page, HxrBuyerOrderApiContext ctx) + throws Exception { + if (ctx == null) { + log.warn("买方订单 API 配置为空,跳过拉取"); + return Optional.empty(); + } + String resolvedToken = ctx.token(); + if (resolvedToken == null || resolvedToken.isBlank()) { + log.warn("未提供 token,跳过买方订单列表拉取"); + return Optional.empty(); + } + String appStr = ctx.appStr(); + if (appStr == null || appStr.isBlank()) { + log.warn("未配置 goodsApiAppStr,跳过买方订单列表拉取"); + return Optional.empty(); + } + String baseUrl = ctx.buyerOrderListBaseUrl(); + if (baseUrl == null || baseUrl.isBlank()) { + log.warn("未配置 buyerOrderListBaseUrl,跳过买方订单列表拉取"); + return Optional.empty(); + } + + int pageLimit = ctx.pageLimit() > 0 ? ctx.pageLimit() : 90; + int safePage = Math.max(1, page); + String uri = UriComponentsBuilder.fromUriString(baseUrl) + .replaceQueryParam("page", safePage) + .replaceQueryParam("limit", pageLimit) + .build() + .encode() + .toUriString(); + + long timestamp = System.currentTimeMillis() / 1000; + String noncestr = randomNoncestr(); + Map signParams = new LinkedHashMap<>(); + signParams.put("page", safePage); + signParams.put("limit", pageLimit); + signParams.put("timestamp", timestamp); + signParams.put("noncestr", noncestr); + String sign = HxrGoodsSignUtil.computeSign(signParams, 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, resolvedToken) + .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("hxr buyer order list HTTP {} page={} bodyPrefix={}", + status, safePage, abbreviate(response.body(), 400)); + return Optional.empty(); + } + + String bodyText = response.body(); + if (bodyText == null || bodyText.isBlank()) { + log.warn("hxr buyer order list empty body page={}", safePage); + return Optional.empty(); + } + + JsonNode root = JSON.readTree(bodyText); + int code = root.path("code").asInt(-1); + String msg = root.path("msg").asText(""); + if (code != 0) { + log.warn("hxr buyer order list api code={} msg={} page={} bodyPrefix={}", + code, msg, safePage, abbreviate(bodyText, 400)); + return Optional.empty(); + } + + JsonNode dataNode = root.get("data"); + if (isErrorPayload(dataNode)) { + log.warn("hxr buyer order list 返回异常 data={} page={} msg={}", dataNode, safePage, msg); + return Optional.empty(); + } + + List rows = parseOrderRows(dataNode); + int count = root.path("count").asInt(rows.size()); + String allMoney = root.path("all_money").asText(""); + boolean hasMore; + int lastPage; + if (dataNode != null && dataNode.isObject()) { + hasMore = dataNode.path("hasmore").asBoolean(false); + lastPage = dataNode.path("last_page").asInt(safePage); + if (lastPage < 1) { + lastPage = safePage; + } + } else { + hasMore = rows.size() >= pageLimit; + lastPage = hasMore ? Integer.MAX_VALUE : safePage; + } + + return Optional.of(new BuyerOrderPageResult(rows, count, allMoney, hasMore, lastPage, safePage)); + } + + private static List parseOrderRows(JsonNode dataNode) throws Exception { + if (dataNode == null || dataNode.isNull()) { + return List.of(); + } + JsonNode listNode; + if (dataNode.isArray()) { + listNode = dataNode; + } else if (dataNode.isObject()) { + listNode = dataNode.get("list"); + } else { + return List.of(); + } + if (listNode == null || !listNode.isArray() || listNode.isEmpty()) { + return List.of(); + } + if (listNode.get(0).isTextual() || listNode.get(0).isNumber()) { + return List.of(); + } + CollectionType listType = + JSON.getTypeFactory().constructCollectionType(List.class, HxrOrderRow.class); + List rows = JSON.convertValue(listNode, listType); + return rows != null ? rows : new ArrayList<>(); + } + + 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 boolean isErrorPayload(JsonNode dataNode) { + if (dataNode == null || dataNode.isNull()) { + return false; + } + if (!dataNode.isArray() || dataNode.isEmpty()) { + return false; + } + return dataNode.get(0).isTextual() || dataNode.get(0).isNumber(); + } + + private static String abbreviate(String s, int maxLen) { + if (s == null) { + return ""; + } + return s.length() <= maxLen ? s : s.substring(0, maxLen) + "..."; + } + + public record BuyerOrderPageResult( + List rows, + int count, + String allMoney, + boolean hasMore, + int lastPage, + int page) { + } +} diff --git a/src/main/java/com/rj/service/ILbBuyerShoppingService.java b/src/main/java/com/rj/service/ILbBuyerShoppingService.java index 7bdb1a3..46e3470 100644 --- a/src/main/java/com/rj/service/ILbBuyerShoppingService.java +++ b/src/main/java/com/rj/service/ILbBuyerShoppingService.java @@ -1,6 +1,7 @@ package com.rj.service; import com.baomidou.mybatisplus.extension.service.IService; +import com.rj.dto.LbBuyerShoppingPullRequest; import com.rj.entity.LbBuyerShopping; import java.math.BigDecimal; @@ -35,4 +36,9 @@ public interface ILbBuyerShoppingService extends IService { String buyTimeEnd, String confirmTimeStart, String confirmTimeEnd); + + /** + * 按手机号模拟第三方登录获取 token,分页拉取买方订单列表并解析入库。 + */ + Map pullFromThirdParty(LbBuyerShoppingPullRequest request); } diff --git a/src/main/java/com/rj/service/ILbThirdIntegrationConfigService.java b/src/main/java/com/rj/service/ILbThirdIntegrationConfigService.java index bc1357b..e5c97c5 100644 --- a/src/main/java/com/rj/service/ILbThirdIntegrationConfigService.java +++ b/src/main/java/com/rj/service/ILbThirdIntegrationConfigService.java @@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.extension.service.IService; import com.rj.dto.LbThirdIntegrationCredentialUpdateRequest; import com.rj.dto.hxr.HxrAdminOrderApiContext; import com.rj.dto.hxr.HxrAdminUserApiContext; +import com.rj.dto.hxr.HxrBuyerOrderApiContext; import com.rj.dto.hxr.HxrFansApiContext; import com.rj.dto.hxr.HxrGoodsApiContext; import com.rj.dto.hxr.HxrUserLoginApiContext; @@ -57,6 +58,11 @@ public interface ILbThirdIntegrationConfigService extends IService resolveAdminOrderApiContext(String tenantId); + /** + * 按租户 id 解析后台买方订单列表 API 运行时配置(buyer_order_list_path、buyer_order_list_limit)。 + */ + Optional resolveAdminBuyerOrderApiContext(String tenantId); + /** * 按租户 id 解析用户登录 API 运行时配置(login_api_path、Origin、Referer、appStr)。 */ @@ -67,4 +73,10 @@ public interface ILbThirdIntegrationConfigService extends IService resolveFansApiContext(String tenantId, String token); + + /** + * 按租户 id 解析买方订单列表 API 运行时配置(buyer_order_list_path、buyer_order_list_limit、Origin、Referer、appStr); + * {@code token} 为登录用户 token,必填。 + */ + Optional resolveBuyerOrderApiContext(String tenantId, String token); } diff --git a/src/main/java/com/rj/service/impl/LbBuyerShoppingServiceImpl.java b/src/main/java/com/rj/service/impl/LbBuyerShoppingServiceImpl.java index 41f8712..11f8cf1 100644 --- a/src/main/java/com/rj/service/impl/LbBuyerShoppingServiceImpl.java +++ b/src/main/java/com/rj/service/impl/LbBuyerShoppingServiceImpl.java @@ -3,18 +3,31 @@ 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.rj.dto.LbBuyerShoppingPullRequest; +import com.rj.dto.hxr.HxrBuyerOrderApiContext; +import com.rj.dto.hxr.HxrOrderRow; +import com.rj.dto.hxr.HxrUserLoginApiContext; import com.rj.entity.LbBuyerShopping; import com.rj.mapper.LbBuyerShoppingMapper; +import com.rj.service.HxrAdminBuyerOrderSelectService; +import com.rj.service.HxrAdminUserLoginService; import com.rj.service.ILbBuyerShoppingService; +import com.rj.service.ILbThirdIntegrationConfigService; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DuplicateKeyException; import org.springframework.stereotype.Service; import java.math.BigDecimal; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; +import java.util.Optional; /** * 买方购物表 lb_buyer_shopping 服务实现 @@ -25,6 +38,17 @@ public class LbBuyerShoppingServiceImpl extends ServiceImpl implements ILbBuyerShoppingService { + private static final String DEFAULT_SIMULATE_LOGIN_PASSWORD = "123456"; + + @Autowired + private ILbThirdIntegrationConfigService lbThirdIntegrationConfigService; + + @Autowired + private HxrAdminUserLoginService hxrAdminUserLoginService; + + @Autowired + private HxrAdminBuyerOrderSelectService hxrAdminBuyerOrderSelectService; + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); @@ -261,6 +285,311 @@ public class LbBuyerShoppingServiceImpl } } + @Override + public Map pullFromThirdParty(LbBuyerShoppingPullRequest request) { + Map result = new HashMap<>(); + try { + if (request == null) { + result.put("success", false); + result.put("message", "请求体不能为空"); + return result; + } + if (request.getTenantId() == null || request.getTenantId().trim().isEmpty()) { + result.put("success", false); + result.put("message", "tenantId不能为空"); + return result; + } + if (request.getMobiles() == null || request.getMobiles().isEmpty()) { + result.put("success", false); + result.put("message", "手机号列表不能为空"); + return result; + } + + String tenantId = request.getTenantId().trim(); + Optional loginCtxOpt = + lbThirdIntegrationConfigService.resolveUserLoginApiContext(tenantId); + if (loginCtxOpt.isEmpty()) { + result.put("success", false); + result.put("message", + "未找到该租户的第三方集成配置,或配置未启用、URL 不完整(请检查 lb_third_integration_config)"); + return result; + } + HxrUserLoginApiContext loginCtx = loginCtxOpt.get(); + + List> items = new ArrayList<>(); + int successCount = 0; + int failCount = 0; + int totalSaved = 0; + int totalFetched = 0; + String requestPassword = request.getPassword() != null ? request.getPassword().trim() : ""; + + for (String rawMobile : request.getMobiles()) { + Map item = new LinkedHashMap<>(); + if (rawMobile == null || rawMobile.trim().isEmpty()) { + item.put("mobile", rawMobile); + item.put("loginSuccess", false); + item.put("orderFetchSuccess", false); + item.put("apiCode", -1); + item.put("apiMsg", "手机号为空"); + items.add(item); + failCount++; + continue; + } + + String mobile = rawMobile.trim(); + item.put("mobile", mobile); + String password = !requestPassword.isEmpty() + ? requestPassword + : DEFAULT_SIMULATE_LOGIN_PASSWORD; + + try { + HxrAdminUserLoginService.LoginApiResult loginResult = + hxrAdminUserLoginService.login(mobile, password, loginCtx); + item.put("httpStatus", loginResult.httpStatus()); + item.put("apiCode", loginResult.apiCode()); + item.put("apiMsg", loginResult.apiMsg()); + item.put("loginSuccess", loginResult.success()); + item.put("parsed", loginResult.parsed()); + + if (!loginResult.success()) { + item.put("orderFetchSuccess", false); + failCount++; + items.add(item); + continue; + } + + String token = HxrAdminUserLoginService.extractToken(loginResult.parsed()); + if (token == null || token.isBlank()) { + item.put("orderFetchSuccess", false); + item.put("orderFetchMsg", "响应中无 data.userinfo.token"); + failCount++; + items.add(item); + continue; + } + + Optional orderCtxOpt = + lbThirdIntegrationConfigService.resolveBuyerOrderApiContext(tenantId, token); + if (orderCtxOpt.isEmpty()) { + item.put("orderFetchSuccess", false); + item.put("orderFetchMsg", + "未找到买方订单 API 配置,或配置未启用、URL/凭证不完整(请检查 lb_third_integration_config)"); + failCount++; + items.add(item); + continue; + } + HxrBuyerOrderApiContext orderCtx = orderCtxOpt.get(); + item.put("buyerOrderListUrl", orderCtx.buyerOrderListBaseUrl()); + + Map fetchResult = fetchAndSaveBuyerOrders(orderCtx, mobile); + item.putAll(fetchResult); + if (Boolean.TRUE.equals(fetchResult.get("orderFetchSuccess"))) { + successCount++; + totalSaved += toInt(fetchResult.get("ordersSaved")); + totalFetched += toInt(fetchResult.get("ordersFetched")); + } else { + failCount++; + } + } catch (Exception e) { + log.warn("买方购物第三方拉取异常 mobile={} tenantId={}", mobile, tenantId, e); + item.put("loginSuccess", false); + item.put("orderFetchSuccess", false); + item.put("apiCode", -1); + item.put("apiMsg", "请求异常:" + e.getMessage()); + failCount++; + } + items.add(item); + } + + result.put("success", true); + result.put("message", "买方购物列表拉取完成"); + result.put("data", items); + result.put("successCount", successCount); + result.put("failCount", failCount); + result.put("ordersSaved", totalSaved); + result.put("ordersFetched", totalFetched); + result.put("loginApiUrl", loginCtx.loginApiUrl()); + return result; + } catch (Exception e) { + log.error("买方购物第三方拉取异常", e); + result.put("success", false); + result.put("message", "拉取异常:" + e.getMessage()); + return result; + } + } + + private Map fetchAndSaveBuyerOrders(HxrBuyerOrderApiContext ctx, String buyerMobile) { + Map result = new LinkedHashMap<>(); + int page = 1; + int totalSaved = 0; + int totalFetched = 0; + Integer remoteCount = null; + String allMoney = null; + HxrAdminBuyerOrderSelectService.BuyerOrderPageResult firstPage = null; + + try { + while (true) { + Optional opt = + hxrAdminBuyerOrderSelectService.fetchBuyerOrderPage(page, ctx); + if (opt.isEmpty()) { + if (page == 1) { + result.put("orderFetchSuccess", false); + result.put("orderFetchMsg", + "未拉取到买方订单(请检查 lb_third_integration_config 中 token、appStr、URL 或网络)"); + result.put("ordersSaved", 0); + result.put("ordersFetched", 0); + return result; + } + result.put("orderFetchSuccess", false); + result.put("orderFetchMsg", "第 " + page + " 页拉取失败,已成功保存前 " + + (page - 1) + " 页共 " + totalSaved + " 条订单"); + result.put("ordersSaved", totalSaved); + result.put("ordersFetched", totalFetched); + if (remoteCount != null) { + result.put("remoteCount", remoteCount); + } + if (allMoney != null) { + result.put("allMoney", allMoney); + } + result.put("orderPages", page - 1); + return result; + } + + HxrAdminBuyerOrderSelectService.BuyerOrderPageResult pageResult = opt.get(); + if (firstPage == null) { + firstPage = pageResult; + remoteCount = pageResult.count(); + allMoney = pageResult.allMoney(); + } + + List rows = pageResult.rows(); + if (rows == null || rows.isEmpty()) { + break; + } + totalFetched += rows.size(); + + for (HxrOrderRow row : rows) { + LbBuyerShopping entity = toLbBuyerShopping(row, buyerMobile); + if (entity.getId() == null) { + continue; + } + applyDefaults(entity); + if (entity.getCreatedAt() == null) { + entity.setCreatedAt(LocalDateTime.now()); + } + try { + LbBuyerShopping existing = this.getById(entity.getId()); + boolean ok = existing != null ? this.updateById(entity) : this.save(entity); + if (ok) { + totalSaved++; + } + } catch (DuplicateKeyException e) { + if (this.updateById(entity)) { + totalSaved++; + } + } + } + + int pageLimit = ctx.pageLimit() > 0 ? ctx.pageLimit() : 90; + boolean hasMore = pageResult.hasMore(); + int lastPage = pageResult.lastPage(); + if (pageResult.rows().size() < pageLimit) { + break; + } + if (!hasMore || page >= lastPage) { + break; + } + page++; + } + + result.put("orderFetchSuccess", true); + result.put("orderFetchMsg", totalSaved == 0 ? "接口成功,无订单数据" : "买方订单拉取完成"); + result.put("ordersSaved", totalSaved); + result.put("ordersFetched", totalFetched); + result.put("remoteCount", remoteCount != null ? remoteCount : totalFetched); + if (allMoney != null && !allMoney.isBlank()) { + result.put("allMoney", allMoney); + } + result.put("lastPage", firstPage != null ? firstPage.lastPage() : page); + result.put("orderPages", page); + return result; + } catch (Exception e) { + log.warn("拉取买方订单异常 buyerMobile={}", buyerMobile, e); + result.put("orderFetchSuccess", false); + result.put("orderFetchMsg", "拉取买方订单异常:" + e.getMessage()); + result.put("ordersSaved", totalSaved); + result.put("ordersFetched", totalFetched); + if (remoteCount != null) { + result.put("remoteCount", remoteCount); + } + result.put("orderPages", page > 1 ? page - 1 : 0); + return result; + } + } + + private static LbBuyerShopping toLbBuyerShopping(HxrOrderRow row, String fallbackBuyerMobile) { + LbBuyerShopping entity = new LbBuyerShopping(); + entity.setId(row.id()); + entity.setSellerId(row.sellerId()); + entity.setMerchandiseId(row.merchandiseId()); + entity.setBuyerId(row.buyerId()); + entity.setOrderSn(row.orderSn()); + entity.setTotalMoney(parseMoney(row.totalMoney())); + entity.setCreatedAt(parseFlexibleDateTime(row.createdAt())); + entity.setBuyTime(parseFlexibleDateTime(row.buyTime())); + entity.setConfirmTime(parseFlexibleDateTime(row.confirmTime())); + entity.setSellerNickname(row.sellerName()); + entity.setSellerMobile(row.sellerPhone()); + entity.setBuyerNickname(row.buyerName()); + String buyerMobile = row.buyerPhone(); + if (buyerMobile == null || buyerMobile.isBlank()) { + buyerMobile = fallbackBuyerMobile; + } + entity.setBuyerMobile(buyerMobile); + return entity; + } + + private static BigDecimal parseMoney(String money) { + if (money == null || money.trim().isEmpty()) { + return null; + } + try { + return new BigDecimal(money.trim()); + } catch (NumberFormatException e) { + return null; + } + } + + private static LocalDateTime parseFlexibleDateTime(String text) { + if (text == null || text.trim().isEmpty()) { + return null; + } + String trimmed = text.trim(); + try { + return LocalDateTime.parse(trimmed, DATE_TIME_FORMATTER); + } catch (DateTimeParseException ignored) { + // continue + } + try { + return LocalDateTime.parse(trimmed, DateTimeFormatter.ISO_LOCAL_DATE_TIME); + } catch (DateTimeParseException e) { + return null; + } + } + + private static int toInt(Object value) { + if (value instanceof Number number) { + return number.intValue(); + } + if (value == null) { + return 0; + } + try { + return Integer.parseInt(value.toString()); + } catch (NumberFormatException e) { + return 0; + } + } + private void applyDefaults(LbBuyerShopping entity) { if (entity.getTotalMoney() == null) { entity.setTotalMoney(BigDecimal.ZERO); diff --git a/src/main/java/com/rj/service/impl/LbThirdIntegrationConfigServiceImpl.java b/src/main/java/com/rj/service/impl/LbThirdIntegrationConfigServiceImpl.java index ae5b50c..96f1ab1 100644 --- a/src/main/java/com/rj/service/impl/LbThirdIntegrationConfigServiceImpl.java +++ b/src/main/java/com/rj/service/impl/LbThirdIntegrationConfigServiceImpl.java @@ -8,6 +8,7 @@ import com.rj.common.LbThirdIntegrationConstants; import com.rj.dto.LbThirdIntegrationCredentialUpdateRequest; import com.rj.dto.hxr.HxrAdminOrderApiContext; import com.rj.dto.hxr.HxrAdminUserApiContext; +import com.rj.dto.hxr.HxrBuyerOrderApiContext; import com.rj.dto.hxr.HxrFansApiContext; import com.rj.dto.hxr.HxrGoodsApiContext; import com.rj.dto.hxr.HxrUserLoginApiContext; @@ -398,6 +399,37 @@ public class LbThirdIntegrationConfigServiceImpl } } + @Override + public Optional resolveBuyerOrderApiContext(String tenantId, String token) { + if (!StringUtils.hasText(tenantId) || !StringUtils.hasText(token)) { + return Optional.empty(); + } + LbThirdIntegrationConfig config = getByTenantIdOrNull(tenantId.trim()); + if (config == null) { + return Optional.empty(); + } + if (config.getEnabled() == null || config.getEnabled() != 1) { + return Optional.empty(); + } + + String appStr = config.getGoodsApiAppStr(); + if (!StringUtils.hasText(appStr)) { + return Optional.empty(); + } + + try { + return Optional.of(new HxrBuyerOrderApiContext( + LbThirdIntegrationConfigUtil.resolveBuyerOrderListBaseUrl(config), + LbThirdIntegrationConfigUtil.resolveBuyerOrderListLimit(config), + LbThirdIntegrationConfigUtil.resolveGoodsApiOrigin(config), + LbThirdIntegrationConfigUtil.resolveOrderReferer(config), + token.trim(), + appStr.trim())); + } catch (IllegalStateException e) { + return Optional.empty(); + } + } + @Override public Optional resolveFansApiContext(String tenantId, String token) { if (!StringUtils.hasText(tenantId) || !StringUtils.hasText(token)) { @@ -431,6 +463,15 @@ public class LbThirdIntegrationConfigServiceImpl @Override public Optional resolveAdminOrderApiContext(String tenantId) { + return resolveAdminOrderApiContext(tenantId, false); + } + + @Override + public Optional resolveAdminBuyerOrderApiContext(String tenantId) { + return resolveAdminOrderApiContext(tenantId, true); + } + + private Optional resolveAdminOrderApiContext(String tenantId, boolean buyerOrderList) { if (!StringUtils.hasText(tenantId)) { return Optional.empty(); } @@ -448,9 +489,15 @@ public class LbThirdIntegrationConfigServiceImpl } try { + String baseUrl = buyerOrderList + ? LbThirdIntegrationConfigUtil.resolveBuyerOrderListBaseUrl(config) + : LbThirdIntegrationConfigUtil.resolveOrderSelectBaseUrl(config); + int pageLimit = buyerOrderList + ? LbThirdIntegrationConfigUtil.resolveBuyerOrderListLimit(config) + : LbThirdIntegrationConfigUtil.resolveOrderPageLimit(config); return Optional.of(new HxrAdminOrderApiContext( - LbThirdIntegrationConfigUtil.resolveOrderSelectBaseUrl(config), - LbThirdIntegrationConfigUtil.resolveOrderPageLimit(config), + baseUrl, + pageLimit, cookieHeader.trim(), LbThirdIntegrationConfigUtil.resolveOrderReferer(config))); } catch (IllegalStateException e) { @@ -581,6 +628,9 @@ public class LbThirdIntegrationConfigServiceImpl if (!StringUtils.hasText(entity.getBuyApiPath())) { entity.setBuyApiPath(LbThirdIntegrationConstants.DEFAULT_BUY_API_PATH); } + if (!StringUtils.hasText(entity.getBuyerOrderListPath())) { + entity.setBuyerOrderListPath(LbThirdIntegrationConstants.DEFAULT_BUYER_ORDER_LIST_PATH); + } if (!StringUtils.hasText(entity.getFansApiPath())) { entity.setFansApiPath(LbThirdIntegrationConstants.DEFAULT_FANS_API_PATH); } @@ -590,6 +640,9 @@ public class LbThirdIntegrationConfigServiceImpl if (entity.getOrderPageLimit() == null) { entity.setOrderPageLimit(LbThirdIntegrationConstants.DEFAULT_ORDER_PAGE_LIMIT); } + if (entity.getBuyerOrderListLimit() == null) { + entity.setBuyerOrderListLimit(LbThirdIntegrationConstants.DEFAULT_BUYER_ORDER_LIST_LIMIT); + } if (entity.getUserPageLimit() == null) { entity.setUserPageLimit(LbThirdIntegrationConstants.DEFAULT_USER_PAGE_LIMIT); } diff --git a/src/main/java/com/rj/util/LbThirdIntegrationConfigUtil.java b/src/main/java/com/rj/util/LbThirdIntegrationConfigUtil.java index 8b624d3..be636e7 100644 --- a/src/main/java/com/rj/util/LbThirdIntegrationConfigUtil.java +++ b/src/main/java/com/rj/util/LbThirdIntegrationConfigUtil.java @@ -165,6 +165,27 @@ public final class LbThirdIntegrationConfigUtil { return limit; } + public static String resolveBuyerOrderListBaseUrl(LbThirdIntegrationConfig config) { + String base = trimTrailingSlash(requireText(config.getAdminBaseUrl(), "adminBaseUrl")); + String path = config.getBuyerOrderListPath(); + if (!StringUtils.hasText(path)) { + path = LbThirdIntegrationConstants.DEFAULT_BUYER_ORDER_LIST_PATH; + } + path = path.trim(); + if (!path.startsWith("/")) { + path = "/" + path; + } + return base + path; + } + + public static int resolveBuyerOrderListLimit(LbThirdIntegrationConfig config) { + Integer limit = config.getBuyerOrderListLimit(); + if (limit == null || limit < 1) { + return LbThirdIntegrationConstants.DEFAULT_BUYER_ORDER_LIST_LIMIT; + } + return limit; + } + /** * 组装 Cookie 请求头:优先完整 cookie,否则 {@code PHPSID=} + phpsid。 */ diff --git a/src/main/sql/lb_third_integration_config.sql b/src/main/sql/lb_third_integration_config.sql index 698b37c..1fbfb84 100644 --- a/src/main/sql/lb_third_integration_config.sql +++ b/src/main/sql/lb_third_integration_config.sql @@ -16,10 +16,12 @@ CREATE TABLE `lb_third_integration_config` ( `user_update_path` VARCHAR(128) NOT NULL DEFAULT '/app/admin/user/update' COMMENT '用户更新 API 路径', `goods_api_path` VARCHAR(128) NOT NULL DEFAULT '/api/order/goods' COMMENT '货品列表 API 路径', `buy_api_path` VARCHAR(128) NOT NULL DEFAULT '/api/order/buy' COMMENT '抢购 API 路径', + `buyer_order_list_path` VARCHAR(128) NOT NULL DEFAULT '/app/admin/order/buy/select' COMMENT '买方订单列表 API 路径', `fans_api_path` VARCHAR(128) NOT NULL DEFAULT '/api/share/select' COMMENT '粉丝列表 API 路径', `login_api_path` VARCHAR(128) NOT NULL DEFAULT '/api/user/login' COMMENT '后台登录 API 路径', `order_page_limit` INT NOT NULL DEFAULT 90 COMMENT '订单分页 limit', + `buyer_order_list_limit` INT NOT NULL DEFAULT 90 COMMENT '买方订单列表分页 limit', `user_page_limit` INT NOT NULL DEFAULT 90 COMMENT '用户分页 limit', `goods_page_limit` INT NOT NULL DEFAULT 20 COMMENT '货品分页 limit', `fans_page_limit` INT NOT NULL DEFAULT 10 COMMENT '粉丝分页 limit', diff --git a/src/main/sql/lb_third_integration_config_alter_add_buyer_order_list.sql b/src/main/sql/lb_third_integration_config_alter_add_buyer_order_list.sql new file mode 100644 index 0000000..99000bf --- /dev/null +++ b/src/main/sql/lb_third_integration_config_alter_add_buyer_order_list.sql @@ -0,0 +1,8 @@ +-- 升级脚本:lb_third_integration_config 增加买方订单列表 API 配置列 +SET NAMES utf8mb4; + +ALTER TABLE `lb_third_integration_config` + ADD COLUMN `buyer_order_list_path` VARCHAR(128) NOT NULL DEFAULT '/app/admin/order/buy/select' + COMMENT '买方订单列表 API 路径' AFTER `buy_api_path`, + ADD COLUMN `buyer_order_list_limit` INT NOT NULL DEFAULT 90 + COMMENT '买方订单列表分页 limit' AFTER `order_page_limit`;