feat(pdd): 图书无规格发品 input_max_spec_num=0 时单 SKU、spec_id_list=0、catRuleRaw 与 isNoSpecBookCatRule

Made-with: Cursor
This commit is contained in:
huangyujie 2026-03-25 15:05:56 +08:00
parent e7e7c02e24
commit 678fc70e1c
4 changed files with 176 additions and 4 deletions

View File

@ -15,7 +15,7 @@ import java.util.List;
/** /**
* 拼多多发布POP 凭证仅来自本次请求的 {@link ExternalGoodsUpsertRequest#getPddPopAuth()}不依赖 {@code o_shop} * 拼多多发布POP 凭证仅来自本次请求的 {@link ExternalGoodsUpsertRequest#getPddPopAuth()}不依赖 {@code o_shop}
* 可选自动拉取类目规则与 spec.id * 可选自动拉取类目规则与 spec.id图书等 {@code input_max_spec_num=0} 类目可走无规格单 SKU{@code spec_id_list} 为字面量 0
* *
* @author guochengyu * @author guochengyu
*/ */
@ -93,6 +93,7 @@ public class ExternalPddPublishService {
boolean catFetched = false; boolean catFetched = false;
boolean specAuto = false; boolean specAuto = false;
String autoDetail = null; String autoDetail = null;
boolean noSpecBookPublish = false;
List<ExternalPddProperties.PddSkuOverride> effectiveOverrides = null; List<ExternalPddProperties.PddSkuOverride> effectiveOverrides = null;
if (!CollectionUtils.isEmpty(props.getSkuOverrides())) { if (!CollectionUtils.isEmpty(props.getSkuOverrides())) {
@ -105,6 +106,24 @@ public class ExternalPddPublishService {
if (ar.isResolved() && ar.getOverrides() != null && ar.getOverrides().size() == skuRows.size()) { if (ar.isResolved() && ar.getOverrides() != null && ar.getOverrides().size() == skuRows.size()) {
effectiveOverrides = ar.getOverrides(); effectiveOverrides = ar.getOverrides();
specAuto = true; specAuto = true;
} else if (PddOpenApiSupport.isNoSpecBookCatRule(ar.getCatRuleRaw()) && skuRows.size() == 1) {
noSpecBookPublish = true;
autoDetail = "类目 input_max_spec_num=0拼多多无规格发品单 sku_listspec_id_list=\"0\",未调用 spec.id.get";
log.info("[PDD] no-spec book publish shopId={} outGoodsId={}", req.getShopId(), req.getOutGoodsId());
} else if (PddOpenApiSupport.isNoSpecBookCatRule(ar.getCatRuleRaw())) {
log.info("[PDD] publish skipped shopId={} outGoodsId={} reason=no-spec-requires-single-sku skuCount={}",
req.getShopId(), req.getOutGoodsId(), skuRows.size());
return PddPublishLaneResultVo.builder()
.attempted(false)
.success(false)
.message("无规格类目发布仅支持 1 条 SKU当前 " + skuRows.size()
+ " 条;请合并为单 SKU 或配置 external.pdd.sku-overrides / 使用有销售规格的类目")
.shopCredentialSource(cred.getSource())
.catRuleFetched(catFetched)
.catRuleSnippet(catRuleSnippet)
.specAutoResolved(false)
.autoResolveDetail(autoDetail)
.build();
} else { } else {
log.info("[PDD] publish skipped shopId={} outGoodsId={} reason=spec-auto-resolve-fail detail={}", log.info("[PDD] publish skipped shopId={} outGoodsId={} reason=spec-auto-resolve-fail detail={}",
req.getShopId(), req.getOutGoodsId(), autoDetail); req.getShopId(), req.getOutGoodsId(), autoDetail);
@ -136,9 +155,11 @@ public class ExternalPddPublishService {
} }
try { try {
log.info("[PDD] pdd.goods.add begin shopId={} outGoodsId={} catId={} skuCount={}", log.info("[PDD] pdd.goods.add begin shopId={} outGoodsId={} catId={} skuCount={} noSpecBook={}",
req.getShopId(), req.getOutGoodsId(), catId, skuRows.size()); req.getShopId(), req.getOutGoodsId(), catId, skuRows.size(), noSpecBookPublish);
String goodsAddRootJson = paramBuilder.buildParamJson(goods, skus, req, props, effectiveOverrides); String goodsAddRootJson = noSpecBookPublish
? paramBuilder.buildParamJsonNoSpecBook(goods, skus, req, props)
: paramBuilder.buildParamJson(goods, skus, req, props, effectiveOverrides);
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);

View File

@ -29,6 +29,8 @@ public class PddCatRuleSpecAutoResolver {
private boolean resolved; private boolean resolved;
private boolean catRuleFetched; private boolean catRuleFetched;
private String catRuleSnippet; private String catRuleSnippet;
/** {@code pdd.goods.cat.rule.get} 原始响应体(成功或失败),供发布层判断是否走无规格单 SKU 逻辑 */
private String catRuleRaw;
} }
/** /**
@ -43,6 +45,7 @@ public class PddCatRuleSpecAutoResolver {
} }
try { try {
String raw = fetchCatRuleJson(cred, gatewayUrl, catId); String raw = fetchCatRuleJson(cred, gatewayUrl, catId);
r.setCatRuleRaw(raw);
r.setCatRuleFetched(true); r.setCatRuleFetched(true);
r.setCatRuleSnippet(PddOpenApiSupport.snippet(raw, 2000)); r.setCatRuleSnippet(PddOpenApiSupport.snippet(raw, 2000));
if (PddOpenApiSupport.isError(raw)) { if (PddOpenApiSupport.isError(raw)) {

View File

@ -150,6 +150,131 @@ public class PddGoodsAddParamBuilder {
return JSON.toJSONString(root); return JSON.toJSONString(root);
} }
/**
* 拼多多无规格发品如图书 {@code input_max_spec_num=0}仅一条 {@code sku_list}
* {@code spec_id_list} 固定为 {@code "0"}{@code spec_detail_list} 为空根级 {@code is_sku=false}不调用 {@code spec.id.get}
* <p>调用方须保证过滤后仅 1 条有效 SKU</p>
*/
public String buildParamJsonNoSpecBook(OGoods goods, List<OGoodsSku> skus, ExternalGoodsUpsertRequest req,
ExternalPddProperties props) {
Long catId = resolveCatId(req.getCategoryCode(), props);
Long costTpl = resolveCostTemplate(req.getLogisticsTemplateCode(), props);
if (catId == null || catId <= 0) {
throw new IllegalStateException("缺少拼多多类目映射external.pdd.category-map 中未找到 categoryCode="
+ req.getCategoryCode() + " 或 DEFAULT");
}
if (costTpl == null || costTpl <= 0) {
throw new IllegalStateException("缺少运费模板映射external.pdd.cost-template-map 中未找到 logisticsTemplateCode="
+ req.getLogisticsTemplateCode() + " 或 DEFAULT");
}
List<OGoodsSku> skuRows = skus == null ? List.of() : skus.stream()
.filter(s -> s != null && StringUtils.hasText(s.getOuterErpSkuId())).toList();
if (skuRows.size() != 1) {
throw new IllegalStateException("无规格发布仅允许 1 条有效 SKU当前=" + skuRows.size());
}
OGoodsSku row = skuRows.get(0);
JSONObject root = new JSONObject();
root.put("auto_fill_spu_property", true);
root.put("cat_id", String.valueOf(catId));
root.put("cost_template_id", costTpl);
root.put("country_id", props.getDefaultCountryId());
root.put("goods_type", props.getDefaultGoodsType());
root.put("goods_name", goods.getName());
root.put("is_folt", props.isDefaultIsFolt());
root.put("is_pre_sale", props.isDefaultIsPreSale());
root.put("is_refundable", props.isDefaultIsRefundable());
root.put("second_hand", props.isDefaultSecondHand());
root.put("shipment_limit_second", resolveShipmentSeconds(req, props));
root.put("is_sku", false);
List<String> carousel = resolveCarousel(goods, req);
if (carousel.isEmpty()) {
throw new IllegalStateException("carousel_gallery 不能为空");
}
root.put("carousel_gallery", carousel);
List<String> detail = resolveDetailImages(goods);
if (detail.isEmpty()) {
detail = List.of(carousel.get(0));
}
root.put("detail_gallery", detail);
long groupFen = yuanToFen(firstNonNull(row.getGroupPrice(), row.getRetailPrice()));
long singleFen = yuanToFen(firstNonNull(row.getRetailPrice(), row.getGroupPrice()));
if (groupFen <= 0) {
throw new IllegalStateException("SKU 团购价/拼团价无效(换算为 0 分outSkuId=" + row.getOuterErpSkuId());
}
if (singleFen <= groupFen) {
singleFen = groupFen + 100;
}
long marketFen = resolveMarketPriceFen(req, skuRows);
if (marketFen <= singleFen) {
marketFen = singleFen + 100;
}
if (StringUtils.hasText(goods.getOuterErpGoodsId())) {
root.put("out_goods_id", goods.getOuterErpGoodsId());
}
long qty = row.getStockQty() != null ? row.getStockQty().longValue() : 0L;
root.put("price", singleFen);
root.put("quantity", qty);
String desc = resolveGoodsDescForPdd(goods, req);
if (StringUtils.hasText(desc)) {
root.put("goods_desc", desc);
}
JSONArray skuList = new JSONArray();
JSONObject sku = new JSONObject();
sku.put("is_onsale", 1);
sku.put("limit_quantity", 999L);
sku.put("multi_price", groupFen);
sku.put("price", singleFen);
sku.put("quantity", qty);
sku.put("weight", row.getWeightGram() != null ? row.getWeightGram() : 1000L);
sku.put("spec_id_list", "0");
sku.put("spec_detail_list", new JSONArray());
sku.put("thumb_url", StringUtils.hasText(row.getSkuImageUrl()) ? row.getSkuImageUrl() : carousel.get(0));
if (StringUtils.hasText(row.getOuterErpSkuId())) {
sku.put("out_sku_sn", row.getOuterErpSkuId());
}
sku.put("sku_properties", new JSONArray());
skuList.add(sku);
root.put("market_price", marketFen);
root.put("sku_list", skuList);
root.put("goods_properties", new JSONArray());
return JSON.toJSONString(root);
}
private static String resolveGoodsDescForPdd(OGoods goods, ExternalGoodsUpsertRequest req) {
if (goods != null && StringUtils.hasText(goods.getCanonicalExt())) {
try {
JSONObject o = JSON.parseObject(goods.getCanonicalExt());
if (o != null) {
String d = o.getString("goodsDesc");
if (!StringUtils.hasText(d)) {
d = o.getString("goods_desc");
}
if (StringUtils.hasText(d)) {
return d.trim();
}
}
} catch (Exception ignored) {
}
}
if (req != null && StringUtils.hasText(req.getTitle())) {
return req.getTitle().trim();
}
if (goods != null && StringUtils.hasText(goods.getName())) {
return goods.getName().trim();
}
return "";
}
private static long resolveShipmentSeconds(ExternalGoodsUpsertRequest req, ExternalPddProperties props) { private static long resolveShipmentSeconds(ExternalGoodsUpsertRequest req, ExternalPddProperties props) {
if (req.getShipWithinHours() != null && req.getShipWithinHours() > 0) { if (req.getShipWithinHours() != null && req.getShipWithinHours() > 0) {
return req.getShipWithinHours() * 3600L; return req.getShipWithinHours() * 3600L;

View File

@ -227,6 +227,29 @@ public final class PddOpenApiSupport {
} }
} }
/**
* 是否可按无规格图书类发布类目规则中 {@code goods_properties_rule.input_max_spec_num==0}与拼多多无规格发品前提一致
*/
public static boolean isNoSpecBookCatRule(String catRuleBody) {
if (!StringUtils.hasText(catRuleBody) || isError(catRuleBody)) {
return false;
}
try {
JSONObject root = JSON.parseObject(catRuleBody);
JSONObject inner = unwrapCatRulePayload(root);
if (inner == null) {
return false;
}
JSONObject gpr = inner.getJSONObject("goods_properties_rule");
if (gpr == null) {
return false;
}
return gpr.getIntValue("input_max_spec_num") == 0;
} catch (Exception e) {
return false;
}
}
/** /**
* POP 成功体多为 {@code goods_cat_rule_get_response}部分环境为 {@code cat_rule_get_response}与日志一致 * POP 成功体多为 {@code goods_cat_rule_get_response}部分环境为 {@code cat_rule_get_response}与日志一致
*/ */