fix(pdd): upsert 成功后用 detail.get 回填 o_goods_sku 的 pddSkuId

- 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
This commit is contained in:
huangyujie 2026-04-01 13:45:40 +08:00
parent d4f4c79104
commit f271eb2300
3 changed files with 147 additions and 12 deletions

View File

@ -151,7 +151,7 @@ public class ExternalGoodsAppServiceImpl implements ExternalGoodsAppService {
ExternalGoodsUpsertResultVo vo = out.build(); ExternalGoodsUpsertResultVo vo = out.build();
if ("PDD".equalsIgnoreCase(req.getPlatform())) { 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={}", log.info("[external/upsert] PDD summary shopId={} outGoodsId={} erpGoodsId={} attempted={} success={} lane={} credentialSource={} message={} goodsAddSnippet={} catRuleFetched={} catRuleSnippet={} specAutoResolved={} autoResolveDetail={}",
req.getShopId(), req.getShopId(),
req.getOutGoodsId(), req.getOutGoodsId(),

View File

@ -2,9 +2,13 @@ package cn.qihangerp.service.external.pdd;
import cn.qihangerp.model.entity.OGoods; import cn.qihangerp.model.entity.OGoods;
import cn.qihangerp.model.entity.OGoodsSku; 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.ExternalGoodsUpsertResultVo;
import cn.qihangerp.model.vo.ExternalPddGoodsDetailResultVo;
import cn.qihangerp.module.service.OGoodsService; import cn.qihangerp.module.service.OGoodsService;
import cn.qihangerp.module.service.OGoodsSkuService; import cn.qihangerp.module.service.OGoodsSkuService;
import cn.qihangerp.service.external.ExternalPddGoodsDetailAppService;
import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject; import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
@ -14,12 +18,17 @@ import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import java.util.Date; import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
/** /**
* 拼多多发品成功后 POP {@code goods_id}/{@code sku_id} 写入 {@code o_goods}/{@code o_goods_sku}.{@code canonicalExt} * <p><b>平台商品映射由 ERP-Open 落库</b>中台仅调用 {@code /external/goods/upsert}改库存等 HTTP不持久化拼多多/京东等平台侧 ID
* {@code /external/goods/pdd/quantity/update} {@code outGoodsId}+{@code outSkuId} 解析调用库存接口 * 各渠道在 {@code o_goods}/{@code o_goods_sku}.{@code canonicalExt} 使用<strong>独立键名</strong>拼多多为 {@value #CANONICAL_PDD_GOODS_ID}{@value #CANONICAL_PDD_SKU_ID}
* 未来京东/天猫可并行增加 {@code jdSkuId} 改库存接口按平台从本库解析后再调 POP</p>
*
* <p>拼多多{@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} 回填</p>
*/ */
@Slf4j @Slf4j
@Component @Component
@ -31,11 +40,15 @@ public class OGoodsPddMappingPersistence {
private final OGoodsService goodsService; private final OGoodsService goodsService;
private final OGoodsSkuService skuService; private final OGoodsSkuService skuService;
private final ExternalPddGoodsDetailAppService externalPddGoodsDetailAppService;
/** /**
* upsert 拼多多链路成功{@link ExternalGoodsUpsertResultVo#getPddPublishSuccess()}==true后调用 * 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())) { if (erpGoodsId == null || vo == null || !Boolean.TRUE.equals(vo.getPddPublishSuccess())) {
return; return;
} }
@ -52,20 +65,63 @@ public class OGoodsPddMappingPersistence {
g.setUpdateTime(new Date()); g.setUpdateTime(new Date());
goodsService.updateById(g); goodsService.updateById(g);
Map<String, Long> outerToPddSku = Map.of(); Map<String, Long> outerToPddSku = new LinkedHashMap<>();
String lane = vo.getPddPublishLane(); String lane = vo.getPddPublishLane();
if (lane != null && "GOODS_ADD".equalsIgnoreCase(lane.trim())) { if (lane != null && "GOODS_ADD".equalsIgnoreCase(lane.trim())) {
outerToPddSku = PddOpenApiSupport.parseOuterKeyToPddSkuIdFromGoodsAddResponse(vo.getPddResponseSnippet()); outerToPddSku.putAll(PddOpenApiSupport.parseOuterKeyToPddSkuIdFromGoodsAddResponse(vo.getPddResponseSnippet()));
} }
if (outerToPddSku.isEmpty()) { int fromAdd = applyPddSkuMappings(erpGoodsId, outerToPddSku);
log.info("[PDD] persisted pddGoodsId on o_goods only (no sku_list parse) erpGoodsId={} lane={}", if (!outerToPddSku.isEmpty()) {
erpGoodsId, vo.getPddPublishLane()); 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; 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<String, Long> 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<String, Long> outerToPddSku) {
if (outerToPddSku == null || outerToPddSku.isEmpty()) {
return 0;
}
List<OGoodsSku> rows = skuService.list(new LambdaQueryWrapper<OGoodsSku>() List<OGoodsSku> rows = skuService.list(new LambdaQueryWrapper<OGoodsSku>()
.eq(OGoodsSku::getGoodsId, erpGoodsId)); .eq(OGoodsSku::getGoodsId, erpGoodsId));
if (rows == null || rows.isEmpty()) { if (rows == null || rows.isEmpty()) {
return; return 0;
} }
int updated = 0; int updated = 0;
for (OGoodsSku row : rows) { for (OGoodsSku row : rows) {
@ -81,8 +137,40 @@ public class OGoodsPddMappingPersistence {
skuService.updateById(row); skuService.updateById(row);
updated++; updated++;
} }
log.info("[PDD] persisted pddGoodsId + pddSkuId mappings erpGoodsId={} skuRowsUpdated={}", return updated;
erpGoodsId, updated); }
private boolean anySkuMissingPddSkuId(Long erpGoodsId) {
List<OGoodsSku> rows = skuService.list(new LambdaQueryWrapper<OGoodsSku>()
.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) { private static String mergeJsonLongField(String existingJson, String field, long value) {

View File

@ -175,6 +175,53 @@ public final class PddOpenApiSupport {
return out; 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}
* <p>用于 {@code INFORMATION_UPDATE} 等未返回 {@code goods_add_response.sku_list} 的发品路径之后 ERP 本地补全 {@code o_goods_sku.canonicalExt.pddSkuId}</p>
*/
public static Map<String, Long> parseOuterKeyToPddSkuIdFromGoodsDetailGetResponse(String raw) {
Map<String, Long> 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) { private static String firstNonBlank(String a, String b, String c) {
if (StringUtils.hasText(a)) { if (StringUtils.hasText(a)) {
return a.trim(); return a.trim();