抢购货品同步

This commit is contained in:
2026-05-26 17:25:55 +08:00
parent 32fce665ea
commit 947bb21c8b
9 changed files with 327 additions and 0 deletions

View File

@@ -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<HxrLbGoodsSelectResponse> 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<String> 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) + "...";
}
}

View File

@@ -27,4 +27,9 @@ public interface ILbGoodsService extends IService<LbGoods> {
String createdAtEnd,
String updatedAtStart,
String updatedAtEnd);
/**
* 分页调用第三方 {@code /api/order/goods},解析后 upsert 到 {@code lb_goods}。
*/
Map<String, Object> syncFromHxrGoods(String tenantId);
}

View File

@@ -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<LbGoodsMapper, LbGoods> implements ILbGoodsService {
private static final DateTimeFormatter DATETIME_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private final HxrAdminGoodsService hxrAdminGoodsService;
@Override
public Map<String, Object> add(LbGoods entity) {
Map<String, Object> result = new HashMap<>();
@@ -226,6 +239,138 @@ public class LbGoodsServiceImpl extends ServiceImpl<LbGoodsMapper, LbGoods> impl
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public Map<String, Object> syncFromHxrGoods(String tenantId) {
Map<String, Object> 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<HxrLbGoodsSelectResponse> 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<LbGoods> rows = pageData != null ? pageData.list() : null;
if (rows == null || rows.isEmpty()) {
break;
}
List<LbGoods> 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<LbGoods> entities) {
if (entities == null || entities.isEmpty()) {
return 0;
}
Map<Long, LbGoods> deduped = new LinkedHashMap<>();
for (LbGoods entity : entities) {
if (entity.getId() != null) {
deduped.put(entity.getId(), entity);
}
}
if (deduped.isEmpty()) {
return 0;
}
List<LbGoods> 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);