From 020747dbf19cba456f85c282b984e4043fdbf9eb Mon Sep 17 00:00:00 2001
From: huangyujie <27665451@qq.com>
Date: Wed, 25 Mar 2026 11:52:23 +0800
Subject: [PATCH] =?UTF-8?q?fix(pdd):=20POP=20=E4=B8=8E=E5=AE=98=E6=96=B9?=
=?UTF-8?q?=E4=B8=80=E8=87=B4=E2=80=94=E2=80=94cat.rule/spec.id=20?=
=?UTF-8?q?=E9=A1=B6=E5=B1=82=E8=A1=A8=E5=8D=95=EF=BC=9Bgoods.add=20?=
=?UTF-8?q?=E5=B1=95=E5=B9=B3=E6=A0=B9=20JSON=20=E4=B8=BA=E9=A1=B6?=
=?UTF-8?q?=E5=B1=82=E5=AD=97=E6=AE=B5=EF=BC=88invokeGoodsAdd=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Made-with: Cursor
---
.../pdd/ExternalPddPublishService.java | 10 +--
.../pdd/PddCatRuleSpecAutoResolver.java | 15 +---
.../external/pdd/PddGoodsAddParamBuilder.java | 3 +-
.../external/pdd/PddOpenApiSupport.java | 80 +++++++++++++++--
.../service/external/pdd/PddPopClient.java | 85 ++++++++++++++++---
5 files changed, 158 insertions(+), 35 deletions(-)
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 9ba341ec..fd3e2703 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
@@ -123,8 +123,8 @@ public class ExternalPddPublishService {
if (props.isAutoFetchCatRule() && !catFetched) {
try {
- String raw = pddPopClient.invoke(gateway, cred.getAppKey(), cred.getAppSecret(), cred.getAccessToken(),
- "pdd.goods.cat.rule.get", PddOpenApiSupport.catRuleGetParamJson(catId));
+ String raw = pddPopClient.invokeTopLevelBiz(gateway, cred.getAppKey(), cred.getAppSecret(), cred.getAccessToken(),
+ "pdd.goods.cat.rule.get", PddOpenApiSupport.catRuleGetTopLevelParams(catId));
catRuleSnippet = PddOpenApiSupport.snippet(raw, 2000);
catFetched = true;
} catch (Exception e) {
@@ -138,9 +138,9 @@ public class ExternalPddPublishService {
try {
log.info("[PDD] pdd.goods.add begin shopId={} outGoodsId={} catId={} skuCount={}",
req.getShopId(), req.getOutGoodsId(), catId, skuRows.size());
- String paramJson = paramBuilder.buildParamJson(goods, skus, req, props, effectiveOverrides);
- String raw = pddPopClient.invoke(gateway, cred.getAppKey(), cred.getAppSecret(), cred.getAccessToken(),
- "pdd.goods.add", paramJson);
+ String goodsAddRootJson = paramBuilder.buildParamJson(goods, skus, req, props, effectiveOverrides);
+ String raw = pddPopClient.invokeGoodsAdd(gateway, cred.getAppKey(), cred.getAppSecret(), cred.getAccessToken(),
+ goodsAddRootJson);
boolean ok = !PddOpenApiSupport.isError(raw);
String errMsg = ok ? null : PddOpenApiSupport.formatError(raw);
if (ok) {
diff --git a/service/src/main/java/cn/qihangerp/service/external/pdd/PddCatRuleSpecAutoResolver.java b/service/src/main/java/cn/qihangerp/service/external/pdd/PddCatRuleSpecAutoResolver.java
index d5d4a44a..2c6553bd 100644
--- a/service/src/main/java/cn/qihangerp/service/external/pdd/PddCatRuleSpecAutoResolver.java
+++ b/service/src/main/java/cn/qihangerp/service/external/pdd/PddCatRuleSpecAutoResolver.java
@@ -3,8 +3,6 @@ package cn.qihangerp.service.external.pdd;
import cn.qihangerp.model.entity.OGoodsSku;
import cn.qihangerp.model.request.ExternalGoodsUpsertRequest;
import cn.qihangerp.service.external.shop.PddShopCredential;
-import com.alibaba.fastjson2.JSON;
-import com.alibaba.fastjson2.JSONObject;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.springframework.stereotype.Service;
@@ -75,13 +73,13 @@ public class PddCatRuleSpecAutoResolver {
if (catId <= 0) {
throw new IllegalArgumentException("cat_id 必须为正数,当前=" + catId);
}
- return popClient.invoke(
+ return popClient.invokeTopLevelBiz(
gatewayUrl,
cred.getAppKey(),
cred.getAppSecret(),
cred.getAccessToken(),
"pdd.goods.cat.rule.get",
- PddOpenApiSupport.catRuleGetParamJson(catId)
+ PddOpenApiSupport.catRuleGetTopLevelParams(catId)
);
}
@@ -102,18 +100,13 @@ public class PddCatRuleSpecAutoResolver {
if (!StringUtils.hasText(specName)) {
throw new IllegalStateException("无法解析 SKU 规格名:outSkuId=" + row.getOuterErpSkuId());
}
- JSONObject p = new JSONObject();
- p.put("cat_id", catId);
- p.put("parent_spec_id", parentSpecId);
- p.put("spec_name", specName.trim());
-
- String body = popClient.invoke(
+ String body = popClient.invokeTopLevelBiz(
gatewayUrl,
cred.getAppKey(),
cred.getAppSecret(),
cred.getAccessToken(),
"pdd.goods.spec.id.get",
- JSON.toJSONString(p)
+ PddOpenApiSupport.specIdGetTopLevelParams(catId, parentSpecId, specName.trim())
);
if (PddOpenApiSupport.isError(body)) {
throw new IllegalStateException("pdd.goods.spec.id.get 失败 spec_name=" + specName + " : "
diff --git a/service/src/main/java/cn/qihangerp/service/external/pdd/PddGoodsAddParamBuilder.java b/service/src/main/java/cn/qihangerp/service/external/pdd/PddGoodsAddParamBuilder.java
index 5502351c..80f7da78 100644
--- a/service/src/main/java/cn/qihangerp/service/external/pdd/PddGoodsAddParamBuilder.java
+++ b/service/src/main/java/cn/qihangerp/service/external/pdd/PddGoodsAddParamBuilder.java
@@ -17,7 +17,8 @@ import java.util.List;
import java.util.Map;
/**
- * 将 Canonical 落库结果组装为 pdd.goods.add 的 param_json。
+ * 将 Canonical 落库结果组装为 {@code pdd.goods.add} 的单根业务 JSON(字段名为 POP 下划线形式);
+ * {@link cn.qihangerp.service.external.pdd.PddPopClient#invokeGoodsAdd} 会将其展开为官方网关要求的表单顶层字段(与 POP curl / SDK 一致,不使用 {@code param_json})。
*
* @author guochengyu
*/
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 aa21b138..a7d5080d 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
@@ -5,6 +5,8 @@ import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import org.springframework.util.StringUtils;
+import java.math.BigDecimal;
+import java.math.BigInteger;
import java.util.LinkedHashMap;
import java.util.Map;
@@ -19,17 +21,81 @@ public final class PddOpenApiSupport {
}
/**
- * {@code pdd.goods.cat.rule.get} 的 {@code param_json}。
- *
开放平台约定:除 {@code cat_id} 外,新发品场景须传 {@code goods_id=0};缺省时部分网关会误报「cat_id 不能为空」。
+ * {@code pdd.goods.cat.rule.get} 的表单顶层业务参数(与官方 POP curl / Java SDK 一致,不走 {@code param_json})。
+ * 新发品无拼多多 {@code goods_id} 时传 {@code "0"}。
*/
- public static String catRuleGetParamJson(long catId) {
+ public static Map catRuleGetTopLevelParams(long catId) {
if (catId <= 0) {
throw new IllegalArgumentException("cat_id 必须为正数: " + catId);
}
- Map m = new LinkedHashMap<>();
- m.put("cat_id", catId);
- m.put("goods_id", 0L);
- return JSON.toJSONString(m);
+ Map m = new LinkedHashMap<>();
+ m.put("cat_id", String.valueOf(catId));
+ m.put("goods_id", "0");
+ return m;
+ }
+
+ /**
+ * {@code pdd.goods.spec.id.get} 的表单顶层业务参数(与 POP 常见调用方式一致)。
+ */
+ public static Map specIdGetTopLevelParams(long catId, long parentSpecId, String specName) {
+ if (catId <= 0 || parentSpecId <= 0 || !StringUtils.hasText(specName)) {
+ throw new IllegalArgumentException("cat_id、parent_spec_id、spec_name 无效");
+ }
+ Map m = new LinkedHashMap<>();
+ m.put("cat_id", String.valueOf(catId));
+ m.put("parent_spec_id", String.valueOf(parentSpecId));
+ m.put("spec_name", specName.trim());
+ return m;
+ }
+
+ /**
+ * 将单根业务 JSON(如 {@link cn.qihangerp.service.external.pdd.PddGoodsAddParamBuilder#buildParamJson} 的输出)
+ * 展开为 POP 网关表单顶层键值,与官方 {@code pdd.goods.add} curl 一致(各字段与 {@code type}、{@code client_id} 同级,
+ * 数组/嵌套对象字段值为 JSON 字符串,不使用 {@code param_json})。
+ */
+ public static Map flattenPopTopLevelFromRootJson(String rootJson) {
+ if (!StringUtils.hasText(rootJson)) {
+ throw new IllegalArgumentException("rootJson 不能为空");
+ }
+ JSONObject root = JSON.parseObject(rootJson);
+ if (root == null || root.isEmpty()) {
+ throw new IllegalArgumentException("rootJson 解析为空对象");
+ }
+ Map out = new LinkedHashMap<>();
+ for (String key : root.keySet()) {
+ Object v = root.get(key);
+ if (v == null) {
+ continue;
+ }
+ String s = toPddTopLevelFormValue(v);
+ if (s == null) {
+ continue;
+ }
+ out.put(key, s);
+ }
+ return out;
+ }
+
+ private static String toPddTopLevelFormValue(Object v) {
+ if (v instanceof String s) {
+ return s;
+ }
+ if (v instanceof Boolean b) {
+ return b ? "true" : "false";
+ }
+ if (v instanceof Number n) {
+ if (v instanceof Double || v instanceof Float) {
+ return Double.toString(n.doubleValue());
+ }
+ if (v instanceof BigDecimal bd) {
+ return bd.stripTrailingZeros().toPlainString();
+ }
+ if (v instanceof BigInteger bi) {
+ return bi.toString();
+ }
+ return Long.toString(n.longValue());
+ }
+ return JSON.toJSONString(v);
}
public static String snippet(String s, int max) {
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 50be9841..b34c46b7 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
@@ -17,8 +17,13 @@ import java.util.Map;
/**
* 拼多多 POP HTTP 调用(application/x-www-form-urlencoded)。
* 每次请求无论成功失败均打日志(含 HTTP 状态、耗时、响应片段;client_id 脱敏)。
- *
- * @author guochengyu
+ * 入参形态(与官方 POP curl / Java SDK 一致):
+ *
+ * - {@link #invokeGoodsAdd} — {@code pdd.goods.add}:业务字段全部在表单顶层({@code sku_list}、{@code carousel_gallery} 等为 JSON 字符串),
+ * 无 {@code param_json}、无 {@code version}
+ * - {@link #invokeTopLevelBiz} — 如 {@code pdd.goods.cat.rule.get} 的 {@code cat_id}/{@code goods_id} 等顶层字段
+ * - {@link #invoke} — 仅当接口要求整包 {@code param_json} 时使用(少数场景)
+ *
*/
@Slf4j
@Component
@@ -29,27 +34,85 @@ public class PddPopClient {
.build();
/**
- * @param type 如 pdd.goods.add
- * @param paramJson 业务参数 JSON 字符串(将作为 param_json 传递)
+ * {@code pdd.goods.add}:将 {@link cn.qihangerp.service.external.pdd.PddGoodsAddParamBuilder} 生成的根 JSON 展开为顶层表单后调用网关。
+ */
+ public String invokeGoodsAdd(String gatewayUrl, String clientId, String clientSecret, String accessToken,
+ String goodsAddRootJson) throws Exception {
+ Map biz = PddOpenApiSupport.flattenPopTopLevelFromRootJson(goodsAddRootJson);
+ String logPayload = PddOpenApiSupport.snippet(goodsAddRootJson, 400);
+ if (!StringUtils.hasText(gatewayUrl)) {
+ throw new IllegalArgumentException("gatewayUrl 不能为空");
+ }
+ Map params = buildBaseParams(clientId, accessToken, "pdd.goods.add", false);
+ if (biz != null) {
+ for (Map.Entry e : biz.entrySet()) {
+ if (e.getKey() == null || e.getValue() == null) {
+ continue;
+ }
+ params.put(e.getKey(), e.getValue());
+ }
+ }
+ return postSignedAndLog(gatewayUrl, clientId, clientSecret, "pdd.goods.add", params, logPayload);
+ }
+
+ /**
+ * 业务参数整包放入 {@code param_json}(并带 {@code version=V1})。仅适用于仍要求该形态的接口。
*/
public String invoke(String gatewayUrl, String clientId, String clientSecret, String accessToken,
String type, String paramJson) throws Exception {
if (!StringUtils.hasText(gatewayUrl)) {
throw new IllegalArgumentException("gatewayUrl 不能为空");
}
+ Map params = buildBaseParams(clientId, accessToken, type, true);
+ if (StringUtils.hasText(paramJson)) {
+ params.put("param_json", paramJson);
+ }
+ String logPayload = StringUtils.hasText(paramJson) ? PddOpenApiSupport.snippet(paramJson, 400) : "";
+ return postSignedAndLog(gatewayUrl, clientId, clientSecret, type, params, logPayload);
+ }
+
+ /**
+ * 业务参数全部在表单顶层(与官方 {@code curl} / Java SDK 一致),例如
+ * {@code pdd.goods.cat.rule.get} 的 {@code cat_id}、{@code goods_id}。
+ * 不包含 {@code param_json};{@code version} 也不加入(与官方示例 curl 一致,避免签名校验差异)。
+ */
+ public String invokeTopLevelBiz(String gatewayUrl, String clientId, String clientSecret, String accessToken,
+ String type, Map topLevelBizParams) throws Exception {
+ if (!StringUtils.hasText(gatewayUrl)) {
+ throw new IllegalArgumentException("gatewayUrl 不能为空");
+ }
+ Map params = buildBaseParams(clientId, accessToken, type, false);
+ if (topLevelBizParams != null) {
+ for (Map.Entry e : topLevelBizParams.entrySet()) {
+ if (e.getKey() == null || e.getValue() == null) {
+ continue;
+ }
+ params.put(e.getKey(), e.getValue());
+ }
+ }
+ String logPayload = topLevelBizParams == null ? "" : PddOpenApiSupport.snippet(topLevelBizParams.toString(), 400);
+ return postSignedAndLog(gatewayUrl, clientId, clientSecret, type, params, logPayload);
+ }
+
+ private static Map buildBaseParams(String clientId, String accessToken, String type,
+ boolean withVersion) {
long ts = System.currentTimeMillis() / 1000L;
Map params = new HashMap<>();
params.put("type", type);
params.put("client_id", clientId);
params.put("timestamp", String.valueOf(ts));
params.put("data_type", "JSON");
- params.put("version", "V1");
+ if (withVersion) {
+ params.put("version", "V1");
+ }
if (StringUtils.hasText(accessToken)) {
params.put("access_token", accessToken);
}
- if (StringUtils.hasText(paramJson)) {
- params.put("param_json", paramJson);
- }
+ return params;
+ }
+
+ private String postSignedAndLog(String gatewayUrl, String clientId, String clientSecret, String type,
+ Map params, String paramLogSnippet) throws Exception {
String sign = PddPopSignUtil.sign(params, clientSecret);
params.put("sign", sign);
@@ -73,9 +136,9 @@ public class PddPopClient {
String snippet = PddOpenApiSupport.snippet(raw, 600);
if (!httpOk || popBizError) {
String errSummary = popBizError ? PddOpenApiSupport.formatError(raw) : "";
- String paramSnippet = StringUtils.hasText(paramJson) ? PddOpenApiSupport.snippet(paramJson, 400) : "";
- log.warn("PDD_POP api={} host={} clientId={} httpStatus={} durationMs={} popBizError={} errSummary={} paramJsonSnippet={} bodySnippet={}",
- type, host, clientMasked, httpStatus, durationMs, popBizError, errSummary, paramSnippet, snippet);
+ 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={} bodySnippet={}",
type, host, clientMasked, httpStatus, durationMs, snippet);