From 412be9715827e2f266ff5e03609185dc5e8c5c91 Mon Sep 17 00:00:00 2001 From: huangyujie <27665451@qq.com> Date: Wed, 25 Mar 2026 17:05:45 +0800 Subject: [PATCH] =?UTF-8?q?feat(pdd):=20=E6=8C=89=20cat.rule=20=E5=BF=85?= =?UTF-8?q?=E5=A1=AB=E8=A1=A5=E5=85=A8=20goods=5Fproperties=EF=BC=8C?= =?UTF-8?q?=E6=8B=89=E5=8F=96=E7=B1=BB=E7=9B=AE=E8=A7=84=E5=88=99=E4=B8=8E?= =?UTF-8?q?=20attributes/refPid=20=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Made-with: Cursor --- .../src/main/resources/application.yml | 4 + .../src/main/resources/nacos/erp-api.yaml | 2 + .../request/ExternalGoodsUpsertRequest.java | 7 + .../external/pdd/ExternalPddProperties.java | 11 + .../pdd/ExternalPddPublishService.java | 65 +++- .../external/pdd/PddGoodsAddParamBuilder.java | 293 +++++++++++++++++- .../external/pdd/PddOpenApiSupport.java | 113 +++++++ 7 files changed, 479 insertions(+), 16 deletions(-) diff --git a/api/erp-api/src/main/resources/application.yml b/api/erp-api/src/main/resources/application.yml index 0df70592..a402920c 100644 --- a/api/erp-api/src/main/resources/application.yml +++ b/api/erp-api/src/main/resources/application.yml @@ -49,6 +49,10 @@ external: group-buy-discount-percent: 75 # 单买价(分)= 拼单价(分)+ 本值,默认 200=2 元 single-buy-add-fen: 200 + # 图书 ISBN 在 goods_properties 的 ref_pid(与 cat.rule 一致;0=不自动拼 ext.isbn) + isbn-goods-property-ref-pid: 425 + # 按 pdd.goods.cat.rule.get 中 goods_properties_rule 必填项自动补全 goods_properties + fill-goods-properties-from-cat-rule: true # categoryCode(请求体)-> 拼多多叶子类目 cat_id(与 maindata default-category-code=DEFAULT 对齐) category-map: DEFAULT: "15693" diff --git a/api/erp-api/src/main/resources/nacos/erp-api.yaml b/api/erp-api/src/main/resources/nacos/erp-api.yaml index 9f760f67..135039e1 100644 --- a/api/erp-api/src/main/resources/nacos/erp-api.yaml +++ b/api/erp-api/src/main/resources/nacos/erp-api.yaml @@ -49,6 +49,8 @@ external: two-pieces-discount: 99 group-buy-discount-percent: 75 single-buy-add-fen: 200 + isbn-goods-property-ref-pid: 425 + fill-goods-properties-from-cat-rule: true category-map: DEFAULT: "15693" cost-template-map: diff --git a/model/src/main/java/cn/qihangerp/model/request/ExternalGoodsUpsertRequest.java b/model/src/main/java/cn/qihangerp/model/request/ExternalGoodsUpsertRequest.java index a578fb2d..94011f57 100644 --- a/model/src/main/java/cn/qihangerp/model/request/ExternalGoodsUpsertRequest.java +++ b/model/src/main/java/cn/qihangerp/model/request/ExternalGoodsUpsertRequest.java @@ -1,6 +1,7 @@ package cn.qihangerp.model.request; import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; import java.math.BigDecimal; @@ -161,6 +162,12 @@ public class ExternalGoodsUpsertRequest { private String name; private String value; private String unit; + /** 拼多多类目属性 {@code ref_pid},与 {@code value} 一并写入 {@code goods_properties} */ + @JsonProperty("refPid") + @JsonAlias({"ref_pid", "refPid"}) + private Long refPid; + @JsonAlias({"vid"}) + private Long vid; } @Data diff --git a/service/src/main/java/cn/qihangerp/service/external/pdd/ExternalPddProperties.java b/service/src/main/java/cn/qihangerp/service/external/pdd/ExternalPddProperties.java index 37377f09..caaae393 100644 --- a/service/src/main/java/cn/qihangerp/service/external/pdd/ExternalPddProperties.java +++ b/service/src/main/java/cn/qihangerp/service/external/pdd/ExternalPddProperties.java @@ -61,6 +61,17 @@ public class ExternalPddProperties { */ private long singleBuyAddFen = 200L; + /** + * 图书等类目「ISBN编号」在 {@code goods_properties} 中的 {@code ref_pid}(以 {@code pdd.goods.cat.rule.get} 为准,常见 425)。 + * 当请求 {@code ext.isbn} 有值且本值 > 0 时自动拼接一条属性;为 0 则关闭自动拼接。 + */ + private long isbnGoodsPropertyRefPid = 425L; + + /** + * 是否根据 {@code pdd.goods.cat.rule.get} 中 {@code goods_properties_rule} 的必填项自动补全 {@code goods_properties}。 + */ + private boolean fillGoodsPropertiesFromCatRule = true; + /** * 与单次请求 {@code skus} 列表顺序一一对应(过滤掉 null 后的顺序), * 每条对应 PDD sku_list 一项的 spec_id_list 与 sku_properties。 diff --git a/service/src/main/java/cn/qihangerp/service/external/pdd/ExternalPddPublishService.java b/service/src/main/java/cn/qihangerp/service/external/pdd/ExternalPddPublishService.java index e1c1c0ba..489e540a 100644 --- a/service/src/main/java/cn/qihangerp/service/external/pdd/ExternalPddPublishService.java +++ b/service/src/main/java/cn/qihangerp/service/external/pdd/ExternalPddPublishService.java @@ -96,21 +96,22 @@ public class ExternalPddPublishService { boolean noSpecBookPublish = false; List effectiveOverrides = null; + PddCatRuleSpecAutoResolver.AutoResolveResult resolveResult = null; if (!CollectionUtils.isEmpty(props.getSkuOverrides())) { effectiveOverrides = props.getSkuOverrides(); } else if (props.isAutoResolveSpecIdsWhenSkuOverridesEmpty()) { - PddCatRuleSpecAutoResolver.AutoResolveResult ar = catRuleSpecAutoResolver.resolve(cred, gateway, req, skuRows, catId); - catRuleSnippet = ar.getCatRuleSnippet(); - catFetched = ar.isCatRuleFetched(); - autoDetail = ar.getDetail(); - if (ar.isResolved() && ar.getOverrides() != null && ar.getOverrides().size() == skuRows.size()) { - effectiveOverrides = ar.getOverrides(); + resolveResult = catRuleSpecAutoResolver.resolve(cred, gateway, req, skuRows, catId); + catRuleSnippet = resolveResult.getCatRuleSnippet(); + catFetched = resolveResult.isCatRuleFetched(); + autoDetail = resolveResult.getDetail(); + if (resolveResult.isResolved() && resolveResult.getOverrides() != null && resolveResult.getOverrides().size() == skuRows.size()) { + effectiveOverrides = resolveResult.getOverrides(); specAuto = true; - } else if (PddOpenApiSupport.isNoSpecBookCatRule(ar.getCatRuleRaw()) && skuRows.size() == 1) { + } else if (PddOpenApiSupport.isNoSpecBookCatRule(resolveResult.getCatRuleRaw()) && skuRows.size() == 1) { noSpecBookPublish = true; autoDetail = "类目 input_max_spec_num=0:拼多多无规格发品(单 sku_list,spec_id_list=\"[]\" 字符串,price/multi_price 分数字符串,未调用 spec.id.get)"; log.info("[PDD] no-spec book publish shopId={} outGoodsId={}", req.getShopId(), req.getOutGoodsId()); - } else if (PddOpenApiSupport.isNoSpecBookCatRule(ar.getCatRuleRaw())) { + } else if (PddOpenApiSupport.isNoSpecBookCatRule(resolveResult.getCatRuleRaw())) { log.info("[PDD] publish skipped shopId={} outGoodsId={} reason=no-spec-requires-single-sku skuCount={}", req.getShopId(), req.getOutGoodsId(), skuRows.size()); return PddPublishLaneResultVo.builder() @@ -140,11 +141,12 @@ public class ExternalPddPublishService { } } + String autoFetchedCatRuleRaw = null; if (props.isAutoFetchCatRule() && !catFetched) { try { - String raw = pddPopClient.invokeTopLevelBiz(gateway, cred.getAppKey(), cred.getAppSecret(), cred.getAccessToken(), + autoFetchedCatRuleRaw = pddPopClient.invokeTopLevelBiz(gateway, cred.getAppKey(), cred.getAppSecret(), cred.getAccessToken(), "pdd.goods.cat.rule.get", PddOpenApiSupport.catRuleGetTopLevelParams(catId)); - catRuleSnippet = PddOpenApiSupport.snippet(raw, 2000); + catRuleSnippet = PddOpenApiSupport.snippet(autoFetchedCatRuleRaw, 2000); catFetched = true; } catch (Exception e) { log.warn("pdd.goods.cat.rule.get shopId={} outGoodsId={} failed: {}", @@ -154,12 +156,15 @@ public class ExternalPddPublishService { } } + String catRuleRawForGoodsProps = resolveCatRuleRawForGoodsProperties( + cred, gateway, catId, resolveResult, autoFetchedCatRuleRaw, props, req); + try { log.info("[PDD] pdd.goods.add begin shopId={} outGoodsId={} catId={} skuCount={} noSpecBook={}", req.getShopId(), req.getOutGoodsId(), catId, skuRows.size(), noSpecBookPublish); String goodsAddRootJson = noSpecBookPublish - ? paramBuilder.buildParamJsonNoSpecBook(goods, skus, req, props) - : paramBuilder.buildParamJson(goods, skus, req, props, effectiveOverrides); + ? paramBuilder.buildParamJsonNoSpecBook(goods, skus, req, props, catRuleRawForGoodsProps) + : paramBuilder.buildParamJson(goods, skus, req, props, effectiveOverrides, catRuleRawForGoodsProps); String raw = pddPopClient.invokeGoodsAdd(gateway, cred.getAppKey(), cred.getAppSecret(), cred.getAccessToken(), goodsAddRootJson); boolean ok = !PddOpenApiSupport.isError(raw); @@ -222,4 +227,40 @@ public class ExternalPddPublishService { } return s.trim(); } + + /** + * 供 {@code goods_properties} 自动补全:优先规格解析链路上的 cat.rule,其次 {@code auto-fetch-cat-rule} 拉取结果,最后再请求一次。 + */ + private String resolveCatRuleRawForGoodsProperties(PddShopCredential cred, String gateway, long catId, + PddCatRuleSpecAutoResolver.AutoResolveResult resolveResult, + String autoFetchedCatRuleRaw, + ExternalPddProperties props, + ExternalGoodsUpsertRequest req) { + if (!props.isFillGoodsPropertiesFromCatRule()) { + return null; + } + String raw = resolveResult != null ? resolveResult.getCatRuleRaw() : null; + if (usableCatRuleJson(raw)) { + return raw; + } + if (usableCatRuleJson(autoFetchedCatRuleRaw)) { + return autoFetchedCatRuleRaw; + } + try { + return catRuleSpecAutoResolver.fetchCatRuleJson(cred, gateway, catId); + } catch (Exception e) { + log.warn("pdd.goods.cat.rule.get (goods_properties) shopId={} outGoodsId={} catId={} err={}", + req != null ? req.getShopId() : null, + req != null ? req.getOutGoodsId() : null, + catId, + e.getMessage()); + return null; + } + } + + private static boolean usableCatRuleJson(String raw) { + return StringUtils.hasText(raw) + && !PddOpenApiSupport.isError(raw) + && raw.contains("goods_properties_rule"); + } } diff --git a/service/src/main/java/cn/qihangerp/service/external/pdd/PddGoodsAddParamBuilder.java b/service/src/main/java/cn/qihangerp/service/external/pdd/PddGoodsAddParamBuilder.java index 09f41b35..3ec597eb 100644 --- a/service/src/main/java/cn/qihangerp/service/external/pdd/PddGoodsAddParamBuilder.java +++ b/service/src/main/java/cn/qihangerp/service/external/pdd/PddGoodsAddParamBuilder.java @@ -13,8 +13,11 @@ import org.springframework.util.StringUtils; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Map; +import java.util.Set; /** * 将 Canonical 落库结果组装为 {@code pdd.goods.add} 的单根业务 JSON(字段名为 POP 下划线形式); @@ -35,7 +38,8 @@ public class PddGoodsAddParamBuilder { } public String buildParamJson(OGoods goods, List skus, ExternalGoodsUpsertRequest req, - ExternalPddProperties props, List effectiveSkuOverrides) { + ExternalPddProperties props, List effectiveSkuOverrides, + String catRuleRaw) { Long catId = resolveCatId(req.getCategoryCode(), props); Long costTpl = resolveCostTemplate(req.getLogisticsTemplateCode(), props); if (catId == null || catId <= 0) { @@ -142,7 +146,7 @@ public class PddGoodsAddParamBuilder { long marketFen = resolvePddMarketPriceFen(maxSuggestedFen, maxSingleFen); root.put("market_price", marketFen); root.put("sku_list", skuList); - root.put("goods_properties", new JSONArray()); + root.put("goods_properties", buildGoodsProperties(goods, req, props, catRuleRaw)); return JSON.toJSONString(root); } @@ -154,7 +158,7 @@ public class PddGoodsAddParamBuilder { *

调用方须保证过滤后仅 1 条有效 SKU。

*/ public String buildParamJsonNoSpecBook(OGoods goods, List skus, ExternalGoodsUpsertRequest req, - ExternalPddProperties props) { + ExternalPddProperties props, String catRuleRaw) { Long catId = resolveCatId(req.getCategoryCode(), props); Long costTpl = resolveCostTemplate(req.getLogisticsTemplateCode(), props); if (catId == null || catId <= 0) { @@ -236,11 +240,292 @@ public class PddGoodsAddParamBuilder { root.put("market_price", marketFen); root.put("sku_list", skuList); - root.put("goods_properties", new JSONArray()); + root.put("goods_properties", buildGoodsProperties(goods, req, props, catRuleRaw)); return JSON.toJSONString(root); } + /** + * {@code pdd.goods.add} 根级 {@code goods_properties}: + *
    + *
  1. 请求 {@code attributes}({@code refPid} + {@code value} 或 {@code vid})
  2. + *
  3. {@code ext.isbn}(及配置的 {@link ExternalPddProperties#getIsbnGoodsPropertyRefPid()})兼容旧逻辑
  4. + *
  5. 若开启 {@link ExternalPddProperties#isFillGoodsPropertiesFromCatRule()} 且传入 {@code pdd.goods.cat.rule.get} 成功体: + * 解析 {@code goods_properties_rule.properties} 中 {@code required=true} 且非 {@code is_sku} 的项并自动补全(书名、ISBN、枚举单选等)
  6. + *
+ */ + private static JSONArray buildGoodsProperties(OGoods goods, ExternalGoodsUpsertRequest req, ExternalPddProperties props, + String catRuleRaw) { + JSONArray arr = new JSONArray(); + Set seenRefPid = new HashSet<>(); + if (req != null && !CollectionUtils.isEmpty(req.getAttributes())) { + for (ExternalGoodsUpsertRequest.AttributeItem a : req.getAttributes()) { + if (a == null || a.getRefPid() == null) { + continue; + } + if (!StringUtils.hasText(a.getValue()) && a.getVid() == null) { + continue; + } + long rp = a.getRefPid(); + if (!seenRefPid.add(rp)) { + continue; + } + JSONObject o = new JSONObject(); + o.put("ref_pid", rp); + if (a.getVid() != null) { + o.put("vid", a.getVid()); + } + o.put("value", a.getValue() == null ? "" : a.getValue().trim()); + o.put("punit", StringUtils.hasText(a.getUnit()) ? a.getUnit().trim() : ""); + arr.add(o); + } + } + String isbn = resolveIsbnFromExt(req, goods); + long isbnPid = props.getIsbnGoodsPropertyRefPid(); + if (StringUtils.hasText(isbn) && isbnPid > 0 && seenRefPid.add(isbnPid)) { + JSONObject o = new JSONObject(); + o.put("ref_pid", isbnPid); + o.put("value", isbn.trim()); + o.put("punit", ""); + arr.add(o); + } + + boolean usableRule = props.isFillGoodsPropertiesFromCatRule() + && StringUtils.hasText(catRuleRaw) + && !PddOpenApiSupport.isError(catRuleRaw) + && catRuleRaw.contains("goods_properties_rule"); + List requiredRows = usableRule + ? PddOpenApiSupport.listRequiredGoodsLevelPropertyRules(catRuleRaw) + : List.of(); + if (!requiredRows.isEmpty()) { + for (PddOpenApiSupport.CatGoodsPropertyRuleRow row : requiredRows) { + if (seenRefPid.contains(row.getRefPid())) { + continue; + } + ResolvedGoodsPropValue rv = resolveGoodsPropValueForCatRule(row, goods, req, props); + if (rv == null || (rv.vid() == null && !StringUtils.hasText(rv.value()))) { + continue; + } + appendGoodsPropertyEntry(arr, seenRefPid, row.getRefPid(), rv.vid(), rv.value(), ""); + } + List missing = new ArrayList<>(); + for (PddOpenApiSupport.CatGoodsPropertyRuleRow row : requiredRows) { + if (!seenRefPid.contains(row.getRefPid())) { + missing.add(row.getRefPid() + "[" + (row.getName() == null ? "" : row.getName()) + "]"); + } + } + if (!missing.isEmpty()) { + throw new IllegalStateException("当前类目 goods_properties 必填未补齐(见 pdd.goods.cat.rule.get):" + + String.join(", ", missing) + + ";请传 attributes(refPid+value 或 vid)、ext.pdd_prop_、ext.isbn(ISBN)等"); + } + } + return arr; + } + + private static void appendGoodsPropertyEntry(JSONArray arr, Set seenRefPid, long refPid, Long vid, String value, + String punit) { + if (!seenRefPid.add(refPid)) { + return; + } + JSONObject o = new JSONObject(); + o.put("ref_pid", refPid); + if (vid != null) { + o.put("vid", vid); + } + o.put("value", value == null ? "" : value); + o.put("punit", punit == null ? "" : punit); + arr.add(o); + } + + private record ResolvedGoodsPropValue(Long vid, String value) { + } + + private static ResolvedGoodsPropValue resolveGoodsPropValueForCatRule(PddOpenApiSupport.CatGoodsPropertyRuleRow rule, + OGoods goods, ExternalGoodsUpsertRequest req, + ExternalPddProperties props) { + if (req != null && !CollectionUtils.isEmpty(req.getAttributes())) { + for (ExternalGoodsUpsertRequest.AttributeItem a : req.getAttributes()) { + if (a == null || a.getRefPid() == null || a.getRefPid() != rule.getRefPid()) { + continue; + } + if (StringUtils.hasText(a.getValue()) || a.getVid() != null) { + return new ResolvedGoodsPropValue(a.getVid(), + a.getValue() == null ? "" : a.getValue().trim()); + } + } + } + JSONArray opts = rule.getValueOptions(); + if (opts != null && !opts.isEmpty()) { + if (opts.size() == 1) { + return pickEnumOption(opts.getJSONObject(0)); + } + String hint = goodsPropertyExtHint(rule, req, goods); + if (StringUtils.hasText(hint)) { + ResolvedGoodsPropValue m = matchEnumOption(opts, hint.trim()); + if (m != null) { + return m; + } + } + if (props.getIsbnGoodsPropertyRefPid() == rule.getRefPid()) { + String isbn = resolveIsbnFromExt(req, goods); + if (StringUtils.hasText(isbn)) { + ResolvedGoodsPropValue m = matchEnumOption(opts, isbn.trim()); + if (m != null) { + return m; + } + } + } + return null; + } + String v = goodsPropertyExtHint(rule, req, goods); + if (!StringUtils.hasText(v)) { + v = heuristicGoodsPropertyText(rule, goods, req, props); + } + if (!StringUtils.hasText(v)) { + return null; + } + v = applyMaxValueLen(v, rule.getMaxValueHint()); + return new ResolvedGoodsPropValue(null, v); + } + + private static ResolvedGoodsPropValue pickEnumOption(JSONObject opt) { + if (opt == null) { + return null; + } + Long vid = opt.getLong("vid"); + if (vid == null || vid <= 0) { + vid = opt.getLong("id"); + } + String val = opt.getString("value"); + if (!StringUtils.hasText(val)) { + val = opt.getString("name"); + } + if (vid == null || vid <= 0) { + vid = null; + } + return new ResolvedGoodsPropValue(vid, val == null ? "" : val); + } + + private static ResolvedGoodsPropValue matchEnumOption(JSONArray opts, String hint) { + if (opts == null || opts.isEmpty() || !StringUtils.hasText(hint)) { + return null; + } + String h = hint.trim().toLowerCase(Locale.ROOT); + for (int i = 0; i < opts.size(); i++) { + JSONObject opt = opts.getJSONObject(i); + if (opt == null) { + continue; + } + String val = opt.getString("value"); + if (!StringUtils.hasText(val)) { + val = opt.getString("name"); + } + if (!StringUtils.hasText(val)) { + continue; + } + String lv = val.trim().toLowerCase(Locale.ROOT); + if (lv.equals(h) || lv.contains(h) || h.contains(lv)) { + return pickEnumOption(opt); + } + } + return null; + } + + private static String goodsPropertyExtHint(PddOpenApiSupport.CatGoodsPropertyRuleRow rule, + ExternalGoodsUpsertRequest req, OGoods goods) { + String k1 = "pdd_prop_" + rule.getRefPid(); + String v = extFromCanonicalOrReq(req, goods, k1); + if (StringUtils.hasText(v)) { + return v; + } + if (StringUtils.hasText(rule.getName())) { + v = extFromCanonicalOrReq(req, goods, compactExtKey(rule.getName())); + if (StringUtils.hasText(v)) { + return v; + } + } + return null; + } + + private static String compactExtKey(String name) { + return name.replaceAll("\\s+", "").toLowerCase(Locale.ROOT); + } + + private static String heuristicGoodsPropertyText(PddOpenApiSupport.CatGoodsPropertyRuleRow rule, OGoods goods, + ExternalGoodsUpsertRequest req, ExternalPddProperties props) { + String name = rule.getName(); + if (StringUtils.hasText(name)) { + if (name.contains("ISBN") || name.contains("isbn")) { + return resolveIsbnFromExt(req, goods); + } + if (name.contains("书名")) { + if (goods != null && StringUtils.hasText(goods.getName())) { + return goods.getName().trim(); + } + if (req != null && StringUtils.hasText(req.getTitle())) { + return req.getTitle().trim(); + } + } + } + if (props.getIsbnGoodsPropertyRefPid() == rule.getRefPid()) { + return resolveIsbnFromExt(req, goods); + } + return null; + } + + private static String applyMaxValueLen(String value, String maxValueHint) { + if (!StringUtils.hasText(value) || !StringUtils.hasText(maxValueHint)) { + return value; + } + try { + int max = Integer.parseInt(maxValueHint.trim()); + if (max > 0 && value.length() > max) { + return value.substring(0, max); + } + } catch (NumberFormatException ignored) { + } + return value; + } + + private static String extFromCanonicalOrReq(ExternalGoodsUpsertRequest req, OGoods goods, String key) { + if (!StringUtils.hasText(key)) { + return null; + } + if (req != null && req.getExt() != null) { + String v = req.getExt().get(key); + if (StringUtils.hasText(v)) { + return v.trim(); + } + } + if (goods == null || !StringUtils.hasText(goods.getCanonicalExt())) { + return null; + } + try { + JSONObject c = JSON.parseObject(goods.getCanonicalExt()); + if (c == null) { + return null; + } + JSONObject ext = c.getJSONObject("ext"); + if (ext != null) { + String v = ext.getString(key); + if (StringUtils.hasText(v)) { + return v.trim(); + } + } + } catch (Exception ignored) { + } + return null; + } + + private static String resolveIsbnFromExt(ExternalGoodsUpsertRequest req, OGoods goods) { + String a = extFromCanonicalOrReq(req, goods, "isbn"); + if (StringUtils.hasText(a)) { + return a; + } + return extFromCanonicalOrReq(req, goods, "ISBN"); + } + private static String resolveGoodsDescForPdd(OGoods goods, ExternalGoodsUpsertRequest req) { if (goods != null && StringUtils.hasText(goods.getCanonicalExt())) { try { diff --git a/service/src/main/java/cn/qihangerp/service/external/pdd/PddOpenApiSupport.java b/service/src/main/java/cn/qihangerp/service/external/pdd/PddOpenApiSupport.java index 202ae339..22b66c95 100644 --- a/service/src/main/java/cn/qihangerp/service/external/pdd/PddOpenApiSupport.java +++ b/service/src/main/java/cn/qihangerp/service/external/pdd/PddOpenApiSupport.java @@ -7,8 +7,11 @@ import org.springframework.util.StringUtils; import java.math.BigDecimal; import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; import java.util.Set; @@ -508,4 +511,114 @@ public final class PddOpenApiSupport { return null; } } + + /** + * {@code goods_properties_rule.properties} 中「商品级」必填项({@code required} 为真且非 {@code is_sku}), + * 用于 {@code pdd.goods.add} 根级 {@code goods_properties} 自动补全。 + */ + public static List listRequiredGoodsLevelPropertyRules(String catRuleBody) { + if (!StringUtils.hasText(catRuleBody) || isError(catRuleBody)) { + return List.of(); + } + try { + JSONObject root = JSON.parseObject(catRuleBody); + if (root == null) { + return List.of(); + } + JSONObject inner = unwrapCatRulePayload(root); + if (inner == null) { + return List.of(); + } + JSONObject gpr = inner.getJSONObject("goods_properties_rule"); + if (gpr == null) { + return List.of(); + } + JSONArray props = gpr.getJSONArray("properties"); + if (props == null || props.isEmpty()) { + return List.of(); + } + List out = new ArrayList<>(); + for (int i = 0; i < props.size(); i++) { + Object el = props.get(i); + if (!(el instanceof JSONObject p)) { + continue; + } + if (!isTruthyRequired(p.get("required"))) { + continue; + } + if (p.getBooleanValue("is_sku") || p.getBooleanValue("isSku")) { + continue; + } + long refPid = p.getLongValue("ref_pid"); + if (refPid <= 0) { + refPid = p.getLongValue("refPid"); + } + if (refPid <= 0) { + continue; + } + String name = p.getString("name"); + JSONArray values = p.getJSONArray("values"); + if (values == null || values.isEmpty()) { + values = p.getJSONArray("property_values"); + } + String maxVal = p.getString("max_value"); + if (!StringUtils.hasText(maxVal)) { + maxVal = p.getString("maxValue"); + } + out.add(new CatGoodsPropertyRuleRow(refPid, name, values, maxVal)); + } + return out.isEmpty() ? List.of() : Collections.unmodifiableList(out); + } catch (Exception e) { + return List.of(); + } + } + + private static boolean isTruthyRequired(Object v) { + if (v == null) { + return false; + } + if (v instanceof Boolean b) { + return b; + } + if (v instanceof Number n) { + return n.intValue() == 1; + } + String s = String.valueOf(v).trim(); + return "1".equals(s) || "true".equalsIgnoreCase(s); + } + + /** + * 一条商品级类目属性规则(来自 {@code pdd.goods.cat.rule.get})。 + */ + public static final class CatGoodsPropertyRuleRow { + private final long refPid; + private final String name; + /** 可选值列表(非空时表示需从枚举中选,元素多为含 {@code vid}/{@code value} 的对象) */ + private final JSONArray valueOptions; + /** 文本最大长度提示(平台返回的字符串,多为数字) */ + private final String maxValueHint; + + public CatGoodsPropertyRuleRow(long refPid, String name, JSONArray valueOptions, String maxValueHint) { + this.refPid = refPid; + this.name = name; + this.valueOptions = valueOptions; + this.maxValueHint = maxValueHint; + } + + public long getRefPid() { + return refPid; + } + + public String getName() { + return name; + } + + public JSONArray getValueOptions() { + return valueOptions; + } + + public String getMaxValueHint() { + return maxValueHint; + } + } }