From 22152216dd25dac0a788de545f8cee5fcc941097 Mon Sep 17 00:00:00 2001 From: huangyujie <27665451@qq.com> Date: Thu, 26 Mar 2026 16:06:38 +0800 Subject: [PATCH] =?UTF-8?q?feat(external):=20=E6=8B=BC=E5=A4=9A=E5=A4=9A?= =?UTF-8?q?=20goods.detail.get=20=E7=8B=AC=E7=AB=8B=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E4=B8=8E=20upsert=20=E5=9B=9E=E4=BC=A0=20pddGoodsId?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - upsert 响应增加 pddGoodsId(goods_add_response.goods_id),与 erpGoodsId 区分 - 新增 POST /external/pdd/goods/detail,供主数据在 add 成功后二次调用 - PddOpenApiSupport: goodsDetailGetTopLevelParams、parseGoodsIdFromGoodsAddResponse - detail.get 成功/失败全量响应体日志 - ExternalGoodsRequestLogSupport: detail 请求脱敏日志 Made-with: Cursor --- .../ExternalPddGoodsController.java | 55 +++++++++++ .../ExternalGoodsRequestLogSupport.java | 23 +++++ .../ExternalPddGoodsDetailRequest.java | 16 +++ .../model/vo/ExternalGoodsUpsertResultVo.java | 5 + .../vo/ExternalPddGoodsDetailResultVo.java | 28 ++++++ .../model/vo/PddPublishLaneResultVo.java | 3 + .../ExternalPddGoodsDetailAppService.java | 12 +++ .../impl/ExternalGoodsAppServiceImpl.java | 1 + .../ExternalPddGoodsDetailAppServiceImpl.java | 99 +++++++++++++++++++ .../pdd/ExternalPddPublishService.java | 6 +- .../external/pdd/PddOpenApiSupport.java | 35 +++++++ .../service/external/pdd/PddPopClient.java | 12 +++ 12 files changed, 293 insertions(+), 2 deletions(-) create mode 100644 api/erp-api/src/main/java/cn/qihangerp/erp/controller/ExternalPddGoodsController.java create mode 100644 model/src/main/java/cn/qihangerp/model/request/ExternalPddGoodsDetailRequest.java create mode 100644 model/src/main/java/cn/qihangerp/model/vo/ExternalPddGoodsDetailResultVo.java create mode 100644 service/src/main/java/cn/qihangerp/service/external/ExternalPddGoodsDetailAppService.java create mode 100644 service/src/main/java/cn/qihangerp/service/external/impl/ExternalPddGoodsDetailAppServiceImpl.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 new file mode 100644 index 00000000..728fa2d7 --- /dev/null +++ b/api/erp-api/src/main/java/cn/qihangerp/erp/controller/ExternalPddGoodsController.java @@ -0,0 +1,55 @@ +package cn.qihangerp.erp.controller; + +import cn.qihangerp.common.AjaxResult; +import cn.qihangerp.model.request.ExternalPddGoodsDetailRequest; +import cn.qihangerp.model.vo.ExternalPddGoodsDetailResultVo; +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 lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 拼多多专用外部接口(与 {@code /external/goods/upsert} 编排解耦)。 + * + */ +@Slf4j +@AllArgsConstructor +@RestController +@RequestMapping("/external/pdd/goods") +public class ExternalPddGoodsController extends BaseController { + + private final ExternalPddGoodsDetailAppService externalPddGoodsDetailAppService; + private final ExternalGoodsApiLogProperties goodsApiLogProperties; + + @PostMapping("/detail") + public AjaxResult detail(@RequestBody ExternalPddGoodsDetailRequest req) { + if (goodsApiLogProperties.isLogFullRequest()) { + log.info("[external/pdd/goods/detail] request={}", ExternalGoodsRequestLogSupport.pddDetailRequestJsonForLog(req)); + } + if (req == null || req.getPddGoodsId() == null || req.getPddGoodsId() <= 0) { + return AjaxResult.error("参数错误:pddGoodsId 不能为空且须为正数"); + } + 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"); + } + ExternalPddGoodsDetailResultVo vo = externalPddGoodsDetailAppService.fetchGoodsDetail(req); + if (!Boolean.TRUE.equals(vo.getPopBizSuccess())) { + return AjaxResult.error(StringUtils.hasText(vo.getMessage()) ? vo.getMessage() : "pdd.goods.detail.get 失败"); + } + 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 00d60642..a2923f9f 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,6 +1,7 @@ package cn.qihangerp.erp.support; import cn.qihangerp.model.request.ExternalGoodsUpsertRequest; +import cn.qihangerp.model.request.ExternalPddGoodsDetailRequest; import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONObject; import org.springframework.util.StringUtils; @@ -18,6 +19,28 @@ public final class ExternalGoodsRequestLogSupport { /** * 序列化为 JSON 并对 {@link ExternalGoodsUpsertRequest#getPddPopAuth()} 脱敏(不修改入参对象)。 */ + /** + * {@link ExternalPddGoodsDetailRequest} 脱敏日志(不修改入参)。 + */ + public static String pddDetailRequestJsonForLog(ExternalPddGoodsDetailRequest 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}"; + } + } + public static String upsertRequestJsonForLog(ExternalGoodsUpsertRequest req) { if (req == null) { return "null"; diff --git a/model/src/main/java/cn/qihangerp/model/request/ExternalPddGoodsDetailRequest.java b/model/src/main/java/cn/qihangerp/model/request/ExternalPddGoodsDetailRequest.java new file mode 100644 index 00000000..0109fc5b --- /dev/null +++ b/model/src/main/java/cn/qihangerp/model/request/ExternalPddGoodsDetailRequest.java @@ -0,0 +1,16 @@ +package cn.qihangerp.model.request; + +import lombok.Data; + +/** + * {@code POST /external/pdd/goods/detail}:由调用方(如主数据)在 {@code pdd.goods.add} 成功后二次请求,拉取 {@code pdd.goods.detail.get}。 + *

凭证形态与 {@link ExternalGoodsUpsertRequest#getPddPopAuth()} 一致。

+ */ +@Data +public class ExternalPddGoodsDetailRequest { + + /** 拼多多 {@code goods_add_response.goods_id} */ + private Long pddGoodsId; + + private ExternalGoodsUpsertRequest.PddPopAuth pddPopAuth; +} diff --git a/model/src/main/java/cn/qihangerp/model/vo/ExternalGoodsUpsertResultVo.java b/model/src/main/java/cn/qihangerp/model/vo/ExternalGoodsUpsertResultVo.java index 5e4730f5..379e7ae0 100644 --- a/model/src/main/java/cn/qihangerp/model/vo/ExternalGoodsUpsertResultVo.java +++ b/model/src/main/java/cn/qihangerp/model/vo/ExternalGoodsUpsertResultVo.java @@ -32,6 +32,11 @@ public class ExternalGoodsUpsertResultVo implements Serializable { /** 拼多多网关响应片段(便于排错) */ private String pddResponseSnippet; + /** + * 拼多多侧商品 ID({@code goods_add_response.goods_id});与 {@link #goodsId}(ERP 本地 {@code o_goods.id})不同。 + */ + private Long pddGoodsId; + // ---- 店铺凭证与自动拉取类目/spec(仅 platform=PDD 时有意义) ---- /** REQUEST(请求体 pddPopAuth)/ NONE;与 {@code o_shop} 无关 */ diff --git a/model/src/main/java/cn/qihangerp/model/vo/ExternalPddGoodsDetailResultVo.java b/model/src/main/java/cn/qihangerp/model/vo/ExternalPddGoodsDetailResultVo.java new file mode 100644 index 00000000..b1d5ef0f --- /dev/null +++ b/model/src/main/java/cn/qihangerp/model/vo/ExternalPddGoodsDetailResultVo.java @@ -0,0 +1,28 @@ +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/detail} 返回:拼多多网关原始 JSON(供主数据解析 {@code goods_detail_get_response})。 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ExternalPddGoodsDetailResultVo implements Serializable { + + /** 是否 POP 业务成功(非 error_response) */ + private Boolean popBizSuccess; + + private String message; + + /** + * 拼多多网关响应全文(解密后的 body 字符串),与 {@link cn.qihangerp.service.external.pdd.PddPopClient} 日志一致。 + */ + private String popResponseBody; +} diff --git a/model/src/main/java/cn/qihangerp/model/vo/PddPublishLaneResultVo.java b/model/src/main/java/cn/qihangerp/model/vo/PddPublishLaneResultVo.java index 028f584c..e55ed0aa 100644 --- a/model/src/main/java/cn/qihangerp/model/vo/PddPublishLaneResultVo.java +++ b/model/src/main/java/cn/qihangerp/model/vo/PddPublishLaneResultVo.java @@ -23,6 +23,9 @@ public class PddPublishLaneResultVo implements Serializable { private String message; private String goodsAddSnippet; + /** 拼多多 {@code goods_add_response.goods_id};仅 add 成功且能解析时有值 */ + private Long pddGoodsId; + /** REQUEST / NONE */ private String shopCredentialSource; diff --git a/service/src/main/java/cn/qihangerp/service/external/ExternalPddGoodsDetailAppService.java b/service/src/main/java/cn/qihangerp/service/external/ExternalPddGoodsDetailAppService.java new file mode 100644 index 00000000..30b5dfb6 --- /dev/null +++ b/service/src/main/java/cn/qihangerp/service/external/ExternalPddGoodsDetailAppService.java @@ -0,0 +1,12 @@ +package cn.qihangerp.service.external; + +import cn.qihangerp.model.request.ExternalPddGoodsDetailRequest; +import cn.qihangerp.model.vo.ExternalPddGoodsDetailResultVo; + +/** + * 拼多多商品详情({@code pdd.goods.detail.get}),与 upsert/add 编排解耦。 + */ +public interface ExternalPddGoodsDetailAppService { + + ExternalPddGoodsDetailResultVo fetchGoodsDetail(ExternalPddGoodsDetailRequest 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 ade225a9..3914bfe9 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 @@ -137,6 +137,7 @@ public class ExternalGoodsAppServiceImpl implements ExternalGoodsAppService { out.pddPublishSuccess(lane.getSuccess()); out.pddPublishMessage(lane.getMessage()); out.pddResponseSnippet(lane.getGoodsAddSnippet()); + out.pddGoodsId(lane.getPddGoodsId()); out.shopCredentialSource(lane.getShopCredentialSource()); out.pddCatRuleFetched(lane.getCatRuleFetched()); out.pddCatRuleSnippet(lane.getCatRuleSnippet()); diff --git a/service/src/main/java/cn/qihangerp/service/external/impl/ExternalPddGoodsDetailAppServiceImpl.java b/service/src/main/java/cn/qihangerp/service/external/impl/ExternalPddGoodsDetailAppServiceImpl.java new file mode 100644 index 00000000..a4be93cc --- /dev/null +++ b/service/src/main/java/cn/qihangerp/service/external/impl/ExternalPddGoodsDetailAppServiceImpl.java @@ -0,0 +1,99 @@ +package cn.qihangerp.service.external.impl; + +import cn.qihangerp.model.request.ExternalGoodsUpsertRequest; +import cn.qihangerp.model.request.ExternalPddGoodsDetailRequest; +import cn.qihangerp.model.vo.ExternalPddGoodsDetailResultVo; +import cn.qihangerp.service.external.ExternalPddGoodsDetailAppService; +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 ExternalPddGoodsDetailAppServiceImpl implements ExternalPddGoodsDetailAppService { + + private final ExternalPddProperties props; + private final PddPopClient pddPopClient; + + @Override + public ExternalPddGoodsDetailResultVo fetchGoodsDetail(ExternalPddGoodsDetailRequest req) { + if (req == null || req.getPddGoodsId() == null || req.getPddGoodsId() <= 0) { + return ExternalPddGoodsDetailResultVo.builder() + .popBizSuccess(false) + .message("pddGoodsId 无效") + .popResponseBody(null) + .build(); + } + PddShopCredential cred = resolveCredential(req); + if (cred == null || !StringUtils.hasText(cred.getAppKey()) || !StringUtils.hasText(cred.getAppSecret()) + || !StringUtils.hasText(cred.getAccessToken())) { + return ExternalPddGoodsDetailResultVo.builder() + .popBizSuccess(false) + .message("pddPopAuth 不完整:需 appKey、appSecret、accessToken") + .popResponseBody(null) + .build(); + } + String gateway = StringUtils.hasText(cred.getGatewayUrl()) ? cred.getGatewayUrl() : props.getGatewayUrl(); + if (!StringUtils.hasText(gateway)) { + return ExternalPddGoodsDetailResultVo.builder() + .popBizSuccess(false) + .message("未配置 POP gatewayUrl(请求体或 external.pdd.gateway-url)") + .popResponseBody(null) + .build(); + } + try { + String raw = pddPopClient.invokeTopLevelBiz( + gateway, + cred.getAppKey(), + cred.getAppSecret(), + cred.getAccessToken(), + "pdd.goods.detail.get", + PddOpenApiSupport.goodsDetailGetTopLevelParams(req.getPddGoodsId())); + boolean ok = !PddOpenApiSupport.isError(raw); + return ExternalPddGoodsDetailResultVo.builder() + .popBizSuccess(ok) + .message(ok ? "pdd.goods.detail.get 调用成功" : ("pdd.goods.detail.get 失败: " + PddOpenApiSupport.formatError(raw))) + .popResponseBody(raw) + .build(); + } catch (Exception e) { + log.warn("[PDD] pdd.goods.detail.get exception pddGoodsId={} err={}", req.getPddGoodsId(), e.getMessage(), e); + return ExternalPddGoodsDetailResultVo.builder() + .popBizSuccess(false) + .message(e.getMessage()) + .popResponseBody(null) + .build(); + } + } + + private static PddShopCredential resolveCredential(ExternalPddGoodsDetailRequest 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/ExternalPddPublishService.java b/service/src/main/java/cn/qihangerp/service/external/pdd/ExternalPddPublishService.java index a9ab06e1..de853685 100644 --- a/service/src/main/java/cn/qihangerp/service/external/pdd/ExternalPddPublishService.java +++ b/service/src/main/java/cn/qihangerp/service/external/pdd/ExternalPddPublishService.java @@ -172,9 +172,10 @@ public class ExternalPddPublishService { goodsAddRootJson); boolean ok = !PddOpenApiSupport.isError(raw); String errMsg = ok ? null : PddOpenApiSupport.formatError(raw); + Long pddGoodsId = ok ? PddOpenApiSupport.parseGoodsIdFromGoodsAddResponse(raw) : null; if (ok) { - log.info("[PDD] pdd.goods.add end shopId={} outGoodsId={} success=true msg={} snippet={}", - req.getShopId(), req.getOutGoodsId(), "pdd.goods.add 调用成功", + log.info("[PDD] pdd.goods.add end shopId={} outGoodsId={} success=true pddGoodsId={} msg={} snippet={}", + req.getShopId(), req.getOutGoodsId(), pddGoodsId, "pdd.goods.add 调用成功", PddOpenApiSupport.snippet(raw, 800)); } else { log.warn("[PDD] pdd.goods.add end shopId={} outGoodsId={} success=false err={} snippet={}", @@ -185,6 +186,7 @@ public class ExternalPddPublishService { .success(ok) .message(ok ? "pdd.goods.add 调用成功" : ("pdd.goods.add 失败: " + errMsg)) .goodsAddSnippet(PddOpenApiSupport.snippet(raw, 2000)) + .pddGoodsId(pddGoodsId) .shopCredentialSource(cred.getSource()) .catRuleFetched(catFetched) .catRuleSnippet(catRuleSnippet) 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 48c97928..68d9c561 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 @@ -39,6 +39,41 @@ public final class PddOpenApiSupport { return m; } + /** + * {@code pdd.goods.detail.get} 表单顶层参数(与 POP 文档一致:{@code goods_id})。 + */ + public static Map goodsDetailGetTopLevelParams(long goodsId) { + if (goodsId <= 0) { + throw new IllegalArgumentException("goods_id 必须为正数: " + goodsId); + } + Map m = new LinkedHashMap<>(); + m.put("goods_id", String.valueOf(goodsId)); + return m; + } + + /** + * 从 {@code pdd.goods.add} 原始响应 JSON 中解析 {@code goods_add_response.goods_id}(拼多多侧商品 ID)。 + */ + public static Long parseGoodsIdFromGoodsAddResponse(String raw) { + if (!StringUtils.hasText(raw)) { + return null; + } + try { + JSONObject root = JSON.parseObject(raw); + if (root == null) { + return null; + } + JSONObject add = root.getJSONObject("goods_add_response"); + if (add == null) { + return null; + } + Long id = add.getLong("goods_id"); + return id != null && id > 0 ? id : null; + } catch (Exception e) { + return null; + } + } + /** * {@code pdd.goods.spec.id.get} 的表单顶层业务参数(与 POP 常见调用方式一致)。 */ 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 72330ba3..84bcb1c9 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 @@ -208,6 +208,18 @@ public class PddPopClient { } return raw; } + if ("pdd.goods.detail.get".equals(type)) { + if (!httpOk || popBizError) { + String errSummary = popBizError ? PddOpenApiSupport.formatError(raw) : ""; + log.warn("PDD_POP api={} host={} clientId={} httpStatus={} durationMs={} popBizError={} errSummary={} paramPayloadSnippet={} fullResponseBody={}", + type, host, clientMasked, httpStatus, durationMs, popBizError, errSummary, + paramLogSnippet != null ? paramLogSnippet : "", raw); + } else { + log.info("PDD_POP api={} host={} clientId={} httpStatus={} durationMs={} fullResponseBody={}", + type, host, clientMasked, httpStatus, durationMs, raw); + } + return raw; + } String snippet = PddOpenApiSupport.snippet(raw, 600); if (!httpOk || popBizError) { String errSummary = popBizError ? PddOpenApiSupport.formatError(raw) : "";