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 fd3e2703..77b72371 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 @@ -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 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); diff --git a/service/src/main/java/cn/qihangerp/service/external/pdd/PddCatRuleSpecAutoResolver.java b/service/src/main/java/cn/qihangerp/service/external/pdd/PddCatRuleSpecAutoResolver.java index b738dd89..87c781b2 100644 --- a/service/src/main/java/cn/qihangerp/service/external/pdd/PddCatRuleSpecAutoResolver.java +++ b/service/src/main/java/cn/qihangerp/service/external/pdd/PddCatRuleSpecAutoResolver.java @@ -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)) { 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 80f7da78..fc1db536 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 @@ -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}。 + *

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

+ */ + public String buildParamJsonNoSpecBook(OGoods goods, List 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 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 carousel = resolveCarousel(goods, req); + if (carousel.isEmpty()) { + throw new IllegalStateException("carousel_gallery 不能为空"); + } + root.put("carousel_gallery", carousel); + + List 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; 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 ae0f9314..202ae339 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 @@ -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}(与日志一致)。 */