fix(pdd): POP 与官方一致——cat.rule/spec.id 顶层表单;goods.add 展平根 JSON 为顶层字段(invokeGoodsAdd)
Made-with: Cursor
This commit is contained in:
parent
8dce7135e0
commit
020747dbf1
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 + " : "
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue