From 947bb21c8bf8d7e5dd0037a28278da022e3fe7ad Mon Sep 17 00:00:00 2001 From: cst61 Date: Tue, 26 May 2026 17:25:55 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8A=A2=E8=B4=AD=E8=B4=A7=E5=93=81=E5=90=8C?= =?UTF-8?q?=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/rj/config/HxrAdminProperties.java | 10 ++ .../com/rj/controller/LbGoodsController.java | 16 ++ .../com/rj/dto/hxr/HxrLbGoodsPageData.java | 8 + .../rj/dto/hxr/HxrLbGoodsSelectResponse.java | 4 + .../java/com/rj/mapper/LbGoodsMapper.java | 5 + .../com/rj/service/HxrAdminGoodsService.java | 101 ++++++++++++ .../java/com/rj/service/ILbGoodsService.java | 5 + .../rj/service/impl/LbGoodsServiceImpl.java | 145 ++++++++++++++++++ src/main/resources/mapper/LbGoodsMapper.xml | 33 ++++ 9 files changed, 327 insertions(+) create mode 100644 src/main/java/com/rj/dto/hxr/HxrLbGoodsPageData.java create mode 100644 src/main/java/com/rj/dto/hxr/HxrLbGoodsSelectResponse.java create mode 100644 src/main/java/com/rj/service/HxrAdminGoodsService.java create mode 100644 src/main/resources/mapper/LbGoodsMapper.xml diff --git a/src/main/java/com/rj/config/HxrAdminProperties.java b/src/main/java/com/rj/config/HxrAdminProperties.java index c586356..8b5a1be 100644 --- a/src/main/java/com/rj/config/HxrAdminProperties.java +++ b/src/main/java/com/rj/config/HxrAdminProperties.java @@ -60,6 +60,16 @@ public class HxrAdminProperties { */ private String userUpdateUrl = "https://hxrdhoutai.hxrdsm.cn/app/admin/user/update"; + /** + * 货品列表 API({@code /api/order/goods});实际请求会覆盖 {@code page}、{@code limit}。 + */ + private String goodsApiUrl = "https://hxrdhoutai.hxrdsm.cn/api/order/goods?page=1&limit=20"; + + /** + * 货品列表 API 请求头 {@code token}。 + */ + private String goodsApiToken = "c2c8cd18-90ff-4114-982b-6658d1121d51"; + /** * 组装 Cookie 请求头:优先 {@link #cookie},否则 {@code PHPSID=}{@link #phpsid}。 * diff --git a/src/main/java/com/rj/controller/LbGoodsController.java b/src/main/java/com/rj/controller/LbGoodsController.java index a7bbc39..2e50fa0 100644 --- a/src/main/java/com/rj/controller/LbGoodsController.java +++ b/src/main/java/com/rj/controller/LbGoodsController.java @@ -110,4 +110,20 @@ public class LbGoodsController { } return ResponseEntity.internalServerError().body(result); } + + @PostMapping("/sync-from-hxr") + @Operation( + summary = "从 hxrd 第三方同步货品", + description = + "分页调用 GET /api/order/goods(每页 limit=20,请求头 token)," + + "解析 data.list 后 upsert 到 lb_goods;需配置 hxr.admin.goods-api-token") + public ResponseEntity> syncFromHxr( + @Parameter(description = "租户 id", required = true) @RequestParam String tenantId) { + Map result = lbGoodsService.syncFromHxrGoods(tenantId); + Boolean success = (Boolean) result.get("success"); + if (success != null && success) { + return ResponseEntity.ok(result); + } + return ResponseEntity.badRequest().body(result); + } } diff --git a/src/main/java/com/rj/dto/hxr/HxrLbGoodsPageData.java b/src/main/java/com/rj/dto/hxr/HxrLbGoodsPageData.java new file mode 100644 index 0000000..3d72e01 --- /dev/null +++ b/src/main/java/com/rj/dto/hxr/HxrLbGoodsPageData.java @@ -0,0 +1,8 @@ +package com.rj.dto.hxr; + +import com.rj.entity.LbGoods; + +import java.util.List; + +/** hxrd {@code /api/order/goods} 响应中的 {@code data} 节点。 */ +public record HxrLbGoodsPageData(List list, boolean hasmore, int lastPage) {} diff --git a/src/main/java/com/rj/dto/hxr/HxrLbGoodsSelectResponse.java b/src/main/java/com/rj/dto/hxr/HxrLbGoodsSelectResponse.java new file mode 100644 index 0000000..18c9042 --- /dev/null +++ b/src/main/java/com/rj/dto/hxr/HxrLbGoodsSelectResponse.java @@ -0,0 +1,4 @@ +package com.rj.dto.hxr; + +/** hxrd {@code /api/order/goods} 顶层 JSON。 */ +public record HxrLbGoodsSelectResponse(int code, String msg, HxrLbGoodsPageData data) {} diff --git a/src/main/java/com/rj/mapper/LbGoodsMapper.java b/src/main/java/com/rj/mapper/LbGoodsMapper.java index 7bbd74c..1461e2c 100644 --- a/src/main/java/com/rj/mapper/LbGoodsMapper.java +++ b/src/main/java/com/rj/mapper/LbGoodsMapper.java @@ -3,7 +3,12 @@ package com.rj.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.rj.entity.LbGoods; import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; @Mapper public interface LbGoodsMapper extends BaseMapper { + + int upsertBatch(@Param("list") List list); } diff --git a/src/main/java/com/rj/service/HxrAdminGoodsService.java b/src/main/java/com/rj/service/HxrAdminGoodsService.java new file mode 100644 index 0000000..76e0a33 --- /dev/null +++ b/src/main/java/com/rj/service/HxrAdminGoodsService.java @@ -0,0 +1,101 @@ +package com.rj.service; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.rj.config.HxrAdminProperties; +import com.rj.dto.hxr.HxrLbGoodsSelectResponse; +import lombok.RequiredArgsConstructor; +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.time.Duration; +import java.util.Optional; + +/** + * 调用 hxrd {@code /api/order/goods} 货品列表(请求头 {@code token} 鉴权)。 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class HxrAdminGoodsService { + + public static final int GOODS_PAGE_SIZE = 20; + + private static final ObjectMapper JSON = new ObjectMapper() + .setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .registerModule(new JavaTimeModule()) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + + private final HxrAdminProperties properties; + + /** + * 分页拉取货品列表。 + * + * @param page 页码,从 1 开始 + */ + public Optional fetchGoodsPage(int page) throws Exception { + String token = properties.getGoodsApiToken(); + if (token == null || token.isBlank()) { + log.warn("hxr.admin 未配置 goods-api-token,跳过 /api/order/goods"); + return Optional.empty(); + } + + String uri = UriComponentsBuilder.fromUriString(properties.getGoodsApiUrl()) + .replaceQueryParam("page", Math.max(1, page)) + .replaceQueryParam("limit", GOODS_PAGE_SIZE) + .build() + .encode() + .toUriString(); + + HttpClient client = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(35)) + .followRedirects(HttpClient.Redirect.NORMAL) + .build(); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(uri)) + .timeout(Duration.ofSeconds(120)) + .header("Accept", "application/json, text/plain, */*") + .header("token", token.trim()) + .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") + .GET() + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + int status = response.statusCode(); + if (status < 200 || status >= 300) { + log.warn("hxr /api/order/goods HTTP {} page={} bodyPrefix={}", + status, page, abbreviate(response.body(), 400)); + return Optional.empty(); + } + + String bodyText = response.body(); + if (bodyText == null || bodyText.isBlank()) { + log.warn("hxr /api/order/goods empty body page={}", page); + return Optional.empty(); + } + + HxrLbGoodsSelectResponse body = JSON.readValue(bodyText, HxrLbGoodsSelectResponse.class); + if (body.code() != 0) { + log.warn("hxr /api/order/goods api code={} msg={} page={}", body.code(), body.msg(), page); + return Optional.empty(); + } + return Optional.of(body); + } + + private static String abbreviate(String s, int maxLen) { + if (s == null) { + return ""; + } + return s.length() <= maxLen ? s : s.substring(0, maxLen) + "..."; + } +} diff --git a/src/main/java/com/rj/service/ILbGoodsService.java b/src/main/java/com/rj/service/ILbGoodsService.java index beeafed..ee314b5 100644 --- a/src/main/java/com/rj/service/ILbGoodsService.java +++ b/src/main/java/com/rj/service/ILbGoodsService.java @@ -27,4 +27,9 @@ public interface ILbGoodsService extends IService { String createdAtEnd, String updatedAtStart, String updatedAtEnd); + + /** + * 分页调用第三方 {@code /api/order/goods},解析后 upsert 到 {@code lb_goods}。 + */ + Map syncFromHxrGoods(String tenantId); } diff --git a/src/main/java/com/rj/service/impl/LbGoodsServiceImpl.java b/src/main/java/com/rj/service/impl/LbGoodsServiceImpl.java index 632a07d..cc70ce4 100644 --- a/src/main/java/com/rj/service/impl/LbGoodsServiceImpl.java +++ b/src/main/java/com/rj/service/impl/LbGoodsServiceImpl.java @@ -3,25 +3,38 @@ 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.hxr.HxrLbGoodsPageData; +import com.rj.dto.hxr.HxrLbGoodsSelectResponse; import com.rj.entity.LbGoods; import com.rj.mapper.LbGoodsMapper; +import com.rj.service.HxrAdminGoodsService; import com.rj.service.ILbGoodsService; +import com.rj.tenant.TenantContextHolder; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; 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; @Slf4j @Service +@RequiredArgsConstructor public class LbGoodsServiceImpl extends ServiceImpl implements ILbGoodsService { private static final DateTimeFormatter DATETIME_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + private final HxrAdminGoodsService hxrAdminGoodsService; + @Override public Map add(LbGoods entity) { Map result = new HashMap<>(); @@ -226,6 +239,138 @@ public class LbGoodsServiceImpl extends ServiceImpl impl } } + @Override + @Transactional(rollbackFor = Exception.class) + public Map syncFromHxrGoods(String tenantId) { + Map result = new HashMap<>(); + try { + if (tenantId == null || tenantId.trim().isEmpty()) { + result.put("success", false); + result.put("message", "tenantId不能为空"); + return result; + } + + String tid = tenantId.trim(); + String previousTenantId = TenantContextHolder.getTenantId(); + TenantContextHolder.setTenantId(tid); + try { + HxrLbGoodsPageData firstPageData = null; + int page = 1; + int totalSynced = 0; + while (true) { + Optional opt = hxrAdminGoodsService.fetchGoodsPage(page); + if (opt.isEmpty()) { + if (page == 1) { + result.put("success", false); + result.put("message", "未拉取到货品(请检查 hxr.admin.goods-api-token、网络或第三方返回)"); + result.put("synced", 0); + return result; + } + result.put("success", false); + result.put("message", "第 " + page + " 页拉取失败,已成功同步前 " + + (page - 1) + " 页共 " + totalSynced + " 条"); + result.put("synced", totalSynced); + if (firstPageData != null) { + result.put("lastPage", firstPageData.lastPage()); + } + result.put("pages", page - 1); + return result; + } + + HxrLbGoodsSelectResponse body = opt.get(); + HxrLbGoodsPageData pageData = body.data(); + if (firstPageData == null && pageData != null) { + firstPageData = pageData; + } + List rows = pageData != null ? pageData.list() : null; + if (rows == null || rows.isEmpty()) { + break; + } + + List entities = new ArrayList<>(rows.size()); + LocalDateTime now = LocalDateTime.now(); + for (LbGoods row : rows) { + if (row.getId() == null) { + continue; + } + row.setTenantId(tid); + applyDefaults(row); + if (row.getCreatedAt() == null) { + row.setCreatedAt(now); + } + if (row.getUpdatedAt() == null) { + row.setUpdatedAt(now); + } + entities.add(row); + } + + int upserted = upsertBatch(entities); + if (upserted < 0) { + result.put("success", false); + result.put("message", "第 " + page + " 页保存失败,已成功同步前 " + + (page - 1) + " 页共 " + totalSynced + " 条"); + result.put("synced", totalSynced); + if (firstPageData != null) { + result.put("lastPage", firstPageData.lastPage()); + } + result.put("pages", page - 1); + return result; + } + totalSynced += upserted; + + boolean hasMore = pageData != null && pageData.hasmore(); + int lastPage = pageData != null ? pageData.lastPage() : page; + if (!hasMore || page >= lastPage) { + break; + } + page++; + } + + result.put("success", true); + result.put("message", totalSynced == 0 ? "接口成功,无货品数据" : "同步完成"); + result.put("synced", totalSynced); + result.put("lastPage", firstPageData != null ? firstPageData.lastPage() : page); + result.put("pages", page); + return result; + } finally { + if (previousTenantId != null) { + TenantContextHolder.setTenantId(previousTenantId); + } else { + TenantContextHolder.clear(); + } + } + } catch (Exception e) { + result.put("success", false); + result.put("message", "同步异常:" + e.getMessage()); + result.put("synced", 0); + log.error("syncFromHxrGoods failed", e); + return result; + } + } + + private int upsertBatch(List entities) { + if (entities == null || entities.isEmpty()) { + return 0; + } + Map deduped = new LinkedHashMap<>(); + for (LbGoods entity : entities) { + if (entity.getId() != null) { + deduped.put(entity.getId(), entity); + } + } + if (deduped.isEmpty()) { + return 0; + } + List rows = new ArrayList<>(deduped.values()); + try { + baseMapper.upsertBatch(rows); + return rows.size(); + } catch (Exception e) { + log.error("lb_goods upsertBatch failed, size={}", rows.size(), e); + return -1; + } + } + private static void applyDefaults(LbGoods entity) { if (entity.getPrice() == null) { entity.setPrice(BigDecimal.ZERO); diff --git a/src/main/resources/mapper/LbGoodsMapper.xml b/src/main/resources/mapper/LbGoodsMapper.xml new file mode 100644 index 0000000..cfbb56a --- /dev/null +++ b/src/main/resources/mapper/LbGoodsMapper.xml @@ -0,0 +1,33 @@ + + + + + + INSERT INTO lb_goods ( + id, tenant_id, old_id, user_id, title, image, price, total_money, + quantity, seller_id, is_show, status, created_at, updated_at + ) VALUES + + ( + #{item.id}, #{item.tenantId}, #{item.oldId}, #{item.userId}, #{item.title}, + #{item.image}, #{item.price}, #{item.totalMoney}, #{item.quantity}, + #{item.sellerId}, #{item.isShow}, #{item.status}, #{item.createdAt}, #{item.updatedAt} + ) + + ON DUPLICATE KEY UPDATE + tenant_id = VALUES(tenant_id), + old_id = VALUES(old_id), + user_id = VALUES(user_id), + title = VALUES(title), + image = VALUES(image), + price = VALUES(price), + total_money = VALUES(total_money), + quantity = VALUES(quantity), + seller_id = VALUES(seller_id), + is_show = VALUES(is_show), + status = VALUES(status), + created_at = VALUES(created_at), + updated_at = VALUES(updated_at) + + +