feat(pdd): 图书无规格发品 input_max_spec_num=0 时单 SKU、spec_id_list=0、catRuleRaw 与 isNoSpecBookCatRule
Made-with: Cursor
This commit is contained in:
parent
e7e7c02e24
commit
678fc70e1c
|
|
@ -15,7 +15,7 @@ import java.util.List;
|
|||
|
||||
/**
|
||||
* 拼多多发布: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
|
||||
*/
|
||||
|
|
@ -93,6 +93,7 @@ public class ExternalPddPublishService {
|
|||
boolean catFetched = false;
|
||||
boolean specAuto = false;
|
||||
String autoDetail = null;
|
||||
boolean noSpecBookPublish = false;
|
||||
|
||||
List<ExternalPddProperties.PddSkuOverride> effectiveOverrides = null;
|
||||
if (!CollectionUtils.isEmpty(props.getSkuOverrides())) {
|
||||
|
|
@ -105,6 +106,24 @@ public class ExternalPddPublishService {
|
|||
if (ar.isResolved() && ar.getOverrides() != null && ar.getOverrides().size() == skuRows.size()) {
|
||||
effectiveOverrides = ar.getOverrides();
|
||||
specAuto = true;
|
||||
} else if (PddOpenApiSupport.isNoSpecBookCatRule(ar.getCatRuleRaw()) && skuRows.size() == 1) {
|
||||
noSpecBookPublish = true;
|
||||
autoDetail = "类目 input_max_spec_num=0:拼多多无规格发品(单 sku_list,spec_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 {
|
||||
log.info("[PDD] publish skipped shopId={} outGoodsId={} reason=spec-auto-resolve-fail detail={}",
|
||||
req.getShopId(), req.getOutGoodsId(), autoDetail);
|
||||
|
|
@ -136,9 +155,11 @@ public class ExternalPddPublishService {
|
|||
}
|
||||
|
||||
try {
|
||||
log.info("[PDD] pdd.goods.add begin shopId={} outGoodsId={} catId={} skuCount={}",
|
||||
req.getShopId(), req.getOutGoodsId(), catId, skuRows.size());
|
||||
String goodsAddRootJson = paramBuilder.buildParamJson(goods, skus, req, props, effectiveOverrides);
|
||||
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);
|
||||
String raw = pddPopClient.invokeGoodsAdd(gateway, cred.getAppKey(), cred.getAppSecret(), cred.getAccessToken(),
|
||||
goodsAddRootJson);
|
||||
boolean ok = !PddOpenApiSupport.isError(raw);
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ public class PddCatRuleSpecAutoResolver {
|
|||
private boolean resolved;
|
||||
private boolean catRuleFetched;
|
||||
private String catRuleSnippet;
|
||||
/** {@code pdd.goods.cat.rule.get} 原始响应体(成功或失败),供发布层判断是否走无规格单 SKU 逻辑 */
|
||||
private String catRuleRaw;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -43,6 +45,7 @@ public class PddCatRuleSpecAutoResolver {
|
|||
}
|
||||
try {
|
||||
String raw = fetchCatRuleJson(cred, gatewayUrl, catId);
|
||||
r.setCatRuleRaw(raw);
|
||||
r.setCatRuleFetched(true);
|
||||
r.setCatRuleSnippet(PddOpenApiSupport.snippet(raw, 2000));
|
||||
if (PddOpenApiSupport.isError(raw)) {
|
||||
|
|
|
|||
|
|
@ -150,6 +150,131 @@ public class PddGoodsAddParamBuilder {
|
|||
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) {
|
||||
if (req.getShipWithinHours() != null && req.getShipWithinHours() > 0) {
|
||||
return req.getShipWithinHours() * 3600L;
|
||||
|
|
|
|||
|
|
@ -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}(与日志一致)。
|
||||
*/
|
||||
|
|
|
|||
Loading…
Reference in New Issue