From 4eac7824fea19203492bb81bfa8dfdde3913c43a Mon Sep 17 00:00:00 2001
From: huangyujie <27665451@qq.com>
Date: Fri, 27 Mar 2026 11:19:33 +0800
Subject: [PATCH] =?UTF-8?q?feat(pdd):=20=E4=BA=8C=E6=AC=A1=E4=B8=8A?=
=?UTF-8?q?=E6=9E=B6=E8=B5=B0=20goods.information.update=EF=BC=8C=E4=B8=8B?=
=?UTF-8?q?=E6=9E=B6=E8=B5=B0=20sale.status.set?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 请求带 pddGoodsId>0 时使用顶层表单调用 pdd.goods.information.update(与 goods.add 同形态)
- 移除 information-update-reshelf-enabled 配置与开关
- 业务成功以 goods_update_response.is_success 为准
- 扩展 ExternalGoodsUpsert/Delist 请求与发布/下架链路
Made-with: Cursor
---
.../controller/ExternalGoodsController.java | 11 +-
.../src/main/resources/application.yml | 2 +
.../request/ExternalGoodsDelistRequest.java | 11 +-
.../request/ExternalGoodsUpsertRequest.java | 9 ++
.../model/vo/ExternalGoodsUpsertResultVo.java | 3 +
.../model/vo/PddPublishLaneResultVo.java | 5 +
.../impl/ExternalGoodsAppServiceImpl.java | 13 +-
.../external/pdd/ExternalPddProperties.java | 5 +
.../pdd/ExternalPddPublishService.java | 143 ++++++++++++++++--
.../external/pdd/PddGoodsAddParamBuilder.java | 17 +++
.../external/pdd/PddOpenApiSupport.java | 46 ++++++
.../service/external/pdd/PddPopClient.java | 26 +++-
12 files changed, 271 insertions(+), 20 deletions(-)
diff --git a/api/erp-api/src/main/java/cn/qihangerp/erp/controller/ExternalGoodsController.java b/api/erp-api/src/main/java/cn/qihangerp/erp/controller/ExternalGoodsController.java
index 5028c835..b9a4daad 100644
--- a/api/erp-api/src/main/java/cn/qihangerp/erp/controller/ExternalGoodsController.java
+++ b/api/erp-api/src/main/java/cn/qihangerp/erp/controller/ExternalGoodsController.java
@@ -26,7 +26,7 @@ import java.util.List;
*
不查询 {@code o_shop}:{@code shopId} 仅作业务侧店铺维度标识写入 {@code o_goods}/{@code o_goods_sku}。
*
* - {@code POST /external/goods/upsert} — 商品上架/同步(新建默认 {@code o_goods.status=1})
- * - {@code POST /external/goods/delist} — 本地下架({@code status=2}),不调各平台下架 API
+ * - {@code POST /external/goods/delist} — 本地下架({@code status=2});可选 {@code pddGoodsId}+{@code pddPopAuth} 调拼多多 {@code pdd.goods.sale.status.set}({@code is_onsale=0})
*
*
* @author guochengyu
@@ -114,7 +114,7 @@ public class ExternalGoodsController extends BaseController {
}
/**
- * 本地下架:将 {@code o_goods.status} 置为 2(已下架)。不调用拼多多等平台的下架接口。
+ * 本地下架:将 {@code o_goods.status} 置为 2;若传 {@code pddGoodsId} 则同步拼多多在售状态(见 {@code external.pdd.sale-status-set-enabled})。
*/
@PostMapping("/delist")
public AjaxResult delist(@RequestBody ExternalGoodsDelistRequest req) {
@@ -127,6 +127,13 @@ public class ExternalGoodsController extends BaseController {
if (!StringUtils.hasText(req.getOutGoodsId())) {
return AjaxResult.error("参数错误:outGoodsId不能为空");
}
+ if (req.getPddGoodsId() != null && req.getPddGoodsId() > 0 && externalPddProperties.isSaleStatusSetEnabled()) {
+ var auth = req.getPddPopAuth();
+ if (auth == null || !StringUtils.hasText(auth.getAppKey()) || !StringUtils.hasText(auth.getAppSecret())
+ || !StringUtils.hasText(auth.getAccessToken())) {
+ return AjaxResult.error("参数错误:传 pddGoodsId 下架拼多多时,pddPopAuth 需提供 appKey、appSecret、accessToken");
+ }
+ }
try {
externalGoodsAppService.delistGoods(req);
return AjaxResult.success();
diff --git a/api/erp-api/src/main/resources/application.yml b/api/erp-api/src/main/resources/application.yml
index edd0df87..2487ca96 100644
--- a/api/erp-api/src/main/resources/application.yml
+++ b/api/erp-api/src/main/resources/application.yml
@@ -38,6 +38,8 @@ external:
# 拼多多 POP:upsert 落库后是否调用 pdd.goods.add(须与 maindata yundt.maindata.erp-open.default-category-code 等对齐)
pdd:
publish-enabled: true
+ # 下架:请求体带 pddGoodsId+pddPopAuth 时走 pdd.goods.sale.status.set(is_onsale=0)
+ sale-status-set-enabled: true
gateway-url: https://gw-api.pinduoduo.com/api/router
# 发布前可选拉类目规则(诊断用)
auto-fetch-cat-rule: false
diff --git a/model/src/main/java/cn/qihangerp/model/request/ExternalGoodsDelistRequest.java b/model/src/main/java/cn/qihangerp/model/request/ExternalGoodsDelistRequest.java
index 385db40c..a71a9e4b 100644
--- a/model/src/main/java/cn/qihangerp/model/request/ExternalGoodsDelistRequest.java
+++ b/model/src/main/java/cn/qihangerp/model/request/ExternalGoodsDelistRequest.java
@@ -3,7 +3,7 @@ package cn.qihangerp.model.request;
import lombok.Data;
/**
- * 外部系统商品下架(本地 {@code o_goods.status=2}),不调用各平台开放 API。
+ * 外部系统商品下架:本地 {@code o_goods.status=2};可选同步拼多多 {@code pdd.goods.sale.status.set}({@code is_onsale=0})。
*
* @author guochengyu
*/
@@ -15,4 +15,13 @@ public class ExternalGoodsDelistRequest {
/** 外部商品 ID(幂等键),与 upsert 的 outGoodsId 一致 */
private String outGoodsId;
+
+ /**
+ * 拼多多 {@code goods_id}。与 {@link #pddPopAuth} 同时传入且开启 {@code external.pdd.sale-status-set-enabled} 时,
+ * 先调 {@code pdd.goods.sale.status.set} 再落库下架。
+ */
+ private Long pddGoodsId;
+
+ /** 拼多多 POP 凭证,与 upsert 的 {@code pddPopAuth} 结构一致 */
+ private ExternalGoodsUpsertRequest.PddPopAuth pddPopAuth;
}
diff --git a/model/src/main/java/cn/qihangerp/model/request/ExternalGoodsUpsertRequest.java b/model/src/main/java/cn/qihangerp/model/request/ExternalGoodsUpsertRequest.java
index 94011f57..b57de55d 100644
--- a/model/src/main/java/cn/qihangerp/model/request/ExternalGoodsUpsertRequest.java
+++ b/model/src/main/java/cn/qihangerp/model/request/ExternalGoodsUpsertRequest.java
@@ -32,6 +32,15 @@ public class ExternalGoodsUpsertRequest {
*/
private String outGoodsId;
+ /**
+ * 拼多多侧商品 ID({@code goods_id})。调用方在「本店该商品货架已上架且 extension 已存 pddGoodsId」时传入,
+ * 服务端将走 {@code pdd.goods.information.update}(与 {@code pdd.goods.add} 相同的顶层表单字段形态),而不再 {@code pdd.goods.add}
+ * (需 {@code publish-enabled=true})。
+ */
+ @JsonProperty("pddGoodsId")
+ @JsonAlias({"pdd_goods_id"})
+ private Long pddGoodsId;
+
/**
* 商品标题
*/
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 379e7ae0..670c9d51 100644
--- a/model/src/main/java/cn/qihangerp/model/vo/ExternalGoodsUpsertResultVo.java
+++ b/model/src/main/java/cn/qihangerp/model/vo/ExternalGoodsUpsertResultVo.java
@@ -37,6 +37,9 @@ public class ExternalGoodsUpsertResultVo implements Serializable {
*/
private Long pddGoodsId;
+ /** {@code GOODS_ADD} / {@code SALE_STATUS_SET}(与拼多多发布链路一致) */
+ private String pddPublishLane;
+
// ---- 店铺凭证与自动拉取类目/spec(仅 platform=PDD 时有意义) ----
/** REQUEST(请求体 pddPopAuth)/ NONE;与 {@code o_shop} 无关 */
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 e55ed0aa..664b7bd8 100644
--- a/model/src/main/java/cn/qihangerp/model/vo/PddPublishLaneResultVo.java
+++ b/model/src/main/java/cn/qihangerp/model/vo/PddPublishLaneResultVo.java
@@ -26,6 +26,11 @@ public class PddPublishLaneResultVo implements Serializable {
/** 拼多多 {@code goods_add_response.goods_id};仅 add 成功且能解析时有值 */
private Long pddGoodsId;
+ /**
+ * {@code GOODS_ADD}:{@code pdd.goods.add};{@code SALE_STATUS_SET}:{@code pdd.goods.sale.status.set}(在售状态)。
+ */
+ private String publishLane;
+
/** REQUEST / NONE */
private String shopCredentialSource;
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 3914bfe9..c084f0c6 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
@@ -108,7 +108,8 @@ public class ExternalGoodsAppServiceImpl implements ExternalGoodsAppService {
.pddCatRuleFetched(null)
.pddCatRuleSnippet(null)
.pddSpecAutoResolved(null)
- .pddAutoResolveDetail(null);
+ .pddAutoResolveDetail(null)
+ .pddPublishLane(null);
if ("PDD".equalsIgnoreCase(req.getPlatform())) {
OGoods g = goodsService.getById(goodsId);
@@ -143,16 +144,18 @@ public class ExternalGoodsAppServiceImpl implements ExternalGoodsAppService {
out.pddCatRuleSnippet(lane.getCatRuleSnippet());
out.pddSpecAutoResolved(lane.getSpecAutoResolved());
out.pddAutoResolveDetail(lane.getAutoResolveDetail());
+ out.pddPublishLane(lane.getPublishLane());
}
ExternalGoodsUpsertResultVo vo = out.build();
if ("PDD".equalsIgnoreCase(req.getPlatform())) {
- log.info("[external/upsert] PDD summary shopId={} outGoodsId={} erpGoodsId={} attempted={} success={} credentialSource={} message={} goodsAddSnippet={} catRuleFetched={} catRuleSnippet={} specAutoResolved={} autoResolveDetail={}",
+ log.info("[external/upsert] PDD summary shopId={} outGoodsId={} erpGoodsId={} attempted={} success={} lane={} credentialSource={} message={} goodsAddSnippet={} catRuleFetched={} catRuleSnippet={} specAutoResolved={} autoResolveDetail={}",
req.getShopId(),
req.getOutGoodsId(),
vo.getGoodsId(),
vo.getPddPublishAttempted(),
vo.getPddPublishSuccess(),
+ vo.getPddPublishLane(),
vo.getShopCredentialSource(),
truncateForLog(vo.getPddPublishMessage(), 600),
truncateForLog(vo.getPddResponseSnippet(), 500),
@@ -188,6 +191,12 @@ public class ExternalGoodsAppServiceImpl implements ExternalGoodsAppService {
if (existing == null) {
throw new IllegalArgumentException("商品不存在:shopId=" + req.getShopId() + ", outGoodsId=" + req.getOutGoodsId());
}
+ PddPublishLaneResultVo popDelist = externalPddPublishService.pddSaleStatusSetForDelist(req);
+ if (Boolean.TRUE.equals(popDelist.getAttempted()) && !Boolean.TRUE.equals(popDelist.getSuccess())) {
+ throw new IllegalArgumentException(StringUtils.hasText(popDelist.getMessage())
+ ? popDelist.getMessage()
+ : "拼多多下架在售状态失败");
+ }
// 1 销售中 2 已下架(与 o_goods.status 注释一致)
existing.setStatus(2);
existing.setUpdateBy("external");
diff --git a/service/src/main/java/cn/qihangerp/service/external/pdd/ExternalPddProperties.java b/service/src/main/java/cn/qihangerp/service/external/pdd/ExternalPddProperties.java
index 68cb3e24..c8c46293 100644
--- a/service/src/main/java/cn/qihangerp/service/external/pdd/ExternalPddProperties.java
+++ b/service/src/main/java/cn/qihangerp/service/external/pdd/ExternalPddProperties.java
@@ -22,6 +22,11 @@ public class ExternalPddProperties {
*/
private boolean publishEnabled = false;
+ /**
+ * 是否允许调用 {@code pdd.goods.sale.status.set}(下架时 {@code is_onsale=0})。
+ */
+ private boolean saleStatusSetEnabled = true;
+
/**
* 发品前是否将外链图片经 POP 图片上传接口上传到拼多多图床,并用返回 URL 替换
* {@code carousel_gallery}/{@code detail_gallery}/{@code sku_list[].thumb_url}(去重上传)。
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 de853685..d33cbdbb 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
@@ -2,6 +2,7 @@ package cn.qihangerp.service.external.pdd;
import cn.qihangerp.model.entity.OGoods;
import cn.qihangerp.model.entity.OGoodsSku;
+import cn.qihangerp.model.request.ExternalGoodsDelistRequest;
import cn.qihangerp.model.request.ExternalGoodsUpsertRequest;
import cn.qihangerp.model.vo.PddPublishLaneResultVo;
import cn.qihangerp.service.external.shop.PddShopCredential;
@@ -11,7 +12,9 @@ import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
+import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Map;
/**
* 拼多多发布:POP 凭证仅来自本次请求的 {@link ExternalGoodsUpsertRequest#getPddPopAuth()};不依赖 {@code o_shop}。
@@ -161,30 +164,63 @@ public class ExternalPddPublishService {
cred, gateway, catId, resolveResult, autoFetchedCatRuleRaw, props, req);
try {
- log.info("[PDD] pdd.goods.add begin shopId={} outGoodsId={} catId={} skuCount={} noSpecBook={}",
- req.getShopId(), req.getOutGoodsId(), catId, skuRows.size(), noSpecBookPublish);
String goodsAddRootJson = noSpecBookPublish
? paramBuilder.buildParamJsonNoSpecBook(goods, skus, req, props, catRuleRawForGoodsProps)
: paramBuilder.buildParamJson(goods, skus, req, props, effectiveOverrides, catRuleRawForGoodsProps);
goodsAddRootJson = pddGoodsImageRehostService.rehostExternalImagesInGoodsAddJson(
goodsAddRootJson, gateway, cred.getAppKey(), cred.getAppSecret(), cred.getAccessToken());
- String raw = pddPopClient.invokeGoodsAdd(gateway, cred.getAppKey(), cred.getAppSecret(), cred.getAccessToken(),
- goodsAddRootJson);
- boolean ok = !PddOpenApiSupport.isError(raw);
- String errMsg = ok ? null : PddOpenApiSupport.formatError(raw);
- Long pddGoodsId = ok ? PddOpenApiSupport.parseGoodsIdFromGoodsAddResponse(raw) : null;
+ boolean useInfoUpdate = req.getPddGoodsId() != null && req.getPddGoodsId() > 0;
+ String raw;
+ String lane;
+ String okMsg;
+ String failPrefix;
+ if (useInfoUpdate) {
+ log.info("[PDD] pdd.goods.information.update begin shopId={} outGoodsId={} pddGoodsId={} catId={} skuCount={} noSpecBook={}",
+ req.getShopId(), req.getOutGoodsId(), req.getPddGoodsId(), catId, skuRows.size(), noSpecBookPublish);
+ String updateJson = paramBuilder.toInformationUpdateParamJson(goodsAddRootJson, req.getPddGoodsId());
+ raw = pddPopClient.invokeGoodsInformationUpdate(gateway, cred.getAppKey(), cred.getAppSecret(),
+ cred.getAccessToken(), updateJson);
+ lane = "INFORMATION_UPDATE";
+ okMsg = "pdd.goods.information.update 成功";
+ failPrefix = "pdd.goods.information.update 失败";
+ } else {
+ log.info("[PDD] pdd.goods.add begin shopId={} outGoodsId={} catId={} skuCount={} noSpecBook={}",
+ req.getShopId(), req.getOutGoodsId(), catId, skuRows.size(), noSpecBookPublish);
+ raw = pddPopClient.invokeGoodsAdd(gateway, cred.getAppKey(), cred.getAppSecret(), cred.getAccessToken(),
+ goodsAddRootJson);
+ lane = "GOODS_ADD";
+ okMsg = "pdd.goods.add 调用成功";
+ failPrefix = "pdd.goods.add 失败";
+ }
+ boolean ok;
+ String errMsg;
+ if (useInfoUpdate) {
+ ok = PddOpenApiSupport.isInformationUpdateBizOk(raw);
+ errMsg = ok ? null
+ : (PddOpenApiSupport.isError(raw)
+ ? PddOpenApiSupport.formatError(raw)
+ : PddOpenApiSupport.summarizeInformationUpdateFailure(raw));
+ } else {
+ ok = !PddOpenApiSupport.isError(raw);
+ errMsg = ok ? null : PddOpenApiSupport.formatError(raw);
+ }
+ Long pddGoodsId = ok
+ ? (useInfoUpdate ? req.getPddGoodsId() : PddOpenApiSupport.parseGoodsIdFromGoodsAddResponse(raw))
+ : null;
if (ok) {
- log.info("[PDD] pdd.goods.add end shopId={} outGoodsId={} success=true pddGoodsId={} msg={} snippet={}",
- req.getShopId(), req.getOutGoodsId(), pddGoodsId, "pdd.goods.add 调用成功",
+ log.info("[PDD] {} end shopId={} outGoodsId={} success=true pddGoodsId={} snippet={}",
+ useInfoUpdate ? "information.update" : "pdd.goods.add",
+ req.getShopId(), req.getOutGoodsId(), pddGoodsId,
PddOpenApiSupport.snippet(raw, 800));
} else {
- log.warn("[PDD] pdd.goods.add end shopId={} outGoodsId={} success=false err={} snippet={}",
+ log.warn("[PDD] {} end shopId={} outGoodsId={} success=false err={} snippet={}",
+ useInfoUpdate ? "information.update" : "pdd.goods.add",
req.getShopId(), req.getOutGoodsId(), errMsg, PddOpenApiSupport.snippet(raw, 800));
}
return PddPublishLaneResultVo.builder()
.attempted(true)
.success(ok)
- .message(ok ? "pdd.goods.add 调用成功" : ("pdd.goods.add 失败: " + errMsg))
+ .message(ok ? okMsg : (failPrefix + ": " + errMsg))
.goodsAddSnippet(PddOpenApiSupport.snippet(raw, 2000))
.pddGoodsId(pddGoodsId)
.shopCredentialSource(cred.getSource())
@@ -192,6 +228,7 @@ public class ExternalPddPublishService {
.catRuleSnippet(catRuleSnippet)
.specAutoResolved(specAuto)
.autoResolveDetail(autoDetail)
+ .publishLane(lane)
.build();
} catch (Exception e) {
log.warn("拼多多发布异常 shopId={} outGoodsId={} err={}", req.getShopId(), req.getOutGoodsId(), e.getMessage(), e);
@@ -204,6 +241,82 @@ public class ExternalPddPublishService {
.catRuleSnippet(catRuleSnippet)
.specAutoResolved(specAuto)
.autoResolveDetail(autoDetail)
+ .publishLane("GOODS_ADD")
+ .build();
+ }
+ }
+
+ /**
+ * 下架:{@code pdd.goods.sale.status.set},{@code is_onsale=0}(与开放平台表单顶层参数一致)。
+ */
+ public PddPublishLaneResultVo pddSaleStatusSetForDelist(ExternalGoodsDelistRequest req) {
+ if (req == null || req.getPddGoodsId() == null || req.getPddGoodsId() <= 0) {
+ return PddPublishLaneResultVo.builder()
+ .attempted(false)
+ .success(null)
+ .message("未传 pddGoodsId,跳过拼多多下架在售状态")
+ .build();
+ }
+ if (!props.isSaleStatusSetEnabled()) {
+ return PddPublishLaneResultVo.builder()
+ .attempted(false)
+ .success(null)
+ .message("external.pdd.sale-status-set-enabled=false,未调用 pdd.goods.sale.status.set")
+ .build();
+ }
+ PddShopCredential cred = credentialFromUpsertAuth(req.getPddPopAuth());
+ if (cred == null || !StringUtils.hasText(cred.getAppKey()) || !StringUtils.hasText(cred.getAppSecret())
+ || !StringUtils.hasText(cred.getAccessToken())) {
+ return PddPublishLaneResultVo.builder()
+ .attempted(false)
+ .success(false)
+ .message("下架同步拼多多需提供 pddPopAuth(appKey、appSecret、accessToken)")
+ .shopCredentialSource("NONE")
+ .build();
+ }
+ String gateway = StringUtils.hasText(cred.getGatewayUrl()) ? cred.getGatewayUrl() : props.getGatewayUrl();
+ return invokePddGoodsSaleStatusSet(req.getShopId(), req.getOutGoodsId(), cred, gateway, req.getPddGoodsId(), 0);
+ }
+
+ private PddPublishLaneResultVo invokePddGoodsSaleStatusSet(Long shopId, String outGoodsId, PddShopCredential cred,
+ String gateway, long goodsId, int isOnsale) {
+ try {
+ log.info("[PDD] pdd.goods.sale.status.set begin shopId={} outGoodsId={} goodsId={} is_onsale={}",
+ shopId, outGoodsId, goodsId, isOnsale);
+ Map biz = new LinkedHashMap<>(2);
+ biz.put("goods_id", String.valueOf(goodsId));
+ biz.put("is_onsale", String.valueOf(isOnsale));
+ String raw = pddPopClient.invokeTopLevelBiz(gateway, cred.getAppKey(), cred.getAppSecret(), cred.getAccessToken(),
+ "pdd.goods.sale.status.set", biz);
+ boolean ok = !PddOpenApiSupport.isError(raw);
+ String errMsg = ok ? null : PddOpenApiSupport.formatError(raw);
+ if (ok) {
+ log.info("[PDD] pdd.goods.sale.status.set end shopId={} outGoodsId={} goodsId={} is_onsale={} snippet={}",
+ shopId, outGoodsId, goodsId, isOnsale, PddOpenApiSupport.snippet(raw, 400));
+ } else {
+ log.warn("[PDD] pdd.goods.sale.status.set end shopId={} outGoodsId={} goodsId={} fail err={} snippet={}",
+ shopId, outGoodsId, goodsId, errMsg, PddOpenApiSupport.snippet(raw, 800));
+ }
+ return PddPublishLaneResultVo.builder()
+ .attempted(true)
+ .success(ok)
+ .message(ok ? "pdd.goods.sale.status.set 成功" : ("pdd.goods.sale.status.set 失败: " + errMsg))
+ .goodsAddSnippet(PddOpenApiSupport.snippet(raw, 2000))
+ .pddGoodsId(goodsId)
+ .shopCredentialSource(cred.getSource())
+ .catRuleFetched(false)
+ .specAutoResolved(false)
+ .publishLane("SALE_STATUS_SET")
+ .build();
+ } catch (Exception e) {
+ log.warn("pdd.goods.sale.status.set 异常 shopId={} outGoodsId={} goodsId={} err={}",
+ shopId, outGoodsId, goodsId, e.getMessage(), e);
+ return PddPublishLaneResultVo.builder()
+ .attempted(true)
+ .success(false)
+ .message("pdd.goods.sale.status.set 异常: " + e.getMessage())
+ .shopCredentialSource(cred.getSource())
+ .publishLane("SALE_STATUS_SET")
.build();
}
}
@@ -212,7 +325,13 @@ public class ExternalPddPublishService {
if (req == null || req.getPddPopAuth() == null) {
return null;
}
- ExternalGoodsUpsertRequest.PddPopAuth a = req.getPddPopAuth();
+ return credentialFromUpsertAuth(req.getPddPopAuth());
+ }
+
+ private PddShopCredential credentialFromUpsertAuth(ExternalGoodsUpsertRequest.PddPopAuth a) {
+ if (a == null) {
+ return null;
+ }
PddShopCredential c = new PddShopCredential();
c.setAppKey(trimToNull(a.getAppKey()));
c.setAppSecret(trimToNull(a.getAppSecret()));
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 2b715f8a..68c85875 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
@@ -151,6 +151,23 @@ public class PddGoodsAddParamBuilder {
return JSON.toJSONString(root);
}
+ /**
+ * 在 {@code pdd.goods.add} 根 JSON 上注入拼多多 {@code goods_id},并移除 {@code out_goods_id},
+ * 供 {@code pdd.goods.information.update} 与 {@code pdd.goods.add} 相同方式展开为表单顶层提交。
+ */
+ public String toInformationUpdateParamJson(String goodsAddRootJson, long pddGoodsId) {
+ if (!StringUtils.hasText(goodsAddRootJson)) {
+ throw new IllegalStateException("goodsAddRootJson 不能为空");
+ }
+ JSONObject root = JSON.parseObject(goodsAddRootJson);
+ if (root == null) {
+ throw new IllegalStateException("无法解析为 JSON 对象");
+ }
+ root.remove("out_goods_id");
+ root.put("goods_id", pddGoodsId);
+ return root.toJSONString();
+ }
+
/**
* 拼多多「无规格」发品(如图书 {@code input_max_spec_num=0}):仅一条 {@code sku_list}。
* 与 POP 文档一致:{@code spec_id_list} 为字符串形式的 JSON 数组,无规格为 {@code "[]"}(有规格如 {@code "[25]"});
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 68d9c561..9beab40b 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
@@ -157,6 +157,52 @@ public final class PddOpenApiSupport {
}
}
+ /**
+ * {@code pdd.goods.information.update}:无 {@code error_response};若存在 {@code goods_update_response} 则须 {@code is_success=true}。
+ */
+ public static boolean isInformationUpdateBizOk(String body) {
+ if (!StringUtils.hasText(body)) {
+ return false;
+ }
+ try {
+ JSONObject root = JSON.parseObject(body);
+ if (root == null) {
+ return false;
+ }
+ if (root.containsKey("error_response")) {
+ return false;
+ }
+ JSONObject gr = root.getJSONObject("goods_update_response");
+ if (gr != null && !gr.getBooleanValue("is_success")) {
+ return false;
+ }
+ return true;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ /** 业务失败摘要(含 {@code goods_update_response} 未成功时) */
+ public static String summarizeInformationUpdateFailure(String body) {
+ try {
+ JSONObject root = JSON.parseObject(body);
+ if (root == null) {
+ return body;
+ }
+ JSONObject gr = root.getJSONObject("goods_update_response");
+ if (gr != null && !gr.getBooleanValue("is_success")) {
+ StringBuilder sb = new StringBuilder("goods_update_response.is_success=false");
+ if (gr.containsKey("goods_commit_id")) {
+ sb.append(", goods_commit_id=").append(gr.get("goods_commit_id"));
+ }
+ return sb.toString();
+ }
+ return formatError(body);
+ } catch (Exception e) {
+ return body;
+ }
+ }
+
public static String formatError(String body) {
try {
JSONObject o = JSON.parseObject(body);
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 84bcb1c9..0ae952e2 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
@@ -20,8 +20,8 @@ import java.util.Map;
* 每次请求无论成功失败均打日志(含 HTTP 状态、耗时、响应片段;client_id 脱敏)。
* 入参形态(与官方 POP curl / Java SDK 一致):
*
- * - {@link #invokeGoodsAdd} — {@code pdd.goods.add}:业务字段全部在表单顶层({@code sku_list}、{@code carousel_gallery} 等为 JSON 字符串),
- * 无 {@code param_json}、无 {@code version}
+ * - {@link #invokeGoodsAdd} / {@link #invokeGoodsInformationUpdate} — {@code pdd.goods.add}、{@code pdd.goods.information.update}:
+ * 根 JSON 展开为表单顶层(与官方 curl / Java SDK 一致),无 {@code param_json}、无 {@code version}
* - {@link #invokeTopLevelBiz} — 如 {@code pdd.goods.cat.rule.get} 的 {@code cat_id}/{@code goods_id} 等顶层字段
* - {@link #invokeGoodsImageUpload} — {@code pdd.goods.image.upload}:urlencoded,{@code image} 为 {@code data:image/*;base64,} + Base64
* - {@link #invoke} — 仅当接口要求整包 {@code param_json} 时使用(少数场景)
@@ -58,8 +58,28 @@ public class PddPopClient {
}
/**
- * 业务参数整包放入 {@code param_json}(并带 {@code version=V1})。仅适用于仍要求该形态的接口。
+ * {@code pdd.goods.information.update}:与 {@link #invokeGoodsAdd} 相同,将根 JSON 展开为顶层表单(官方示例 {@code sku_list}、{@code goods_id} 等与 {@code type} 同级)。
*/
+ public String invokeGoodsInformationUpdate(String gatewayUrl, String clientId, String clientSecret, String accessToken,
+ String informationUpdateRootJson) throws Exception {
+ Map biz = PddOpenApiSupport.flattenPopTopLevelFromRootJson(informationUpdateRootJson);
+ String logPayload = PddOpenApiSupport.snippet(informationUpdateRootJson, 400);
+ if (!StringUtils.hasText(gatewayUrl)) {
+ throw new IllegalArgumentException("gatewayUrl 不能为空");
+ }
+ Map params = buildBaseParams(clientId, accessToken, "pdd.goods.information.update", 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.information.update", params, logPayload,
+ Duration.ofSeconds(60));
+ }
+
public String invoke(String gatewayUrl, String clientId, String clientSecret, String accessToken,
String type, String paramJson) throws Exception {
if (!StringUtils.hasText(gatewayUrl)) {