fix(pdd): 类目规则解析 cat_rule_get_response、properties/全树 parent_spec;无销售维时补充 input_max_spec_num 提示
Made-with: Cursor
This commit is contained in:
parent
020747dbf1
commit
e7e7c02e24
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue