抢购货品同步
This commit is contained in:
@@ -60,6 +60,16 @@ public class HxrAdminProperties {
|
|||||||
*/
|
*/
|
||||||
private String userUpdateUrl = "https://hxrdhoutai.hxrdsm.cn/app/admin/user/update";
|
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}。
|
* 组装 Cookie 请求头:优先 {@link #cookie},否则 {@code PHPSID=}{@link #phpsid}。
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -110,4 +110,20 @@ public class LbGoodsController {
|
|||||||
}
|
}
|
||||||
return ResponseEntity.internalServerError().body(result);
|
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<Map<String, Object>> syncFromHxr(
|
||||||
|
@Parameter(description = "租户 id", required = true) @RequestParam String tenantId) {
|
||||||
|
Map<String, Object> result = lbGoodsService.syncFromHxrGoods(tenantId);
|
||||||
|
Boolean success = (Boolean) result.get("success");
|
||||||
|
if (success != null && success) {
|
||||||
|
return ResponseEntity.ok(result);
|
||||||
|
}
|
||||||
|
return ResponseEntity.badRequest().body(result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
8
src/main/java/com/rj/dto/hxr/HxrLbGoodsPageData.java
Normal file
8
src/main/java/com/rj/dto/hxr/HxrLbGoodsPageData.java
Normal file
@@ -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<LbGoods> list, boolean hasmore, int lastPage) {}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package com.rj.dto.hxr;
|
||||||
|
|
||||||
|
/** hxrd {@code /api/order/goods} 顶层 JSON。 */
|
||||||
|
public record HxrLbGoodsSelectResponse(int code, String msg, HxrLbGoodsPageData data) {}
|
||||||
@@ -3,7 +3,12 @@ package com.rj.mapper;
|
|||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
import com.rj.entity.LbGoods;
|
import com.rj.entity.LbGoods;
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Mapper
|
@Mapper
|
||||||
public interface LbGoodsMapper extends BaseMapper<LbGoods> {
|
public interface LbGoodsMapper extends BaseMapper<LbGoods> {
|
||||||
|
|
||||||
|
int upsertBatch(@Param("list") List<LbGoods> list);
|
||||||
}
|
}
|
||||||
|
|||||||
101
src/main/java/com/rj/service/HxrAdminGoodsService.java
Normal file
101
src/main/java/com/rj/service/HxrAdminGoodsService.java
Normal 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) + "...";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,4 +27,9 @@ public interface ILbGoodsService extends IService<LbGoods> {
|
|||||||
String createdAtEnd,
|
String createdAtEnd,
|
||||||
String updatedAtStart,
|
String updatedAtStart,
|
||||||
String updatedAtEnd);
|
String updatedAtEnd);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页调用第三方 {@code /api/order/goods},解析后 upsert 到 {@code lb_goods}。
|
||||||
|
*/
|
||||||
|
Map<String, Object> syncFromHxrGoods(String tenantId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,25 +3,38 @@ package com.rj.service.impl;
|
|||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
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.entity.LbGoods;
|
||||||
import com.rj.mapper.LbGoodsMapper;
|
import com.rj.mapper.LbGoodsMapper;
|
||||||
|
import com.rj.service.HxrAdminGoodsService;
|
||||||
import com.rj.service.ILbGoodsService;
|
import com.rj.service.ILbGoodsService;
|
||||||
|
import com.rj.tenant.TenantContextHolder;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.time.format.DateTimeParseException;
|
import java.time.format.DateTimeParseException;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
public class LbGoodsServiceImpl extends ServiceImpl<LbGoodsMapper, LbGoods> implements ILbGoodsService {
|
public class LbGoodsServiceImpl extends ServiceImpl<LbGoodsMapper, LbGoods> implements ILbGoodsService {
|
||||||
|
|
||||||
private static final DateTimeFormatter DATETIME_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
private static final DateTimeFormatter DATETIME_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||||
|
|
||||||
|
private final HxrAdminGoodsService hxrAdminGoodsService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<String, Object> add(LbGoods entity) {
|
public Map<String, Object> add(LbGoods entity) {
|
||||||
Map<String, Object> result = new HashMap<>();
|
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) {
|
private static void applyDefaults(LbGoods entity) {
|
||||||
if (entity.getPrice() == null) {
|
if (entity.getPrice() == null) {
|
||||||
entity.setPrice(BigDecimal.ZERO);
|
entity.setPrice(BigDecimal.ZERO);
|
||||||
|
|||||||
33
src/main/resources/mapper/LbGoodsMapper.xml
Normal file
33
src/main/resources/mapper/LbGoodsMapper.xml
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.rj.mapper.LbGoodsMapper">
|
||||||
|
|
||||||
|
<insert id="upsertBatch">
|
||||||
|
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
|
||||||
|
<foreach collection="list" item="item" separator=",">
|
||||||
|
(
|
||||||
|
#{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}
|
||||||
|
)
|
||||||
|
</foreach>
|
||||||
|
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)
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
</mapper>
|
||||||
Reference in New Issue
Block a user