cn.qihangerp.core
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
new file mode 100644
index 00000000..4cae8b61
--- /dev/null
+++ b/api/erp-api/src/main/java/cn/qihangerp/erp/controller/ExternalGoodsController.java
@@ -0,0 +1,124 @@
+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.ExternalGoodsUpsertRequest;
+import cn.qihangerp.model.vo.ExternalGoodsUpsertResultVo;
+import cn.qihangerp.security.common.BaseController;
+import cn.qihangerp.service.external.ExternalGoodsAppService;
+import cn.qihangerp.service.external.pdd.ExternalPddProperties;
+import lombok.AllArgsConstructor;
+import org.springframework.util.StringUtils;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+/**
+ * External goods entry(AK/SK 由安全过滤器校验)。
+ * 不查询 {@code o_shop}:{@code shopId} 仅作业务侧店铺维度标识写入 {@code o_goods}/{@code o_goods_sku}。
+ *
+ * - {@code POST /external/goods/upsert} — 商品上架/同步(新建默认 {@code o_goods.status=1})
+ * - {@code POST /external/goods/delist} — 本地下架({@code status=2}),不调各平台下架 API
+ *
+ *
+ * @author guochengyu
+ */
+@AllArgsConstructor
+@RestController
+@RequestMapping("/external/goods")
+public class ExternalGoodsController extends BaseController {
+ private final ExternalGoodsAppService externalGoodsAppService;
+ private final ExternalPddProperties externalPddProperties;
+
+ @PostMapping("/upsert")
+ public AjaxResult upsert(@RequestBody ExternalGoodsUpsertRequest req) {
+ if (req == null || req.getShopId() == null || req.getShopId() <= 0) {
+ return AjaxResult.error("参数错误:shopId不能为空");
+ }
+ if (!StringUtils.hasText(req.getPlatform())) {
+ return AjaxResult.error("参数错误:platform不能为空");
+ }
+ if (!StringUtils.hasText(req.getOutGoodsId())) {
+ return AjaxResult.error("参数错误:outGoodsId不能为空");
+ }
+ if (!StringUtils.hasText(req.getTitle())) {
+ return AjaxResult.error("参数错误:title不能为空");
+ }
+ if (!StringUtils.hasText(req.getCategoryCode())) {
+ return AjaxResult.error("参数错误:categoryCode不能为空");
+ }
+ if (!hasMainVisual(req)) {
+ return AjaxResult.error("参数错误:mainImage 与 images 至少填一种");
+ }
+ if (req.getSkus() != null) {
+ for (var s : req.getSkus()) {
+ if (s == null || !StringUtils.hasText(s.getOutSkuId())) {
+ continue;
+ }
+ if (s.getSalePrice() == null && s.getRetailPrice() == null) {
+ return AjaxResult.error("参数错误:sku 需填写 salePrice 或 retailPrice,outSkuId=" + s.getOutSkuId());
+ }
+ }
+ }
+
+ EnumShopType platform;
+ try {
+ platform = EnumShopType.valueOf(req.getPlatform().trim());
+ } catch (Exception ex) {
+ return AjaxResult.error("platform不合法");
+ }
+
+ if (EnumShopType.PDD.equals(platform) && externalPddProperties.isPublishEnabled()) {
+ 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");
+ }
+ }
+
+ ExternalGoodsUpsertResultVo vo = externalGoodsAppService.upsertGoods(req);
+ return AjaxResult.success(vo);
+ }
+
+ /**
+ * 本地下架:将 {@code o_goods.status} 置为 2(已下架)。不调用拼多多等平台的下架接口。
+ */
+ @PostMapping("/delist")
+ public AjaxResult delist(@RequestBody ExternalGoodsDelistRequest req) {
+ if (req == null || req.getShopId() == null || req.getShopId() <= 0) {
+ return AjaxResult.error("参数错误:shopId不能为空");
+ }
+ if (!StringUtils.hasText(req.getOutGoodsId())) {
+ return AjaxResult.error("参数错误:outGoodsId不能为空");
+ }
+ try {
+ externalGoodsAppService.delistGoods(req);
+ return AjaxResult.success();
+ } catch (IllegalArgumentException ex) {
+ return AjaxResult.error(ex.getMessage());
+ }
+ }
+
+ private static boolean hasMainVisual(ExternalGoodsUpsertRequest req) {
+ if (StringUtils.hasText(req.getMainImage())) {
+ return true;
+ }
+ List imgs = req.getImages();
+ if (imgs == null || imgs.isEmpty()) {
+ return false;
+ }
+ for (String u : imgs) {
+ if (StringUtils.hasText(u)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/api/erp-api/src/main/java/cn/qihangerp/erp/controller/ExternalShopController.java b/api/erp-api/src/main/java/cn/qihangerp/erp/controller/ExternalShopController.java
new file mode 100644
index 00000000..b3040d10
--- /dev/null
+++ b/api/erp-api/src/main/java/cn/qihangerp/erp/controller/ExternalShopController.java
@@ -0,0 +1,25 @@
+package cn.qihangerp.erp.controller;
+
+import cn.qihangerp.common.AjaxResult;
+import cn.qihangerp.security.common.BaseController;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 历史接口占位:原写入 {@code o_shop} 的店铺授权已弃用。
+ * o_shop 表对外部集成已弃用:拼多多凭证请在 {@code POST /external/goods/upsert} 的 {@code pddPopAuth} 中传入。
+ *
+ * @author guochengyu
+ * @deprecated 固定返回错误,勿再调用
+ */
+@Deprecated
+@RestController
+@RequestMapping("/external/shop")
+public class ExternalShopController extends BaseController {
+
+ @PostMapping("/credential")
+ public AjaxResult pushCredential() {
+ return AjaxResult.error("已停用:o_shop 已弃用,请在 POST /external/goods/upsert 请求体 pddPopAuth 传入拼多多凭证");
+ }
+}
diff --git a/api/erp-api/src/main/java/cn/qihangerp/erp/serviceImpl/DeepSeekService.java b/api/erp-api/src/main/java/cn/qihangerp/erp/serviceImpl/DeepSeekService.java
deleted file mode 100644
index 4dddf36f..00000000
--- a/api/erp-api/src/main/java/cn/qihangerp/erp/serviceImpl/DeepSeekService.java
+++ /dev/null
@@ -1,365 +0,0 @@
-//package cn.qihangerp.erp.serviceImpl;
-//
-//import com.fasterxml.jackson.databind.ObjectMapper;
-//import jakarta.annotation.PostConstruct;
-//import okhttp3.*;
-//import org.slf4j.Logger;
-//import org.slf4j.LoggerFactory;
-//import org.springframework.beans.factory.annotation.Value;
-//import org.springframework.stereotype.Service;
-//import java.io.IOException;
-//import java.util.*;
-//import java.util.concurrent.TimeUnit;
-//
-//@Service
-//public class DeepSeekService {
-//
-// private static final Logger log = LoggerFactory.getLogger(DeepSeekService.class);
-// private static final MediaType JSON = MediaType.get("application/json; charset=utf-8");
-//
-// @Value("${deepseek.api.key}")
-// private String apiKey;
-//
-// @Value("${deepseek.api.endpoint:https://api.deepseek.com/v1/chat/completions}")
-// private String apiEndpoint;
-//
-// @Value("${deepseek.api.model:deepseek-chat}")
-// private String model;
-//
-// private OkHttpClient okHttpClient;
-// private final ObjectMapper objectMapper;
-//
-// // 缓存最近一次成功的分析结果
-// private Map cachedAnalysis = new HashMap<>();
-//
-// public DeepSeekService(ObjectMapper objectMapper) {
-// this.objectMapper = objectMapper;
-// }
-//
-// @PostConstruct
-// public void init() {
-// // 配置具有重试和连接池功能的OkHttpClient
-// this.okHttpClient = new OkHttpClient.Builder()
-// .connectTimeout(15, TimeUnit.SECONDS) // 连接超时
-// .readTimeout(30, TimeUnit.SECONDS) // 读取超时
-// .writeTimeout(15, TimeUnit.SECONDS) // 写入超时
-// .connectionPool(new ConnectionPool(5, 5, TimeUnit.MINUTES)) // 连接池
-// .addInterceptor(new RetryInterceptor(3)) // 自定义重试拦截器
-// .addInterceptor(new LoggingInterceptor()) // 日志拦截器
-// .build();
-// }
-//
-// /**
-// * 调用DeepSeek API - 带有Spring Retry重试机制
-// */
-//// @Retryable(
-//// value = {IOException.class, RuntimeException.class},
-//// maxAttempts = 3,
-//// backoff = @Backoff(delay = 1000, multiplier = 2, maxDelay = 10000)
-//// )
-// public Map analyzeData(Map formattedData, String analysisType) {
-// String cacheKey = generateCacheKey(formattedData, analysisType);
-//
-// try {
-// // 1. 构建请求体
-// String requestBody = buildRequestBody(formattedData, analysisType);
-// RequestBody body = RequestBody.create(requestBody, JSON);
-//
-// // 2. 构建请求
-// Request request = new Request.Builder()
-// .url(apiEndpoint)
-// .header("Authorization", "Bearer " + apiKey)
-// .header("Content-Type", "application/json")
-// .post(body)
-// .build();
-//
-// // 3. 执行请求并处理响应
-// try (Response response = okHttpClient.newCall(request).execute()) {
-// if (!response.isSuccessful()) {
-// handleErrorResponse(response, cacheKey);
-// }
-//
-// String responseBody = response.body().string();
-// Map result = parseResponse(responseBody, analysisType);
-//
-// // 缓存成功的结果
-// cacheSuccessfulResult(cacheKey, result);
-// return result;
-// }
-//
-// } catch (Exception e) {
-// log.error("调用DeepSeek API失败,尝试使用缓存或降级方案", e);
-// return getFallbackAnalysis(cacheKey, analysisType);
-// }
-// }
-//
-// /**
-// * 为补货建议优化的专用方法
-// */
-// public Map generateReplenishmentSuggestions(Map inventoryData) {
-// try {
-// String prompt = buildReplenishmentPrompt(inventoryData);
-//
-// Map requestBody = Map.of(
-// "model", model,
-// "messages", List.of(
-// Map.of("role", "system", "content",
-// "你是一个经验丰富的电商库存管理专家,擅长制定补货策略。"),
-// Map.of("role", "user", "content", prompt)
-// ),
-// "temperature", 0.2,
-// "max_tokens", 1500,
-// "response_format", Map.of("type", "json_object")
-// );
-//
-// String jsonBody = objectMapper.writeValueAsString(requestBody);
-//
-// Request request = new Request.Builder()
-// .url(apiEndpoint)
-// .header("Authorization", "Bearer " + apiKey)
-// .post(RequestBody.create(jsonBody, JSON))
-// .build();
-//
-// // 设置更短的超时时间,因为补货建议需要快速响应
-// OkHttpClient quickClient = okHttpClient.newBuilder()
-// .readTimeout(15, TimeUnit.SECONDS)
-// .build();
-//
-// try (Response response = quickClient.newCall(request).execute()) {
-// if (response.isSuccessful()) {
-// String responseBody = response.body().string();
-// return parseReplenishmentResponse(responseBody);
-// } else {
-// // 如果API失败,使用本地算法生成补货建议
-// return generateLocalReplenishmentSuggestions(inventoryData);
-// }
-// }
-//
-// } catch (Exception e) {
-// log.warn("AI补货建议失败,使用本地算法", e);
-// return generateLocalReplenishmentSuggestions(inventoryData);
-// }
-// }
-//
-// /**
-// * 本地补货算法 - 服务降级方案
-// */
-// private Map generateLocalReplenishmentSuggestions(Map inventoryData) {
-// List