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);