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();