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 b9a4daad..7acfa7f6 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 @@ -3,12 +3,15 @@ package cn.qihangerp.erp.controller; import cn.qihangerp.common.AjaxResult; import cn.qihangerp.common.enums.EnumShopType; import cn.qihangerp.model.request.ExternalGoodsDelistRequest; +import cn.qihangerp.model.request.ExternalGoodsPddQuantityUpdateRequest; import cn.qihangerp.model.request.ExternalGoodsUpsertRequest; +import cn.qihangerp.model.vo.ExternalGoodsPddQuantityUpdateResultVo; import cn.qihangerp.model.vo.ExternalGoodsUpsertResultVo; import cn.qihangerp.security.common.BaseController; import cn.qihangerp.erp.config.ExternalGoodsApiLogProperties; import cn.qihangerp.erp.support.ExternalGoodsRequestLogSupport; import cn.qihangerp.service.external.ExternalGoodsAppService; +import cn.qihangerp.service.external.ExternalGoodsPddQuantityUpdateAppService; import cn.qihangerp.service.external.pdd.ExternalPddProperties; import com.alibaba.fastjson2.JSON; import lombok.AllArgsConstructor; @@ -27,6 +30,7 @@ import java.util.List; * * * @author guochengyu @@ -37,6 +41,7 @@ import java.util.List; @RequestMapping("/external/goods") public class ExternalGoodsController extends BaseController { private final ExternalGoodsAppService externalGoodsAppService; + private final ExternalGoodsPddQuantityUpdateAppService externalGoodsPddQuantityUpdateAppService; private final ExternalPddProperties externalPddProperties; private final ExternalGoodsApiLogProperties goodsApiLogProperties; @@ -142,6 +147,42 @@ public class ExternalGoodsController extends BaseController { } } + /** + * 拼多多改库存:入参与 upsert 对齐({@code shopId}、{@code outGoodsId}、{@code outSkuId}),不传拼多多 goods/sku id。 + */ + @PostMapping("/pdd/quantity/update") + public AjaxResult pddQuantityUpdate(@RequestBody ExternalGoodsPddQuantityUpdateRequest req) { + if (goodsApiLogProperties.isLogFullRequest()) { + log.info("[external/goods/pdd/quantity/update] request={}", + ExternalGoodsRequestLogSupport.goodsPddQuantityUpdateRequestJsonForLog(req)); + } + if (req == null || req.getShopId() == null || req.getShopId() <= 0) { + return AjaxResult.error("参数错误:shopId不能为空"); + } + if (!StringUtils.hasText(req.getOutGoodsId())) { + return AjaxResult.error("参数错误:outGoodsId不能为空"); + } + if (!StringUtils.hasText(req.getOutSkuId())) { + return AjaxResult.error("参数错误:outSkuId不能为空"); + } + if (req.getQuantity() == null || req.getQuantity() < 0) { + return AjaxResult.error("参数错误:quantity 不能为空且不能为负"); + } + if (req.getPddPopAuth() == null) { + return AjaxResult.error("参数错误:pddPopAuth 不能为空"); + } + var auth = req.getPddPopAuth(); + if (!StringUtils.hasText(auth.getAppKey()) || !StringUtils.hasText(auth.getAppSecret()) + || !StringUtils.hasText(auth.getAccessToken())) { + return AjaxResult.error("参数错误:pddPopAuth 需提供 appKey、appSecret、accessToken"); + } + ExternalGoodsPddQuantityUpdateResultVo vo = externalGoodsPddQuantityUpdateAppService.updateQuantity(req); + if (!Boolean.TRUE.equals(vo.getPopBizSuccess())) { + return AjaxResult.error(StringUtils.hasText(vo.getMessage()) ? vo.getMessage() : "pdd.goods.quantity.update 失败"); + } + return AjaxResult.success(vo); + } + private static String safeHostForLog(String gatewayUrl) { if (!StringUtils.hasText(gatewayUrl)) { return ""; diff --git a/api/erp-api/src/main/java/cn/qihangerp/erp/controller/ExternalPddGoodsController.java b/api/erp-api/src/main/java/cn/qihangerp/erp/controller/ExternalPddGoodsController.java index ee33ded7..28caa501 100644 --- a/api/erp-api/src/main/java/cn/qihangerp/erp/controller/ExternalPddGoodsController.java +++ b/api/erp-api/src/main/java/cn/qihangerp/erp/controller/ExternalPddGoodsController.java @@ -2,14 +2,11 @@ package cn.qihangerp.erp.controller; import cn.qihangerp.common.AjaxResult; import cn.qihangerp.model.request.ExternalPddGoodsDetailRequest; -import cn.qihangerp.model.request.ExternalPddGoodsQuantityUpdateRequest; import cn.qihangerp.model.vo.ExternalPddGoodsDetailResultVo; -import cn.qihangerp.model.vo.ExternalPddGoodsQuantityUpdateResultVo; import cn.qihangerp.security.common.BaseController; import cn.qihangerp.erp.config.ExternalGoodsApiLogProperties; import cn.qihangerp.erp.support.ExternalGoodsRequestLogSupport; import cn.qihangerp.service.external.ExternalPddGoodsDetailAppService; -import cn.qihangerp.service.external.ExternalPddGoodsQuantityUpdateAppService; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.util.StringUtils; @@ -22,8 +19,8 @@ import org.springframework.web.bind.annotation.RestController; * 拼多多专用外部接口(与 {@code /external/goods/upsert} 编排解耦)。 * + *

改库存请使用 {@code POST /external/goods/pdd/quantity/update}(与 upsert 相同 out 键)。

*/ @Slf4j @AllArgsConstructor @@ -32,7 +29,6 @@ import org.springframework.web.bind.annotation.RestController; public class ExternalPddGoodsController extends BaseController { private final ExternalPddGoodsDetailAppService externalPddGoodsDetailAppService; - private final ExternalPddGoodsQuantityUpdateAppService externalPddGoodsQuantityUpdateAppService; private final ExternalGoodsApiLogProperties goodsApiLogProperties; @PostMapping("/detail") @@ -57,34 +53,4 @@ public class ExternalPddGoodsController extends BaseController { } return AjaxResult.success(vo); } - - @PostMapping("/quantity/update") - public AjaxResult quantityUpdate(@RequestBody ExternalPddGoodsQuantityUpdateRequest req) { - if (goodsApiLogProperties.isLogFullRequest()) { - log.info("[external/pdd/goods/quantity/update] request={}", - ExternalGoodsRequestLogSupport.pddQuantityUpdateRequestJsonForLog(req)); - } - if (req == null || req.getGoodsId() == null || req.getGoodsId() <= 0) { - return AjaxResult.error("参数错误:goodsId 不能为空且须为正数"); - } - if (req.getSkuId() == null || req.getSkuId() <= 0) { - return AjaxResult.error("参数错误:skuId 不能为空且须为正数(拼多多 sku_id)"); - } - if (req.getQuantity() == null || req.getQuantity() < 0) { - return AjaxResult.error("参数错误:quantity 不能为空且不能为负"); - } - if (req.getPddPopAuth() == null) { - return AjaxResult.error("参数错误:pddPopAuth 不能为空"); - } - var auth = req.getPddPopAuth(); - if (!StringUtils.hasText(auth.getAppKey()) || !StringUtils.hasText(auth.getAppSecret()) - || !StringUtils.hasText(auth.getAccessToken())) { - return AjaxResult.error("参数错误:pddPopAuth 需提供 appKey、appSecret、accessToken"); - } - ExternalPddGoodsQuantityUpdateResultVo vo = externalPddGoodsQuantityUpdateAppService.updateQuantity(req); - if (!Boolean.TRUE.equals(vo.getPopBizSuccess())) { - return AjaxResult.error(StringUtils.hasText(vo.getMessage()) ? vo.getMessage() : "pdd.goods.quantity.update 失败"); - } - return AjaxResult.success(vo); - } } diff --git a/api/erp-api/src/main/java/cn/qihangerp/erp/support/ExternalGoodsRequestLogSupport.java b/api/erp-api/src/main/java/cn/qihangerp/erp/support/ExternalGoodsRequestLogSupport.java index 673a2cfa..7080db1e 100644 --- a/api/erp-api/src/main/java/cn/qihangerp/erp/support/ExternalGoodsRequestLogSupport.java +++ b/api/erp-api/src/main/java/cn/qihangerp/erp/support/ExternalGoodsRequestLogSupport.java @@ -1,8 +1,8 @@ package cn.qihangerp.erp.support; import cn.qihangerp.model.request.ExternalGoodsUpsertRequest; +import cn.qihangerp.model.request.ExternalGoodsPddQuantityUpdateRequest; import cn.qihangerp.model.request.ExternalPddGoodsDetailRequest; -import cn.qihangerp.model.request.ExternalPddGoodsQuantityUpdateRequest; import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONObject; import org.springframework.util.StringUtils; @@ -18,9 +18,9 @@ public final class ExternalGoodsRequestLogSupport { } /** - * {@link ExternalPddGoodsQuantityUpdateRequest} 脱敏日志(不修改入参)。 + * {@link ExternalGoodsPddQuantityUpdateRequest} 脱敏日志(不修改入参)。 */ - public static String pddQuantityUpdateRequestJsonForLog(ExternalPddGoodsQuantityUpdateRequest req) { + public static String goodsPddQuantityUpdateRequestJsonForLog(ExternalGoodsPddQuantityUpdateRequest req) { if (req == null) { return "null"; } diff --git a/model/src/main/java/cn/qihangerp/model/request/ExternalGoodsPddQuantityUpdateRequest.java b/model/src/main/java/cn/qihangerp/model/request/ExternalGoodsPddQuantityUpdateRequest.java new file mode 100644 index 00000000..6d88c7ca --- /dev/null +++ b/model/src/main/java/cn/qihangerp/model/request/ExternalGoodsPddQuantityUpdateRequest.java @@ -0,0 +1,42 @@ +package cn.qihangerp.model.request; + +import lombok.Data; + +/** + * {@code POST /external/goods/pdd/quantity/update}:与 {@link ExternalGoodsUpsertRequest}/{@link ExternalGoodsDelistRequest} + * 相同维度键({@code shopId}+{@code outGoodsId}+{@code outSkuId}),由 ERP-Open 根据 {@code o_goods}/{@code o_goods_sku}.{@code canonicalExt} + * 中已落库的拼多多 ID 调用 {@code pdd.goods.quantity.update}。调用方无需传拼多多 {@code goods_id}/{@code sku_id}。 + */ +@Data +public class ExternalGoodsPddQuantityUpdateRequest { + + /** 与 upsert/delist 一致:业务侧店铺维度标识 */ + private Long shopId; + + /** 与 upsert 的 {@code outGoodsId} 一致:外部(中台)商品 ID,对应 {@code o_goods.outer_erp_goods_id} */ + private String outGoodsId; + + /** 与 upsert SKU 的 {@code outSkuId} 一致:外部(中台)SKU ID,对应 {@code o_goods_sku.outer_erp_sku_id} */ + private String outSkuId; + + /** 库存数量 */ + private Long quantity; + + /** + * 更新类型,参见拼多多开放平台;常见 {@code 1}。 + */ + private Integer updateType; + + /** + * 是否使用 {@code force_update}(默认 {@code true})。 + */ + private Boolean forceUpdate; + + /** + * 可选,透传 POP {@code outer_id}(与 {@code pdd.goods.quantity.update} 一致)。 + */ + private String outerId; + + /** 拼多多 POP 凭证,与 upsert 的 {@link ExternalGoodsUpsertRequest#getPddPopAuth()} 结构一致 */ + private ExternalGoodsUpsertRequest.PddPopAuth pddPopAuth; +} diff --git a/model/src/main/java/cn/qihangerp/model/request/ExternalPddGoodsQuantityUpdateRequest.java b/model/src/main/java/cn/qihangerp/model/request/ExternalPddGoodsQuantityUpdateRequest.java deleted file mode 100644 index 8a0143a5..00000000 --- a/model/src/main/java/cn/qihangerp/model/request/ExternalPddGoodsQuantityUpdateRequest.java +++ /dev/null @@ -1,35 +0,0 @@ -package cn.qihangerp.model.request; - -import lombok.Data; - -/** - * {@code POST /external/pdd/goods/quantity/update}:调用拼多多 {@code pdd.goods.quantity.update} 更新 SKU 库存。 - *

凭证形态与 {@link ExternalGoodsUpsertRequest#getPddPopAuth()} 一致。

- */ -@Data -public class ExternalPddGoodsQuantityUpdateRequest { - - /** 拼多多商品 {@code goods_id} */ - private Long goodsId; - - /** 拼多多 {@code sku_id}(与 POP 文档一致) */ - private Long skuId; - - /** 可选,商家外部编码 {@code outer_id} */ - private String outerId; - - /** 库存数量 */ - private Long quantity; - - /** - * 更新类型,参见拼多多开放平台;常见 {@code 1}(与官方示例一致)。 - */ - private Integer updateType; - - /** - * 是否使用 {@code force_update}(默认 {@code true},与官方示例一致)。 - */ - private Boolean forceUpdate; - - private ExternalGoodsUpsertRequest.PddPopAuth pddPopAuth; -} diff --git a/model/src/main/java/cn/qihangerp/model/vo/ExternalGoodsPddQuantityUpdateResultVo.java b/model/src/main/java/cn/qihangerp/model/vo/ExternalGoodsPddQuantityUpdateResultVo.java new file mode 100644 index 00000000..61545763 --- /dev/null +++ b/model/src/main/java/cn/qihangerp/model/vo/ExternalGoodsPddQuantityUpdateResultVo.java @@ -0,0 +1,32 @@ +package cn.qihangerp.model.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * {@code /external/goods/pdd/quantity/update} 返回:解析出的拼多多 ID(便于排障)及 POP 响应摘要。 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ExternalGoodsPddQuantityUpdateResultVo implements Serializable { + + private Boolean popBizSuccess; + + private Boolean quantityUpdateSuccess; + + private String message; + + private String popResponseBody; + + /** 实际调用 POP 时使用的拼多多 {@code goods_id}(来自本地映射) */ + private Long resolvedPddGoodsId; + + /** 实际调用 POP 时使用的拼多多 {@code sku_id}(来自本地映射) */ + private Long resolvedPddSkuId; +} diff --git a/model/src/main/java/cn/qihangerp/model/vo/ExternalPddGoodsQuantityUpdateResultVo.java b/model/src/main/java/cn/qihangerp/model/vo/ExternalPddGoodsQuantityUpdateResultVo.java deleted file mode 100644 index 8fb491cb..00000000 --- a/model/src/main/java/cn/qihangerp/model/vo/ExternalPddGoodsQuantityUpdateResultVo.java +++ /dev/null @@ -1,31 +0,0 @@ -package cn.qihangerp.model.vo; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.io.Serializable; - -/** - * {@code /external/pdd/goods/quantity/update} 返回:含 POP 网关原始 JSON 与业务 {@code is_success} 解析结果。 - */ -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class ExternalPddGoodsQuantityUpdateResultVo implements Serializable { - - /** - * 整体是否成功:无 {@code error_response} 且 {@code goods_quantity_update_response.is_success=true}。 - */ - private Boolean popBizSuccess; - - /** 与响应体 {@code goods_quantity_update_response.is_success} 一致(解析失败时为 false) */ - private Boolean quantityUpdateSuccess; - - private String message; - - /** 拼多多网关响应全文 */ - private String popResponseBody; -} diff --git a/service/src/main/java/cn/qihangerp/service/external/ExternalGoodsPddQuantityUpdateAppService.java b/service/src/main/java/cn/qihangerp/service/external/ExternalGoodsPddQuantityUpdateAppService.java new file mode 100644 index 00000000..e3207ef2 --- /dev/null +++ b/service/src/main/java/cn/qihangerp/service/external/ExternalGoodsPddQuantityUpdateAppService.java @@ -0,0 +1,12 @@ +package cn.qihangerp.service.external; + +import cn.qihangerp.model.request.ExternalGoodsPddQuantityUpdateRequest; +import cn.qihangerp.model.vo.ExternalGoodsPddQuantityUpdateResultVo; + +/** + * 按 {@code outGoodsId}/{@code outSkuId} 解析本地拼多多映射后调用 {@code pdd.goods.quantity.update}。 + */ +public interface ExternalGoodsPddQuantityUpdateAppService { + + ExternalGoodsPddQuantityUpdateResultVo updateQuantity(ExternalGoodsPddQuantityUpdateRequest req); +} diff --git a/service/src/main/java/cn/qihangerp/service/external/ExternalPddGoodsQuantityUpdateAppService.java b/service/src/main/java/cn/qihangerp/service/external/ExternalPddGoodsQuantityUpdateAppService.java deleted file mode 100644 index 8401b441..00000000 --- a/service/src/main/java/cn/qihangerp/service/external/ExternalPddGoodsQuantityUpdateAppService.java +++ /dev/null @@ -1,12 +0,0 @@ -package cn.qihangerp.service.external; - -import cn.qihangerp.model.request.ExternalPddGoodsQuantityUpdateRequest; -import cn.qihangerp.model.vo.ExternalPddGoodsQuantityUpdateResultVo; - -/** - * 拼多多库存更新({@code pdd.goods.quantity.update})。 - */ -public interface ExternalPddGoodsQuantityUpdateAppService { - - ExternalPddGoodsQuantityUpdateResultVo updateQuantity(ExternalPddGoodsQuantityUpdateRequest req); -} 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 c084f0c6..3e14ea16 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 @@ -12,6 +12,7 @@ import cn.qihangerp.module.service.OGoodsService; import cn.qihangerp.module.service.OGoodsSkuService; import cn.qihangerp.service.external.ExternalGoodsAppService; import cn.qihangerp.service.external.pdd.ExternalPddPublishService; +import cn.qihangerp.service.external.pdd.OGoodsPddMappingPersistence; import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONObject; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; @@ -40,6 +41,7 @@ public class ExternalGoodsAppServiceImpl implements ExternalGoodsAppService { private final OGoodsService goodsService; private final OGoodsSkuService skuService; private final ExternalPddPublishService externalPddPublishService; + private final OGoodsPddMappingPersistence oGoodsPddMappingPersistence; @Override @Transactional(rollbackFor = Exception.class) @@ -149,6 +151,7 @@ public class ExternalGoodsAppServiceImpl implements ExternalGoodsAppService { ExternalGoodsUpsertResultVo vo = out.build(); if ("PDD".equalsIgnoreCase(req.getPlatform())) { + oGoodsPddMappingPersistence.persistAfterPddPublishSuccess(goodsId, vo); log.info("[external/upsert] PDD summary shopId={} outGoodsId={} erpGoodsId={} attempted={} success={} lane={} credentialSource={} message={} goodsAddSnippet={} catRuleFetched={} catRuleSnippet={} specAutoResolved={} autoResolveDetail={}", req.getShopId(), req.getOutGoodsId(), diff --git a/service/src/main/java/cn/qihangerp/service/external/impl/ExternalGoodsPddQuantityUpdateAppServiceImpl.java b/service/src/main/java/cn/qihangerp/service/external/impl/ExternalGoodsPddQuantityUpdateAppServiceImpl.java new file mode 100644 index 00000000..ef02d0f5 --- /dev/null +++ b/service/src/main/java/cn/qihangerp/service/external/impl/ExternalGoodsPddQuantityUpdateAppServiceImpl.java @@ -0,0 +1,184 @@ +package cn.qihangerp.service.external.impl; + +import cn.qihangerp.model.entity.OGoods; +import cn.qihangerp.model.entity.OGoodsSku; +import cn.qihangerp.model.request.ExternalGoodsPddQuantityUpdateRequest; +import cn.qihangerp.model.request.ExternalGoodsUpsertRequest; +import cn.qihangerp.model.vo.ExternalGoodsPddQuantityUpdateResultVo; +import cn.qihangerp.module.service.OGoodsService; +import cn.qihangerp.module.service.OGoodsSkuService; +import cn.qihangerp.service.external.ExternalGoodsPddQuantityUpdateAppService; +import cn.qihangerp.service.external.pdd.ExternalPddProperties; +import cn.qihangerp.service.external.pdd.OGoodsPddMappingPersistence; +import cn.qihangerp.service.external.pdd.PddOpenApiSupport; +import cn.qihangerp.service.external.pdd.PddPopClient; +import cn.qihangerp.service.external.shop.PddShopCredential; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +/** + * @author guochengyu + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class ExternalGoodsPddQuantityUpdateAppServiceImpl implements ExternalGoodsPddQuantityUpdateAppService { + + private final ExternalPddProperties props; + private final PddPopClient pddPopClient; + private final OGoodsService goodsService; + private final OGoodsSkuService skuService; + + @Override + public ExternalGoodsPddQuantityUpdateResultVo updateQuantity(ExternalGoodsPddQuantityUpdateRequest req) { + if (req == null || req.getShopId() == null || req.getShopId() <= 0) { + return fail("shopId 无效", null, false, null, null); + } + if (!StringUtils.hasText(req.getOutGoodsId())) { + return fail("outGoodsId 不能为空", null, false, null, null); + } + if (!StringUtils.hasText(req.getOutSkuId())) { + return fail("outSkuId 不能为空", null, false, null, null); + } + if (req.getQuantity() == null || req.getQuantity() < 0) { + return fail("quantity 无效:不能为空且不能为负", null, false, null, null); + } + + String outGoodsId = req.getOutGoodsId().trim(); + String outSkuId = req.getOutSkuId().trim(); + + OGoods goods = goodsService.getOne(new LambdaQueryWrapper() + .eq(OGoods::getShopId, req.getShopId()) + .eq(OGoods::getOuterErpGoodsId, outGoodsId) + .last("LIMIT 1")); + if (goods == null) { + return fail("商品不存在:shopId=" + req.getShopId() + ", outGoodsId=" + outGoodsId, null, false, null, null); + } + Long pddGoodsId = readCanonicalLong(goods.getCanonicalExt(), OGoodsPddMappingPersistence.CANONICAL_PDD_GOODS_ID); + if (pddGoodsId == null || pddGoodsId <= 0) { + return fail("本地未找到拼多多 goods_id:请先 upsert 发品成功以写入 o_goods.canonicalExt.pddGoodsId", null, false, null, null); + } + + OGoodsSku skuRow = skuService.getOne(new LambdaQueryWrapper() + .eq(OGoodsSku::getShopId, req.getShopId()) + .eq(OGoodsSku::getGoodsId, goods.getId()) + .eq(OGoodsSku::getOuterErpSkuId, outSkuId) + .last("LIMIT 1")); + if (skuRow == null) { + return fail("SKU 不存在:shopId=" + req.getShopId() + ", outSkuId=" + outSkuId, null, false, pddGoodsId, null); + } + Long pddSkuId = readCanonicalLong(skuRow.getCanonicalExt(), OGoodsPddMappingPersistence.CANONICAL_PDD_SKU_ID); + if (pddSkuId == null || pddSkuId <= 0) { + return fail("本地未找到拼多多 sku_id:请先通过 pdd.goods.add 成功以写入 o_goods_sku.canonicalExt.pddSkuId(information.update 路径可能未回填 SKU 映射)", + null, false, pddGoodsId, null); + } + + PddShopCredential cred = resolveCredential(req); + if (cred == null || !StringUtils.hasText(cred.getAppKey()) || !StringUtils.hasText(cred.getAppSecret()) + || !StringUtils.hasText(cred.getAccessToken())) { + return fail("pddPopAuth 不完整:需 appKey、appSecret、accessToken", null, false, pddGoodsId, pddSkuId); + } + String gateway = StringUtils.hasText(cred.getGatewayUrl()) ? cred.getGatewayUrl() : props.getGatewayUrl(); + if (!StringUtils.hasText(gateway)) { + return fail("未配置 POP gatewayUrl", null, false, pddGoodsId, pddSkuId); + } + + int updateType = req.getUpdateType() != null ? req.getUpdateType() : 1; + boolean forceUpdate = req.getForceUpdate() == null || Boolean.TRUE.equals(req.getForceUpdate()); + + try { + var biz = PddOpenApiSupport.goodsQuantityUpdateTopLevelParams( + pddGoodsId, + pddSkuId, + req.getOuterId(), + req.getQuantity(), + updateType, + forceUpdate); + String raw = pddPopClient.invokeTopLevelBiz( + gateway, + cred.getAppKey(), + cred.getAppSecret(), + cred.getAccessToken(), + "pdd.goods.quantity.update", + biz); + boolean noPopError = !PddOpenApiSupport.isError(raw); + boolean innerOk = PddOpenApiSupport.parseGoodsQuantityUpdateIsSuccess(raw); + boolean ok = noPopError && innerOk; + String msg; + if (!noPopError) { + msg = "pdd.goods.quantity.update POP错误: " + PddOpenApiSupport.formatError(raw); + } else if (!innerOk) { + msg = "pdd.goods.quantity.update 业务失败: goods_quantity_update_response.is_success=false 或响应缺字段"; + } else { + msg = "pdd.goods.quantity.update 成功"; + } + return ExternalGoodsPddQuantityUpdateResultVo.builder() + .popBizSuccess(ok) + .quantityUpdateSuccess(innerOk) + .message(msg) + .popResponseBody(raw) + .resolvedPddGoodsId(pddGoodsId) + .resolvedPddSkuId(pddSkuId) + .build(); + } catch (Exception e) { + log.warn("[PDD] pdd.goods.quantity.update exception shopId={} outGoodsId={} outSkuId={} err={}", + req.getShopId(), outGoodsId, outSkuId, e.getMessage(), e); + return fail(e.getMessage(), null, false, pddGoodsId, pddSkuId); + } + } + + private static Long readCanonicalLong(String json, String key) { + if (!StringUtils.hasText(json) || !StringUtils.hasText(key)) { + return null; + } + try { + JSONObject o = JSON.parseObject(json); + if (o == null || !o.containsKey(key)) { + return null; + } + return o.getLong(key); + } catch (Exception e) { + return null; + } + } + + private static ExternalGoodsPddQuantityUpdateResultVo fail(String message, String raw, boolean innerOk, + Long resolvedPddGoodsId, Long resolvedPddSkuId) { + return ExternalGoodsPddQuantityUpdateResultVo.builder() + .popBizSuccess(false) + .quantityUpdateSuccess(innerOk) + .message(message) + .popResponseBody(raw) + .resolvedPddGoodsId(resolvedPddGoodsId) + .resolvedPddSkuId(resolvedPddSkuId) + .build(); + } + + private static PddShopCredential resolveCredential(ExternalGoodsPddQuantityUpdateRequest req) { + if (req == null || req.getPddPopAuth() == null) { + return null; + } + ExternalGoodsUpsertRequest.PddPopAuth a = req.getPddPopAuth(); + PddShopCredential c = new PddShopCredential(); + c.setAppKey(trimToNull(a.getAppKey())); + c.setAppSecret(trimToNull(a.getAppSecret())); + c.setAccessToken(trimToNull(a.getAccessToken())); + if (StringUtils.hasText(a.getGatewayUrl())) { + c.setGatewayUrl(a.getGatewayUrl().trim()); + } + c.setSource("REQUEST"); + return c; + } + + private static String trimToNull(String s) { + if (!StringUtils.hasText(s)) { + return null; + } + return s.trim(); + } +} diff --git a/service/src/main/java/cn/qihangerp/service/external/impl/ExternalPddGoodsQuantityUpdateAppServiceImpl.java b/service/src/main/java/cn/qihangerp/service/external/impl/ExternalPddGoodsQuantityUpdateAppServiceImpl.java deleted file mode 100644 index 4f575fbe..00000000 --- a/service/src/main/java/cn/qihangerp/service/external/impl/ExternalPddGoodsQuantityUpdateAppServiceImpl.java +++ /dev/null @@ -1,121 +0,0 @@ -package cn.qihangerp.service.external.impl; - -import cn.qihangerp.model.request.ExternalGoodsUpsertRequest; -import cn.qihangerp.model.request.ExternalPddGoodsQuantityUpdateRequest; -import cn.qihangerp.model.vo.ExternalPddGoodsQuantityUpdateResultVo; -import cn.qihangerp.service.external.ExternalPddGoodsQuantityUpdateAppService; -import cn.qihangerp.service.external.pdd.ExternalPddProperties; -import cn.qihangerp.service.external.pdd.PddOpenApiSupport; -import cn.qihangerp.service.external.pdd.PddPopClient; -import cn.qihangerp.service.external.shop.PddShopCredential; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.util.StringUtils; - -/** - * @author guochengyu - */ -@Slf4j -@Service -@RequiredArgsConstructor -public class ExternalPddGoodsQuantityUpdateAppServiceImpl implements ExternalPddGoodsQuantityUpdateAppService { - - private final ExternalPddProperties props; - private final PddPopClient pddPopClient; - - @Override - public ExternalPddGoodsQuantityUpdateResultVo updateQuantity(ExternalPddGoodsQuantityUpdateRequest req) { - if (req == null || req.getGoodsId() == null || req.getGoodsId() <= 0) { - return fail("goodsId 无效", null, false); - } - if (req.getSkuId() == null || req.getSkuId() <= 0) { - return fail("skuId 无效:须传拼多多 sku_id", null, false); - } - if (req.getQuantity() == null || req.getQuantity() < 0) { - return fail("quantity 无效:不能为空且不能为负", null, false); - } - int updateType = req.getUpdateType() != null ? req.getUpdateType() : 1; - boolean forceUpdate = req.getForceUpdate() == null || Boolean.TRUE.equals(req.getForceUpdate()); - - PddShopCredential cred = resolveCredential(req); - if (cred == null || !StringUtils.hasText(cred.getAppKey()) || !StringUtils.hasText(cred.getAppSecret()) - || !StringUtils.hasText(cred.getAccessToken())) { - return fail("pddPopAuth 不完整:需 appKey、appSecret、accessToken", null, false); - } - String gateway = StringUtils.hasText(cred.getGatewayUrl()) ? cred.getGatewayUrl() : props.getGatewayUrl(); - if (!StringUtils.hasText(gateway)) { - return fail("未配置 POP gatewayUrl(请求体或 external.pdd.gateway-url)", null, false); - } - - try { - var biz = PddOpenApiSupport.goodsQuantityUpdateTopLevelParams( - req.getGoodsId(), - req.getSkuId(), - req.getOuterId(), - req.getQuantity(), - updateType, - forceUpdate); - String raw = pddPopClient.invokeTopLevelBiz( - gateway, - cred.getAppKey(), - cred.getAppSecret(), - cred.getAccessToken(), - "pdd.goods.quantity.update", - biz); - boolean noPopError = !PddOpenApiSupport.isError(raw); - boolean innerOk = PddOpenApiSupport.parseGoodsQuantityUpdateIsSuccess(raw); - boolean ok = noPopError && innerOk; - String msg; - if (!noPopError) { - msg = "pdd.goods.quantity.update POP错误: " + PddOpenApiSupport.formatError(raw); - } else if (!innerOk) { - msg = "pdd.goods.quantity.update 业务失败: goods_quantity_update_response.is_success=false 或响应缺字段"; - } else { - msg = "pdd.goods.quantity.update 成功"; - } - return ExternalPddGoodsQuantityUpdateResultVo.builder() - .popBizSuccess(ok) - .quantityUpdateSuccess(innerOk) - .message(msg) - .popResponseBody(raw) - .build(); - } catch (Exception e) { - log.warn("[PDD] pdd.goods.quantity.update exception goodsId={} skuId={} err={}", - req.getGoodsId(), req.getSkuId(), e.getMessage(), e); - return fail(e.getMessage(), null, false); - } - } - - private static ExternalPddGoodsQuantityUpdateResultVo fail(String message, String raw, boolean innerOk) { - return ExternalPddGoodsQuantityUpdateResultVo.builder() - .popBizSuccess(false) - .quantityUpdateSuccess(innerOk) - .message(message) - .popResponseBody(raw) - .build(); - } - - private static PddShopCredential resolveCredential(ExternalPddGoodsQuantityUpdateRequest req) { - if (req == null || req.getPddPopAuth() == null) { - return null; - } - ExternalGoodsUpsertRequest.PddPopAuth a = req.getPddPopAuth(); - PddShopCredential c = new PddShopCredential(); - c.setAppKey(trimToNull(a.getAppKey())); - c.setAppSecret(trimToNull(a.getAppSecret())); - c.setAccessToken(trimToNull(a.getAccessToken())); - if (StringUtils.hasText(a.getGatewayUrl())) { - c.setGatewayUrl(a.getGatewayUrl().trim()); - } - c.setSource("REQUEST"); - return c; - } - - private static String trimToNull(String s) { - if (!StringUtils.hasText(s)) { - return null; - } - return s.trim(); - } -} diff --git a/service/src/main/java/cn/qihangerp/service/external/pdd/OGoodsPddMappingPersistence.java b/service/src/main/java/cn/qihangerp/service/external/pdd/OGoodsPddMappingPersistence.java new file mode 100644 index 00000000..8ab4fcd8 --- /dev/null +++ b/service/src/main/java/cn/qihangerp/service/external/pdd/OGoodsPddMappingPersistence.java @@ -0,0 +1,105 @@ +package cn.qihangerp.service.external.pdd; + +import cn.qihangerp.model.entity.OGoods; +import cn.qihangerp.model.entity.OGoodsSku; +import cn.qihangerp.model.vo.ExternalGoodsUpsertResultVo; +import cn.qihangerp.module.service.OGoodsService; +import cn.qihangerp.module.service.OGoodsSkuService; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * 拼多多发品成功后,将 POP {@code goods_id}/{@code sku_id} 写入 {@code o_goods}/{@code o_goods_sku}.{@code canonicalExt}, + * 供 {@code /external/goods/pdd/quantity/update} 按 {@code outGoodsId}+{@code outSkuId} 解析调用库存接口。 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class OGoodsPddMappingPersistence { + + public static final String CANONICAL_PDD_GOODS_ID = "pddGoodsId"; + public static final String CANONICAL_PDD_SKU_ID = "pddSkuId"; + + private final OGoodsService goodsService; + private final OGoodsSkuService skuService; + + /** + * 在 upsert 拼多多链路成功({@link ExternalGoodsUpsertResultVo#getPddPublishSuccess()}==true)后调用。 + */ + public void persistAfterPddPublishSuccess(Long erpGoodsId, ExternalGoodsUpsertResultVo vo) { + if (erpGoodsId == null || vo == null || !Boolean.TRUE.equals(vo.getPddPublishSuccess())) { + return; + } + Long pddGoodsId = vo.getPddGoodsId(); + if (pddGoodsId == null || pddGoodsId <= 0) { + log.warn("[PDD] skip persist mapping: pddGoodsId missing erpGoodsId={}", erpGoodsId); + return; + } + OGoods g = goodsService.getById(erpGoodsId); + if (g == null) { + return; + } + g.setCanonicalExt(mergeJsonLongField(g.getCanonicalExt(), CANONICAL_PDD_GOODS_ID, pddGoodsId)); + g.setUpdateTime(new Date()); + goodsService.updateById(g); + + Map outerToPddSku = Map.of(); + String lane = vo.getPddPublishLane(); + if (lane != null && "GOODS_ADD".equalsIgnoreCase(lane.trim())) { + outerToPddSku = PddOpenApiSupport.parseOuterKeyToPddSkuIdFromGoodsAddResponse(vo.getPddResponseSnippet()); + } + if (outerToPddSku.isEmpty()) { + log.info("[PDD] persisted pddGoodsId on o_goods only (no sku_list parse) erpGoodsId={} lane={}", + erpGoodsId, vo.getPddPublishLane()); + return; + } + List rows = skuService.list(new LambdaQueryWrapper() + .eq(OGoodsSku::getGoodsId, erpGoodsId)); + if (rows == null || rows.isEmpty()) { + return; + } + int updated = 0; + for (OGoodsSku row : rows) { + if (row == null || !StringUtils.hasText(row.getOuterErpSkuId())) { + continue; + } + Long pddSkuId = outerToPddSku.get(row.getOuterErpSkuId().trim()); + if (pddSkuId == null || pddSkuId <= 0) { + continue; + } + row.setCanonicalExt(mergeJsonLongField(row.getCanonicalExt(), CANONICAL_PDD_SKU_ID, pddSkuId)); + row.setUpdateTime(new Date()); + skuService.updateById(row); + updated++; + } + log.info("[PDD] persisted pddGoodsId + pddSkuId mappings erpGoodsId={} skuRowsUpdated={}", + erpGoodsId, updated); + } + + private static String mergeJsonLongField(String existingJson, String field, long value) { + JSONObject o; + if (StringUtils.hasText(existingJson)) { + try { + o = JSON.parseObject(existingJson); + } catch (Exception e) { + o = new JSONObject(); + } + if (o == null) { + o = new JSONObject(); + } + } else { + o = new JSONObject(); + } + o.put(field, value); + return o.toJSONString(); + } +} 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 81f111ff..b7b3ad9a 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 @@ -129,6 +129,65 @@ public final class PddOpenApiSupport { } } + /** + * 从 {@code pdd.goods.add} 成功响应中解析 {@code goods_add_response.sku_list[]}: + * 按 {@code out_sku_sn}/{@code outer_id}/{@code out_sku_id} 与外部 {@code outSkuId} 对齐,映射到拼多多 {@code sku_id}。 + */ + public static Map parseOuterKeyToPddSkuIdFromGoodsAddResponse(String raw) { + Map out = new LinkedHashMap<>(); + if (!StringUtils.hasText(raw)) { + return out; + } + try { + JSONObject root = JSON.parseObject(raw); + if (root == null) { + return out; + } + JSONObject add = root.getJSONObject("goods_add_response"); + if (add == null) { + return out; + } + JSONArray skuList = add.getJSONArray("sku_list"); + if (skuList == null || skuList.isEmpty()) { + return out; + } + for (int i = 0; i < skuList.size(); i++) { + JSONObject sku = skuList.getJSONObject(i); + if (sku == null) { + continue; + } + Long skuId = sku.getLong("sku_id"); + if (skuId == null || skuId <= 0) { + continue; + } + String outerKey = firstNonBlank( + sku.getString("out_sku_sn"), + sku.getString("outer_id"), + sku.getString("out_sku_id")); + if (!StringUtils.hasText(outerKey)) { + continue; + } + out.putIfAbsent(outerKey.trim(), skuId); + } + } catch (Exception ignored) { + // ignore + } + return out; + } + + private static String firstNonBlank(String a, String b, String c) { + if (StringUtils.hasText(a)) { + return a.trim(); + } + if (StringUtils.hasText(b)) { + return b.trim(); + } + if (StringUtils.hasText(c)) { + return c.trim(); + } + return null; + } + /** * {@code pdd.goods.spec.id.get} 的表单顶层业务参数(与 POP 常见调用方式一致)。 */