From f271eb2300a0b309890c4f683c08fe1d09db2a19 Mon Sep 17 00:00:00 2001 From: huangyujie <27665451@qq.com> Date: Wed, 1 Apr 2026 13:45:40 +0800 Subject: [PATCH] =?UTF-8?q?fix(pdd):=20upsert=20=E6=88=90=E5=8A=9F?= =?UTF-8?q?=E5=90=8E=E7=94=A8=20detail.get=20=E5=9B=9E=E5=A1=AB=20o=5Fgood?= =?UTF-8?q?s=5Fsku=20=E7=9A=84=20pddSkuId?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GOODS_ADD 仍从 goods_add_response.sku_list 解析;INFORMATION_UPDATE 等路径无 sku_list 时自动调 pdd.goods.detail.get - 解析 goods_detail_get_response.sku_list 写入 canonicalExt.pddSkuId,供 quantity/update 使用 - persistAfterPddPublishSuccess 增加 ExternalGoodsUpsertRequest 入参以携带 pddPopAuth - 类注释说明平台映射归 ERP-Open、canonicalExt 键可扩展多平台 Made-with: Cursor --- .../impl/ExternalGoodsAppServiceImpl.java | 2 +- .../pdd/OGoodsPddMappingPersistence.java | 110 ++++++++++++++++-- .../external/pdd/PddOpenApiSupport.java | 47 ++++++++ 3 files changed, 147 insertions(+), 12 deletions(-) diff --git a/service/src/main/java/cn/qihangerp/service/external/impl/ExternalGoodsAppServiceImpl.java b/service/src/main/java/cn/qihangerp/service/external/impl/ExternalGoodsAppServiceImpl.java index 3e14ea16..86818c7d 100644 --- a/service/src/main/java/cn/qihangerp/service/external/impl/ExternalGoodsAppServiceImpl.java +++ b/service/src/main/java/cn/qihangerp/service/external/impl/ExternalGoodsAppServiceImpl.java @@ -151,7 +151,7 @@ public class ExternalGoodsAppServiceImpl implements ExternalGoodsAppService { ExternalGoodsUpsertResultVo vo = out.build(); if ("PDD".equalsIgnoreCase(req.getPlatform())) { - oGoodsPddMappingPersistence.persistAfterPddPublishSuccess(goodsId, vo); + oGoodsPddMappingPersistence.persistAfterPddPublishSuccess(goodsId, req, vo); log.info("[external/upsert] PDD summary shopId={} outGoodsId={} erpGoodsId={} attempted={} success={} lane={} credentialSource={} message={} goodsAddSnippet={} catRuleFetched={} catRuleSnippet={} specAutoResolved={} autoResolveDetail={}", req.getShopId(), req.getOutGoodsId(), diff --git a/service/src/main/java/cn/qihangerp/service/external/pdd/OGoodsPddMappingPersistence.java b/service/src/main/java/cn/qihangerp/service/external/pdd/OGoodsPddMappingPersistence.java index 8ab4fcd8..5773c851 100644 --- a/service/src/main/java/cn/qihangerp/service/external/pdd/OGoodsPddMappingPersistence.java +++ b/service/src/main/java/cn/qihangerp/service/external/pdd/OGoodsPddMappingPersistence.java @@ -2,9 +2,13 @@ package cn.qihangerp.service.external.pdd; import cn.qihangerp.model.entity.OGoods; import cn.qihangerp.model.entity.OGoodsSku; +import cn.qihangerp.model.request.ExternalGoodsUpsertRequest; +import cn.qihangerp.model.request.ExternalPddGoodsDetailRequest; import cn.qihangerp.model.vo.ExternalGoodsUpsertResultVo; +import cn.qihangerp.model.vo.ExternalPddGoodsDetailResultVo; import cn.qihangerp.module.service.OGoodsService; import cn.qihangerp.module.service.OGoodsSkuService; +import cn.qihangerp.service.external.ExternalPddGoodsDetailAppService; import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONObject; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; @@ -14,12 +18,17 @@ import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import java.util.Date; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /** - * 拼多多发品成功后,将 POP {@code goods_id}/{@code sku_id} 写入 {@code o_goods}/{@code o_goods_sku}.{@code canonicalExt}, - * 供 {@code /external/goods/pdd/quantity/update} 按 {@code outGoodsId}+{@code outSkuId} 解析调用库存接口。 + *

平台商品映射由 ERP-Open 落库:中台仅调用 {@code /external/goods/upsert}、改库存等 HTTP,不持久化拼多多/京东等平台侧 ID。 + * 各渠道在 {@code o_goods}/{@code o_goods_sku}.{@code canonicalExt} 使用独立键名(拼多多为 {@value #CANONICAL_PDD_GOODS_ID}、{@value #CANONICAL_PDD_SKU_ID}), + * 未来京东/天猫可并行增加 {@code jdSkuId} 等,改库存接口按平台从本库解析后再调 POP。

+ * + *

拼多多:{@code pdd.goods.add} 响应可解析 SKU 映射;{@code pdd.goods.information.update} 往往不带 {@code sku_list}, + * 因此在 upsert 成功后若仍有 SKU 未写入 {@value #CANONICAL_PDD_SKU_ID},将自动补调 {@code pdd.goods.detail.get} 回填。

*/ @Slf4j @Component @@ -31,11 +40,15 @@ public class OGoodsPddMappingPersistence { private final OGoodsService goodsService; private final OGoodsSkuService skuService; + private final ExternalPddGoodsDetailAppService externalPddGoodsDetailAppService; /** * 在 upsert 拼多多链路成功({@link ExternalGoodsUpsertResultVo#getPddPublishSuccess()}==true)后调用。 + * + * @param req 用于携带 {@code pddPopAuth},在需补拉 {@code pdd.goods.detail.get} 时使用;可为 {@code null}(则无法补全 SKU 映射) */ - public void persistAfterPddPublishSuccess(Long erpGoodsId, ExternalGoodsUpsertResultVo vo) { + public void persistAfterPddPublishSuccess(Long erpGoodsId, ExternalGoodsUpsertRequest req, + ExternalGoodsUpsertResultVo vo) { if (erpGoodsId == null || vo == null || !Boolean.TRUE.equals(vo.getPddPublishSuccess())) { return; } @@ -52,20 +65,63 @@ public class OGoodsPddMappingPersistence { g.setUpdateTime(new Date()); goodsService.updateById(g); - Map outerToPddSku = Map.of(); + Map outerToPddSku = new LinkedHashMap<>(); String lane = vo.getPddPublishLane(); if (lane != null && "GOODS_ADD".equalsIgnoreCase(lane.trim())) { - outerToPddSku = PddOpenApiSupport.parseOuterKeyToPddSkuIdFromGoodsAddResponse(vo.getPddResponseSnippet()); + outerToPddSku.putAll(PddOpenApiSupport.parseOuterKeyToPddSkuIdFromGoodsAddResponse(vo.getPddResponseSnippet())); } - if (outerToPddSku.isEmpty()) { - log.info("[PDD] persisted pddGoodsId on o_goods only (no sku_list parse) erpGoodsId={} lane={}", - erpGoodsId, vo.getPddPublishLane()); + int fromAdd = applyPddSkuMappings(erpGoodsId, outerToPddSku); + if (!outerToPddSku.isEmpty()) { + log.info("[PDD] persisted pddGoodsId + sku from GOODS_ADD snippet erpGoodsId={} lane={} skuRowsUpdated={}", + erpGoodsId, lane, fromAdd); + } else { + log.info("[PDD] persisted pddGoodsId on o_goods (no GOODS_ADD sku_list) erpGoodsId={} lane={}", + erpGoodsId, lane); + } + + if (!anySkuMissingPddSkuId(erpGoodsId)) { return; } + if (req == null || req.getPddPopAuth() == null) { + log.warn("[PDD] skip detail.get backfill: missing pddPopAuth erpGoodsId={} lane={}", + erpGoodsId, lane); + return; + } + + ExternalPddGoodsDetailRequest dreq = new ExternalPddGoodsDetailRequest(); + dreq.setPddGoodsId(pddGoodsId); + dreq.setPddPopAuth(req.getPddPopAuth()); + ExternalPddGoodsDetailResultVo detailVo = externalPddGoodsDetailAppService.fetchGoodsDetail(dreq); + if (!Boolean.TRUE.equals(detailVo.getPopBizSuccess()) || !StringUtils.hasText(detailVo.getPopResponseBody())) { + log.warn("[PDD] detail.get backfill failed erpGoodsId={} pddGoodsId={} msg={}", + erpGoodsId, pddGoodsId, detailVo.getMessage()); + return; + } + Map fromDetail = + PddOpenApiSupport.parseOuterKeyToPddSkuIdFromGoodsDetailGetResponse(detailVo.getPopResponseBody()); + int fromDetailCount = applyPddSkuMappings(erpGoodsId, fromDetail); + if (fromDetailCount > 0) { + log.info("[PDD] detail.get backfill applied erpGoodsId={} pddGoodsId={} skuRowsUpdated={}", + erpGoodsId, pddGoodsId, fromDetailCount); + } else if (anySkuMissingPddSkuId(erpGoodsId)) { + log.warn("[PDD] detail.get ok but no sku_id matched outer_erp_sku_id erpGoodsId={} pddGoodsId={}", + erpGoodsId, pddGoodsId); + } + } + + /** + * 将 {@code outerKey -> 拼多多 sku_id} 合并写入 {@code o_goods_sku.canonicalExt}(键 {@value #CANONICAL_PDD_SKU_ID})。 + * + * @return 实际更新行数 + */ + private int applyPddSkuMappings(Long erpGoodsId, Map outerToPddSku) { + if (outerToPddSku == null || outerToPddSku.isEmpty()) { + return 0; + } List rows = skuService.list(new LambdaQueryWrapper() .eq(OGoodsSku::getGoodsId, erpGoodsId)); if (rows == null || rows.isEmpty()) { - return; + return 0; } int updated = 0; for (OGoodsSku row : rows) { @@ -81,8 +137,40 @@ public class OGoodsPddMappingPersistence { skuService.updateById(row); updated++; } - log.info("[PDD] persisted pddGoodsId + pddSkuId mappings erpGoodsId={} skuRowsUpdated={}", - erpGoodsId, updated); + return updated; + } + + private boolean anySkuMissingPddSkuId(Long erpGoodsId) { + List rows = skuService.list(new LambdaQueryWrapper() + .eq(OGoodsSku::getGoodsId, erpGoodsId)); + if (rows == null || rows.isEmpty()) { + return false; + } + for (OGoodsSku row : rows) { + if (row == null || !StringUtils.hasText(row.getOuterErpSkuId())) { + continue; + } + Long v = readCanonicalLong(row.getCanonicalExt(), CANONICAL_PDD_SKU_ID); + if (v == null || v <= 0) { + return true; + } + } + return false; + } + + private static Long readCanonicalLong(String json, String key) { + if (!StringUtils.hasText(json) || !StringUtils.hasText(key)) { + return null; + } + try { + JSONObject o = JSON.parseObject(json); + if (o == null || !o.containsKey(key)) { + return null; + } + return o.getLong(key); + } catch (Exception e) { + return null; + } } private static String mergeJsonLongField(String existingJson, String field, long value) { 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 b7b3ad9a..b7e20430 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 @@ -175,6 +175,53 @@ public final class PddOpenApiSupport { return out; } + /** + * 从 {@code pdd.goods.detail.get} 成功响应中解析 {@code goods_detail_get_response.sku_list[]}, + * 与 {@link #parseOuterKeyToPddSkuIdFromGoodsAddResponse} 对齐:按 {@code out_sku_sn}/{@code outer_id}/{@code out_sku_id} 映射拼多多 {@code sku_id}。 + *

用于 {@code INFORMATION_UPDATE} 等未返回 {@code goods_add_response.sku_list} 的发品路径之后,在 ERP 本地补全 {@code o_goods_sku.canonicalExt.pddSkuId}。

+ */ + public static Map parseOuterKeyToPddSkuIdFromGoodsDetailGetResponse(String raw) { + Map out = new LinkedHashMap<>(); + if (!StringUtils.hasText(raw)) { + return out; + } + try { + JSONObject root = JSON.parseObject(raw); + if (root == null) { + return out; + } + JSONObject detail = root.getJSONObject("goods_detail_get_response"); + if (detail == null) { + return out; + } + JSONArray skuList = detail.getJSONArray("sku_list"); + if (skuList == null || skuList.isEmpty()) { + return out; + } + for (int i = 0; i < skuList.size(); i++) { + JSONObject sku = skuList.getJSONObject(i); + if (sku == null) { + continue; + } + Long skuId = sku.getLong("sku_id"); + if (skuId == null || skuId <= 0) { + continue; + } + String outerKey = firstNonBlank( + sku.getString("out_sku_sn"), + sku.getString("outer_id"), + sku.getString("out_sku_id")); + if (!StringUtils.hasText(outerKey)) { + continue; + } + out.putIfAbsent(outerKey.trim(), skuId); + } + } catch (Exception ignored) { + // ignore + } + return out; + } + private static String firstNonBlank(String a, String b, String c) { if (StringUtils.hasText(a)) { return a.trim();