feat(external): PDD quantity by outGoodsId/outSkuId; persist POP ids in canonicalExt
- Remove /external/pdd/goods/quantity/update and ExternalPddGoodsQuantityUpdateRequest. - Add POST /external/goods/pdd/quantity/update with shopId+outGoodsId+outSkuId; resolve pddGoodsId/pddSkuId from o_goods/o_goods_sku canonicalExt. - On successful PDD upsert, persist pddGoodsId and per-SKU pddSkuId (from goods_add_response.sku_list) via OGoodsPddMappingPersistence. - Parse outer key via out_sku_sn/outer_id/out_sku_id in PddOpenApiSupport. Made-with: Cursor
This commit is contained in:
parent
bda4302e59
commit
f8312ad4b7
|
|
@ -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;
|
|||
* <ul>
|
||||
* <li>{@code POST /external/goods/upsert} — 商品上架/同步(新建默认 {@code o_goods.status=1})</li>
|
||||
* <li>{@code POST /external/goods/delist} — 本地下架({@code status=2});可选 {@code pddGoodsId}+{@code pddPopAuth} 调拼多多 {@code pdd.goods.sale.status.set}({@code is_onsale=0})</li>
|
||||
* <li>{@code POST /external/goods/pdd/quantity/update} — 按 {@code shopId}+{@code outGoodsId}+{@code outSkuId} 解析本地映射后调 {@code pdd.goods.quantity.update}</li>
|
||||
* </ul>
|
||||
*
|
||||
* @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 "";
|
||||
|
|
|
|||
|
|
@ -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} 编排解耦)。
|
||||
* <ul>
|
||||
* <li>{@code POST /external/pdd/goods/detail} — {@code pdd.goods.detail.get}</li>
|
||||
* <li>{@code POST /external/pdd/goods/quantity/update} — {@code pdd.goods.quantity.update}</li>
|
||||
* </ul>
|
||||
* <p>改库存请使用 {@code POST /external/goods/pdd/quantity/update}(与 upsert 相同 out 键)。</p>
|
||||
*/
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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 库存。
|
||||
* <p>凭证形态与 {@link ExternalGoodsUpsertRequest#getPddPopAuth()} 一致。</p>
|
||||
*/
|
||||
@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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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<OGoods>()
|
||||
.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<OGoodsSku>()
|
||||
.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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
105
service/src/main/java/cn/qihangerp/service/external/pdd/OGoodsPddMappingPersistence.java
vendored
Normal file
105
service/src/main/java/cn/qihangerp/service/external/pdd/OGoodsPddMappingPersistence.java
vendored
Normal file
|
|
@ -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<String, Long> 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<OGoodsSku> rows = skuService.list(new LambdaQueryWrapper<OGoodsSku>()
|
||||
.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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String, Long> parseOuterKeyToPddSkuIdFromGoodsAddResponse(String raw) {
|
||||
Map<String, Long> 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 常见调用方式一致)。
|
||||
*/
|
||||
|
|
|
|||
Loading…
Reference in New Issue