feat(pdd): ISBN 规范化与校验位校验;external upsert 请求日志脱敏 pddPopAuth
Made-with: Cursor
This commit is contained in:
parent
ec1af6c757
commit
e2cc135ce3
|
|
@ -11,7 +11,8 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
|
|||
public class ExternalGoodsApiLogProperties {
|
||||
|
||||
/**
|
||||
* 是否打印 upsert/delist 的完整请求体(JSON,含 pddPopAuth 等敏感字段;生产务必关闭)。
|
||||
* 是否打印 upsert 的请求体 JSON({@code pddPopAuth} 中 appSecret 全掩、appKey/accessToken 首尾各保留若干字符便于排障)。
|
||||
* delist 请求体无凭证字段,仍为完整 JSON。
|
||||
*/
|
||||
private boolean logFullRequest = false;
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import cn.qihangerp.model.request.ExternalGoodsUpsertRequest;
|
|||
import cn.qihangerp.model.vo.ExternalGoodsUpsertResultVo;
|
||||
import cn.qihangerp.security.common.BaseController;
|
||||
import cn.qihangerp.erp.config.ExternalGoodsApiLogProperties;
|
||||
import cn.qihangerp.erp.support.ExternalGoodsRequestLogSupport;
|
||||
import cn.qihangerp.service.external.ExternalGoodsAppService;
|
||||
import cn.qihangerp.service.external.pdd.ExternalPddProperties;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
|
|
@ -42,7 +43,7 @@ public class ExternalGoodsController extends BaseController {
|
|||
@PostMapping("/upsert")
|
||||
public AjaxResult upsert(@RequestBody ExternalGoodsUpsertRequest req) {
|
||||
if (goodsApiLogProperties.isLogFullRequest()) {
|
||||
log.info("[external/goods/upsert] request={}", req == null ? "null" : JSON.toJSONString(req));
|
||||
log.info("[external/goods/upsert] request={}", ExternalGoodsRequestLogSupport.upsertRequestJsonForLog(req));
|
||||
}
|
||||
if (req == null || req.getShopId() == null || req.getShopId() <= 0) {
|
||||
return AjaxResult.error("参数错误:shopId不能为空");
|
||||
|
|
|
|||
|
|
@ -0,0 +1,79 @@
|
|||
package cn.qihangerp.erp.support;
|
||||
|
||||
import cn.qihangerp.model.request.ExternalGoodsUpsertRequest;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* 对外商品接口请求日志脱敏:{@code pddPopAuth} 中 appSecret 全掩、appKey/accessToken 首尾保留便于排障。
|
||||
*
|
||||
* @author guochengyu
|
||||
*/
|
||||
public final class ExternalGoodsRequestLogSupport {
|
||||
|
||||
private ExternalGoodsRequestLogSupport() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化为 JSON 并对 {@link ExternalGoodsUpsertRequest#getPddPopAuth()} 脱敏(不修改入参对象)。
|
||||
*/
|
||||
public static String upsertRequestJsonForLog(ExternalGoodsUpsertRequest req) {
|
||||
if (req == null) {
|
||||
return "null";
|
||||
}
|
||||
try {
|
||||
JSONObject root = JSON.parseObject(JSON.toJSONString(req));
|
||||
if (root == null) {
|
||||
return "{}";
|
||||
}
|
||||
JSONObject auth = root.getJSONObject("pddPopAuth");
|
||||
if (auth != null && !auth.isEmpty()) {
|
||||
maskPddPopAuth(auth);
|
||||
}
|
||||
return root.toJSONString();
|
||||
} catch (Exception e) {
|
||||
return "{\"_logSanitizeError\":true}";
|
||||
}
|
||||
}
|
||||
|
||||
private static void maskPddPopAuth(JSONObject auth) {
|
||||
replaceSecret(auth, "appKey", 4, 4);
|
||||
replaceSecret(auth, "appSecret", 0, 0);
|
||||
replaceSecret(auth, "accessToken", 4, 4);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param keepHead keepTail 均为 0 时表示整段替换为 {@code ***}(用于 appSecret)
|
||||
*/
|
||||
private static void replaceSecret(JSONObject auth, String field, int keepHead, int keepTail) {
|
||||
if (!auth.containsKey(field)) {
|
||||
return;
|
||||
}
|
||||
String raw = auth.getString(field);
|
||||
if (!StringUtils.hasText(raw)) {
|
||||
return;
|
||||
}
|
||||
if (keepHead == 0 && keepTail == 0) {
|
||||
auth.put(field, "***");
|
||||
return;
|
||||
}
|
||||
auth.put(field, maskKeepEnds(raw, keepHead, keepTail));
|
||||
}
|
||||
|
||||
private static String maskKeepEnds(String s, int head, int tail) {
|
||||
if (!StringUtils.hasText(s)) {
|
||||
return s;
|
||||
}
|
||||
if (head < 0) {
|
||||
head = 0;
|
||||
}
|
||||
if (tail < 0) {
|
||||
tail = 0;
|
||||
}
|
||||
if (s.length() <= head + tail) {
|
||||
return "***";
|
||||
}
|
||||
return s.substring(0, head) + "***" + s.substring(s.length() - tail);
|
||||
}
|
||||
}
|
||||
|
|
@ -31,7 +31,7 @@ external:
|
|||
api-key: xh-uat-erp-api-ak
|
||||
secret-key: xh-uat-erp-api-sk-9f2d3c4b5a6e7d8c
|
||||
timestamp-skew-ms: 300000
|
||||
# 上下架接口全量请求体日志;生产上线后改为 false
|
||||
# 上下架接口请求体日志(upsert 时 pddPopAuth 已脱敏);生产可关
|
||||
goods:
|
||||
log-full-request: true
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ external:
|
|||
secret-key: xh-uat-erp-api-sk-9f2d3c4b5a6e7d8c
|
||||
timestamp-skew-ms: 300000
|
||||
goods:
|
||||
# 生产上线后改为 false
|
||||
# upsert 请求体日志(pddPopAuth 已脱敏);生产可关
|
||||
log-full-request: true
|
||||
|
||||
# 拼多多 POP:与 jar 内 application.yml 对齐;Nacos 下发会覆盖本地
|
||||
|
|
|
|||
|
|
@ -275,7 +275,15 @@ public class PddGoodsAddParamBuilder {
|
|||
if (a.getVid() != null) {
|
||||
o.put("vid", a.getVid());
|
||||
}
|
||||
o.put("value", a.getValue() == null ? "" : a.getValue().trim());
|
||||
String attrVal = a.getValue() == null ? "" : a.getValue().trim();
|
||||
if (a.getVid() == null && StringUtils.hasText(attrVal) && props.getIsbnGoodsPropertyRefPid() > 0
|
||||
&& rp == props.getIsbnGoodsPropertyRefPid()) {
|
||||
String n = normalizeIsbnForPddGoodsProperty(attrVal);
|
||||
if (StringUtils.hasText(n)) {
|
||||
attrVal = n;
|
||||
}
|
||||
}
|
||||
o.put("value", attrVal);
|
||||
o.put("punit", StringUtils.hasText(a.getUnit()) ? a.getUnit().trim() : "");
|
||||
arr.add(o);
|
||||
}
|
||||
|
|
@ -283,9 +291,13 @@ public class PddGoodsAddParamBuilder {
|
|||
String isbn = resolveIsbnFromExt(req, goods);
|
||||
long isbnPid = props.getIsbnGoodsPropertyRefPid();
|
||||
if (StringUtils.hasText(isbn) && isbnPid > 0 && seenRefPid.add(isbnPid)) {
|
||||
String isbnVal = normalizeIsbnForPddGoodsProperty(isbn);
|
||||
if (!StringUtils.hasText(isbnVal)) {
|
||||
isbnVal = isbn.trim();
|
||||
}
|
||||
JSONObject o = new JSONObject();
|
||||
o.put("ref_pid", isbnPid);
|
||||
o.put("value", isbn.trim());
|
||||
o.put("value", isbnVal);
|
||||
o.put("punit", "");
|
||||
arr.add(o);
|
||||
}
|
||||
|
|
@ -350,8 +362,15 @@ public class PddGoodsAddParamBuilder {
|
|||
continue;
|
||||
}
|
||||
if (StringUtils.hasText(a.getValue()) || a.getVid() != null) {
|
||||
return new ResolvedGoodsPropValue(a.getVid(),
|
||||
a.getValue() == null ? "" : a.getValue().trim());
|
||||
String val = a.getValue() == null ? "" : a.getValue().trim();
|
||||
if (a.getVid() == null && StringUtils.hasText(val) && props.getIsbnGoodsPropertyRefPid() > 0
|
||||
&& rule.getRefPid() == props.getIsbnGoodsPropertyRefPid()) {
|
||||
String n = normalizeIsbnForPddGoodsProperty(val);
|
||||
if (StringUtils.hasText(n)) {
|
||||
val = n;
|
||||
}
|
||||
}
|
||||
return new ResolvedGoodsPropValue(a.getVid(), val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -370,7 +389,11 @@ public class PddGoodsAddParamBuilder {
|
|||
if (props.getIsbnGoodsPropertyRefPid() == rule.getRefPid()) {
|
||||
String isbn = resolveIsbnFromExt(req, goods);
|
||||
if (StringUtils.hasText(isbn)) {
|
||||
ResolvedGoodsPropValue m = matchEnumOption(opts, isbn.trim());
|
||||
String isbnMatchHint = normalizeIsbnForPddGoodsProperty(isbn);
|
||||
if (!StringUtils.hasText(isbnMatchHint)) {
|
||||
isbnMatchHint = isbn.trim();
|
||||
}
|
||||
ResolvedGoodsPropValue m = matchEnumOption(opts, isbnMatchHint);
|
||||
if (m != null) {
|
||||
return m;
|
||||
}
|
||||
|
|
@ -386,6 +409,7 @@ public class PddGoodsAddParamBuilder {
|
|||
return null;
|
||||
}
|
||||
v = applyMaxValueLen(v, rule.getMaxValueHint());
|
||||
v = normalizeIsbnGoodsPropertyValueIfApplicable(rule, props, v);
|
||||
return new ResolvedGoodsPropValue(null, v);
|
||||
}
|
||||
|
||||
|
|
@ -457,7 +481,12 @@ public class PddGoodsAddParamBuilder {
|
|||
String name = rule.getName();
|
||||
if (StringUtils.hasText(name)) {
|
||||
if (name.contains("ISBN") || name.contains("isbn")) {
|
||||
return resolveIsbnFromExt(req, goods);
|
||||
String raw = resolveIsbnFromExt(req, goods);
|
||||
if (!StringUtils.hasText(raw)) {
|
||||
return null;
|
||||
}
|
||||
String n = normalizeIsbnForPddGoodsProperty(raw);
|
||||
return StringUtils.hasText(n) ? n : raw.trim();
|
||||
}
|
||||
if (name.contains("书名")) {
|
||||
if (goods != null && StringUtils.hasText(goods.getName())) {
|
||||
|
|
@ -469,7 +498,12 @@ public class PddGoodsAddParamBuilder {
|
|||
}
|
||||
}
|
||||
if (props.getIsbnGoodsPropertyRefPid() == rule.getRefPid()) {
|
||||
return resolveIsbnFromExt(req, goods);
|
||||
String raw = resolveIsbnFromExt(req, goods);
|
||||
if (!StringUtils.hasText(raw)) {
|
||||
return null;
|
||||
}
|
||||
String n = normalizeIsbnForPddGoodsProperty(raw);
|
||||
return StringUtils.hasText(n) ? n : raw.trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
@ -526,6 +560,110 @@ public class PddGoodsAddParamBuilder {
|
|||
return extFromCanonicalOrReq(req, goods, "ISBN");
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼多多「ISBN编号」属性通常校验为 13 位纯数字;对横杠/空格、全角数字做清洗,并将合法 ISBN-10 转为 ISBN-13(978 前缀)。
|
||||
* <p>13 位时必须满足 ISBN-13(EAN-13)校验位,否则平台会报「格式不正确」。</p>
|
||||
*/
|
||||
private static String normalizeIsbnForPddGoodsProperty(String raw) {
|
||||
if (!StringUtils.hasText(raw)) {
|
||||
return null;
|
||||
}
|
||||
String s = normalizeFullWidthAsciiDigits(raw.trim());
|
||||
StringBuilder digits = new StringBuilder();
|
||||
for (int i = 0; i < s.length(); i++) {
|
||||
char c = s.charAt(i);
|
||||
if (c >= '0' && c <= '9') {
|
||||
digits.append(c);
|
||||
} else if ((c == 'X' || c == 'x') && digits.length() == 9) {
|
||||
digits.append('X');
|
||||
}
|
||||
}
|
||||
String compact = digits.toString();
|
||||
if (compact.length() == 13 && compact.chars().allMatch(Character::isDigit)) {
|
||||
if (!isIsbn13CheckDigitValid(compact)) {
|
||||
int expect = computeIsbn13CheckDigit(compact.substring(0, 12));
|
||||
throw new IllegalStateException("ISBN-13 校验位错误(拼多多会拒收):当前=" + compact
|
||||
+ ",末位应为 " + expect + ",请核对主档或 ext.isbn 是否为笔误");
|
||||
}
|
||||
return compact;
|
||||
}
|
||||
if (compact.length() == 10) {
|
||||
String first9 = compact.substring(0, 9);
|
||||
if (!first9.chars().allMatch(Character::isDigit)) {
|
||||
return null;
|
||||
}
|
||||
char last = compact.charAt(9);
|
||||
if (!(Character.isDigit(last) || last == 'X')) {
|
||||
return null;
|
||||
}
|
||||
return isbn10CompactToIsbn13(first9);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String normalizeFullWidthAsciiDigits(String s) {
|
||||
StringBuilder sb = new StringBuilder(s.length());
|
||||
for (int i = 0; i < s.length(); i++) {
|
||||
char c = s.charAt(i);
|
||||
if (c >= '0' && c <= '9') {
|
||||
sb.append((char) ('0' + (c - '0')));
|
||||
} else {
|
||||
sb.append(c);
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/** @param nineDigits ISBN-10 前 9 位(均为数字) */
|
||||
private static String isbn10CompactToIsbn13(String nineDigits) {
|
||||
String core = "978" + nineDigits;
|
||||
return core + computeIsbn13CheckDigit(core);
|
||||
}
|
||||
|
||||
/** 前 12 位均为数字时,返回 ISBN-13 第 13 位校验码(0–9)。 */
|
||||
private static int computeIsbn13CheckDigit(String twelveDigits) {
|
||||
if (twelveDigits == null || twelveDigits.length() != 12) {
|
||||
throw new IllegalArgumentException("ISBN-13 校验位计算需要恰好 12 位数字");
|
||||
}
|
||||
int sum = 0;
|
||||
for (int i = 0; i < 12; i++) {
|
||||
int d = twelveDigits.charAt(i) - '0';
|
||||
if (d < 0 || d > 9) {
|
||||
throw new IllegalArgumentException("ISBN-13 校验位计算仅允许数字");
|
||||
}
|
||||
sum += (i % 2 == 0) ? d : d * 3;
|
||||
}
|
||||
return (10 - (sum % 10)) % 10;
|
||||
}
|
||||
|
||||
private static boolean isIsbn13CheckDigitValid(String isbn13) {
|
||||
if (isbn13 == null || isbn13.length() != 13 || !isbn13.chars().allMatch(Character::isDigit)) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return computeIsbn13CheckDigit(isbn13.substring(0, 12)) == (isbn13.charAt(12) - '0');
|
||||
} catch (IllegalArgumentException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static String normalizeIsbnGoodsPropertyValueIfApplicable(PddOpenApiSupport.CatGoodsPropertyRuleRow rule,
|
||||
ExternalPddProperties props, String value) {
|
||||
if (!StringUtils.hasText(value)) {
|
||||
return value;
|
||||
}
|
||||
boolean isbnRule = props.getIsbnGoodsPropertyRefPid() > 0 && rule.getRefPid() == props.getIsbnGoodsPropertyRefPid();
|
||||
if (!isbnRule && StringUtils.hasText(rule.getName())) {
|
||||
String n = rule.getName();
|
||||
isbnRule = n.contains("ISBN") || n.contains("isbn");
|
||||
}
|
||||
if (!isbnRule) {
|
||||
return value;
|
||||
}
|
||||
String n = normalizeIsbnForPddGoodsProperty(value);
|
||||
return StringUtils.hasText(n) ? n : value;
|
||||
}
|
||||
|
||||
private static String resolveGoodsDescForPdd(OGoods goods, ExternalGoodsUpsertRequest req) {
|
||||
if (goods != null && StringUtils.hasText(goods.getCanonicalExt())) {
|
||||
try {
|
||||
|
|
|
|||
Loading…
Reference in New Issue