feat(pdd): 按 cat.rule 必填补全 goods_properties,拉取类目规则与 attributes/refPid 支持

Made-with: Cursor
This commit is contained in:
huangyujie 2026-03-25 17:05:45 +08:00
parent 89c239149c
commit 412be97158
7 changed files with 479 additions and 16 deletions

View File

@ -49,6 +49,10 @@ external:
group-buy-discount-percent: 75 group-buy-discount-percent: 75
# 单买价(分)= 拼单价(分)+ 本值,默认 200=2 元 # 单买价(分)= 拼单价(分)+ 本值,默认 200=2 元
single-buy-add-fen: 200 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 对齐) # categoryCode请求体-> 拼多多叶子类目 cat_id与 maindata default-category-code=DEFAULT 对齐)
category-map: category-map:
DEFAULT: "15693" DEFAULT: "15693"

View File

@ -49,6 +49,8 @@ external:
two-pieces-discount: 99 two-pieces-discount: 99
group-buy-discount-percent: 75 group-buy-discount-percent: 75
single-buy-add-fen: 200 single-buy-add-fen: 200
isbn-goods-property-ref-pid: 425
fill-goods-properties-from-cat-rule: true
category-map: category-map:
DEFAULT: "15693" DEFAULT: "15693"
cost-template-map: cost-template-map:

View File

@ -1,6 +1,7 @@
package cn.qihangerp.model.request; package cn.qihangerp.model.request;
import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data; import lombok.Data;
import java.math.BigDecimal; import java.math.BigDecimal;
@ -161,6 +162,12 @@ public class ExternalGoodsUpsertRequest {
private String name; private String name;
private String value; private String value;
private String unit; 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 @Data

View File

@ -61,6 +61,17 @@ public class ExternalPddProperties {
*/ */
private long singleBuyAddFen = 200L; 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 后的顺序 * 与单次请求 {@code skus} 列表顺序一一对应过滤掉 null 后的顺序
* 每条对应 PDD sku_list 一项的 spec_id_list sku_properties * 每条对应 PDD sku_list 一项的 spec_id_list sku_properties

View File

@ -96,21 +96,22 @@ public class ExternalPddPublishService {
boolean noSpecBookPublish = false; boolean noSpecBookPublish = false;
List<ExternalPddProperties.PddSkuOverride> effectiveOverrides = null; List<ExternalPddProperties.PddSkuOverride> effectiveOverrides = null;
PddCatRuleSpecAutoResolver.AutoResolveResult resolveResult = null;
if (!CollectionUtils.isEmpty(props.getSkuOverrides())) { if (!CollectionUtils.isEmpty(props.getSkuOverrides())) {
effectiveOverrides = props.getSkuOverrides(); effectiveOverrides = props.getSkuOverrides();
} else if (props.isAutoResolveSpecIdsWhenSkuOverridesEmpty()) { } else if (props.isAutoResolveSpecIdsWhenSkuOverridesEmpty()) {
PddCatRuleSpecAutoResolver.AutoResolveResult ar = catRuleSpecAutoResolver.resolve(cred, gateway, req, skuRows, catId); resolveResult = catRuleSpecAutoResolver.resolve(cred, gateway, req, skuRows, catId);
catRuleSnippet = ar.getCatRuleSnippet(); catRuleSnippet = resolveResult.getCatRuleSnippet();
catFetched = ar.isCatRuleFetched(); catFetched = resolveResult.isCatRuleFetched();
autoDetail = ar.getDetail(); autoDetail = resolveResult.getDetail();
if (ar.isResolved() && ar.getOverrides() != null && ar.getOverrides().size() == skuRows.size()) { if (resolveResult.isResolved() && resolveResult.getOverrides() != null && resolveResult.getOverrides().size() == skuRows.size()) {
effectiveOverrides = ar.getOverrides(); effectiveOverrides = resolveResult.getOverrides();
specAuto = true; specAuto = true;
} else if (PddOpenApiSupport.isNoSpecBookCatRule(ar.getCatRuleRaw()) && skuRows.size() == 1) { } else if (PddOpenApiSupport.isNoSpecBookCatRule(resolveResult.getCatRuleRaw()) && skuRows.size() == 1) {
noSpecBookPublish = true; noSpecBookPublish = true;
autoDetail = "类目 input_max_spec_num=0拼多多无规格发品单 sku_listspec_id_list=\"[]\" 字符串price/multi_price 分数字符串,未调用 spec.id.get"; autoDetail = "类目 input_max_spec_num=0拼多多无规格发品单 sku_listspec_id_list=\"[]\" 字符串price/multi_price 分数字符串,未调用 spec.id.get";
log.info("[PDD] no-spec book publish shopId={} outGoodsId={}", req.getShopId(), req.getOutGoodsId()); 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={}", log.info("[PDD] publish skipped shopId={} outGoodsId={} reason=no-spec-requires-single-sku skuCount={}",
req.getShopId(), req.getOutGoodsId(), skuRows.size()); req.getShopId(), req.getOutGoodsId(), skuRows.size());
return PddPublishLaneResultVo.builder() return PddPublishLaneResultVo.builder()
@ -140,11 +141,12 @@ public class ExternalPddPublishService {
} }
} }
String autoFetchedCatRuleRaw = null;
if (props.isAutoFetchCatRule() && !catFetched) { if (props.isAutoFetchCatRule() && !catFetched) {
try { 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)); "pdd.goods.cat.rule.get", PddOpenApiSupport.catRuleGetTopLevelParams(catId));
catRuleSnippet = PddOpenApiSupport.snippet(raw, 2000); catRuleSnippet = PddOpenApiSupport.snippet(autoFetchedCatRuleRaw, 2000);
catFetched = true; catFetched = true;
} catch (Exception e) { } catch (Exception e) {
log.warn("pdd.goods.cat.rule.get shopId={} outGoodsId={} failed: {}", 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 { try {
log.info("[PDD] pdd.goods.add begin shopId={} outGoodsId={} catId={} skuCount={} noSpecBook={}", log.info("[PDD] pdd.goods.add begin shopId={} outGoodsId={} catId={} skuCount={} noSpecBook={}",
req.getShopId(), req.getOutGoodsId(), catId, skuRows.size(), noSpecBookPublish); req.getShopId(), req.getOutGoodsId(), catId, skuRows.size(), noSpecBookPublish);
String goodsAddRootJson = noSpecBookPublish String goodsAddRootJson = noSpecBookPublish
? paramBuilder.buildParamJsonNoSpecBook(goods, skus, req, props) ? paramBuilder.buildParamJsonNoSpecBook(goods, skus, req, props, catRuleRawForGoodsProps)
: paramBuilder.buildParamJson(goods, skus, req, props, effectiveOverrides); : paramBuilder.buildParamJson(goods, skus, req, props, effectiveOverrides, catRuleRawForGoodsProps);
String raw = pddPopClient.invokeGoodsAdd(gateway, cred.getAppKey(), cred.getAppSecret(), cred.getAccessToken(), String raw = pddPopClient.invokeGoodsAdd(gateway, cred.getAppKey(), cred.getAppSecret(), cred.getAccessToken(),
goodsAddRootJson); goodsAddRootJson);
boolean ok = !PddOpenApiSupport.isError(raw); boolean ok = !PddOpenApiSupport.isError(raw);
@ -222,4 +227,40 @@ public class ExternalPddPublishService {
} }
return s.trim(); 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");
}
} }

View File

@ -13,8 +13,11 @@ import org.springframework.util.StringUtils;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.RoundingMode; import java.math.RoundingMode;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Set;
/** /**
* Canonical 落库结果组装为 {@code pdd.goods.add} <strong>单根业务 JSON</strong>字段名为 POP 下划线形式 * Canonical 落库结果组装为 {@code pdd.goods.add} <strong>单根业务 JSON</strong>字段名为 POP 下划线形式
@ -35,7 +38,8 @@ public class PddGoodsAddParamBuilder {
} }
public String buildParamJson(OGoods goods, List<OGoodsSku> skus, ExternalGoodsUpsertRequest req, public String buildParamJson(OGoods goods, List<OGoodsSku> skus, ExternalGoodsUpsertRequest req,
ExternalPddProperties props, List<ExternalPddProperties.PddSkuOverride> effectiveSkuOverrides) { ExternalPddProperties props, List<ExternalPddProperties.PddSkuOverride> effectiveSkuOverrides,
String catRuleRaw) {
Long catId = resolveCatId(req.getCategoryCode(), props); Long catId = resolveCatId(req.getCategoryCode(), props);
Long costTpl = resolveCostTemplate(req.getLogisticsTemplateCode(), props); Long costTpl = resolveCostTemplate(req.getLogisticsTemplateCode(), props);
if (catId == null || catId <= 0) { if (catId == null || catId <= 0) {
@ -142,7 +146,7 @@ public class PddGoodsAddParamBuilder {
long marketFen = resolvePddMarketPriceFen(maxSuggestedFen, maxSingleFen); long marketFen = resolvePddMarketPriceFen(maxSuggestedFen, maxSingleFen);
root.put("market_price", marketFen); root.put("market_price", marketFen);
root.put("sku_list", skuList); root.put("sku_list", skuList);
root.put("goods_properties", new JSONArray()); root.put("goods_properties", buildGoodsProperties(goods, req, props, catRuleRaw));
return JSON.toJSONString(root); return JSON.toJSONString(root);
} }
@ -154,7 +158,7 @@ public class PddGoodsAddParamBuilder {
* <p>调用方须保证过滤后仅 1 条有效 SKU</p> * <p>调用方须保证过滤后仅 1 条有效 SKU</p>
*/ */
public String buildParamJsonNoSpecBook(OGoods goods, List<OGoodsSku> skus, ExternalGoodsUpsertRequest req, public String buildParamJsonNoSpecBook(OGoods goods, List<OGoodsSku> skus, ExternalGoodsUpsertRequest req,
ExternalPddProperties props) { ExternalPddProperties props, String catRuleRaw) {
Long catId = resolveCatId(req.getCategoryCode(), props); Long catId = resolveCatId(req.getCategoryCode(), props);
Long costTpl = resolveCostTemplate(req.getLogisticsTemplateCode(), props); Long costTpl = resolveCostTemplate(req.getLogisticsTemplateCode(), props);
if (catId == null || catId <= 0) { if (catId == null || catId <= 0) {
@ -236,11 +240,292 @@ public class PddGoodsAddParamBuilder {
root.put("market_price", marketFen); root.put("market_price", marketFen);
root.put("sku_list", skuList); root.put("sku_list", skuList);
root.put("goods_properties", new JSONArray()); root.put("goods_properties", buildGoodsProperties(goods, req, props, catRuleRaw));
return JSON.toJSONString(root); return JSON.toJSONString(root);
} }
/**
* {@code pdd.goods.add} 根级 {@code goods_properties}
* <ol>
* <li>请求 {@code attributes}{@code refPid} + {@code value} {@code vid}</li>
* <li>{@code ext.isbn}及配置的 {@link ExternalPddProperties#getIsbnGoodsPropertyRefPid()}兼容旧逻辑</li>
* <li>若开启 {@link ExternalPddProperties#isFillGoodsPropertiesFromCatRule()} 且传入 {@code pdd.goods.cat.rule.get} 成功体
* 解析 {@code goods_properties_rule.properties} {@code required=true} 且非 {@code is_sku} 的项并自动补全书名ISBN枚举单选等</li>
* </ol>
*/
private static JSONArray buildGoodsProperties(OGoods goods, ExternalGoodsUpsertRequest req, ExternalPddProperties props,
String catRuleRaw) {
JSONArray arr = new JSONArray();
Set<Long> 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<PddOpenApiSupport.CatGoodsPropertyRuleRow> 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<String> 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_<refPid>、ext.isbnISBN");
}
}
return arr;
}
private static void appendGoodsPropertyEntry(JSONArray arr, Set<Long> 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) { private static String resolveGoodsDescForPdd(OGoods goods, ExternalGoodsUpsertRequest req) {
if (goods != null && StringUtils.hasText(goods.getCanonicalExt())) { if (goods != null && StringUtils.hasText(goods.getCanonicalExt())) {
try { try {

View File

@ -7,8 +7,11 @@ import org.springframework.util.StringUtils;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@ -508,4 +511,114 @@ public final class PddOpenApiSupport {
return null; return null;
} }
} }
/**
* {@code goods_properties_rule.properties} 商品级必填项{@code required} 为真且非 {@code is_sku}
* 用于 {@code pdd.goods.add} 根级 {@code goods_properties} 自动补全
*/
public static List<CatGoodsPropertyRuleRow> 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<CatGoodsPropertyRuleRow> 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;
}
}
} }