从抢货接口读取货物包
This commit is contained in:
@@ -68,7 +68,22 @@ public class HxrAdminProperties {
|
|||||||
/**
|
/**
|
||||||
* 货品列表 API 请求头 {@code token}。
|
* 货品列表 API 请求头 {@code token}。
|
||||||
*/
|
*/
|
||||||
private String goodsApiToken = "c2c8cd18-90ff-4114-982b-6658d1121d51";
|
private String goodsApiToken = "04e2a8aa-d178-4da4-b97f-057849607824";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 货品 API 请求头 {@code Origin},与浏览器前端域一致。
|
||||||
|
*/
|
||||||
|
private String goodsApiOrigin = "https://hxrdweb.hxrdsm.cn";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 货品 API 请求头 {@code Referer}。
|
||||||
|
*/
|
||||||
|
private String goodsApiReferer = "https://hxrdweb.hxrdsm.cn/";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 货品 API 签名密钥(前端 configs.appStr)。
|
||||||
|
*/
|
||||||
|
private String goodsApiAppStr = "ssniQQ3UP2Vr8mXwaugssgaOLzQo0cX5";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 组装 Cookie 请求头:优先 {@link #cookie},否则 {@code PHPSID=}{@link #phpsid}。
|
* 组装 Cookie 请求头:优先 {@link #cookie},否则 {@code PHPSID=}{@link #phpsid}。
|
||||||
|
|||||||
@@ -115,8 +115,8 @@ public class LbGoodsController {
|
|||||||
@Operation(
|
@Operation(
|
||||||
summary = "从 hxrd 第三方同步货品",
|
summary = "从 hxrd 第三方同步货品",
|
||||||
description =
|
description =
|
||||||
"分页调用 GET /api/order/goods(每页 limit=20,请求头 token),"
|
"分页调用 GET /api/order/goods(每页 limit=20,自动计算 S/T/N 签名),"
|
||||||
+ "解析 data.list 后 upsert 到 lb_goods;需配置 hxr.admin.goods-api-token")
|
+ "解析 data.list 后 upsert 到 lb_goods;需配置 hxr.admin.goods-api-token、goods-api-app-str")
|
||||||
public ResponseEntity<Map<String, Object>> syncFromHxr(
|
public ResponseEntity<Map<String, Object>> syncFromHxr(
|
||||||
@Parameter(description = "租户 id", required = true) @RequestParam String tenantId) {
|
@Parameter(description = "租户 id", required = true) @RequestParam String tenantId) {
|
||||||
Map<String, Object> result = lbGoodsService.syncFromHxrGoods(tenantId);
|
Map<String, Object> result = lbGoodsService.syncFromHxrGoods(tenantId);
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package com.rj.dto.hxr;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonParser;
|
||||||
|
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||||
|
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.type.CollectionType;
|
||||||
|
import com.rj.entity.LbGoods;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 兼容第三方 {@code data} 两种形态:
|
||||||
|
* <ul>
|
||||||
|
* <li>对象:{@code {"list":[...],"hasmore":true,"last_page":13}}</li>
|
||||||
|
* <li>数组:{@code [...]}(按每页条数推断是否还有下一页)</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
public class HxrLbGoodsPageDataDeserializer extends JsonDeserializer<HxrLbGoodsPageData> {
|
||||||
|
|
||||||
|
private static final int DEFAULT_PAGE_SIZE = 20;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HxrLbGoodsPageData deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
|
||||||
|
JsonNode node = p.getCodec().readTree(p);
|
||||||
|
if (node == null || node.isNull()) {
|
||||||
|
return emptyPage();
|
||||||
|
}
|
||||||
|
if (node.isArray()) {
|
||||||
|
if (node.isEmpty()) {
|
||||||
|
return emptyPage();
|
||||||
|
}
|
||||||
|
if (node.get(0).isTextual() || node.get(0).isNumber()) {
|
||||||
|
return emptyPage();
|
||||||
|
}
|
||||||
|
List<LbGoods> list = readGoodsList(ctxt, node);
|
||||||
|
boolean hasMore = list.size() >= DEFAULT_PAGE_SIZE;
|
||||||
|
int lastPage = hasMore ? Integer.MAX_VALUE : 1;
|
||||||
|
return new HxrLbGoodsPageData(list, hasMore, lastPage);
|
||||||
|
}
|
||||||
|
JsonNode listNode = node.get("list");
|
||||||
|
List<LbGoods> list = listNode != null && listNode.isArray()
|
||||||
|
? readGoodsList(ctxt, listNode)
|
||||||
|
: List.of();
|
||||||
|
boolean hasmore = node.path("hasmore").asBoolean(false);
|
||||||
|
int lastPage = node.path("last_page").asInt(1);
|
||||||
|
if (lastPage < 1) {
|
||||||
|
lastPage = 1;
|
||||||
|
}
|
||||||
|
return new HxrLbGoodsPageData(list, hasmore, lastPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<LbGoods> readGoodsList(DeserializationContext ctxt, JsonNode listNode)
|
||||||
|
throws IOException {
|
||||||
|
CollectionType listType =
|
||||||
|
ctxt.getTypeFactory().constructCollectionType(List.class, LbGoods.class);
|
||||||
|
List<LbGoods> list = ctxt.readTreeAsValue(listNode, listType);
|
||||||
|
return list != null ? list : new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HxrLbGoodsPageData emptyPage() {
|
||||||
|
return new HxrLbGoodsPageData(List.of(), false, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,9 @@
|
|||||||
package com.rj.dto.hxr;
|
package com.rj.dto.hxr;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||||
|
|
||||||
/** hxrd {@code /api/order/goods} 顶层 JSON。 */
|
/** hxrd {@code /api/order/goods} 顶层 JSON。 */
|
||||||
public record HxrLbGoodsSelectResponse(int code, String msg, HxrLbGoodsPageData data) {}
|
public record HxrLbGoodsSelectResponse(
|
||||||
|
int code,
|
||||||
|
String msg,
|
||||||
|
@JsonDeserialize(using = HxrLbGoodsPageDataDeserializer.class) HxrLbGoodsPageData data) {}
|
||||||
|
|||||||
@@ -39,6 +39,10 @@ public class LbDeductionAmount implements Serializable {
|
|||||||
@Schema(description = "抵扣金额")
|
@Schema(description = "抵扣金额")
|
||||||
private BigDecimal dikouAmt;
|
private BigDecimal dikouAmt;
|
||||||
|
|
||||||
|
@TableField("tixian_amt")
|
||||||
|
@Schema(description = "提现金额")
|
||||||
|
private BigDecimal tixianAmt;
|
||||||
|
|
||||||
@TableField("original_text")
|
@TableField("original_text")
|
||||||
@Schema(description = "原始文本信息")
|
@Schema(description = "原始文本信息")
|
||||||
private String originalText;
|
private String originalText;
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
package com.rj.service;
|
package com.rj.service;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
|
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
|
||||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||||
import com.rj.config.HxrAdminProperties;
|
import com.rj.config.HxrAdminProperties;
|
||||||
import com.rj.dto.hxr.HxrLbGoodsSelectResponse;
|
import com.rj.dto.hxr.HxrLbGoodsSelectResponse;
|
||||||
|
import com.rj.util.HxrGoodsSignUtil;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -16,11 +18,14 @@ import java.net.URI;
|
|||||||
import java.net.http.HttpClient;
|
import java.net.http.HttpClient;
|
||||||
import java.net.http.HttpRequest;
|
import java.net.http.HttpRequest;
|
||||||
import java.net.http.HttpResponse;
|
import java.net.http.HttpResponse;
|
||||||
|
import java.security.SecureRandom;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 调用 hxrd {@code /api/order/goods} 货品列表(请求头 {@code token} 鉴权)。
|
* 调用 hxrd {@code /api/order/goods} 货品列表(请求头 {@code token} + {@code S/T/N} 鉴权)。
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@@ -29,6 +34,12 @@ public class HxrAdminGoodsService {
|
|||||||
|
|
||||||
public static final int GOODS_PAGE_SIZE = 20;
|
public static final int GOODS_PAGE_SIZE = 20;
|
||||||
|
|
||||||
|
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 ObjectMapper JSON = new ObjectMapper()
|
private static final ObjectMapper JSON = new ObjectMapper()
|
||||||
.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
|
.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
|
||||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
||||||
@@ -48,25 +59,46 @@ public class HxrAdminGoodsService {
|
|||||||
log.warn("hxr.admin 未配置 goods-api-token,跳过 /api/order/goods");
|
log.warn("hxr.admin 未配置 goods-api-token,跳过 /api/order/goods");
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
String appStr = properties.getGoodsApiAppStr();
|
||||||
|
if (appStr == null || appStr.isBlank()) {
|
||||||
|
log.warn("hxr.admin 未配置 goods-api-app-str,跳过 /api/order/goods");
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
int safePage = Math.max(1, page);
|
||||||
String uri = UriComponentsBuilder.fromUriString(properties.getGoodsApiUrl())
|
String uri = UriComponentsBuilder.fromUriString(properties.getGoodsApiUrl())
|
||||||
.replaceQueryParam("page", Math.max(1, page))
|
.replaceQueryParam("page", safePage)
|
||||||
.replaceQueryParam("limit", GOODS_PAGE_SIZE)
|
.replaceQueryParam("limit", GOODS_PAGE_SIZE)
|
||||||
.build()
|
.build()
|
||||||
.encode()
|
.encode()
|
||||||
.toUriString();
|
.toUriString();
|
||||||
|
|
||||||
|
long timestamp = System.currentTimeMillis() / 1000;
|
||||||
|
String noncestr = randomNoncestr();
|
||||||
|
Map<String, Object> signParams = new LinkedHashMap<>();
|
||||||
|
signParams.put("page", safePage);
|
||||||
|
signParams.put("limit", GOODS_PAGE_SIZE);
|
||||||
|
signParams.put("timestamp", timestamp);
|
||||||
|
signParams.put("noncestr", noncestr);
|
||||||
|
String sign = HxrGoodsSignUtil.computeSign(signParams, appStr.trim());
|
||||||
|
|
||||||
HttpClient client = HttpClient.newBuilder()
|
HttpClient client = HttpClient.newBuilder()
|
||||||
.connectTimeout(Duration.ofSeconds(35))
|
.connectTimeout(Duration.ofSeconds(60))
|
||||||
.followRedirects(HttpClient.Redirect.NORMAL)
|
.followRedirects(HttpClient.Redirect.NORMAL)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
.uri(URI.create(uri))
|
.uri(URI.create(uri))
|
||||||
.timeout(Duration.ofSeconds(120))
|
.timeout(Duration.ofSeconds(120))
|
||||||
.header("Accept", "application/json, text/plain, */*")
|
.header("Accept", "application/json,*/*")
|
||||||
|
.header("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8")
|
||||||
|
.header("Origin", properties.getGoodsApiOrigin())
|
||||||
|
.header("Referer", properties.getGoodsApiReferer())
|
||||||
|
.header("User-Agent", USER_AGENT)
|
||||||
.header("token", token.trim())
|
.header("token", token.trim())
|
||||||
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
|
.header("S", sign)
|
||||||
|
.header("T", String.valueOf(timestamp))
|
||||||
|
.header("N", noncestr)
|
||||||
.GET()
|
.GET()
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
@@ -74,24 +106,58 @@ public class HxrAdminGoodsService {
|
|||||||
int status = response.statusCode();
|
int status = response.statusCode();
|
||||||
if (status < 200 || status >= 300) {
|
if (status < 200 || status >= 300) {
|
||||||
log.warn("hxr /api/order/goods HTTP {} page={} bodyPrefix={}",
|
log.warn("hxr /api/order/goods HTTP {} page={} bodyPrefix={}",
|
||||||
status, page, abbreviate(response.body(), 400));
|
status, safePage, abbreviate(response.body(), 400));
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
String bodyText = response.body();
|
String bodyText = response.body();
|
||||||
if (bodyText == null || bodyText.isBlank()) {
|
if (bodyText == null || bodyText.isBlank()) {
|
||||||
log.warn("hxr /api/order/goods empty body page={}", page);
|
log.warn("hxr /api/order/goods empty body page={}", safePage);
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
HxrLbGoodsSelectResponse body = JSON.readValue(bodyText, HxrLbGoodsSelectResponse.class);
|
JsonNode root = JSON.readTree(bodyText);
|
||||||
if (body.code() != 0) {
|
int code = root.path("code").asInt(-1);
|
||||||
log.warn("hxr /api/order/goods api code={} msg={} page={}", body.code(), body.msg(), page);
|
String msg = root.path("msg").asText("");
|
||||||
|
if (code != 0) {
|
||||||
|
log.warn("hxr /api/order/goods api code={} msg={} page={} bodyPrefix={}",
|
||||||
|
code, msg, safePage, abbreviate(bodyText, 400));
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
JsonNode dataNode = root.get("data");
|
||||||
|
if (isErrorPayload(dataNode)) {
|
||||||
|
log.warn("hxr /api/order/goods 返回异常 data={} page={} msg={}", dataNode, safePage, msg);
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
HxrLbGoodsSelectResponse body = JSON.treeToValue(root, HxrLbGoodsSelectResponse.class);
|
||||||
return Optional.of(body);
|
return Optional.of(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 与前端 {@code Math.random().toString(36).slice(-5)} 接近的 5 位随机串。 */
|
||||||
|
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) {
|
private static String abbreviate(String s, int maxLen) {
|
||||||
if (s == null) {
|
if (s == null) {
|
||||||
return "";
|
return "";
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ public class LbDeductionAmountServiceImpl
|
|||||||
/** 优先匹配「申请…抵扣…金额数字」,避免误用「已提现金额」。 */
|
/** 优先匹配「申请…抵扣…金额数字」,避免误用「已提现金额」。 */
|
||||||
private static final Pattern DIKOU_AMT_PATTERN =
|
private static final Pattern DIKOU_AMT_PATTERN =
|
||||||
Pattern.compile("申请[^\\n]*?抵扣[^\\n]*?金额\\s*(\\d+(?:\\.\\d+)?)", Pattern.CASE_INSENSITIVE);
|
Pattern.compile("申请[^\\n]*?抵扣[^\\n]*?金额\\s*(\\d+(?:\\.\\d+)?)", Pattern.CASE_INSENSITIVE);
|
||||||
|
private static final Pattern TIXIAN_AMT_PATTERN =
|
||||||
|
Pattern.compile("已提现金额\\s*(\\d+(?:\\.\\d+)?)", Pattern.CASE_INSENSITIVE);
|
||||||
|
|
||||||
private static final String PARSE_SYSTEM_PROMPT = """
|
private static final String PARSE_SYSTEM_PROMPT = """
|
||||||
你是信息抽取助手。用户会提供「抵扣金记录」自然语言,常见为多行固定格式,例如:
|
你是信息抽取助手。用户会提供「抵扣金记录」自然语言,常见为多行固定格式,例如:
|
||||||
@@ -64,11 +66,12 @@ public class LbDeductionAmountServiceImpl
|
|||||||
1. 首行或文中「M月d日」(如 5月21日)仅作业务日期参考,不要写入 JSON(服务端会从原文补 createTime)。
|
1. 首行或文中「M月d日」(如 5月21日)仅作业务日期参考,不要写入 JSON(服务端会从原文补 createTime)。
|
||||||
2. 「姓名 + 空格 + 11位手机号」同一行:userName 为姓名,userPhone 为手机号。
|
2. 「姓名 + 空格 + 11位手机号」同一行:userName 为姓名,userPhone 为手机号。
|
||||||
3. dikouAmt 取含「申请」且含「抵扣」且含「金额」那一行中的数字(如上例 2678);不要用「已提现金额」作为 dikouAmt。
|
3. dikouAmt 取含「申请」且含「抵扣」且含「金额」那一行中的数字(如上例 2678);不要用「已提现金额」作为 dikouAmt。
|
||||||
4. FXJ、SJF 等字母为业务代号,忽略即可。
|
4. tixianAmt 取含「已提现金额」那一行中的数字(如上例 2678);若无该行可省略。
|
||||||
5. 金额只输出数字,不要带「元」等单位。
|
5. FXJ、SJF 等字母为业务代号,忽略即可。
|
||||||
|
6. 金额只输出数字,不要带「元」等单位。
|
||||||
|
|
||||||
请只输出一个 JSON 对象,不要 markdown 代码块,不要解释性文字。
|
请只输出一个 JSON 对象,不要 markdown 代码块,不要解释性文字。
|
||||||
字段名(必须完全一致):userName、userPhone、dikouAmt。
|
字段名(必须完全一致):userName、userPhone、dikouAmt、tixianAmt。
|
||||||
createTime 若填写,格式 yyyy-MM-dd HH:mm:ss;服务端仅将非当前年的年份校正为系统当前年,月日不变。
|
createTime 若填写,格式 yyyy-MM-dd HH:mm:ss;服务端仅将非当前年的年份校正为系统当前年,月日不变。
|
||||||
不要输出 id、tenantId、originalText、updateTime。
|
不要输出 id、tenantId、originalText、updateTime。
|
||||||
""";
|
""";
|
||||||
@@ -132,10 +135,11 @@ public class LbDeductionAmountServiceImpl
|
|||||||
|
|
||||||
enrichFromDeductionText(entity, trimmedText);
|
enrichFromDeductionText(entity, trimmedText);
|
||||||
log.debug(
|
log.debug(
|
||||||
"parseFromTextByLlmAndSave:userName={} userPhone={} dikouAmt={} createTime={}",
|
"parseFromTextByLlmAndSave:userName={} userPhone={} dikouAmt={} tixianAmt={} createTime={}",
|
||||||
entity.getUserName(),
|
entity.getUserName(),
|
||||||
entity.getUserPhone(),
|
entity.getUserPhone(),
|
||||||
entity.getDikouAmt(),
|
entity.getDikouAmt(),
|
||||||
|
entity.getTixianAmt(),
|
||||||
entity.getCreateTime());
|
entity.getCreateTime());
|
||||||
|
|
||||||
return add(entity);
|
return add(entity);
|
||||||
@@ -185,6 +189,7 @@ public class LbDeductionAmountServiceImpl
|
|||||||
fillCreateTimeFromText(entity, text);
|
fillCreateTimeFromText(entity, text);
|
||||||
// 含「申请…抵扣…金额」时以该行为准,避免模型误用「已提现金额」
|
// 含「申请…抵扣…金额」时以该行为准,避免模型误用「已提现金额」
|
||||||
fillDikouAmtFromText(entity, text);
|
fillDikouAmtFromText(entity, text);
|
||||||
|
fillTixianAmtFromText(entity, text);
|
||||||
if (entity.getUserPhone() == null || entity.getUserPhone().isBlank()) {
|
if (entity.getUserPhone() == null || entity.getUserPhone().isBlank()) {
|
||||||
fillPhoneFromText(entity, text);
|
fillPhoneFromText(entity, text);
|
||||||
}
|
}
|
||||||
@@ -221,6 +226,13 @@ public class LbDeductionAmountServiceImpl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void fillTixianAmtFromText(LbDeductionAmount entity, String text) {
|
||||||
|
Matcher matcher = TIXIAN_AMT_PATTERN.matcher(text);
|
||||||
|
if (matcher.find()) {
|
||||||
|
entity.setTixianAmt(new BigDecimal(matcher.group(1)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void fillPhoneFromText(LbDeductionAmount entity, String text) {
|
private void fillPhoneFromText(LbDeductionAmount entity, String text) {
|
||||||
Matcher matcher = MOBILE_PHONE_PATTERN.matcher(text);
|
Matcher matcher = MOBILE_PHONE_PATTERN.matcher(text);
|
||||||
if (matcher.find()) {
|
if (matcher.find()) {
|
||||||
@@ -283,6 +295,9 @@ public class LbDeductionAmountServiceImpl
|
|||||||
if (entity.getDikouAmt() == null) {
|
if (entity.getDikouAmt() == null) {
|
||||||
entity.setDikouAmt(BigDecimal.ZERO);
|
entity.setDikouAmt(BigDecimal.ZERO);
|
||||||
}
|
}
|
||||||
|
if (entity.getTixianAmt() == null) {
|
||||||
|
entity.setTixianAmt(BigDecimal.ZERO);
|
||||||
|
}
|
||||||
|
|
||||||
if (entity.getId() == null || entity.getId().trim().isEmpty()) {
|
if (entity.getId() == null || entity.getId().trim().isEmpty()) {
|
||||||
entity.setId(UUID.randomUUID().toString());
|
entity.setId(UUID.randomUUID().toString());
|
||||||
|
|||||||
@@ -262,7 +262,7 @@ public class LbGoodsServiceImpl extends ServiceImpl<LbGoodsMapper, LbGoods> impl
|
|||||||
if (opt.isEmpty()) {
|
if (opt.isEmpty()) {
|
||||||
if (page == 1) {
|
if (page == 1) {
|
||||||
result.put("success", false);
|
result.put("success", false);
|
||||||
result.put("message", "未拉取到货品(请检查 hxr.admin.goods-api-token、网络或第三方返回)");
|
result.put("message", "未拉取到货品(请检查 hxr.admin.goods-api-token、goods-api-app-str 或网络)");
|
||||||
result.put("synced", 0);
|
result.put("synced", 0);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
58
src/main/java/com/rj/util/HxrGoodsSignUtil.java
Normal file
58
src/main/java/com/rj/util/HxrGoodsSignUtil.java
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package com.rj.util;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/** hxrd 前端 {@code getSignHeader} 对应的 SHA256 签名。 */
|
||||||
|
public final class HxrGoodsSignUtil {
|
||||||
|
|
||||||
|
private HxrGoodsSignUtil() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按前端规则生成签名:参数键排序 → {@code k=v&...} → 拼接 appStr → SHA256 hex。
|
||||||
|
*
|
||||||
|
* @param params 请求参数 + {@code timestamp} + {@code noncestr}
|
||||||
|
* @param appStr 前端 configs.appStr
|
||||||
|
*/
|
||||||
|
public static String computeSign(Map<String, Object> params, String appStr) {
|
||||||
|
TreeMap<String, Object> sorted = new TreeMap<>();
|
||||||
|
if (params != null) {
|
||||||
|
for (Map.Entry<String, Object> entry : params.entrySet()) {
|
||||||
|
Object value = entry.getValue();
|
||||||
|
if (value == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String text = String.valueOf(value);
|
||||||
|
if (text.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
sorted.put(entry.getKey(), value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String payload = sorted.entrySet().stream()
|
||||||
|
.map(e -> e.getKey() + "=" + e.getValue())
|
||||||
|
.collect(Collectors.joining("&"));
|
||||||
|
if (appStr != null && !appStr.isEmpty()) {
|
||||||
|
payload += appStr;
|
||||||
|
}
|
||||||
|
return sha256Hex(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String sha256Hex(String input) {
|
||||||
|
try {
|
||||||
|
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||||
|
byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8));
|
||||||
|
StringBuilder sb = new StringBuilder(hash.length * 2);
|
||||||
|
for (byte b : hash) {
|
||||||
|
sb.append(String.format("%02x", b));
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new IllegalStateException("SHA-256 not available", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,11 @@ hxr:
|
|||||||
pull-initial-delay-ms: 0
|
pull-initial-delay-ms: 0
|
||||||
cookie: PHPSID=f3710baf6381da418aa832660ccb9d08
|
cookie: PHPSID=f3710baf6381da418aa832660ccb9d08
|
||||||
phpsid: "f3710baf6381da418aa832660ccb9d08"
|
phpsid: "f3710baf6381da418aa832660ccb9d08"
|
||||||
|
# 货品同步 /api/order/goods(LbGoodsController#sync-from-hxr)
|
||||||
|
goods-api-token: "04e2a8aa-d178-4da4-b97f-057849607824"
|
||||||
|
goods-api-app-str: "ssniQQ3UP2Vr8mXwaugssgaOLzQo0cX5"
|
||||||
|
goods-api-origin: "https://hxrdweb.hxrdsm.cn"
|
||||||
|
goods-api-referer: "https://hxrdweb.hxrdsm.cn/"
|
||||||
# DashScope API配置
|
# DashScope API配置
|
||||||
dashscope:
|
dashscope:
|
||||||
api:
|
api:
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE `lb_deduction_amount`
|
||||||
|
ADD COLUMN `tixian_amt` DECIMAL(18, 2) NULL DEFAULT 0 COMMENT '提现金额' AFTER `dikou_amt`;
|
||||||
26
src/test/java/com/rj/util/HxrGoodsSignUtilTest.java
Normal file
26
src/test/java/com/rj/util/HxrGoodsSignUtilTest.java
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package com.rj.util;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
class HxrGoodsSignUtilTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void computeSign_matchesBrowserDevToolsSample() {
|
||||||
|
Map<String, Object> params = new LinkedHashMap<>();
|
||||||
|
params.put("page", 11);
|
||||||
|
params.put("limit", 20);
|
||||||
|
params.put("timestamp", 1779789545L);
|
||||||
|
params.put("noncestr", "4s3yi");
|
||||||
|
|
||||||
|
String sign = HxrGoodsSignUtil.computeSign(params, "ssniQQ3UP2Vr8mXwaugssgaOLzQo0cX5");
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
"b9da2cce2cabf074401de36687dc90669ffe5a9ab9cc841bdaed412a13cbe536",
|
||||||
|
sign);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user