feat(pdd): 新增 sku-canonical-sync 接口供主数据回写 pddSkuId

主数据在 detail.get 成功后回调,将 POP sku_list 映射合并写入 o_goods_sku.canonical_ext。

Made-with: Cursor
This commit is contained in:
huangyujie 2026-04-10 21:59:15 +08:00
parent 03a45c3eb4
commit 3b5ba130d5
6 changed files with 140 additions and 1 deletions

View File

@ -3,12 +3,15 @@ package cn.qihangerp.erp.controller;
import cn.qihangerp.common.AjaxResult;
import cn.qihangerp.model.request.ExternalPddGoodsDetailRequest;
import cn.qihangerp.model.request.ExternalPddGoodsLatestCommitStatusRequest;
import cn.qihangerp.model.request.ExternalPddSkuCanonicalSyncRequest;
import cn.qihangerp.model.vo.ExternalPddGoodsDetailResultVo;
import cn.qihangerp.model.vo.ExternalPddSkuCanonicalSyncResultVo;
import cn.qihangerp.security.common.BaseController;
import cn.qihangerp.erp.config.ExternalGoodsApiLogProperties;
import cn.qihangerp.erp.support.ExternalGoodsRequestLogSupport;
import cn.qihangerp.service.external.ExternalPddGoodsDetailAppService;
import cn.qihangerp.service.external.ExternalPddGoodsLatestCommitStatusAppService;
import cn.qihangerp.service.external.ExternalPddSkuCanonicalSyncAppService;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
@ -22,6 +25,7 @@ import org.springframework.web.bind.annotation.RestController;
* <ul>
* <li>{@code POST /external/pdd/goods/detail} {@code pdd.goods.detail.get}</li>
* <li>{@code POST /external/pdd/goods/latest-commit-status} {@code pdd.goods.latest.commit.status.get}</li>
* <li>{@code POST /external/pdd/goods/sku-canonical-sync} 主数据在 detail 成功后回写 {@code o_goods_sku.canonical_ext.pddSkuId}</li>
* </ul>
* <p>改库存请使用 {@code POST /external/goods/pdd/quantity/update} upsert 相同 out </p>
*/
@ -33,6 +37,7 @@ public class ExternalPddGoodsController extends BaseController {
private final ExternalPddGoodsDetailAppService externalPddGoodsDetailAppService;
private final ExternalPddGoodsLatestCommitStatusAppService externalPddGoodsLatestCommitStatusAppService;
private final ExternalPddSkuCanonicalSyncAppService externalPddSkuCanonicalSyncAppService;
private final ExternalGoodsApiLogProperties goodsApiLogProperties;
@PostMapping("/detail")
@ -76,9 +81,32 @@ public class ExternalPddGoodsController extends BaseController {
return AjaxResult.error("参数错误pddPopAuth 需提供 appKey、appSecret、accessToken");
}
ExternalPddGoodsDetailResultVo vo = externalPddGoodsLatestCommitStatusAppService.fetchLatestCommitStatus(req);
int n = req.getPddGoodsIds().size();
log.info(
"[external/pdd/goods/latest-commit-status] goodsIdCount={} popBizSuccess={} msg={}",
n,
vo.getPopBizSuccess(),
vo.getMessage());
if (!Boolean.TRUE.equals(vo.getPopBizSuccess())) {
return AjaxResult.error(StringUtils.hasText(vo.getMessage()) ? vo.getMessage() : "pdd.goods.latest.commit.status.get 失败");
}
return AjaxResult.success(vo);
}
@PostMapping("/sku-canonical-sync")
public AjaxResult skuCanonicalSync(@RequestBody ExternalPddSkuCanonicalSyncRequest req) {
if (goodsApiLogProperties.isLogFullRequest()) {
int rawLen = req != null && req.getGoodsDetailPopRaw() != null ? req.getGoodsDetailPopRaw().length() : 0;
log.info("[external/pdd/goods/sku-canonical-sync] erpGoodsId={} goodsDetailPopRawLen={}",
req != null ? req.getErpGoodsId() : null, rawLen);
}
if (req == null || req.getErpGoodsId() == null || req.getErpGoodsId() <= 0) {
return AjaxResult.error("参数错误erpGoodsId 不能为空且须为正数");
}
if (!StringUtils.hasText(req.getGoodsDetailPopRaw())) {
return AjaxResult.error("参数错误goodsDetailPopRaw 不能为空");
}
ExternalPddSkuCanonicalSyncResultVo vo = externalPddSkuCanonicalSyncAppService.syncFromGoodsDetailPop(req);
return AjaxResult.success(vo);
}
}

View File

@ -0,0 +1,21 @@
package cn.qihangerp.model.request;
import lombok.Data;
/**
* {@code POST /external/pdd/goods/sku-canonical-sync}主数据在 {@code detail.get} 成功后回写
* {@code o_goods_sku.canonical_ext.pddSkuId}
* <p> {@link ExternalPddGoodsDetailAppService#fetchGoodsDetail} 返回的 {@code popResponseBody} 同源拼多多 POP 原始 JSON
* 须含 {@code goods_detail_get_response.sku_list}</p>
*/
@Data
public class ExternalPddSkuCanonicalSyncRequest {
/** ERP {@code o_goods.id} */
private Long erpGoodsId;
/**
* {@code pdd.goods.detail.get} 成功响应原始 JSON顶层含 {@code goods_detail_get_response}
*/
private String goodsDetailPopRaw;
}

View File

@ -0,0 +1,25 @@
package cn.qihangerp.model.vo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* {@code /external/pdd/goods/sku-canonical-sync} 业务结果
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ExternalPddSkuCanonicalSyncResultVo implements Serializable {
private static final long serialVersionUID = 1L;
/** 实际更新了 {@code canonical_ext} 的 SKU 行数 */
private int updatedSkuRows;
private String message;
}

View File

@ -0,0 +1,12 @@
package cn.qihangerp.service.external;
import cn.qihangerp.model.request.ExternalPddSkuCanonicalSyncRequest;
import cn.qihangerp.model.vo.ExternalPddSkuCanonicalSyncResultVo;
/**
* 主数据回调 {@code detail.get} 中的 SKU 映射写入 {@code o_goods_sku.canonical_ext}
*/
public interface ExternalPddSkuCanonicalSyncAppService {
ExternalPddSkuCanonicalSyncResultVo syncFromGoodsDetailPop(ExternalPddSkuCanonicalSyncRequest req);
}

View File

@ -0,0 +1,37 @@
package cn.qihangerp.service.external.impl;
import cn.qihangerp.model.entity.OGoods;
import cn.qihangerp.model.request.ExternalPddSkuCanonicalSyncRequest;
import cn.qihangerp.model.vo.ExternalPddSkuCanonicalSyncResultVo;
import cn.qihangerp.module.service.OGoodsService;
import cn.qihangerp.service.external.ExternalPddSkuCanonicalSyncAppService;
import cn.qihangerp.service.external.pdd.OGoodsPddMappingPersistence;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
/**
* @author guochengyu
*/
@Service
@RequiredArgsConstructor
public class ExternalPddSkuCanonicalSyncAppServiceImpl implements ExternalPddSkuCanonicalSyncAppService {
private final OGoodsService goodsService;
private final OGoodsPddMappingPersistence oGoodsPddMappingPersistence;
@Override
public ExternalPddSkuCanonicalSyncResultVo syncFromGoodsDetailPop(ExternalPddSkuCanonicalSyncRequest req) {
OGoods g = goodsService.getById(req.getErpGoodsId());
if (g == null) {
return ExternalPddSkuCanonicalSyncResultVo.builder()
.updatedSkuRows(0)
.message("o_goods 不存在: " + req.getErpGoodsId())
.build();
}
int n = oGoodsPddMappingPersistence.mergePddSkuCanonicalFromGoodsDetailPop(
req.getErpGoodsId(), req.getGoodsDetailPopRaw().trim());
return ExternalPddSkuCanonicalSyncResultVo.builder()
.updatedSkuRows(n)
.message(n > 0 ? "已合并 pddSkuId" : "无匹配 outerErpSkuId 或未解析到 sku_list")
.build();
}
}

View File

@ -26,7 +26,8 @@ import java.util.Map;
*
* <p>拼多多仅根据 {@code pdd.goods.add} 成功响应中的 {@code sku_list} 回填 {@value #CANONICAL_PDD_SKU_ID}<strong>不在本服务内</strong>调用
* {@code pdd.goods.detail.get}{@code pdd.goods.information.update} 若未带 {@code sku_list} {@code pddSkuId} 须由主数据在适当时机
* {@code POST /external/pdd/goods/detail} 后自行落库或后续扩展同步接口写回 ERP当前 detail 接口仅返回 JSON 给调用方</p>
* {@code POST /external/pdd/goods/detail} 成功后由主数据再调 {@code POST /external/pdd/goods/sku-canonical-sync}
* {@code sku_list} 映射合并写入 {@code o_goods_sku.canonical_ext} {@value #CANONICAL_PDD_SKU_ID}</p>
*/
@Slf4j
@Component
@ -78,6 +79,21 @@ public class OGoodsPddMappingPersistence {
}
}
/**
* {@code pdd.goods.detail.get} POP 原始 JSON 解析 {@code sku_list} {@code outerErpSkuId} 合并写入
* {@code o_goods_sku.canonicalExt.pddSkuId} {@link #persistAfterPddPublishSuccess} GOODS_ADD 片段逻辑一致
*
* @return 实际更新行数
*/
public int mergePddSkuCanonicalFromGoodsDetailPop(Long erpGoodsId, String goodsDetailPopRaw) {
if (erpGoodsId == null || erpGoodsId <= 0) {
return 0;
}
Map<String, Long> outerToPddSku =
PddOpenApiSupport.parseOuterKeyToPddSkuIdFromGoodsDetailGetResponse(goodsDetailPopRaw);
return applyPddSkuMappings(erpGoodsId, outerToPddSku);
}
/**
* {@code outerKey -> 拼多多 sku_id} 合并写入 {@code o_goods_sku.canonicalExt} {@value #CANONICAL_PDD_SKU_ID}
*