From bda4302e59fb314f0b3ad1a993aac01a760cf4a1 Mon Sep 17 00:00:00 2001 From: huangyujie <27665451@qq.com> Date: Mon, 30 Mar 2026 14:46:14 +0800 Subject: [PATCH] =?UTF-8?q?feat(erp-open):=20=E6=8B=BC=E5=A4=9A=E5=A4=9A?= =?UTF-8?q?=E5=A4=96=E9=83=A8=E6=8E=A5=E5=8F=A3=E6=94=AF=E6=8C=81=20pdd.go?= =?UTF-8?q?ods.quantity.update=20=E5=BA=93=E5=AD=98=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 POST /external/pdd/goods/quantity/update,封装 POP 调用与请求/响应模型;Controller 与日志脱敏沿用 goods 外部接口约定;补充 PddOpenApiSupport 对 quantity.update 响应解析。 Made-with: Cursor --- .../ExternalPddGoodsController.java | 35 +++++ .../ExternalGoodsRequestLogSupport.java | 25 +++- ...ExternalPddGoodsQuantityUpdateRequest.java | 35 +++++ ...xternalPddGoodsQuantityUpdateResultVo.java | 31 +++++ ...ernalPddGoodsQuantityUpdateAppService.java | 12 ++ ...lPddGoodsQuantityUpdateAppServiceImpl.java | 121 ++++++++++++++++++ .../external/pdd/PddOpenApiSupport.java | 55 ++++++++ .../service/external/pdd/PddPopClient.java | 16 ++- 8 files changed, 328 insertions(+), 2 deletions(-) create mode 100644 model/src/main/java/cn/qihangerp/model/request/ExternalPddGoodsQuantityUpdateRequest.java create mode 100644 model/src/main/java/cn/qihangerp/model/vo/ExternalPddGoodsQuantityUpdateResultVo.java create mode 100644 service/src/main/java/cn/qihangerp/service/external/ExternalPddGoodsQuantityUpdateAppService.java create mode 100644 service/src/main/java/cn/qihangerp/service/external/impl/ExternalPddGoodsQuantityUpdateAppServiceImpl.java 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 728fa2d7..ee33ded7 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,11 +2,14 @@ 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; @@ -19,6 +22,7 @@ import org.springframework.web.bind.annotation.RestController; * 拼多多专用外部接口(与 {@code /external/goods/upsert} 编排解耦)。 * */ @Slf4j @@ -28,6 +32,7 @@ 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") @@ -52,4 +57,34 @@ 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 a2923f9f..673a2cfa 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 @@ -2,6 +2,7 @@ package cn.qihangerp.erp.support; import cn.qihangerp.model.request.ExternalGoodsUpsertRequest; 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; @@ -17,8 +18,27 @@ public final class ExternalGoodsRequestLogSupport { } /** - * 序列化为 JSON 并对 {@link ExternalGoodsUpsertRequest#getPddPopAuth()} 脱敏(不修改入参对象)。 + * {@link ExternalPddGoodsQuantityUpdateRequest} 脱敏日志(不修改入参)。 */ + public static String pddQuantityUpdateRequestJsonForLog(ExternalPddGoodsQuantityUpdateRequest req) { + if (req == null) { + return "null"; + } + try { + JSONObject root = JSON.parseObject(JSON.toJSONString(req)); + if (root == null) { + return "{}"; + } + JSONObject auth = root.getJSONObject("pddPopAuth"); + if (auth != null && !auth.isEmpty()) { + maskPddPopAuth(auth); + } + return root.toJSONString(); + } catch (Exception e) { + return "{\"_logSanitizeError\":true}"; + } + } + /** * {@link ExternalPddGoodsDetailRequest} 脱敏日志(不修改入参)。 */ @@ -41,6 +61,9 @@ public final class ExternalGoodsRequestLogSupport { } } + /** + * 序列化为 JSON 并对 {@link ExternalGoodsUpsertRequest#getPddPopAuth()} 脱敏(不修改入参对象)。 + */ public static String upsertRequestJsonForLog(ExternalGoodsUpsertRequest req) { if (req == null) { return "null"; diff --git a/model/src/main/java/cn/qihangerp/model/request/ExternalPddGoodsQuantityUpdateRequest.java b/model/src/main/java/cn/qihangerp/model/request/ExternalPddGoodsQuantityUpdateRequest.java new file mode 100644 index 00000000..8a0143a5 --- /dev/null +++ b/model/src/main/java/cn/qihangerp/model/request/ExternalPddGoodsQuantityUpdateRequest.java @@ -0,0 +1,35 @@ +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/ExternalPddGoodsQuantityUpdateResultVo.java b/model/src/main/java/cn/qihangerp/model/vo/ExternalPddGoodsQuantityUpdateResultVo.java new file mode 100644 index 00000000..8fb491cb --- /dev/null +++ b/model/src/main/java/cn/qihangerp/model/vo/ExternalPddGoodsQuantityUpdateResultVo.java @@ -0,0 +1,31 @@ +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/ExternalPddGoodsQuantityUpdateAppService.java b/service/src/main/java/cn/qihangerp/service/external/ExternalPddGoodsQuantityUpdateAppService.java new file mode 100644 index 00000000..8401b441 --- /dev/null +++ b/service/src/main/java/cn/qihangerp/service/external/ExternalPddGoodsQuantityUpdateAppService.java @@ -0,0 +1,12 @@ +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/ExternalPddGoodsQuantityUpdateAppServiceImpl.java b/service/src/main/java/cn/qihangerp/service/external/impl/ExternalPddGoodsQuantityUpdateAppServiceImpl.java new file mode 100644 index 00000000..4f575fbe --- /dev/null +++ b/service/src/main/java/cn/qihangerp/service/external/impl/ExternalPddGoodsQuantityUpdateAppServiceImpl.java @@ -0,0 +1,121 @@ +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/PddOpenApiSupport.java b/service/src/main/java/cn/qihangerp/service/external/pdd/PddOpenApiSupport.java index 9beab40b..81f111ff 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 @@ -51,6 +51,61 @@ public final class PddOpenApiSupport { return m; } + /** + * {@code pdd.goods.quantity.update} 表单顶层参数(与 POP 官方 curl / Java SDK 一致:{@code goods_id}、{@code sku_id}、{@code quantity}、 + * {@code update_type}、{@code force_update}、可选 {@code outer_id})。 + */ + public static Map goodsQuantityUpdateTopLevelParams(long goodsId, Long skuId, String outerId, + long quantity, int updateType, boolean forceUpdate) { + if (goodsId <= 0) { + throw new IllegalArgumentException("goods_id 必须为正数: " + goodsId); + } + if (quantity < 0) { + throw new IllegalArgumentException("quantity 不能为负: " + quantity); + } + Map m = new LinkedHashMap<>(); + m.put("goods_id", String.valueOf(goodsId)); + if (skuId != null && skuId > 0) { + m.put("sku_id", String.valueOf(skuId)); + } + if (StringUtils.hasText(outerId)) { + m.put("outer_id", outerId.trim()); + } + m.put("quantity", String.valueOf(quantity)); + m.put("update_type", String.valueOf(updateType)); + m.put("force_update", forceUpdate ? "true" : "false"); + return m; + } + + /** + * 解析 {@code pdd.goods.quantity.update} 响应中的 {@code goods_quantity_update_response.is_success}。 + */ + public static boolean parseGoodsQuantityUpdateIsSuccess(String raw) { + if (!StringUtils.hasText(raw)) { + return false; + } + try { + JSONObject root = JSON.parseObject(raw); + if (root == null) { + return false; + } + JSONObject resp = root.getJSONObject("goods_quantity_update_response"); + if (resp == null) { + return false; + } + Object v = resp.get("is_success"); + if (v instanceof Boolean b) { + return b; + } + if (v != null) { + return "true".equalsIgnoreCase(String.valueOf(v).trim()); + } + return false; + } catch (Exception e) { + return false; + } + } + /** * 从 {@code pdd.goods.add} 原始响应 JSON 中解析 {@code goods_add_response.goods_id}(拼多多侧商品 ID)。 */ diff --git a/service/src/main/java/cn/qihangerp/service/external/pdd/PddPopClient.java b/service/src/main/java/cn/qihangerp/service/external/pdd/PddPopClient.java index 0ae952e2..e3b23cda 100644 --- a/service/src/main/java/cn/qihangerp/service/external/pdd/PddPopClient.java +++ b/service/src/main/java/cn/qihangerp/service/external/pdd/PddPopClient.java @@ -22,7 +22,7 @@ import java.util.Map; * @@ -240,6 +240,20 @@ public class PddPopClient { } return raw; } + if ("pdd.goods.quantity.update".equals(type)) { + String snippet = PddOpenApiSupport.snippet(raw, 800); + if (!httpOk || popBizError) { + String errSummary = popBizError ? PddOpenApiSupport.formatError(raw) : ""; + log.warn("PDD_POP api={} host={} clientId={} httpStatus={} durationMs={} popBizError={} errSummary={} paramPayloadSnippet={} bodySnippet={}", + type, host, clientMasked, httpStatus, durationMs, popBizError, errSummary, + paramLogSnippet != null ? paramLogSnippet : "", snippet); + } else { + log.info("PDD_POP api={} host={} clientId={} httpStatus={} durationMs={} quantityOk={} bodySnippet={}", + type, host, clientMasked, httpStatus, durationMs, + PddOpenApiSupport.parseGoodsQuantityUpdateIsSuccess(raw), snippet); + } + return raw; + } String snippet = PddOpenApiSupport.snippet(raw, 600); if (!httpOk || popBizError) { String errSummary = popBizError ? PddOpenApiSupport.formatError(raw) : "";