From a4e3421a1021692edd3c74eff60237208006ac8c Mon Sep 17 00:00:00 2001 From: huangyujie <27665451@qq.com> Date: Thu, 16 Apr 2026 14:03:53 +0800 Subject: [PATCH] feat(erp-open): add Weixin publish lane and platform rename Introduce WEIXIN platform handling for external goods upsert with dedicated sphPopAuth validation and result fields, and add a placeholder Sph publish service behind external.sph.publish-enabled. Rename EnumShopType WEI to WEIXIN and update related references to keep internal channel behavior consistent. Made-with: Cursor --- .../controller/ExternalGoodsController.java | 20 ++++++ .../erp/mq/ApiMessageServiceImpl.java | 4 +- .../cn/qihangerp/erp/mq/KafkaMQConsumer.java | 4 +- .../ExternalGoodsRequestLogSupport.java | 4 ++ .../src/main/resources/application.yml | 4 ++ .../cn/qihangerp/oms/wei/WeiApiCommon.java | 2 +- .../wei/controller/WeiRefundController.java | 2 +- .../qihangerp/common/enums/EnumShopType.java | 2 +- docs/yunxi-pdd-goods-listing-gateway.md | 2 +- .../request/ExternalGoodsUpsertRequest.java | 23 +++++- .../model/vo/ExternalGoodsUpsertResultVo.java | 15 ++++ .../model/vo/SphPublishLaneResultVo.java | 28 ++++++++ .../service/impl/WeiOrderServiceImpl.java | 4 +- .../impl/ExternalGoodsAppServiceImpl.java | 50 ++++++++++++- .../sph/ExternalSphConfiguration.java | 12 ++++ .../external/sph/ExternalSphProperties.java | 19 +++++ .../sph/ExternalSphPublishService.java | 70 +++++++++++++++++++ 17 files changed, 253 insertions(+), 12 deletions(-) create mode 100644 model/src/main/java/cn/qihangerp/model/vo/SphPublishLaneResultVo.java create mode 100644 service/src/main/java/cn/qihangerp/service/external/sph/ExternalSphConfiguration.java create mode 100644 service/src/main/java/cn/qihangerp/service/external/sph/ExternalSphProperties.java create mode 100644 service/src/main/java/cn/qihangerp/service/external/sph/ExternalSphPublishService.java 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 79c82bea..6712d3c1 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 @@ -13,6 +13,7 @@ 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 cn.qihangerp.service.external.sph.ExternalSphProperties; import com.alibaba.fastjson2.JSON; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -43,6 +44,7 @@ public class ExternalGoodsController extends BaseController { private final ExternalGoodsAppService externalGoodsAppService; private final ExternalGoodsPddQuantityUpdateAppService externalGoodsPddQuantityUpdateAppService; private final ExternalPddProperties externalPddProperties; + private final ExternalSphProperties externalSphProperties; private final ExternalGoodsApiLogProperties goodsApiLogProperties; @PostMapping("/upsert") @@ -106,6 +108,17 @@ public class ExternalGoodsController extends BaseController { } } + if (EnumShopType.WEIXIN.equals(platform) && externalSphProperties.isPublishEnabled()) { + if (req.getSphPopAuth() == null) { + return AjaxResult.error("参数错误:开启微信视频号小店发布时 sphPopAuth 不能为空"); + } + var sAuth = req.getSphPopAuth(); + if (!StringUtils.hasText(sAuth.getAppKey()) || !StringUtils.hasText(sAuth.getAppSecret()) + || !StringUtils.hasText(sAuth.getAccessToken())) { + return AjaxResult.error("参数错误:sphPopAuth 需提供 appKey、appSecret、accessToken"); + } + } + ExternalGoodsUpsertResultVo vo = externalGoodsAppService.upsertGoods(req); log.info("[external/goods/upsert] response shopId={} outGoodsId={} erpGoodsId={} platform={}", req.getShopId(), req.getOutGoodsId(), vo.getGoodsId(), req.getPlatform()); @@ -115,6 +128,13 @@ public class ExternalGoodsController extends BaseController { vo.getPddPublishSuccess(), truncateLog(vo.getPddPublishMessage(), 800)); } + if (EnumShopType.WEIXIN.equals(platform)) { + log.info("[external/goods/upsert] sphPublish attempted={} success={} message={} sphProductId={}", + vo.getSphPublishAttempted(), + vo.getSphPublishSuccess(), + truncateLog(vo.getSphPublishMessage(), 800), + vo.getSphProductId()); + } return AjaxResult.success(vo); } diff --git a/api/erp-api/src/main/java/cn/qihangerp/erp/mq/ApiMessageServiceImpl.java b/api/erp-api/src/main/java/cn/qihangerp/erp/mq/ApiMessageServiceImpl.java index 3f8bee06..efdddee0 100644 --- a/api/erp-api/src/main/java/cn/qihangerp/erp/mq/ApiMessageServiceImpl.java +++ b/api/erp-api/src/main/java/cn/qihangerp/erp/mq/ApiMessageServiceImpl.java @@ -76,7 +76,7 @@ // } else if (mqMessage.getShopType().getIndex() == EnumShopType.OFFLINE.getIndex()) { // log.info("订单消息OFFLINE"); // orderService.offlineOrderMessage(mqMessage.getKeyId()); -// } else if (mqMessage.getShopType().getIndex() == EnumShopType.WEI.getIndex()) { +// } else if (mqMessage.getShopType().getIndex() == EnumShopType.WEIXIN.getIndex()) { // log.info("订单消息WEI"); // JSONObject jsonObject = openApiService.getWeiOrderDetail(mqMessage.getKeyId()); // if (jsonObject.getInteger("code") != 200 || jsonObject.getJSONObject("data") == null) { @@ -132,7 +132,7 @@ // // JSONObject refundDetail = jsonObject.getJSONObject("data"); // refundService.douRefundMessage(mqMessage.getKeyId(), refundDetail); -// } else if (mqMessage.getShopType().getIndex() == EnumShopType.WEI.getIndex()) { +// } else if (mqMessage.getShopType().getIndex() == EnumShopType.WEIXIN.getIndex()) { // log.info("退款消息WEI"); // JSONObject jsonObject = openApiService.getWeiRefundDetail(mqMessage.getKeyId()); // if (jsonObject.getInteger("code") != 200 || jsonObject.getJSONObject("data") == null) { diff --git a/api/erp-api/src/main/java/cn/qihangerp/erp/mq/KafkaMQConsumer.java b/api/erp-api/src/main/java/cn/qihangerp/erp/mq/KafkaMQConsumer.java index c65b2926..f266963a 100644 --- a/api/erp-api/src/main/java/cn/qihangerp/erp/mq/KafkaMQConsumer.java +++ b/api/erp-api/src/main/java/cn/qihangerp/erp/mq/KafkaMQConsumer.java @@ -44,7 +44,7 @@ // } else if(vo.getShopType().getIndex() == EnumShopType.DOU.getIndex()) { // logger.info("Kafka订单消息DOU"+vo.getKeyId()); //// orderService.douOrderMessage(vo.getKeyId()); -// } else if(vo.getShopType().getIndex() == EnumShopType.WEI.getIndex()) { +// } else if(vo.getShopType().getIndex() == EnumShopType.WEIXIN.getIndex()) { // logger.info("Kafka订单消息WEI"+vo.getKeyId()); //// orderService.weiOrderMessage(vo.getKeyId()); // } @@ -70,7 +70,7 @@ // } else if(vo.getShopType().getIndex() == EnumShopType.DOU.getIndex()) { // logger.info("Kafka售后消息DOU"+vo.getKeyId()); //// refundService.douRefundMessage(vo.getKeyId()); -// } else if(vo.getShopType().getIndex() == EnumShopType.WEI.getIndex()) { +// } else if(vo.getShopType().getIndex() == EnumShopType.WEIXIN.getIndex()) { // logger.info("Kafka售后消息WEI"+vo.getKeyId()); //// refundService.weiRefundMessage(vo.getKeyId()); // } 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 7080db1e..a5535b23 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 @@ -77,6 +77,10 @@ public final class ExternalGoodsRequestLogSupport { if (auth != null && !auth.isEmpty()) { maskPddPopAuth(auth); } + JSONObject sphAuth = root.getJSONObject("sphPopAuth"); + if (sphAuth != null && !sphAuth.isEmpty()) { + maskPddPopAuth(sphAuth); + } return root.toJSONString(); } catch (Exception e) { return "{\"_logSanitizeError\":true}"; diff --git a/api/erp-api/src/main/resources/application.yml b/api/erp-api/src/main/resources/application.yml index b01df1bb..6cbbbc1c 100644 --- a/api/erp-api/src/main/resources/application.yml +++ b/api/erp-api/src/main/resources/application.yml @@ -77,4 +77,8 @@ external: DEFAULT: "303351846671360" sku-overrides: [] + # 微信视频号小店(platform=WEIXIN):默认不调用渠道发品;接入 OpenAPI 后再开启 + sph: + publish-enabled: false + # 说明:对外商品接口见 temp/yunxi-erp-open-goods-upsert-api.md;Nacos 对齐见 temp/erp-open-erp-api-nacos-yunxi-reference.md diff --git a/api/oms-api/src/main/java/cn/qihangerp/oms/wei/WeiApiCommon.java b/api/oms-api/src/main/java/cn/qihangerp/oms/wei/WeiApiCommon.java index 0c9f9131..b0977b2a 100644 --- a/api/oms-api/src/main/java/cn/qihangerp/oms/wei/WeiApiCommon.java +++ b/api/oms-api/src/main/java/cn/qihangerp/oms/wei/WeiApiCommon.java @@ -29,7 +29,7 @@ public class WeiApiCommon { if (shop == null) { return ResultVo.error(HttpStatus.PARAMS_ERROR,"参数错误,没有找到店铺"); } - if (shop.getType() != EnumShopType.WEI.getIndex()) { + if (shop.getType() != EnumShopType.WEIXIN.getIndex()) { return ResultVo.error(HttpStatus.PARAMS_ERROR, "参数错误,店铺不是微信小店店铺"); } if(!StringUtils.hasText(shop.getAppKey())) { diff --git a/api/oms-api/src/main/java/cn/qihangerp/oms/wei/controller/WeiRefundController.java b/api/oms-api/src/main/java/cn/qihangerp/oms/wei/controller/WeiRefundController.java index 26c76f98..e0d57dc4 100644 --- a/api/oms-api/src/main/java/cn/qihangerp/oms/wei/controller/WeiRefundController.java +++ b/api/oms-api/src/main/java/cn/qihangerp/oms/wei/controller/WeiRefundController.java @@ -34,7 +34,7 @@ public class WeiRefundController extends BaseController { // TODO:需要优化消息格式 if(bo!=null && bo.getIds()!=null) { for(String id: bo.getIds()) { - mqUtils.sendApiMessage(MqMessage.build(EnumShopType.WEI, MqType.REFUND_MESSAGE, id)); + mqUtils.sendApiMessage(MqMessage.build(EnumShopType.WEIXIN, MqType.REFUND_MESSAGE, id)); } } diff --git a/core/common/src/main/java/cn/qihangerp/common/enums/EnumShopType.java b/core/common/src/main/java/cn/qihangerp/common/enums/EnumShopType.java index b9a65e63..5dcf32f0 100644 --- a/core/common/src/main/java/cn/qihangerp/common/enums/EnumShopType.java +++ b/core/common/src/main/java/cn/qihangerp/common/enums/EnumShopType.java @@ -13,7 +13,7 @@ public enum EnumShopType { JDVC("京东自营", 280), PDD("拼多多", 300), DOU("抖店", 400), - WEI("视频号", 500), + WEIXIN("视频号", 500), KWAI("快手小店", 600), XHS("小红书", 700), OFFLINE("拼多多", 999) diff --git a/docs/yunxi-pdd-goods-listing-gateway.md b/docs/yunxi-pdd-goods-listing-gateway.md index 9457061d..bcb76a21 100644 --- a/docs/yunxi-pdd-goods-listing-gateway.md +++ b/docs/yunxi-pdd-goods-listing-gateway.md @@ -65,7 +65,7 @@ ### 4.0 已确认口径(冻结) - `shopId`:**全平台唯一**,可直接作为本服务店铺主键,不考虑跨平台撞号。 -- `platform`:由外部系统显式传入,取值采用 **`PDD`/`TAO`/`JD`/`DOU`/`WEI`/`KWAI`/`XHS`**(后续平台新增再扩展枚举)。 +- `platform`:由外部系统显式传入,取值采用 **`PDD`/`TAO`/`JD`/`DOU`/`WEIXIN`/`KWAI`/`XHS`**(后续平台新增再扩展枚举)。 - AK/SK:**一套即可**(外部系统作为单一调用方)。 - `X-Timestamp`:**毫秒**(ms)。 - `X-Signature`:**Base64** 编码(HMAC-SHA256 输出 bytes → Base64)。 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 b57de55d..fc20820c 100644 --- a/model/src/main/java/cn/qihangerp/model/request/ExternalGoodsUpsertRequest.java +++ b/model/src/main/java/cn/qihangerp/model/request/ExternalGoodsUpsertRequest.java @@ -23,7 +23,7 @@ public class ExternalGoodsUpsertRequest { private Long shopId; /** - * 目标平台枚举:PDD/TAO/JD/DOU/WEI/KWAI/XHS(与 EnumShopType 命名一致) + * 目标平台枚举:PDD/TAO/JD/DOU/WEIXIN/KWAI/XHS(与 EnumShopType 命名一致) */ private String platform; @@ -149,6 +149,12 @@ public class ExternalGoodsUpsertRequest { */ private PddPopAuth pddPopAuth; + /** + * 微信视频号小店凭证(与 {@link PddPopAuth} 形态一致:appId/secret + 店铺 access_token)。 + * 当 {@code platform=WEIXIN}({@link cn.qihangerp.common.enums.EnumShopType#WEIXIN})且 {@code external.sph.publish-enabled=true} 时必填。 + */ + private SphPopAuth sphPopAuth; + @Data public static class PddPopAuth { @JsonAlias({"app_key", "clientId", "client_id"}) @@ -165,6 +171,21 @@ public class ExternalGoodsUpsertRequest { private String gatewayUrl; } + @Data + public static class SphPopAuth { + @JsonAlias({"app_key", "clientId", "client_id"}) + private String appKey; + + @JsonAlias({"app_secret", "clientSecret", "client_secret"}) + private String appSecret; + + @JsonAlias({"access_token", "token", "sessionKey", "session_key"}) + private String accessToken; + + @JsonAlias({"gateway_url", "channelsGatewayUrl"}) + private String gatewayUrl; + } + @Data public static class AttributeItem { private String code; 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 670c9d51..9739b281 100644 --- a/model/src/main/java/cn/qihangerp/model/vo/ExternalGoodsUpsertResultVo.java +++ b/model/src/main/java/cn/qihangerp/model/vo/ExternalGoodsUpsertResultVo.java @@ -52,4 +52,19 @@ public class ExternalGoodsUpsertResultVo implements Serializable { /** 是否通过 spec.id.get 自动生成了 sku 规格 */ private Boolean pddSpecAutoResolved; private String pddAutoResolveDetail; + + // ---- 微信视频号小店(platform=WEIXIN)---- + + /** 是否尝试调用微信侧发品 */ + private Boolean sphPublishAttempted; + + /** 微信发品是否成功(未尝试时为 null) */ + private Boolean sphPublishSuccess; + + private String sphPublishMessage; + + private String sphResponseSnippet; + + /** 小店侧商品 ID(字符串) */ + private String sphProductId; } diff --git a/model/src/main/java/cn/qihangerp/model/vo/SphPublishLaneResultVo.java b/model/src/main/java/cn/qihangerp/model/vo/SphPublishLaneResultVo.java new file mode 100644 index 00000000..094ea146 --- /dev/null +++ b/model/src/main/java/cn/qihangerp/model/vo/SphPublishLaneResultVo.java @@ -0,0 +1,28 @@ +package cn.qihangerp.model.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * 微信视频号小店发品链路诊断(与 {@link PddPublishLaneResultVo} 对称,字段独立避免语义混淆)。 + * + * @author guochengyu + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SphPublishLaneResultVo implements Serializable { + + private Boolean attempted; + private Boolean success; + private String message; + /** 渠道网关/适配层响应片段(脱敏前由服务端控制长度) */ + private String responseSnippet; + /** 小店侧商品 ID(字符串,与微信返回形态一致) */ + private String sphProductId; +} diff --git a/service/src/main/java/cn/qihangerp/module/service/impl/WeiOrderServiceImpl.java b/service/src/main/java/cn/qihangerp/module/service/impl/WeiOrderServiceImpl.java index 5a5255b1..e6395178 100644 --- a/service/src/main/java/cn/qihangerp/module/service/impl/WeiOrderServiceImpl.java +++ b/service/src/main/java/cn/qihangerp/module/service/impl/WeiOrderServiceImpl.java @@ -210,7 +210,7 @@ public class WeiOrderServiceImpl extends ServiceImpl } OOrder order = new OOrder(); order.setOrderNum(weiOrder.getOrderId()); - order.setShopType(EnumShopType.WEI.getIndex()); + order.setShopType(EnumShopType.WEIXIN.getIndex()); order.setShopId(weiOrder.getShopId()); // order.setShipType(confirmBo.getShipType()); order.setShipType(0); @@ -273,7 +273,7 @@ public class WeiOrderServiceImpl extends ServiceImpl oOrderItem.setOrderId(order.getId()); oOrderItem.setOrderNum(order.getOrderNum()); oOrderItem.setSubOrderNum(order.getOrderNum()+"-"+item.getSkuId()); - oOrderItem.setShopType(EnumShopType.WEI.getIndex()); + oOrderItem.setShopType(EnumShopType.WEIXIN.getIndex()); oOrderItem.setShopId(weiOrder.getShopId()); // 商品信息 oOrderItem.setProductId(item.getProductId()); 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 86818c7d..01089cf1 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 @@ -8,10 +8,12 @@ import cn.qihangerp.model.request.ExternalGoodsDelistRequest; import cn.qihangerp.model.request.ExternalGoodsUpsertRequest; import cn.qihangerp.model.vo.ExternalGoodsUpsertResultVo; import cn.qihangerp.model.vo.PddPublishLaneResultVo; +import cn.qihangerp.model.vo.SphPublishLaneResultVo; 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.sph.ExternalSphPublishService; import cn.qihangerp.service.external.pdd.OGoodsPddMappingPersistence; import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONObject; @@ -41,6 +43,7 @@ public class ExternalGoodsAppServiceImpl implements ExternalGoodsAppService { private final OGoodsService goodsService; private final OGoodsSkuService skuService; private final ExternalPddPublishService externalPddPublishService; + private final ExternalSphPublishService externalSphPublishService; private final OGoodsPddMappingPersistence oGoodsPddMappingPersistence; @Override @@ -111,7 +114,12 @@ public class ExternalGoodsAppServiceImpl implements ExternalGoodsAppService { .pddCatRuleSnippet(null) .pddSpecAutoResolved(null) .pddAutoResolveDetail(null) - .pddPublishLane(null); + .pddPublishLane(null) + .sphPublishAttempted(false) + .sphPublishSuccess(null) + .sphPublishMessage(null) + .sphResponseSnippet(null) + .sphProductId(null); if ("PDD".equalsIgnoreCase(req.getPlatform())) { OGoods g = goodsService.getById(goodsId); @@ -149,6 +157,36 @@ public class ExternalGoodsAppServiceImpl implements ExternalGoodsAppService { out.pddPublishLane(lane.getPublishLane()); } + if ("WEIXIN".equalsIgnoreCase(req.getPlatform())) { + OGoods g = goodsService.getById(goodsId); + List skuRows = skuService.list(new LambdaQueryWrapper() + .eq(OGoodsSku::getGoodsId, goodsId)); + Map byOuter = new LinkedHashMap<>(); + for (OGoodsSku row : skuRows) { + if (row != null && StringUtils.hasText(row.getOuterErpSkuId())) { + byOuter.putIfAbsent(row.getOuterErpSkuId(), row); + } + } + List orderedSkus = new ArrayList<>(); + if (!CollectionUtils.isEmpty(req.getSkus())) { + for (ExternalGoodsUpsertRequest.Sku sr : req.getSkus()) { + if (sr == null || !StringUtils.hasText(sr.getOutSkuId())) { + continue; + } + OGoodsSku row = byOuter.get(sr.getOutSkuId()); + if (row != null) { + orderedSkus.add(row); + } + } + } + SphPublishLaneResultVo lane = externalSphPublishService.publish(g, orderedSkus, req); + out.sphPublishAttempted(Boolean.TRUE.equals(lane.getAttempted())); + out.sphPublishSuccess(lane.getSuccess()); + out.sphPublishMessage(lane.getMessage()); + out.sphResponseSnippet(lane.getResponseSnippet()); + out.sphProductId(lane.getSphProductId()); + } + ExternalGoodsUpsertResultVo vo = out.build(); if ("PDD".equalsIgnoreCase(req.getPlatform())) { oGoodsPddMappingPersistence.persistAfterPddPublishSuccess(goodsId, req, vo); @@ -167,6 +205,16 @@ public class ExternalGoodsAppServiceImpl implements ExternalGoodsAppService { vo.getPddSpecAutoResolved(), truncateForLog(vo.getPddAutoResolveDetail(), 300)); } + if ("WEIXIN".equalsIgnoreCase(req.getPlatform())) { + log.info("[external/upsert] WEIXIN summary shopId={} outGoodsId={} erpGoodsId={} attempted={} success={} message={} sphProductId={}", + req.getShopId(), + req.getOutGoodsId(), + vo.getGoodsId(), + vo.getSphPublishAttempted(), + vo.getSphPublishSuccess(), + truncateForLog(vo.getSphPublishMessage(), 600), + vo.getSphProductId()); + } return vo; } diff --git a/service/src/main/java/cn/qihangerp/service/external/sph/ExternalSphConfiguration.java b/service/src/main/java/cn/qihangerp/service/external/sph/ExternalSphConfiguration.java new file mode 100644 index 00000000..f8532a8d --- /dev/null +++ b/service/src/main/java/cn/qihangerp/service/external/sph/ExternalSphConfiguration.java @@ -0,0 +1,12 @@ +package cn.qihangerp.service.external.sph; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +/** + * @author guochengyu + */ +@Configuration +@EnableConfigurationProperties(ExternalSphProperties.class) +public class ExternalSphConfiguration { +} diff --git a/service/src/main/java/cn/qihangerp/service/external/sph/ExternalSphProperties.java b/service/src/main/java/cn/qihangerp/service/external/sph/ExternalSphProperties.java new file mode 100644 index 00000000..92f3745b --- /dev/null +++ b/service/src/main/java/cn/qihangerp/service/external/sph/ExternalSphProperties.java @@ -0,0 +1,19 @@ +package cn.qihangerp.service.external.sph; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * 微信视频号小店(platform=WEIXIN)发品相关开关;应用级密钥由调用方经 {@code sphPopAuth} 传入,与拼多多一致。 + * + * @author guochengyu + */ +@Data +@ConfigurationProperties(prefix = "external.sph") +public class ExternalSphProperties { + + /** + * 是否在 upsert 落库后尝试调用微信小店发品适配(未接入前请保持 false,避免误报失败)。 + */ + private boolean publishEnabled = false; +} diff --git a/service/src/main/java/cn/qihangerp/service/external/sph/ExternalSphPublishService.java b/service/src/main/java/cn/qihangerp/service/external/sph/ExternalSphPublishService.java new file mode 100644 index 00000000..2565b2cf --- /dev/null +++ b/service/src/main/java/cn/qihangerp/service/external/sph/ExternalSphPublishService.java @@ -0,0 +1,70 @@ +package cn.qihangerp.service.external.sph; + +import cn.qihangerp.model.entity.OGoods; +import cn.qihangerp.model.entity.OGoodsSku; +import cn.qihangerp.model.request.ExternalGoodsUpsertRequest; +import cn.qihangerp.model.vo.SphPublishLaneResultVo; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.util.List; + +/** + * 微信视频号小店发品:凭证仅来自本次请求的 {@link ExternalGoodsUpsertRequest#getSphPopAuth()}。 + *

具体 HTTP/OpenAPI 接入在 {@code publishEnabled=true} 后实现;关闭开关时不调用渠道,与拼多多 {@link cn.qihangerp.service.external.pdd.ExternalPddPublishService} 行为对称。

+ * + * @author guochengyu + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class ExternalSphPublishService { + + private final ExternalSphProperties props; + + /** + * 落库成功后可选调用微信侧发品;未开启或未实现时返回 {@code attempted=false},不阻断 ERP 本地 upsert。 + */ + public SphPublishLaneResultVo publish(OGoods goods, List skus, ExternalGoodsUpsertRequest req) { + if (!props.isPublishEnabled()) { + log.info("[SPH] publish skipped shopId={} outGoodsId={} reason=publish-disabled", + req != null ? req.getShopId() : null, + req != null ? req.getOutGoodsId() : null); + return SphPublishLaneResultVo.builder() + .attempted(false) + .success(null) + .message("external.sph.publish-enabled=false,未调用微信视频号小店") + .build(); + } + if (req == null) { + return SphPublishLaneResultVo.builder() + .attempted(false) + .success(false) + .message("请求参数不能为空") + .build(); + } + ExternalGoodsUpsertRequest.SphPopAuth auth = req.getSphPopAuth(); + if (auth == null || !StringUtils.hasText(auth.getAppKey()) || !StringUtils.hasText(auth.getAppSecret()) + || !StringUtils.hasText(auth.getAccessToken())) { + log.info("[SPH] publish skipped shopId={} outGoodsId={} reason=sphPopAuth-incomplete", + req.getShopId(), req.getOutGoodsId()); + return SphPublishLaneResultVo.builder() + .attempted(false) + .success(false) + .message("微信小店凭证不完整:请在请求体 sphPopAuth 中传入 appKey、appSecret、accessToken") + .build(); + } + // TODO: 接入微信视频号小店商品发布 OpenAPI,填充 sphProductId/responseSnippet + log.warn("[SPH] publish-enabled=true 但发品适配尚未实现 shopId={} outGoodsId={} skuCount={}", + req.getShopId(), req.getOutGoodsId(), skus == null ? 0 : skus.size()); + return SphPublishLaneResultVo.builder() + .attempted(true) + .success(false) + .message("微信视频号小店发品接口尚未接入(请关闭 external.sph.publish-enabled 或完成适配开发)") + .responseSnippet(null) + .sphProductId(null) + .build(); + } +}