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();
+ }
+}