fix(pdd): 类目规则解析 cat_rule_get_response、properties/全树 parent_spec;无销售维时补充 input_max_spec_num 提示

Made-with: Cursor
This commit is contained in:
huangyujie 2026-03-25 14:19:06 +08:00
parent 020747dbf1
commit e7e7c02e24
2 changed files with 265 additions and 7 deletions

View File

@ -51,7 +51,8 @@ public class PddCatRuleSpecAutoResolver {
} }
Long parentSpecId = PddOpenApiSupport.findFirstSaleParentSpecId(raw); Long parentSpecId = PddOpenApiSupport.findFirstSaleParentSpecId(raw);
if (parentSpecId == null || parentSpecId <= 0) { if (parentSpecId == null || parentSpecId <= 0) {
r.setDetail("类目规则中未解析到销售 parent_spec_id"); r.setDetail("类目规则中未解析到销售 parent_spec_id"
+ PddOpenApiSupport.suffixHintForMissingSaleParentSpec(raw));
return r; return r;
} }
List<ExternalPddProperties.PddSkuOverride> ovs = buildSkuOverridesBySpecName( List<ExternalPddProperties.PddSkuOverride> ovs = buildSkuOverridesBySpecName(

View File

@ -8,7 +8,9 @@ import org.springframework.util.StringUtils;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map; import java.util.Map;
import java.util.Set;
/** /**
* 解析拼多多 POP 返回 JSON 的通用工具 * 解析拼多多 POP 返回 JSON 的通用工具
@ -139,7 +141,10 @@ public final class PddOpenApiSupport {
} }
/** /**
* 在类目规则 JSON 中递归查找 is_sale true 的节点上的 parent_spec_id取第一个 * 在类目规则 JSON 中解析销售规格对应的 {@code parent_spec_id}取第一个
* <p>兼容{@code is_sale}/{@code isSale}{@code parent_spec_id}/{@code parentSpecId}/字符串数值
* 若整树未命中再尝试 {@code goods_properties_rule.properties}常见 {@code goods_spec_rule} 等数组
* 若全树仅出现<strong>一个</strong>不同的 {@code parent_spec_id}则采用单规格维度类目</p>
*/ */
public static Long findFirstSaleParentSpecId(String catRuleBody) { public static Long findFirstSaleParentSpecId(String catRuleBody) {
if (!StringUtils.hasText(catRuleBody)) { if (!StringUtils.hasText(catRuleBody)) {
@ -150,30 +155,182 @@ public final class PddOpenApiSupport {
if (root == null) { if (root == null) {
return null; return null;
} }
JSONObject inner = root.getJSONObject("goods_cat_rule_get_response"); JSONObject inner = unwrapCatRulePayload(root);
if (inner == null) { if (inner == null) {
inner = root; return null;
} }
long[] holder = new long[]{0L}; long[] holder = new long[]{0L};
walkForSaleParent(inner, holder); walkForSaleParent(inner, holder);
return holder[0] > 0 ? holder[0] : null; if (holder[0] > 0) {
return holder[0];
}
Long fromProps = extractParentSpecFromGoodsPropertiesRule(inner);
if (fromProps != null && fromProps > 0) {
return fromProps;
}
Long fb = fallbackParentSpecFromSpecRuleArrays(inner);
if (fb != null && fb > 0) {
return fb;
}
return uniqueParentSpecIdIfSingle(inner);
} catch (Exception e) { } catch (Exception e) {
return null; return null;
} }
} }
/**
* {@link #findFirstSaleParentSpecId} 为空时根据类目规则返回体补充说明与处置建议例如 {@code input_max_spec_num=0} 的图书属性类目
*/
public static String suffixHintForMissingSaleParentSpec(String catRuleBody) {
if (!StringUtils.hasText(catRuleBody)) {
return "";
}
try {
JSONObject root = JSON.parseObject(catRuleBody);
if (root == null) {
return "";
}
JSONObject inner = unwrapCatRulePayload(root);
if (inner == null) {
return "";
}
JSONObject gpr = inner.getJSONObject("goods_properties_rule");
if (gpr == null) {
return "。返回体中无 goods_properties_rule无法自动解析销售规格请配置 external.pdd.sku-overrides 或核对类目。";
}
int maxSpec = gpr.getIntValue("input_max_spec_num");
JSONArray props = gpr.getJSONArray("properties");
boolean anySale = false;
if (props != null) {
for (int i = 0; i < props.size(); i++) {
Object el = props.get(i);
if (el instanceof JSONObject p) {
if (isTruthySale(p.get("is_sale")) || isTruthySale(p.get("isSale"))) {
anySale = true;
break;
}
}
}
}
StringBuilder sb = new StringBuilder("");
if (maxSpec == 0) {
sb.append(" 拼多多返回 goods_properties_rule.input_max_spec_num=0不允许商家再增自定义规格维");
}
if (!anySale && props != null && !props.isEmpty()) {
sb.append(" properties 中无 is_sale=true 的销售规格行多为书名、ISBN 等普通属性),");
}
sb.append("无法通过 pdd.goods.spec.id.get 自动生成 SKU 的 spec_id_list。");
sb.append(" 请在 external.pdd.sku-overrides 中为每条 SKU 配置 spec-id-list或改用拼多多侧支持「销售规格」的叶子类目并同步调整 category-map。");
return sb.toString();
} catch (Exception e) {
return "。解析类目规则失败;请配置 external.pdd.sku-overrides 或开启全量日志核对 cat_rule 返回结构。";
}
}
/**
* POP 成功体多为 {@code goods_cat_rule_get_response}部分环境为 {@code cat_rule_get_response}与日志一致
*/
private static JSONObject unwrapCatRulePayload(JSONObject root) {
if (root == null) {
return null;
}
JSONObject a = root.getJSONObject("goods_cat_rule_get_response");
if (a != null) {
return a;
}
JSONObject b = root.getJSONObject("cat_rule_get_response");
if (b != null) {
return b;
}
return root;
}
/**
* {@code goods_properties_rule.properties} 里找销售属性行上的 {@code parent_spec_id}
*/
private static Long extractParentSpecFromGoodsPropertiesRule(JSONObject inner) {
if (inner == null) {
return null;
}
JSONObject gpr = inner.getJSONObject("goods_properties_rule");
if (gpr == null) {
return null;
}
JSONArray props = gpr.getJSONArray("properties");
if (props == null || props.isEmpty()) {
return null;
}
for (int i = 0; i < props.size(); i++) {
Object el = props.get(i);
if (!(el instanceof JSONObject p)) {
continue;
}
boolean sale = isTruthySale(p.get("is_sale")) || isTruthySale(p.get("isSale"));
boolean skuRow = p.getBooleanValue("is_sku") || p.getBooleanValue("isSku");
if (!sale && !skuRow) {
continue;
}
Long pid = readParentSpecId(p);
if (pid != null && pid > 0) {
return pid;
}
JSONObject spec = p.getJSONObject("spec");
if (spec != null) {
pid = readParentSpecId(spec);
if (pid != null && pid > 0) {
return pid;
}
}
}
return null;
}
/**
* 全树收集 {@code parent_spec_id}仅当恰好一个不同取值时返回避免多规格维度误选
*/
private static Long uniqueParentSpecIdIfSingle(JSONObject inner) {
Set<Long> ids = new LinkedHashSet<>();
collectAllParentSpecIds(inner, ids);
if (ids.size() == 1) {
return ids.iterator().next();
}
return null;
}
private static void collectAllParentSpecIds(Object node, Set<Long> out) {
if (node instanceof JSONObject jo) {
Long p = readParentSpecId(jo);
if (p != null && p > 0) {
out.add(p);
}
for (String k : jo.keySet()) {
collectAllParentSpecIds(jo.get(k), out);
}
} else if (node instanceof JSONArray ja) {
for (int i = 0; i < ja.size(); i++) {
collectAllParentSpecIds(ja.get(i), out);
}
}
}
private static void walkForSaleParent(Object node, long[] holder) { private static void walkForSaleParent(Object node, long[] holder) {
if (holder[0] > 0) { if (holder[0] > 0) {
return; return;
} }
if (node instanceof JSONObject jo) { if (node instanceof JSONObject jo) {
if (isTruthySale(jo.get("is_sale"))) { boolean sale = isTruthySale(jo.get("is_sale")) || isTruthySale(jo.get("isSale"));
Long p = jo.getLong("parent_spec_id"); if (sale) {
Long p = readParentSpecId(jo);
if (p != null && p > 0) { if (p != null && p > 0) {
holder[0] = p; holder[0] = p;
return; return;
} }
} }
Long pid = readParentSpecId(jo);
if (pid != null && pid > 0 && (isTruthySale(jo.get("sale")) || isOne(jo.get("spec_type")))) {
holder[0] = pid;
return;
}
for (String k : jo.keySet()) { for (String k : jo.keySet()) {
walkForSaleParent(jo.get(k), holder); walkForSaleParent(jo.get(k), holder);
if (holder[0] > 0) { if (holder[0] > 0) {
@ -190,6 +347,106 @@ public final class PddOpenApiSupport {
} }
} }
/**
* 部分类目返回的销售规格列表在固定 key 且字段名与递归路径不一致时在此补解析
*/
private static Long fallbackParentSpecFromSpecRuleArrays(JSONObject inner) {
if (inner == null) {
return null;
}
String[] arrayKeys = {
"goods_spec_rule",
"goods_spec_rules",
"spec_rule_list",
"spec_rules",
"cat_rule",
};
for (String key : arrayKeys) {
JSONArray arr = findFirstArrayByKey(inner, key);
if (arr == null || arr.isEmpty()) {
continue;
}
for (int i = 0; i < arr.size(); i++) {
Object el = arr.get(i);
if (!(el instanceof JSONObject row)) {
continue;
}
Long p = readParentSpecId(row);
if (p == null || p <= 0) {
continue;
}
if (isTruthySale(row.get("is_sale")) || isTruthySale(row.get("isSale"))) {
return p;
}
}
JSONObject first = arr.getJSONObject(0);
if (first != null) {
Long p = readParentSpecId(first);
if (p != null && p > 0 && arr.size() == 1) {
return p;
}
}
}
return null;
}
private static JSONArray findFirstArrayByKey(JSONObject root, String key) {
if (root == null || !StringUtils.hasText(key)) {
return null;
}
JSONArray direct = root.getJSONArray(key);
if (direct != null && !direct.isEmpty()) {
return direct;
}
for (String k : root.keySet()) {
Object v = root.get(k);
if (v instanceof JSONObject child) {
JSONArray nested = findFirstArrayByKey(child, key);
if (nested != null && !nested.isEmpty()) {
return nested;
}
}
}
return null;
}
private static Long readParentSpecId(JSONObject jo) {
if (jo == null) {
return null;
}
Long p = jo.getLong("parent_spec_id");
if (p != null && p > 0) {
return p;
}
p = jo.getLong("parentSpecId");
if (p != null && p > 0) {
return p;
}
String s = jo.getString("parent_spec_id");
if (!StringUtils.hasText(s)) {
s = jo.getString("parentSpecId");
}
if (StringUtils.hasText(s)) {
try {
long v = Long.parseLong(s.trim());
return v > 0 ? v : null;
} catch (NumberFormatException ignored) {
return null;
}
}
return null;
}
private static boolean isOne(Object v) {
if (v == null) {
return false;
}
if (v instanceof Number n) {
return n.intValue() == 1;
}
return "1".equals(String.valueOf(v).trim());
}
private static boolean isTruthySale(Object v) { private static boolean isTruthySale(Object v) {
if (v == null) { if (v == null) {
return false; return false;