From f8312ad4b7c5811aa32a3fa32d8a1fbd6157e5e0 Mon Sep 17 00:00:00 2001
From: huangyujie <27665451@qq.com>
Date: Mon, 30 Mar 2026 17:39:53 +0800
Subject: [PATCH] feat(external): PDD quantity by outGoodsId/outSkuId; persist
POP ids in canonicalExt
- Remove /external/pdd/goods/quantity/update and ExternalPddGoodsQuantityUpdateRequest.
- Add POST /external/goods/pdd/quantity/update with shopId+outGoodsId+outSkuId; resolve pddGoodsId/pddSkuId from o_goods/o_goods_sku canonicalExt.
- On successful PDD upsert, persist pddGoodsId and per-SKU pddSkuId (from goods_add_response.sku_list) via OGoodsPddMappingPersistence.
- Parse outer key via out_sku_sn/outer_id/out_sku_id in PddOpenApiSupport.
Made-with: Cursor
---
.../controller/ExternalGoodsController.java | 41 ++++
.../ExternalPddGoodsController.java | 36 +---
.../ExternalGoodsRequestLogSupport.java | 6 +-
...ExternalGoodsPddQuantityUpdateRequest.java | 42 ++++
...ExternalPddGoodsQuantityUpdateRequest.java | 35 ----
...xternalGoodsPddQuantityUpdateResultVo.java | 32 +++
...xternalPddGoodsQuantityUpdateResultVo.java | 31 ---
...ernalGoodsPddQuantityUpdateAppService.java | 12 ++
...ernalPddGoodsQuantityUpdateAppService.java | 12 --
.../impl/ExternalGoodsAppServiceImpl.java | 3 +
...lGoodsPddQuantityUpdateAppServiceImpl.java | 184 ++++++++++++++++++
...lPddGoodsQuantityUpdateAppServiceImpl.java | 121 ------------
.../pdd/OGoodsPddMappingPersistence.java | 105 ++++++++++
.../external/pdd/PddOpenApiSupport.java | 59 ++++++
14 files changed, 482 insertions(+), 237 deletions(-)
create mode 100644 model/src/main/java/cn/qihangerp/model/request/ExternalGoodsPddQuantityUpdateRequest.java
delete mode 100644 model/src/main/java/cn/qihangerp/model/request/ExternalPddGoodsQuantityUpdateRequest.java
create mode 100644 model/src/main/java/cn/qihangerp/model/vo/ExternalGoodsPddQuantityUpdateResultVo.java
delete mode 100644 model/src/main/java/cn/qihangerp/model/vo/ExternalPddGoodsQuantityUpdateResultVo.java
create mode 100644 service/src/main/java/cn/qihangerp/service/external/ExternalGoodsPddQuantityUpdateAppService.java
delete mode 100644 service/src/main/java/cn/qihangerp/service/external/ExternalPddGoodsQuantityUpdateAppService.java
create mode 100644 service/src/main/java/cn/qihangerp/service/external/impl/ExternalGoodsPddQuantityUpdateAppServiceImpl.java
delete mode 100644 service/src/main/java/cn/qihangerp/service/external/impl/ExternalPddGoodsQuantityUpdateAppServiceImpl.java
create mode 100644 service/src/main/java/cn/qihangerp/service/external/pdd/OGoodsPddMappingPersistence.java
diff --git a/api/erp-api/src/main/java/cn/qihangerp/erp/controller/ExternalGoodsController.java b/api/erp-api/src/main/java/cn/qihangerp/erp/controller/ExternalGoodsController.java
index b9a4daad..7acfa7f6 100644
--- a/api/erp-api/src/main/java/cn/qihangerp/erp/controller/ExternalGoodsController.java
+++ b/api/erp-api/src/main/java/cn/qihangerp/erp/controller/ExternalGoodsController.java
@@ -3,12 +3,15 @@ package cn.qihangerp.erp.controller;
import cn.qihangerp.common.AjaxResult;
import cn.qihangerp.common.enums.EnumShopType;
import cn.qihangerp.model.request.ExternalGoodsDelistRequest;
+import cn.qihangerp.model.request.ExternalGoodsPddQuantityUpdateRequest;
import cn.qihangerp.model.request.ExternalGoodsUpsertRequest;
+import cn.qihangerp.model.vo.ExternalGoodsPddQuantityUpdateResultVo;
import cn.qihangerp.model.vo.ExternalGoodsUpsertResultVo;
import cn.qihangerp.security.common.BaseController;
import cn.qihangerp.erp.config.ExternalGoodsApiLogProperties;
import cn.qihangerp.erp.support.ExternalGoodsRequestLogSupport;
import cn.qihangerp.service.external.ExternalGoodsAppService;
+import cn.qihangerp.service.external.ExternalGoodsPddQuantityUpdateAppService;
import cn.qihangerp.service.external.pdd.ExternalPddProperties;
import com.alibaba.fastjson2.JSON;
import lombok.AllArgsConstructor;
@@ -27,6 +30,7 @@ import java.util.List;
*
* - {@code POST /external/goods/upsert} — 商品上架/同步(新建默认 {@code o_goods.status=1})
* - {@code POST /external/goods/delist} — 本地下架({@code status=2});可选 {@code pddGoodsId}+{@code pddPopAuth} 调拼多多 {@code pdd.goods.sale.status.set}({@code is_onsale=0})
+ * - {@code POST /external/goods/pdd/quantity/update} — 按 {@code shopId}+{@code outGoodsId}+{@code outSkuId} 解析本地映射后调 {@code pdd.goods.quantity.update}
*
*
* @author guochengyu
@@ -37,6 +41,7 @@ import java.util.List;
@RequestMapping("/external/goods")
public class ExternalGoodsController extends BaseController {
private final ExternalGoodsAppService externalGoodsAppService;
+ private final ExternalGoodsPddQuantityUpdateAppService externalGoodsPddQuantityUpdateAppService;
private final ExternalPddProperties externalPddProperties;
private final ExternalGoodsApiLogProperties goodsApiLogProperties;
@@ -142,6 +147,42 @@ public class ExternalGoodsController extends BaseController {
}
}
+ /**
+ * 拼多多改库存:入参与 upsert 对齐({@code shopId}、{@code outGoodsId}、{@code outSkuId}),不传拼多多 goods/sku id。
+ */
+ @PostMapping("/pdd/quantity/update")
+ public AjaxResult pddQuantityUpdate(@RequestBody ExternalGoodsPddQuantityUpdateRequest req) {
+ if (goodsApiLogProperties.isLogFullRequest()) {
+ log.info("[external/goods/pdd/quantity/update] request={}",
+ ExternalGoodsRequestLogSupport.goodsPddQuantityUpdateRequestJsonForLog(req));
+ }
+ if (req == null || req.getShopId() == null || req.getShopId() <= 0) {
+ return AjaxResult.error("参数错误:shopId不能为空");
+ }
+ if (!StringUtils.hasText(req.getOutGoodsId())) {
+ return AjaxResult.error("参数错误:outGoodsId不能为空");
+ }
+ if (!StringUtils.hasText(req.getOutSkuId())) {
+ return AjaxResult.error("参数错误:outSkuId不能为空");
+ }
+ if (req.getQuantity() == null || req.getQuantity() < 0) {
+ return AjaxResult.error("参数错误:quantity 不能为空且不能为负");
+ }
+ if (req.getPddPopAuth() == null) {
+ return AjaxResult.error("参数错误:pddPopAuth 不能为空");
+ }
+ var auth = req.getPddPopAuth();
+ if (!StringUtils.hasText(auth.getAppKey()) || !StringUtils.hasText(auth.getAppSecret())
+ || !StringUtils.hasText(auth.getAccessToken())) {
+ return AjaxResult.error("参数错误:pddPopAuth 需提供 appKey、appSecret、accessToken");
+ }
+ ExternalGoodsPddQuantityUpdateResultVo vo = externalGoodsPddQuantityUpdateAppService.updateQuantity(req);
+ if (!Boolean.TRUE.equals(vo.getPopBizSuccess())) {
+ return AjaxResult.error(StringUtils.hasText(vo.getMessage()) ? vo.getMessage() : "pdd.goods.quantity.update 失败");
+ }
+ return AjaxResult.success(vo);
+ }
+
private static String safeHostForLog(String gatewayUrl) {
if (!StringUtils.hasText(gatewayUrl)) {
return "";
diff --git a/api/erp-api/src/main/java/cn/qihangerp/erp/controller/ExternalPddGoodsController.java b/api/erp-api/src/main/java/cn/qihangerp/erp/controller/ExternalPddGoodsController.java
index ee33ded7..28caa501 100644
--- a/api/erp-api/src/main/java/cn/qihangerp/erp/controller/ExternalPddGoodsController.java
+++ b/api/erp-api/src/main/java/cn/qihangerp/erp/controller/ExternalPddGoodsController.java
@@ -2,14 +2,11 @@ package cn.qihangerp.erp.controller;
import cn.qihangerp.common.AjaxResult;
import cn.qihangerp.model.request.ExternalPddGoodsDetailRequest;
-import cn.qihangerp.model.request.ExternalPddGoodsQuantityUpdateRequest;
import cn.qihangerp.model.vo.ExternalPddGoodsDetailResultVo;
-import cn.qihangerp.model.vo.ExternalPddGoodsQuantityUpdateResultVo;
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.ExternalPddGoodsQuantityUpdateAppService;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
@@ -22,8 +19,8 @@ import org.springframework.web.bind.annotation.RestController;
* 拼多多专用外部接口(与 {@code /external/goods/upsert} 编排解耦)。
*
* - {@code POST /external/pdd/goods/detail} — {@code pdd.goods.detail.get}
- * - {@code POST /external/pdd/goods/quantity/update} — {@code pdd.goods.quantity.update}
*
+ * 改库存请使用 {@code POST /external/goods/pdd/quantity/update}(与 upsert 相同 out 键)。
*/
@Slf4j
@AllArgsConstructor
@@ -32,7 +29,6 @@ import org.springframework.web.bind.annotation.RestController;
public class ExternalPddGoodsController extends BaseController {
private final ExternalPddGoodsDetailAppService externalPddGoodsDetailAppService;
- private final ExternalPddGoodsQuantityUpdateAppService externalPddGoodsQuantityUpdateAppService;
private final ExternalGoodsApiLogProperties goodsApiLogProperties;
@PostMapping("/detail")
@@ -57,34 +53,4 @@ public class ExternalPddGoodsController extends BaseController {
}
return AjaxResult.success(vo);
}
-
- @PostMapping("/quantity/update")
- public AjaxResult quantityUpdate(@RequestBody ExternalPddGoodsQuantityUpdateRequest req) {
- if (goodsApiLogProperties.isLogFullRequest()) {
- log.info("[external/pdd/goods/quantity/update] request={}",
- ExternalGoodsRequestLogSupport.pddQuantityUpdateRequestJsonForLog(req));
- }
- if (req == null || req.getGoodsId() == null || req.getGoodsId() <= 0) {
- return AjaxResult.error("参数错误:goodsId 不能为空且须为正数");
- }
- if (req.getSkuId() == null || req.getSkuId() <= 0) {
- return AjaxResult.error("参数错误:skuId 不能为空且须为正数(拼多多 sku_id)");
- }
- if (req.getQuantity() == null || req.getQuantity() < 0) {
- return AjaxResult.error("参数错误:quantity 不能为空且不能为负");
- }
- if (req.getPddPopAuth() == null) {
- return AjaxResult.error("参数错误:pddPopAuth 不能为空");
- }
- var auth = req.getPddPopAuth();
- if (!StringUtils.hasText(auth.getAppKey()) || !StringUtils.hasText(auth.getAppSecret())
- || !StringUtils.hasText(auth.getAccessToken())) {
- return AjaxResult.error("参数错误:pddPopAuth 需提供 appKey、appSecret、accessToken");
- }
- ExternalPddGoodsQuantityUpdateResultVo vo = externalPddGoodsQuantityUpdateAppService.updateQuantity(req);
- if (!Boolean.TRUE.equals(vo.getPopBizSuccess())) {
- return AjaxResult.error(StringUtils.hasText(vo.getMessage()) ? vo.getMessage() : "pdd.goods.quantity.update 失败");
- }
- return AjaxResult.success(vo);
- }
}
diff --git a/api/erp-api/src/main/java/cn/qihangerp/erp/support/ExternalGoodsRequestLogSupport.java b/api/erp-api/src/main/java/cn/qihangerp/erp/support/ExternalGoodsRequestLogSupport.java
index 673a2cfa..7080db1e 100644
--- a/api/erp-api/src/main/java/cn/qihangerp/erp/support/ExternalGoodsRequestLogSupport.java
+++ b/api/erp-api/src/main/java/cn/qihangerp/erp/support/ExternalGoodsRequestLogSupport.java
@@ -1,8 +1,8 @@
package cn.qihangerp.erp.support;
import cn.qihangerp.model.request.ExternalGoodsUpsertRequest;
+import cn.qihangerp.model.request.ExternalGoodsPddQuantityUpdateRequest;
import cn.qihangerp.model.request.ExternalPddGoodsDetailRequest;
-import cn.qihangerp.model.request.ExternalPddGoodsQuantityUpdateRequest;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import org.springframework.util.StringUtils;
@@ -18,9 +18,9 @@ public final class ExternalGoodsRequestLogSupport {
}
/**
- * {@link ExternalPddGoodsQuantityUpdateRequest} 脱敏日志(不修改入参)。
+ * {@link ExternalGoodsPddQuantityUpdateRequest} 脱敏日志(不修改入参)。
*/
- public static String pddQuantityUpdateRequestJsonForLog(ExternalPddGoodsQuantityUpdateRequest req) {
+ public static String goodsPddQuantityUpdateRequestJsonForLog(ExternalGoodsPddQuantityUpdateRequest req) {
if (req == null) {
return "null";
}
diff --git a/model/src/main/java/cn/qihangerp/model/request/ExternalGoodsPddQuantityUpdateRequest.java b/model/src/main/java/cn/qihangerp/model/request/ExternalGoodsPddQuantityUpdateRequest.java
new file mode 100644
index 00000000..6d88c7ca
--- /dev/null
+++ b/model/src/main/java/cn/qihangerp/model/request/ExternalGoodsPddQuantityUpdateRequest.java
@@ -0,0 +1,42 @@
+package cn.qihangerp.model.request;
+
+import lombok.Data;
+
+/**
+ * {@code POST /external/goods/pdd/quantity/update}:与 {@link ExternalGoodsUpsertRequest}/{@link ExternalGoodsDelistRequest}
+ * 相同维度键({@code shopId}+{@code outGoodsId}+{@code outSkuId}),由 ERP-Open 根据 {@code o_goods}/{@code o_goods_sku}.{@code canonicalExt}
+ * 中已落库的拼多多 ID 调用 {@code pdd.goods.quantity.update}。调用方无需传拼多多 {@code goods_id}/{@code sku_id}。
+ */
+@Data
+public class ExternalGoodsPddQuantityUpdateRequest {
+
+ /** 与 upsert/delist 一致:业务侧店铺维度标识 */
+ private Long shopId;
+
+ /** 与 upsert 的 {@code outGoodsId} 一致:外部(中台)商品 ID,对应 {@code o_goods.outer_erp_goods_id} */
+ private String outGoodsId;
+
+ /** 与 upsert SKU 的 {@code outSkuId} 一致:外部(中台)SKU ID,对应 {@code o_goods_sku.outer_erp_sku_id} */
+ private String outSkuId;
+
+ /** 库存数量 */
+ private Long quantity;
+
+ /**
+ * 更新类型,参见拼多多开放平台;常见 {@code 1}。
+ */
+ private Integer updateType;
+
+ /**
+ * 是否使用 {@code force_update}(默认 {@code true})。
+ */
+ private Boolean forceUpdate;
+
+ /**
+ * 可选,透传 POP {@code outer_id}(与 {@code pdd.goods.quantity.update} 一致)。
+ */
+ private String outerId;
+
+ /** 拼多多 POP 凭证,与 upsert 的 {@link ExternalGoodsUpsertRequest#getPddPopAuth()} 结构一致 */
+ private ExternalGoodsUpsertRequest.PddPopAuth pddPopAuth;
+}
diff --git a/model/src/main/java/cn/qihangerp/model/request/ExternalPddGoodsQuantityUpdateRequest.java b/model/src/main/java/cn/qihangerp/model/request/ExternalPddGoodsQuantityUpdateRequest.java
deleted file mode 100644
index 8a0143a5..00000000
--- a/model/src/main/java/cn/qihangerp/model/request/ExternalPddGoodsQuantityUpdateRequest.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package cn.qihangerp.model.request;
-
-import lombok.Data;
-
-/**
- * {@code POST /external/pdd/goods/quantity/update}:调用拼多多 {@code pdd.goods.quantity.update} 更新 SKU 库存。
- * 凭证形态与 {@link ExternalGoodsUpsertRequest#getPddPopAuth()} 一致。
- */
-@Data
-public class ExternalPddGoodsQuantityUpdateRequest {
-
- /** 拼多多商品 {@code goods_id} */
- private Long goodsId;
-
- /** 拼多多 {@code sku_id}(与 POP 文档一致) */
- private Long skuId;
-
- /** 可选,商家外部编码 {@code outer_id} */
- private String outerId;
-
- /** 库存数量 */
- private Long quantity;
-
- /**
- * 更新类型,参见拼多多开放平台;常见 {@code 1}(与官方示例一致)。
- */
- private Integer updateType;
-
- /**
- * 是否使用 {@code force_update}(默认 {@code true},与官方示例一致)。
- */
- private Boolean forceUpdate;
-
- private ExternalGoodsUpsertRequest.PddPopAuth pddPopAuth;
-}
diff --git a/model/src/main/java/cn/qihangerp/model/vo/ExternalGoodsPddQuantityUpdateResultVo.java b/model/src/main/java/cn/qihangerp/model/vo/ExternalGoodsPddQuantityUpdateResultVo.java
new file mode 100644
index 00000000..61545763
--- /dev/null
+++ b/model/src/main/java/cn/qihangerp/model/vo/ExternalGoodsPddQuantityUpdateResultVo.java
@@ -0,0 +1,32 @@
+package cn.qihangerp.model.vo;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * {@code /external/goods/pdd/quantity/update} 返回:解析出的拼多多 ID(便于排障)及 POP 响应摘要。
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ExternalGoodsPddQuantityUpdateResultVo implements Serializable {
+
+ private Boolean popBizSuccess;
+
+ private Boolean quantityUpdateSuccess;
+
+ private String message;
+
+ private String popResponseBody;
+
+ /** 实际调用 POP 时使用的拼多多 {@code goods_id}(来自本地映射) */
+ private Long resolvedPddGoodsId;
+
+ /** 实际调用 POP 时使用的拼多多 {@code sku_id}(来自本地映射) */
+ private Long resolvedPddSkuId;
+}
diff --git a/model/src/main/java/cn/qihangerp/model/vo/ExternalPddGoodsQuantityUpdateResultVo.java b/model/src/main/java/cn/qihangerp/model/vo/ExternalPddGoodsQuantityUpdateResultVo.java
deleted file mode 100644
index 8fb491cb..00000000
--- a/model/src/main/java/cn/qihangerp/model/vo/ExternalPddGoodsQuantityUpdateResultVo.java
+++ /dev/null
@@ -1,31 +0,0 @@
-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/quantity/update} 返回:含 POP 网关原始 JSON 与业务 {@code is_success} 解析结果。
- */
-@Data
-@Builder
-@NoArgsConstructor
-@AllArgsConstructor
-public class ExternalPddGoodsQuantityUpdateResultVo implements Serializable {
-
- /**
- * 整体是否成功:无 {@code error_response} 且 {@code goods_quantity_update_response.is_success=true}。
- */
- private Boolean popBizSuccess;
-
- /** 与响应体 {@code goods_quantity_update_response.is_success} 一致(解析失败时为 false) */
- private Boolean quantityUpdateSuccess;
-
- private String message;
-
- /** 拼多多网关响应全文 */
- private String popResponseBody;
-}
diff --git a/service/src/main/java/cn/qihangerp/service/external/ExternalGoodsPddQuantityUpdateAppService.java b/service/src/main/java/cn/qihangerp/service/external/ExternalGoodsPddQuantityUpdateAppService.java
new file mode 100644
index 00000000..e3207ef2
--- /dev/null
+++ b/service/src/main/java/cn/qihangerp/service/external/ExternalGoodsPddQuantityUpdateAppService.java
@@ -0,0 +1,12 @@
+package cn.qihangerp.service.external;
+
+import cn.qihangerp.model.request.ExternalGoodsPddQuantityUpdateRequest;
+import cn.qihangerp.model.vo.ExternalGoodsPddQuantityUpdateResultVo;
+
+/**
+ * 按 {@code outGoodsId}/{@code outSkuId} 解析本地拼多多映射后调用 {@code pdd.goods.quantity.update}。
+ */
+public interface ExternalGoodsPddQuantityUpdateAppService {
+
+ ExternalGoodsPddQuantityUpdateResultVo updateQuantity(ExternalGoodsPddQuantityUpdateRequest req);
+}
diff --git a/service/src/main/java/cn/qihangerp/service/external/ExternalPddGoodsQuantityUpdateAppService.java b/service/src/main/java/cn/qihangerp/service/external/ExternalPddGoodsQuantityUpdateAppService.java
deleted file mode 100644
index 8401b441..00000000
--- a/service/src/main/java/cn/qihangerp/service/external/ExternalPddGoodsQuantityUpdateAppService.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package cn.qihangerp.service.external;
-
-import cn.qihangerp.model.request.ExternalPddGoodsQuantityUpdateRequest;
-import cn.qihangerp.model.vo.ExternalPddGoodsQuantityUpdateResultVo;
-
-/**
- * 拼多多库存更新({@code pdd.goods.quantity.update})。
- */
-public interface ExternalPddGoodsQuantityUpdateAppService {
-
- ExternalPddGoodsQuantityUpdateResultVo updateQuantity(ExternalPddGoodsQuantityUpdateRequest req);
-}
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 c084f0c6..3e14ea16 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
@@ -12,6 +12,7 @@ import cn.qihangerp.module.service.OGoodsService;
import cn.qihangerp.module.service.OGoodsSkuService;
import cn.qihangerp.service.external.ExternalGoodsAppService;
import cn.qihangerp.service.external.pdd.ExternalPddPublishService;
+import cn.qihangerp.service.external.pdd.OGoodsPddMappingPersistence;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
@@ -40,6 +41,7 @@ public class ExternalGoodsAppServiceImpl implements ExternalGoodsAppService {
private final OGoodsService goodsService;
private final OGoodsSkuService skuService;
private final ExternalPddPublishService externalPddPublishService;
+ private final OGoodsPddMappingPersistence oGoodsPddMappingPersistence;
@Override
@Transactional(rollbackFor = Exception.class)
@@ -149,6 +151,7 @@ public class ExternalGoodsAppServiceImpl implements ExternalGoodsAppService {
ExternalGoodsUpsertResultVo vo = out.build();
if ("PDD".equalsIgnoreCase(req.getPlatform())) {
+ oGoodsPddMappingPersistence.persistAfterPddPublishSuccess(goodsId, 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/impl/ExternalGoodsPddQuantityUpdateAppServiceImpl.java b/service/src/main/java/cn/qihangerp/service/external/impl/ExternalGoodsPddQuantityUpdateAppServiceImpl.java
new file mode 100644
index 00000000..ef02d0f5
--- /dev/null
+++ b/service/src/main/java/cn/qihangerp/service/external/impl/ExternalGoodsPddQuantityUpdateAppServiceImpl.java
@@ -0,0 +1,184 @@
+package cn.qihangerp.service.external.impl;
+
+import cn.qihangerp.model.entity.OGoods;
+import cn.qihangerp.model.entity.OGoodsSku;
+import cn.qihangerp.model.request.ExternalGoodsPddQuantityUpdateRequest;
+import cn.qihangerp.model.request.ExternalGoodsUpsertRequest;
+import cn.qihangerp.model.vo.ExternalGoodsPddQuantityUpdateResultVo;
+import cn.qihangerp.module.service.OGoodsService;
+import cn.qihangerp.module.service.OGoodsSkuService;
+import cn.qihangerp.service.external.ExternalGoodsPddQuantityUpdateAppService;
+import cn.qihangerp.service.external.pdd.ExternalPddProperties;
+import cn.qihangerp.service.external.pdd.OGoodsPddMappingPersistence;
+import cn.qihangerp.service.external.pdd.PddOpenApiSupport;
+import cn.qihangerp.service.external.pdd.PddPopClient;
+import cn.qihangerp.service.external.shop.PddShopCredential;
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+
+/**
+ * @author guochengyu
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class ExternalGoodsPddQuantityUpdateAppServiceImpl implements ExternalGoodsPddQuantityUpdateAppService {
+
+ private final ExternalPddProperties props;
+ private final PddPopClient pddPopClient;
+ private final OGoodsService goodsService;
+ private final OGoodsSkuService skuService;
+
+ @Override
+ public ExternalGoodsPddQuantityUpdateResultVo updateQuantity(ExternalGoodsPddQuantityUpdateRequest req) {
+ if (req == null || req.getShopId() == null || req.getShopId() <= 0) {
+ return fail("shopId 无效", null, false, null, null);
+ }
+ if (!StringUtils.hasText(req.getOutGoodsId())) {
+ return fail("outGoodsId 不能为空", null, false, null, null);
+ }
+ if (!StringUtils.hasText(req.getOutSkuId())) {
+ return fail("outSkuId 不能为空", null, false, null, null);
+ }
+ if (req.getQuantity() == null || req.getQuantity() < 0) {
+ return fail("quantity 无效:不能为空且不能为负", null, false, null, null);
+ }
+
+ String outGoodsId = req.getOutGoodsId().trim();
+ String outSkuId = req.getOutSkuId().trim();
+
+ OGoods goods = goodsService.getOne(new LambdaQueryWrapper()
+ .eq(OGoods::getShopId, req.getShopId())
+ .eq(OGoods::getOuterErpGoodsId, outGoodsId)
+ .last("LIMIT 1"));
+ if (goods == null) {
+ return fail("商品不存在:shopId=" + req.getShopId() + ", outGoodsId=" + outGoodsId, null, false, null, null);
+ }
+ Long pddGoodsId = readCanonicalLong(goods.getCanonicalExt(), OGoodsPddMappingPersistence.CANONICAL_PDD_GOODS_ID);
+ if (pddGoodsId == null || pddGoodsId <= 0) {
+ return fail("本地未找到拼多多 goods_id:请先 upsert 发品成功以写入 o_goods.canonicalExt.pddGoodsId", null, false, null, null);
+ }
+
+ OGoodsSku skuRow = skuService.getOne(new LambdaQueryWrapper()
+ .eq(OGoodsSku::getShopId, req.getShopId())
+ .eq(OGoodsSku::getGoodsId, goods.getId())
+ .eq(OGoodsSku::getOuterErpSkuId, outSkuId)
+ .last("LIMIT 1"));
+ if (skuRow == null) {
+ return fail("SKU 不存在:shopId=" + req.getShopId() + ", outSkuId=" + outSkuId, null, false, pddGoodsId, null);
+ }
+ Long pddSkuId = readCanonicalLong(skuRow.getCanonicalExt(), OGoodsPddMappingPersistence.CANONICAL_PDD_SKU_ID);
+ if (pddSkuId == null || pddSkuId <= 0) {
+ return fail("本地未找到拼多多 sku_id:请先通过 pdd.goods.add 成功以写入 o_goods_sku.canonicalExt.pddSkuId(information.update 路径可能未回填 SKU 映射)",
+ null, false, pddGoodsId, null);
+ }
+
+ PddShopCredential cred = resolveCredential(req);
+ if (cred == null || !StringUtils.hasText(cred.getAppKey()) || !StringUtils.hasText(cred.getAppSecret())
+ || !StringUtils.hasText(cred.getAccessToken())) {
+ return fail("pddPopAuth 不完整:需 appKey、appSecret、accessToken", null, false, pddGoodsId, pddSkuId);
+ }
+ String gateway = StringUtils.hasText(cred.getGatewayUrl()) ? cred.getGatewayUrl() : props.getGatewayUrl();
+ if (!StringUtils.hasText(gateway)) {
+ return fail("未配置 POP gatewayUrl", null, false, pddGoodsId, pddSkuId);
+ }
+
+ int updateType = req.getUpdateType() != null ? req.getUpdateType() : 1;
+ boolean forceUpdate = req.getForceUpdate() == null || Boolean.TRUE.equals(req.getForceUpdate());
+
+ try {
+ var biz = PddOpenApiSupport.goodsQuantityUpdateTopLevelParams(
+ pddGoodsId,
+ pddSkuId,
+ req.getOuterId(),
+ req.getQuantity(),
+ updateType,
+ forceUpdate);
+ String raw = pddPopClient.invokeTopLevelBiz(
+ gateway,
+ cred.getAppKey(),
+ cred.getAppSecret(),
+ cred.getAccessToken(),
+ "pdd.goods.quantity.update",
+ biz);
+ boolean noPopError = !PddOpenApiSupport.isError(raw);
+ boolean innerOk = PddOpenApiSupport.parseGoodsQuantityUpdateIsSuccess(raw);
+ boolean ok = noPopError && innerOk;
+ String msg;
+ if (!noPopError) {
+ msg = "pdd.goods.quantity.update POP错误: " + PddOpenApiSupport.formatError(raw);
+ } else if (!innerOk) {
+ msg = "pdd.goods.quantity.update 业务失败: goods_quantity_update_response.is_success=false 或响应缺字段";
+ } else {
+ msg = "pdd.goods.quantity.update 成功";
+ }
+ return ExternalGoodsPddQuantityUpdateResultVo.builder()
+ .popBizSuccess(ok)
+ .quantityUpdateSuccess(innerOk)
+ .message(msg)
+ .popResponseBody(raw)
+ .resolvedPddGoodsId(pddGoodsId)
+ .resolvedPddSkuId(pddSkuId)
+ .build();
+ } catch (Exception e) {
+ log.warn("[PDD] pdd.goods.quantity.update exception shopId={} outGoodsId={} outSkuId={} err={}",
+ req.getShopId(), outGoodsId, outSkuId, e.getMessage(), e);
+ return fail(e.getMessage(), null, false, pddGoodsId, pddSkuId);
+ }
+ }
+
+ 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 ExternalGoodsPddQuantityUpdateResultVo fail(String message, String raw, boolean innerOk,
+ Long resolvedPddGoodsId, Long resolvedPddSkuId) {
+ return ExternalGoodsPddQuantityUpdateResultVo.builder()
+ .popBizSuccess(false)
+ .quantityUpdateSuccess(innerOk)
+ .message(message)
+ .popResponseBody(raw)
+ .resolvedPddGoodsId(resolvedPddGoodsId)
+ .resolvedPddSkuId(resolvedPddSkuId)
+ .build();
+ }
+
+ private static PddShopCredential resolveCredential(ExternalGoodsPddQuantityUpdateRequest req) {
+ if (req == null || req.getPddPopAuth() == null) {
+ return null;
+ }
+ ExternalGoodsUpsertRequest.PddPopAuth a = req.getPddPopAuth();
+ PddShopCredential c = new PddShopCredential();
+ c.setAppKey(trimToNull(a.getAppKey()));
+ c.setAppSecret(trimToNull(a.getAppSecret()));
+ c.setAccessToken(trimToNull(a.getAccessToken()));
+ if (StringUtils.hasText(a.getGatewayUrl())) {
+ c.setGatewayUrl(a.getGatewayUrl().trim());
+ }
+ c.setSource("REQUEST");
+ return c;
+ }
+
+ private static String trimToNull(String s) {
+ if (!StringUtils.hasText(s)) {
+ return null;
+ }
+ return s.trim();
+ }
+}
diff --git a/service/src/main/java/cn/qihangerp/service/external/impl/ExternalPddGoodsQuantityUpdateAppServiceImpl.java b/service/src/main/java/cn/qihangerp/service/external/impl/ExternalPddGoodsQuantityUpdateAppServiceImpl.java
deleted file mode 100644
index 4f575fbe..00000000
--- a/service/src/main/java/cn/qihangerp/service/external/impl/ExternalPddGoodsQuantityUpdateAppServiceImpl.java
+++ /dev/null
@@ -1,121 +0,0 @@
-package cn.qihangerp.service.external.impl;
-
-import cn.qihangerp.model.request.ExternalGoodsUpsertRequest;
-import cn.qihangerp.model.request.ExternalPddGoodsQuantityUpdateRequest;
-import cn.qihangerp.model.vo.ExternalPddGoodsQuantityUpdateResultVo;
-import cn.qihangerp.service.external.ExternalPddGoodsQuantityUpdateAppService;
-import cn.qihangerp.service.external.pdd.ExternalPddProperties;
-import cn.qihangerp.service.external.pdd.PddOpenApiSupport;
-import cn.qihangerp.service.external.pdd.PddPopClient;
-import cn.qihangerp.service.external.shop.PddShopCredential;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.stereotype.Service;
-import org.springframework.util.StringUtils;
-
-/**
- * @author guochengyu
- */
-@Slf4j
-@Service
-@RequiredArgsConstructor
-public class ExternalPddGoodsQuantityUpdateAppServiceImpl implements ExternalPddGoodsQuantityUpdateAppService {
-
- private final ExternalPddProperties props;
- private final PddPopClient pddPopClient;
-
- @Override
- public ExternalPddGoodsQuantityUpdateResultVo updateQuantity(ExternalPddGoodsQuantityUpdateRequest req) {
- if (req == null || req.getGoodsId() == null || req.getGoodsId() <= 0) {
- return fail("goodsId 无效", null, false);
- }
- if (req.getSkuId() == null || req.getSkuId() <= 0) {
- return fail("skuId 无效:须传拼多多 sku_id", null, false);
- }
- if (req.getQuantity() == null || req.getQuantity() < 0) {
- return fail("quantity 无效:不能为空且不能为负", null, false);
- }
- int updateType = req.getUpdateType() != null ? req.getUpdateType() : 1;
- boolean forceUpdate = req.getForceUpdate() == null || Boolean.TRUE.equals(req.getForceUpdate());
-
- PddShopCredential cred = resolveCredential(req);
- if (cred == null || !StringUtils.hasText(cred.getAppKey()) || !StringUtils.hasText(cred.getAppSecret())
- || !StringUtils.hasText(cred.getAccessToken())) {
- return fail("pddPopAuth 不完整:需 appKey、appSecret、accessToken", null, false);
- }
- String gateway = StringUtils.hasText(cred.getGatewayUrl()) ? cred.getGatewayUrl() : props.getGatewayUrl();
- if (!StringUtils.hasText(gateway)) {
- return fail("未配置 POP gatewayUrl(请求体或 external.pdd.gateway-url)", null, false);
- }
-
- try {
- var biz = PddOpenApiSupport.goodsQuantityUpdateTopLevelParams(
- req.getGoodsId(),
- req.getSkuId(),
- req.getOuterId(),
- req.getQuantity(),
- updateType,
- forceUpdate);
- String raw = pddPopClient.invokeTopLevelBiz(
- gateway,
- cred.getAppKey(),
- cred.getAppSecret(),
- cred.getAccessToken(),
- "pdd.goods.quantity.update",
- biz);
- boolean noPopError = !PddOpenApiSupport.isError(raw);
- boolean innerOk = PddOpenApiSupport.parseGoodsQuantityUpdateIsSuccess(raw);
- boolean ok = noPopError && innerOk;
- String msg;
- if (!noPopError) {
- msg = "pdd.goods.quantity.update POP错误: " + PddOpenApiSupport.formatError(raw);
- } else if (!innerOk) {
- msg = "pdd.goods.quantity.update 业务失败: goods_quantity_update_response.is_success=false 或响应缺字段";
- } else {
- msg = "pdd.goods.quantity.update 成功";
- }
- return ExternalPddGoodsQuantityUpdateResultVo.builder()
- .popBizSuccess(ok)
- .quantityUpdateSuccess(innerOk)
- .message(msg)
- .popResponseBody(raw)
- .build();
- } catch (Exception e) {
- log.warn("[PDD] pdd.goods.quantity.update exception goodsId={} skuId={} err={}",
- req.getGoodsId(), req.getSkuId(), e.getMessage(), e);
- return fail(e.getMessage(), null, false);
- }
- }
-
- private static ExternalPddGoodsQuantityUpdateResultVo fail(String message, String raw, boolean innerOk) {
- return ExternalPddGoodsQuantityUpdateResultVo.builder()
- .popBizSuccess(false)
- .quantityUpdateSuccess(innerOk)
- .message(message)
- .popResponseBody(raw)
- .build();
- }
-
- private static PddShopCredential resolveCredential(ExternalPddGoodsQuantityUpdateRequest req) {
- if (req == null || req.getPddPopAuth() == null) {
- return null;
- }
- ExternalGoodsUpsertRequest.PddPopAuth a = req.getPddPopAuth();
- PddShopCredential c = new PddShopCredential();
- c.setAppKey(trimToNull(a.getAppKey()));
- c.setAppSecret(trimToNull(a.getAppSecret()));
- c.setAccessToken(trimToNull(a.getAccessToken()));
- if (StringUtils.hasText(a.getGatewayUrl())) {
- c.setGatewayUrl(a.getGatewayUrl().trim());
- }
- c.setSource("REQUEST");
- return c;
- }
-
- private static String trimToNull(String s) {
- if (!StringUtils.hasText(s)) {
- return null;
- }
- return s.trim();
- }
-}
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
new file mode 100644
index 00000000..8ab4fcd8
--- /dev/null
+++ b/service/src/main/java/cn/qihangerp/service/external/pdd/OGoodsPddMappingPersistence.java
@@ -0,0 +1,105 @@
+package cn.qihangerp.service.external.pdd;
+
+import cn.qihangerp.model.entity.OGoods;
+import cn.qihangerp.model.entity.OGoodsSku;
+import cn.qihangerp.model.vo.ExternalGoodsUpsertResultVo;
+import cn.qihangerp.module.service.OGoodsService;
+import cn.qihangerp.module.service.OGoodsSkuService;
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+
+import java.util.Date;
+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} 解析调用库存接口。
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class OGoodsPddMappingPersistence {
+
+ public static final String CANONICAL_PDD_GOODS_ID = "pddGoodsId";
+ public static final String CANONICAL_PDD_SKU_ID = "pddSkuId";
+
+ private final OGoodsService goodsService;
+ private final OGoodsSkuService skuService;
+
+ /**
+ * 在 upsert 拼多多链路成功({@link ExternalGoodsUpsertResultVo#getPddPublishSuccess()}==true)后调用。
+ */
+ public void persistAfterPddPublishSuccess(Long erpGoodsId, ExternalGoodsUpsertResultVo vo) {
+ if (erpGoodsId == null || vo == null || !Boolean.TRUE.equals(vo.getPddPublishSuccess())) {
+ return;
+ }
+ Long pddGoodsId = vo.getPddGoodsId();
+ if (pddGoodsId == null || pddGoodsId <= 0) {
+ log.warn("[PDD] skip persist mapping: pddGoodsId missing erpGoodsId={}", erpGoodsId);
+ return;
+ }
+ OGoods g = goodsService.getById(erpGoodsId);
+ if (g == null) {
+ return;
+ }
+ g.setCanonicalExt(mergeJsonLongField(g.getCanonicalExt(), CANONICAL_PDD_GOODS_ID, pddGoodsId));
+ g.setUpdateTime(new Date());
+ goodsService.updateById(g);
+
+ Map outerToPddSku = Map.of();
+ String lane = vo.getPddPublishLane();
+ if (lane != null && "GOODS_ADD".equalsIgnoreCase(lane.trim())) {
+ outerToPddSku = 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());
+ return;
+ }
+ List rows = skuService.list(new LambdaQueryWrapper()
+ .eq(OGoodsSku::getGoodsId, erpGoodsId));
+ if (rows == null || rows.isEmpty()) {
+ return;
+ }
+ int updated = 0;
+ for (OGoodsSku row : rows) {
+ if (row == null || !StringUtils.hasText(row.getOuterErpSkuId())) {
+ continue;
+ }
+ Long pddSkuId = outerToPddSku.get(row.getOuterErpSkuId().trim());
+ if (pddSkuId == null || pddSkuId <= 0) {
+ continue;
+ }
+ row.setCanonicalExt(mergeJsonLongField(row.getCanonicalExt(), CANONICAL_PDD_SKU_ID, pddSkuId));
+ row.setUpdateTime(new Date());
+ skuService.updateById(row);
+ updated++;
+ }
+ log.info("[PDD] persisted pddGoodsId + pddSkuId mappings erpGoodsId={} skuRowsUpdated={}",
+ erpGoodsId, updated);
+ }
+
+ private static String mergeJsonLongField(String existingJson, String field, long value) {
+ JSONObject o;
+ if (StringUtils.hasText(existingJson)) {
+ try {
+ o = JSON.parseObject(existingJson);
+ } catch (Exception e) {
+ o = new JSONObject();
+ }
+ if (o == null) {
+ o = new JSONObject();
+ }
+ } else {
+ o = new JSONObject();
+ }
+ o.put(field, value);
+ return o.toJSONString();
+ }
+}
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 81f111ff..b7b3ad9a 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
@@ -129,6 +129,65 @@ public final class PddOpenApiSupport {
}
}
+ /**
+ * 从 {@code pdd.goods.add} 成功响应中解析 {@code goods_add_response.sku_list[]}:
+ * 按 {@code out_sku_sn}/{@code outer_id}/{@code out_sku_id} 与外部 {@code outSkuId} 对齐,映射到拼多多 {@code sku_id}。
+ */
+ public static Map parseOuterKeyToPddSkuIdFromGoodsAddResponse(String raw) {
+ Map out = new LinkedHashMap<>();
+ if (!StringUtils.hasText(raw)) {
+ return out;
+ }
+ try {
+ JSONObject root = JSON.parseObject(raw);
+ if (root == null) {
+ return out;
+ }
+ JSONObject add = root.getJSONObject("goods_add_response");
+ if (add == null) {
+ return out;
+ }
+ JSONArray skuList = add.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();
+ }
+ if (StringUtils.hasText(b)) {
+ return b.trim();
+ }
+ if (StringUtils.hasText(c)) {
+ return c.trim();
+ }
+ return null;
+ }
+
/**
* {@code pdd.goods.spec.id.get} 的表单顶层业务参数(与 POP 常见调用方式一致)。
*/