fix(pdd): POP 与官方一致——cat.rule/spec.id 顶层表单;goods.add 展平根 JSON 为顶层字段(invokeGoodsAdd)

Made-with: Cursor
This commit is contained in:
huangyujie 2026-03-25 11:52:23 +08:00
parent 8dce7135e0
commit 020747dbf1
5 changed files with 158 additions and 35 deletions

View File

@ -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) {

View File

@ -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 + " : "

View File

@ -17,7 +17,8 @@ import java.util.List;
import java.util.Map;
/**
* Canonical 落库结果组装为 pdd.goods.add param_json
* Canonical 落库结果组装为 {@code pdd.goods.add} <strong>单根业务 JSON</strong>字段名为 POP 下划线形式
* {@link cn.qihangerp.service.external.pdd.PddPopClient#invokeGoodsAdd} 会将其展开为官方网关要求的<strong>表单顶层</strong>字段 POP curl / SDK 一致不使用 {@code param_json}
*
* @author guochengyu
*/

View File

@ -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}
* <p>开放平台约定 {@code cat_id} 新发品场景须传 {@code goods_id=0}缺省时部分网关会误报cat_id 不能为空</p>
* {@code pdd.goods.cat.rule.get} <strong>表单顶层</strong>业务参数与官方 POP curl / Java SDK 一致不走 {@code param_json}
* <p>新发品无拼多多 {@code goods_id} 时传 {@code "0"}</p>
*/
public static String catRuleGetParamJson(long catId) {
public static Map<String, String> catRuleGetTopLevelParams(long catId) {
if (catId <= 0) {
throw new IllegalArgumentException("cat_id 必须为正数: " + catId);
}
Map<String, Object> m = new LinkedHashMap<>();
m.put("cat_id", catId);
m.put("goods_id", 0L);
return JSON.toJSONString(m);
Map<String, String> 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<String, String> 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<String, String> 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 网关<strong>表单顶层</strong>键值与官方 {@code pdd.goods.add} curl 一致各字段与 {@code type}{@code client_id} 同级
* 数组/嵌套对象字段值为 JSON 字符串<b>不使用</b> {@code param_json}
*/
public static Map<String, String> 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<String, String> 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) {

View File

@ -17,8 +17,13 @@ import java.util.Map;
/**
* 拼多多 POP HTTP 调用application/x-www-form-urlencoded
* <p>每次请求无论成功失败均打日志 HTTP 状态耗时响应片段client_id 脱敏</p>
*
* @author guochengyu
* <p><b>入参形态与官方 POP curl / Java SDK 一致</b></p>
* <ul>
* <li>{@link #invokeGoodsAdd} {@code pdd.goods.add}业务字段全部在表单顶层{@code sku_list}{@code carousel_gallery} 等为 JSON 字符串
* <b></b> {@code param_json}<b></b> {@code version}</li>
* <li>{@link #invokeTopLevelBiz} {@code pdd.goods.cat.rule.get} {@code cat_id}/{@code goods_id} 等顶层字段</li>
* <li>{@link #invoke} 仅当接口要求整包 {@code param_json} 时使用少数场景</li>
* </ul>
*/
@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<String, String> biz = PddOpenApiSupport.flattenPopTopLevelFromRootJson(goodsAddRootJson);
String logPayload = PddOpenApiSupport.snippet(goodsAddRootJson, 400);
if (!StringUtils.hasText(gatewayUrl)) {
throw new IllegalArgumentException("gatewayUrl 不能为空");
}
Map<String, String> params = buildBaseParams(clientId, accessToken, "pdd.goods.add", false);
if (biz != null) {
for (Map.Entry<String, String> 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<String, String> 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}
* <p>不包含 {@code param_json}{@code version} 也不加入与官方示例 curl 一致避免签名校验差异</p>
*/
public String invokeTopLevelBiz(String gatewayUrl, String clientId, String clientSecret, String accessToken,
String type, Map<String, String> topLevelBizParams) throws Exception {
if (!StringUtils.hasText(gatewayUrl)) {
throw new IllegalArgumentException("gatewayUrl 不能为空");
}
Map<String, String> params = buildBaseParams(clientId, accessToken, type, false);
if (topLevelBizParams != null) {
for (Map.Entry<String, String> 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<String, String> buildBaseParams(String clientId, String accessToken, String type,
boolean withVersion) {
long ts = System.currentTimeMillis() / 1000L;
Map<String, String> 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<String, String> 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);