2
0

2 Sitoutukset bcefc96450 ... 4f1dc430f5

Tekijä SHA1 Viesti Päivämäärä
  Zhu Jiaqi 4f1dc430f5 Merge branch 'master' of http://106.14.194.251:3000/dtm/dtm_java 2 viikkoa sitten
  Zhu Jiaqi 6c5c34bf61 生命周期模块AI决策功能+数据上传逻辑更新 2 viikkoa sitten

+ 20 - 0
dtm-admin/src/main/java/com/dtm/web/controller/lifecycle/LifecycleAnalysisController.java

@@ -4,12 +4,16 @@ import java.util.Map;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.GetMapping;
 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.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 import org.springframework.web.multipart.MultipartFile;
 import com.dtm.common.core.domain.AjaxResult;
+import com.dtm.common.utils.StringUtils;
+import com.dtm.lifecycle.domain.LifecycleAiAnalysisRequest;
 import com.dtm.lifecycle.service.ILifecycleAnalysisService;
+import com.dtm.lifecycle.service.ILifecycleAiAnalysisService;
 import com.dtm.lifecycle.service.ILifecycleDatabaseAnalysisService;
 
 /**
@@ -24,6 +28,9 @@ public class LifecycleAnalysisController
     @Autowired
     private ILifecycleDatabaseAnalysisService lifecycleDatabaseAnalysisService;
 
+    @Autowired
+    private ILifecycleAiAnalysisService lifecycleAiAnalysisService;
+
     /**
      * 获取数据库版SKU生命周期分析结果
      */
@@ -41,4 +48,17 @@ public class LifecycleAnalysisController
     {
         return AjaxResult.success(lifecycleDatabaseAnalysisService.getSpuResults(params));
     }
+
+    /**
+     * 基于数据库版生命周期结果生成AI运营建议
+     */
+    @PostMapping("/database/ai-analysis")
+    public AjaxResult analyzeDatabaseLifecycleWithAi(@RequestBody(required = false) LifecycleAiAnalysisRequest request)
+    {
+        if (request == null || StringUtils.isEmpty(request.getEntityType()) || StringUtils.isEmpty(request.getEntityId()))
+        {
+            return AjaxResult.error("entityType和entityId不能为空");
+        }
+        return AjaxResult.success(lifecycleAiAnalysisService.analyze(request));
+    }
 }

+ 18 - 4
dtm-admin/src/main/java/com/dtm/web/controller/upload/PreparedDataUploadController.java

@@ -3,6 +3,8 @@ package com.dtm.web.controller.upload;
 import com.dtm.common.annotation.Anonymous;
 import com.dtm.common.core.domain.AjaxResult;
 import com.dtm.upload.service.PreparedDataUploadService;
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.TypeReference;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.PostMapping;
@@ -11,6 +13,9 @@ import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 import org.springframework.web.multipart.MultipartFile;
 
+import java.util.Collections;
+import java.util.Map;
+
 @Anonymous
 @RestController
 @RequestMapping("/api/upload-data")
@@ -28,9 +33,10 @@ public class PreparedDataUploadController {
 
     @PostMapping("/{key}")
     public AjaxResult upload(@PathVariable("key") String key,
-                             @RequestParam("file") MultipartFile file) {
+                             @RequestParam("file") MultipartFile file,
+                             @RequestParam(value = "fieldMappings", required = false) String fieldMappings) {
         try {
-            return AjaxResult.success("上传成功", uploadService.upload(key, file));
+            return AjaxResult.success("上传成功", uploadService.upload(key, file, parseFieldMappings(fieldMappings)));
         } catch (IllegalArgumentException e) {
             return AjaxResult.error(e.getMessage());
         } catch (Exception e) {
@@ -40,13 +46,21 @@ public class PreparedDataUploadController {
 
     @PostMapping("/{key}/preview")
     public AjaxResult preview(@PathVariable("key") String key,
-                              @RequestParam("file") MultipartFile file) {
+                              @RequestParam("file") MultipartFile file,
+                              @RequestParam(value = "fieldMappings", required = false) String fieldMappings) {
         try {
-            return AjaxResult.success(uploadService.preview(key, file));
+            return AjaxResult.success(uploadService.preview(key, file, parseFieldMappings(fieldMappings)));
         } catch (IllegalArgumentException e) {
             return AjaxResult.error(e.getMessage());
         } catch (Exception e) {
             return AjaxResult.error("预览失败:" + e.getMessage());
         }
     }
+
+    private Map<String, String> parseFieldMappings(String fieldMappings) {
+        if (fieldMappings == null || fieldMappings.trim().isEmpty()) {
+            return Collections.emptyMap();
+        }
+        return JSON.parseObject(fieldMappings, new TypeReference<Map<String, String>>() {});
+    }
 }

+ 1 - 0
dtm-admin/src/main/resources/application-druid.yml

@@ -7,6 +7,7 @@ spring:
             # 主库数据源
             master:
                 url: jdbc:mysql://106.14.194.251:3306/dtm?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+                # 远程服务器ip:106.14.194.251 密码:Dtm@1915116
                 username: root
                 password: Dtm@1915116
             # 从库数据源

+ 6 - 0
dtm-system/pom.xml

@@ -60,6 +60,12 @@
             <artifactId>poi-ooxml</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
+
     </dependencies>
 
 </project>

+ 66 - 0
dtm-system/src/main/java/com/dtm/lifecycle/domain/LifecycleAiAnalysisRequest.java

@@ -0,0 +1,66 @@
+package com.dtm.lifecycle.domain;
+
+import java.util.Map;
+
+public class LifecycleAiAnalysisRequest
+{
+    private String entityType;
+
+    private String entityId;
+
+    private String startDate;
+
+    private String endDate;
+
+    private Map<String, Object> lifecycleData;
+
+    public String getEntityType()
+    {
+        return entityType;
+    }
+
+    public void setEntityType(String entityType)
+    {
+        this.entityType = entityType;
+    }
+
+    public String getEntityId()
+    {
+        return entityId;
+    }
+
+    public void setEntityId(String entityId)
+    {
+        this.entityId = entityId;
+    }
+
+    public String getStartDate()
+    {
+        return startDate;
+    }
+
+    public void setStartDate(String startDate)
+    {
+        this.startDate = startDate;
+    }
+
+    public String getEndDate()
+    {
+        return endDate;
+    }
+
+    public void setEndDate(String endDate)
+    {
+        this.endDate = endDate;
+    }
+
+    public Map<String, Object> getLifecycleData()
+    {
+        return lifecycleData;
+    }
+
+    public void setLifecycleData(Map<String, Object> lifecycleData)
+    {
+        this.lifecycleData = lifecycleData;
+    }
+}

+ 10 - 0
dtm-system/src/main/java/com/dtm/lifecycle/service/ILifecycleAiAnalysisService.java

@@ -0,0 +1,10 @@
+package com.dtm.lifecycle.service;
+
+import com.dtm.lifecycle.domain.LifecycleAiAnalysisRequest;
+
+import java.util.Map;
+
+public interface ILifecycleAiAnalysisService
+{
+    Map<String, Object> analyze(LifecycleAiAnalysisRequest request);
+}

+ 548 - 0
dtm-system/src/main/java/com/dtm/lifecycle/service/impl/LifecycleAiAnalysisServiceImpl.java

@@ -0,0 +1,548 @@
+package com.dtm.lifecycle.service.impl;
+
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONArray;
+import com.alibaba.fastjson2.JSONObject;
+import com.dtm.common.utils.StringUtils;
+import com.dtm.lifecycle.domain.LifecycleAiAnalysisRequest;
+import com.dtm.lifecycle.service.ILifecycleAiAnalysisService;
+import com.dtm.lifecycle.service.ILifecycleDatabaseAnalysisService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.http.client.SimpleClientHttpRequestFactory;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.HttpStatusCodeException;
+import org.springframework.web.client.ResourceAccessException;
+import org.springframework.web.client.RestTemplate;
+
+import javax.annotation.PostConstruct;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+@Service
+public class LifecycleAiAnalysisServiceImpl implements ILifecycleAiAnalysisService
+{
+    private static final Logger log = LoggerFactory.getLogger(LifecycleAiAnalysisServiceImpl.class);
+
+    private static final List<String> STAGES = Arrays.asList("引入期", "成长期", "成熟期", "衰退期");
+
+    private final ILifecycleDatabaseAnalysisService lifecycleDatabaseAnalysisService;
+
+    @Value("${zhipu.api-key:}")
+    private String apiKey;
+
+    @Value("${zhipu.base-url:https://open.bigmodel.cn/api/paas/v4/chat/completions}")
+    private String baseUrl;
+
+    @Value("${zhipu.model:GLM-4-Flash}")
+    private String model;
+
+    @Value("${zhipu.temperature:0.35}")
+    private double temperature;
+
+    @Value("${zhipu.max-tokens:1200}")
+    private int maxTokens;
+
+    @Value("${zhipu.timeout-ms:15000}")
+    private int timeoutMs;
+
+    private RestTemplate restTemplate;
+
+    public LifecycleAiAnalysisServiceImpl(ILifecycleDatabaseAnalysisService lifecycleDatabaseAnalysisService)
+    {
+        this.lifecycleDatabaseAnalysisService = lifecycleDatabaseAnalysisService;
+    }
+
+    @PostConstruct
+    public void init()
+    {
+        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
+        factory.setConnectTimeout(timeoutMs);
+        factory.setReadTimeout(timeoutMs);
+        this.restTemplate = new RestTemplate(factory);
+    }
+
+    @Override
+    public Map<String, Object> analyze(LifecycleAiAnalysisRequest request)
+    {
+        String entityType = normalizeEntityType(request == null ? null : request.getEntityType());
+        String entityId = request == null ? null : trim(request.getEntityId());
+        if (StringUtils.isEmpty(entityType) || StringUtils.isEmpty(entityId))
+        {
+            return buildErrorFallback(entityType, entityId, "请选择需要分析的SKU或SPU。");
+        }
+
+        Map<String, Object> detail = loadEntityDetail(request, entityType, entityId);
+        if (detail == null || detail.isEmpty())
+        {
+            return buildErrorFallback(entityType, entityId, "未找到该" + entityType.toUpperCase() + "的生命周期分析结果。");
+        }
+
+        Map<String, Object> context = buildContext(request, entityType, entityId, detail);
+        String resolvedKey = resolveApiKey();
+        if (StringUtils.isEmpty(resolvedKey))
+        {
+            return buildFallbackAnalysis(context, true, "未配置大模型API Key,已使用本地规则生成建议。");
+        }
+
+        try
+        {
+            String content = callZhipu(context, resolvedKey);
+            Map<String, Object> parsed = parseAiContent(content);
+            if (parsed == null || parsed.isEmpty())
+            {
+                return buildFallbackAnalysis(context, true, "大模型返回格式无法解析,已使用本地规则生成建议。");
+            }
+            parsed.put("fallback", false);
+            parsed.put("model", model);
+            parsed.put("entityType", entityType);
+            parsed.put("entityId", entityId);
+            parsed.put("generatedAt", now());
+            return normalizeAnalysis(parsed, context);
+        }
+        catch (Exception e)
+        {
+            log.error("Lifecycle AI analysis failed: {}", e.getMessage(), e);
+            return buildFallbackAnalysis(context, true, "大模型调用失败,已使用本地规则生成建议。");
+        }
+    }
+
+    private Map<String, Object> loadEntityDetail(LifecycleAiAnalysisRequest request, String entityType, String entityId)
+    {
+        Map<String, Object> pageData = request == null ? null : request.getLifecycleData();
+        if (pageData != null && !pageData.isEmpty())
+        {
+            return pageData;
+        }
+
+        Map<String, Object> params = new HashMap<>();
+        if (!StringUtils.isEmpty(trim(request.getStartDate())))
+        {
+            params.put("startDate", trim(request.getStartDate()));
+        }
+        if (!StringUtils.isEmpty(trim(request.getEndDate())))
+        {
+            params.put("endDate", trim(request.getEndDate()));
+        }
+
+        Map<String, Object> results = "spu".equals(entityType)
+                ? lifecycleDatabaseAnalysisService.getSpuResults(params)
+                : lifecycleDatabaseAnalysisService.getSkuResults(params);
+        Object detail = results == null ? null : results.get(entityId);
+        if (detail instanceof Map)
+        {
+            return castMap(detail);
+        }
+        return null;
+    }
+
+    private Map<String, Object> buildContext(LifecycleAiAnalysisRequest request, String entityType, String entityId, Map<String, Object> detail)
+    {
+        Map<String, Object> context = new LinkedHashMap<>();
+        context.put("entityType", entityType);
+        context.put("entityName", "spu".equals(entityType) ? "SPU" : "SKU");
+        context.put("entityId", entityId);
+        context.put("productName", value(detail, "details"));
+        context.put("dateRange", buildDateRange(request));
+        context.put("currentStage", value(detail, "current_stage"));
+        context.put("isComplete", value(detail, "is_complete"));
+        context.put("completenessScore", value(detail, "completeness_score"));
+        context.put("totalRevenue", value(detail, "total_revenue"));
+        context.put("totalQuantity", value(detail, "total_quantity"));
+        context.put("peakRevenue", value(detail, "peak_revenue"));
+        context.put("peakRevenueDate", value(detail, "peak_revenue_date"));
+        context.put("peakQuantity", value(detail, "peak_quantity"));
+        context.put("peakQuantityDate", value(detail, "peak_quantity_date"));
+        context.put("stageStatistics", value(detail, "stage_statistics"));
+        context.put("completionDetails", value(detail, "completion_details"));
+        context.put("salesSeries", buildSalesSeries(detail));
+        context.put("generatedAt", now());
+        return context;
+    }
+
+    private Map<String, Object> buildDateRange(LifecycleAiAnalysisRequest request)
+    {
+        Map<String, Object> range = new LinkedHashMap<>();
+        range.put("startDate", request == null ? null : trim(request.getStartDate()));
+        range.put("endDate", request == null ? null : trim(request.getEndDate()));
+        return range;
+    }
+
+    private List<Map<String, Object>> buildSalesSeries(Map<String, Object> detail)
+    {
+        List<?> dates = asList(detail.get("date_series"));
+        List<?> revenue = asList(detail.get("revenue_series"));
+        List<?> quantity = asList(detail.get("quantity_series"));
+        if (dates.isEmpty())
+        {
+            return Collections.emptyList();
+        }
+
+        int max = Math.min(dates.size(), Math.min(revenue.size(), quantity.size()));
+        int step = Math.max(1, (int) Math.ceil(max / 80.0));
+        List<Map<String, Object>> series = new ArrayList<>();
+        for (int i = 0; i < max; i += step)
+        {
+            series.add(buildSalesPoint(dates, revenue, quantity, i));
+        }
+        if ((max - 1) % step != 0)
+        {
+            series.add(buildSalesPoint(dates, revenue, quantity, max - 1));
+        }
+        return series;
+    }
+
+    private Map<String, Object> buildSalesPoint(List<?> dates, List<?> revenue, List<?> quantity, int index)
+    {
+        Map<String, Object> point = new LinkedHashMap<>();
+        point.put("date", dates.get(index));
+        point.put("revenue", revenue.get(index));
+        point.put("quantity", quantity.get(index));
+        return point;
+    }
+
+    private String callZhipu(Map<String, Object> context, String resolvedKey)
+    {
+        JSONArray messages = new JSONArray();
+        messages.add(buildMessage("system", buildSystemPrompt()));
+        messages.add(buildMessage("user", "请基于以下生命周期销售数据输出分析JSON:\n" + JSON.toJSONString(context)));
+
+        JSONObject payload = new JSONObject();
+        payload.put("model", model);
+        payload.put("messages", messages);
+        payload.put("temperature", temperature);
+        payload.put("max_tokens", maxTokens);
+        payload.put("stream", false);
+
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_JSON);
+        headers.setBearerAuth(resolvedKey);
+        HttpEntity<String> requestEntity = new HttpEntity<>(payload.toJSONString(), headers);
+
+        ResponseEntity<String> response;
+        try
+        {
+            response = restTemplate.postForEntity(baseUrl, requestEntity, String.class);
+        }
+        catch (HttpStatusCodeException e)
+        {
+            log.error("Lifecycle AI API error: status={} body={}", e.getStatusCode(), e.getResponseBodyAsString());
+            throw e;
+        }
+        catch (ResourceAccessException e)
+        {
+            log.error("Lifecycle AI API access error: {}", e.getMessage());
+            throw e;
+        }
+
+        if (response == null || response.getBody() == null)
+        {
+            return null;
+        }
+        JSONObject body = JSON.parseObject(response.getBody());
+        JSONArray choices = body.getJSONArray("choices");
+        if (choices == null || choices.isEmpty())
+        {
+            return null;
+        }
+        JSONObject message = choices.getJSONObject(0).getJSONObject("message");
+        if (message == null)
+        {
+            return null;
+        }
+        String content = message.getString("content");
+        return !StringUtils.isEmpty(content) ? content : message.getString("reasoning_content");
+    }
+
+    private String buildSystemPrompt()
+    {
+        return "你是商品生命周期运营分析助手。只能基于用户提供的数据分析,不要编造数值。"
+                + "请输出严格JSON,不要使用Markdown。JSON字段必须包含:"
+                + "summary字符串,currentStageAdvice字符串数组,stageAdvice数组,risks字符串数组,nextActions字符串数组。"
+                + "stageAdvice每项包含stage、analysis、suggestions,其中suggestions是字符串数组。"
+                + "建议应覆盖引入期、成长期、成熟期、衰退期,并结合销售额、销量、峰值、阶段占比、完整性评分。";
+    }
+
+    private JSONObject buildMessage(String role, String content)
+    {
+        JSONObject obj = new JSONObject();
+        obj.put("role", role);
+        obj.put("content", content);
+        return obj;
+    }
+
+    private Map<String, Object> parseAiContent(String content)
+    {
+        if (StringUtils.isEmpty(content))
+        {
+            return null;
+        }
+        String json = extractJson(content);
+        if (StringUtils.isEmpty(json))
+        {
+            return null;
+        }
+        JSONObject obj = JSON.parseObject(json);
+        return obj == null ? null : obj;
+    }
+
+    private String extractJson(String content)
+    {
+        String text = content.trim();
+        if (text.startsWith("```"))
+        {
+            int first = text.indexOf('{');
+            int last = text.lastIndexOf('}');
+            return first >= 0 && last > first ? text.substring(first, last + 1) : "";
+        }
+        if (text.startsWith("{"))
+        {
+            return text;
+        }
+        int first = text.indexOf('{');
+        int last = text.lastIndexOf('}');
+        return first >= 0 && last > first ? text.substring(first, last + 1) : "";
+    }
+
+    private Map<String, Object> normalizeAnalysis(Map<String, Object> analysis, Map<String, Object> context)
+    {
+        if (!(analysis.get("currentStageAdvice") instanceof List))
+        {
+            analysis.put("currentStageAdvice", Collections.emptyList());
+        }
+        if (!(analysis.get("stageAdvice") instanceof List))
+        {
+            analysis.put("stageAdvice", buildFallbackStageAdvice(context));
+        }
+        if (!(analysis.get("risks") instanceof List))
+        {
+            analysis.put("risks", Collections.emptyList());
+        }
+        if (!(analysis.get("nextActions") instanceof List))
+        {
+            analysis.put("nextActions", Collections.emptyList());
+        }
+        if (StringUtils.isEmpty(String.valueOf(analysis.get("summary"))))
+        {
+            analysis.put("summary", buildFallbackSummary(context));
+        }
+        return analysis;
+    }
+
+    private Map<String, Object> buildFallbackAnalysis(Map<String, Object> context, boolean fallback, String message)
+    {
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("summary", buildFallbackSummary(context));
+        result.put("currentStageAdvice", buildCurrentStageAdvice(context));
+        result.put("stageAdvice", buildFallbackStageAdvice(context));
+        result.put("risks", buildRisks(context));
+        result.put("nextActions", buildNextActions(context));
+        result.put("fallback", fallback);
+        result.put("fallbackReason", message);
+        result.put("model", model);
+        result.put("entityType", context.get("entityType"));
+        result.put("entityId", context.get("entityId"));
+        result.put("generatedAt", now());
+        return result;
+    }
+
+    private Map<String, Object> buildErrorFallback(String entityType, String entityId, String message)
+    {
+        Map<String, Object> context = new LinkedHashMap<>();
+        context.put("entityType", entityType);
+        context.put("entityId", entityId);
+        context.put("currentStage", "数据不足");
+        context.put("completenessScore", 0);
+        context.put("totalRevenue", 0);
+        context.put("totalQuantity", 0);
+        return buildFallbackAnalysis(context, true, message);
+    }
+
+    private String buildFallbackSummary(Map<String, Object> context)
+    {
+        String entityName = "spu".equals(context.get("entityType")) ? "SPU" : "SKU";
+        return entityName + " " + context.get("entityId") + " 当前处于" + valueText(context.get("currentStage"))
+                + ",生命周期完整性评分为 " + valueText(context.get("completenessScore"))
+                + ",累计销售额 " + valueText(context.get("totalRevenue"))
+                + ",累计销量 " + valueText(context.get("totalQuantity")) + "。";
+    }
+
+    private List<String> buildCurrentStageAdvice(Map<String, Object> context)
+    {
+        String currentStage = String.valueOf(context.get("currentStage"));
+        if ("引入期".equals(currentStage))
+        {
+            return Arrays.asList("聚焦核心渠道验证转化,避免过早大规模铺货。", "跟踪点击、加购、退款和评价反馈,快速修正标题、价格与主图。");
+        }
+        if ("成长期".equals(currentStage))
+        {
+            return Arrays.asList("放大高转化渠道流量,并按销量增速校准补货节奏。", "监控毛利、评价和履约稳定性,避免增长质量下滑。");
+        }
+        if ("成熟期".equals(currentStage))
+        {
+            return Arrays.asList("维持主推资源,控制折扣频率,守住利润表现。", "通过组合、赠品或会员权益提升复购和客单价。");
+        }
+        if ("衰退期".equals(currentStage))
+        {
+            return Arrays.asList("减少低效投放,将预算迁移到成长或潜力商品。", "结合库存水位设置清仓、套装或尾货策略。");
+        }
+        return Arrays.asList("当前数据不足以判断明确阶段,优先补齐连续销售数据。", "检查订单、产品映射和异常峰值,确认生命周期结果可信。");
+    }
+
+    private List<Map<String, Object>> buildFallbackStageAdvice(Map<String, Object> context)
+    {
+        Map<?, ?> stats = context.get("stageStatistics") instanceof Map ? (Map<?, ?>) context.get("stageStatistics") : Collections.emptyMap();
+        List<Map<String, Object>> rows = new ArrayList<>();
+        for (String stage : STAGES)
+        {
+            Map<String, Object> item = new LinkedHashMap<>();
+            item.put("stage", stage);
+            item.put("analysis", buildStageAnalysis(stage, stats.get(stage)));
+            item.put("suggestions", defaultSuggestions(stage));
+            rows.add(item);
+        }
+        return rows;
+    }
+
+    private String buildStageAnalysis(String stage, Object stageStats)
+    {
+        if (!(stageStats instanceof Map))
+        {
+            return stage + "暂无足够阶段统计,建议结合后续销售数据持续观察。";
+        }
+        Map<?, ?> map = (Map<?, ?>) stageStats;
+        return stage + "销售额占比 " + valueText(map.get("revenuePercentage"))
+                + "%,销量占比 " + valueText(map.get("quantityPercentage"))
+                + "%,持续 " + valueText(map.get("durationDays")) + " 天。";
+    }
+
+    private List<String> defaultSuggestions(String stage)
+    {
+        if ("引入期".equals(stage))
+        {
+            return Arrays.asList("控制首批资源投入,验证价格、标题和渠道转化。", "建立试销补货阈值,防止需求转好后断货。");
+        }
+        if ("成长期".equals(stage))
+        {
+            return Arrays.asList("优先放大高转化渠道,按销量增长滚动更新安全库存。", "跟踪毛利和差评变化,避免用低质量增长换规模。");
+        }
+        if ("成熟期".equals(stage))
+        {
+            return Arrays.asList("稳定核心流量和价格带,减少无效折扣。", "通过组合销售、会员权益或内容复购延长销售窗口。");
+        }
+        return Arrays.asList("削减低效投放并规划清仓节奏。", "复盘衰退原因,沉淀到下一代商品选品和定价。");
+    }
+
+    private List<String> buildRisks(Map<String, Object> context)
+    {
+        List<String> risks = new ArrayList<>();
+        Object complete = context.get("isComplete");
+        if (!Boolean.TRUE.equals(complete))
+        {
+            risks.add("生命周期完整性不足,当前阶段判断可能受数据跨度或趋势缺失影响。");
+        }
+        if ("衰退期".equals(context.get("currentStage")))
+        {
+            risks.add("当前处于衰退期,继续投放可能带来预算效率下降和库存占用。");
+        }
+        if (risks.isEmpty())
+        {
+            risks.add("未发现明显结构性风险,但仍需持续监控销量、毛利和库存联动。");
+        }
+        return risks;
+    }
+
+    private List<String> buildNextActions(Map<String, Object> context)
+    {
+        return Arrays.asList(
+                "按阶段统计复核销售额占比、销量占比和持续天数,确认异常峰值是否由活动或缺货造成。",
+                "结合库存、毛利和渠道投放数据,把当前阶段建议转成补货、投放或清仓动作。",
+                "下次刷新生命周期结果后重新生成AI建议,观察阶段变化和建议是否收敛。"
+        );
+    }
+
+    private String normalizeEntityType(String entityType)
+    {
+        String value = trim(entityType);
+        if ("sku".equalsIgnoreCase(value))
+        {
+            return "sku";
+        }
+        if ("spu".equalsIgnoreCase(value))
+        {
+            return "spu";
+        }
+        return "";
+    }
+
+    private String resolveApiKey()
+    {
+        if (!StringUtils.isEmpty(apiKey))
+        {
+            return apiKey.trim();
+        }
+        String sys = System.getProperty("zhipu.api.key");
+        if (!StringUtils.isEmpty(sys))
+        {
+            return sys.trim();
+        }
+        String env = System.getenv("ZHIPU_API_KEY");
+        if (!StringUtils.isEmpty(env))
+        {
+            return env.trim();
+        }
+        return "";
+    }
+
+    private String now()
+    {
+        return LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
+    }
+
+    private Object value(Map<String, Object> map, String key)
+    {
+        return map == null ? null : map.get(key);
+    }
+
+    private String valueText(Object value)
+    {
+        if (value == null)
+        {
+            return "-";
+        }
+        if (value instanceof BigDecimal)
+        {
+            return ((BigDecimal) value).stripTrailingZeros().toPlainString();
+        }
+        return String.valueOf(value);
+    }
+
+    private String trim(String value)
+    {
+        return value == null ? "" : value.trim();
+    }
+
+    private List<?> asList(Object value)
+    {
+        return value instanceof List ? (List<?>) value : Collections.emptyList();
+    }
+
+    @SuppressWarnings("unchecked")
+    private Map<String, Object> castMap(Object value)
+    {
+        return (Map<String, Object>) value;
+    }
+}

+ 204 - 47
dtm-system/src/main/java/com/dtm/lifecycle/service/impl/LifecycleDatabaseAnalysisServiceImpl.java

@@ -181,9 +181,10 @@ public class LifecycleDatabaseAnalysisServiceImpl implements ILifecycleDatabaseA
             }
 
             long lifecycleDays = values.isEmpty() ? 0L : TimeUnit.MILLISECONDS.toDays(values.get(values.size() - 1).day - values.get(0).day) + 1L;
-            Map<String, StageStats> stageStats = buildStageStats(values, lifecycleDays);
+            List<StageSegment> stageSegments = buildStageSegments(values);
+            Map<String, StageStats> stageStats = buildStageStats(values, stageSegments);
             Map<String, Object> stageStatistics = toStageStatistics(stageStats, totalRevenue, totalQuantity);
-            stagesMap.addAll(buildStagesMap(values, lifecycleDays));
+            stagesMap.addAll(buildStagesMap(values, stageSegments));
 
             Map<String, CompletionHit> completion = buildCompletion(values, stageStats, lifecycleDays, peakRevenueIdx, revenueSeries, quantitySeries);
             int score = 0;
@@ -209,7 +210,7 @@ public class LifecycleDatabaseAnalysisServiceImpl implements ILifecycleDatabaseA
             detail.put("smoothed_revenue", revenueSeries);
             detail.put("smoothed_quantity", quantitySeries);
             detail.put("stage_statistics", stageStatistics);
-            detail.put("stage_boundaries", buildStageBoundaries(values, lifecycleDays));
+            detail.put("stage_boundaries", buildStageBoundaries(values, stageSegments));
             detail.put("stages_map", stagesMap);
             detail.put("total_revenue", totalRevenue.setScale(2, RoundingMode.HALF_UP));
             detail.put("total_quantity", totalQuantity);
@@ -224,7 +225,163 @@ public class LifecycleDatabaseAnalysisServiceImpl implements ILifecycleDatabaseA
             return detail;
         }
 
-        private Map<String, StageStats> buildStageStats(List<DailyValue> values, long lifecycleDays)
+        private List<StageSegment> buildStageSegments(List<DailyValue> values)
+        {
+            List<StageSegment> segments = new ArrayList<>();
+            int length = values == null ? 0 : values.size();
+            if (length == 0)
+            {
+                return segments;
+            }
+            if (length < STAGES.size())
+            {
+                for (int i = 0; i < STAGES.size(); i++)
+                {
+                    int index = Math.min(i, length - 1);
+                    segments.add(new StageSegment(STAGES.get(i), index, index));
+                }
+                return segments;
+            }
+
+            double[] series = buildCompositeSeries(values);
+            double[] smooth = smoothSeries(series);
+            int peakIndex = findPeakIndex(smooth);
+            double peak = smooth[peakIndex];
+            double lowThreshold = peak * 0.35;
+            double highThreshold = peak * 0.72;
+
+            int growthStart = findFirstSustainedIndex(smooth, 0, Math.max(0, peakIndex), lowThreshold, true);
+            if (growthStart <= 0)
+            {
+                growthStart = Math.max(1, Math.min(length - 3, peakIndex / 2));
+            }
+
+            int maturityStart = findFirstSustainedIndex(smooth, growthStart, peakIndex, highThreshold, true);
+            if (maturityStart <= growthStart)
+            {
+                maturityStart = Math.max(growthStart + 1, Math.min(length - 2, peakIndex));
+            }
+
+            int declineStart = findLastSustainedIndex(smooth, peakIndex, length - 1, highThreshold, true) + 1;
+            if (declineStart <= maturityStart)
+            {
+                declineStart = Math.max(maturityStart + 1, Math.min(length - 1, peakIndex + Math.max(1, length / 8)));
+            }
+
+            growthStart = clamp(growthStart, 1, length - 3);
+            maturityStart = clamp(maturityStart, growthStart + 1, length - 2);
+            declineStart = clamp(declineStart, maturityStart + 1, length - 1);
+
+            segments.add(new StageSegment("引入期", 0, growthStart - 1));
+            segments.add(new StageSegment("成长期", growthStart, maturityStart - 1));
+            segments.add(new StageSegment("成熟期", maturityStart, declineStart - 1));
+            segments.add(new StageSegment("衰退期", declineStart, length - 1));
+            return segments;
+        }
+
+        private double[] buildCompositeSeries(List<DailyValue> values)
+        {
+            double maxRevenue = 0.0;
+            double maxQuantity = 0.0;
+            for (DailyValue value : values)
+            {
+                maxRevenue = Math.max(maxRevenue, value.revenue.doubleValue());
+                maxQuantity = Math.max(maxQuantity, value.quantity);
+            }
+            double[] series = new double[values.size()];
+            for (int i = 0; i < values.size(); i++)
+            {
+                DailyValue value = values.get(i);
+                double revenueScore = maxRevenue <= 0.0 ? 0.0 : value.revenue.doubleValue() / maxRevenue;
+                double quantityScore = maxQuantity <= 0.0 ? 0.0 : value.quantity / maxQuantity;
+                series[i] = revenueScore * 0.65 + quantityScore * 0.35;
+            }
+            return series;
+        }
+
+        private double[] smoothSeries(double[] series)
+        {
+            if (series == null || series.length == 0)
+            {
+                return new double[0];
+            }
+            int radius = Math.max(1, Math.min(3, series.length / 12));
+            double[] result = new double[series.length];
+            for (int i = 0; i < series.length; i++)
+            {
+                double total = 0.0;
+                int count = 0;
+                for (int j = Math.max(0, i - radius); j <= Math.min(series.length - 1, i + radius); j++)
+                {
+                    total += series[j];
+                    count++;
+                }
+                result[i] = count == 0 ? series[i] : total / count;
+            }
+            return result;
+        }
+
+        private int findPeakIndex(double[] values)
+        {
+            int index = 0;
+            double max = values.length == 0 ? 0.0 : values[0];
+            for (int i = 1; i < values.length; i++)
+            {
+                if (values[i] > max)
+                {
+                    max = values[i];
+                    index = i;
+                }
+            }
+            return index;
+        }
+
+        private int findFirstSustainedIndex(double[] values, int start, int end, double threshold, boolean defaultToEnd)
+        {
+            if (values.length == 0)
+            {
+                return 0;
+            }
+            int safeStart = clamp(start, 0, values.length - 1);
+            int safeEnd = clamp(end, safeStart, values.length - 1);
+            for (int i = safeStart; i <= safeEnd; i++)
+            {
+                if (values[i] >= threshold && (i == safeEnd || values[Math.min(values.length - 1, i + 1)] >= threshold * 0.9))
+                {
+                    return i;
+                }
+            }
+            return defaultToEnd ? safeEnd : safeStart;
+        }
+
+        private int findLastSustainedIndex(double[] values, int start, int end, double threshold, boolean defaultToStart)
+        {
+            if (values.length == 0)
+            {
+                return 0;
+            }
+            int safeStart = clamp(start, 0, values.length - 1);
+            int safeEnd = clamp(end, safeStart, values.length - 1);
+            for (int i = safeEnd; i >= safeStart; i--)
+            {
+                if (values[i] >= threshold && (i == safeStart || values[Math.max(0, i - 1)] >= threshold * 0.9))
+                {
+                    return i;
+                }
+            }
+            return defaultToStart ? safeStart : safeEnd;
+        }
+
+        private int clamp(int value, int min, int max)
+        {
+            if (max < min)
+            {
+                return min;
+            }
+            return Math.max(min, Math.min(max, value));
+        }
+
+        private Map<String, StageStats> buildStageStats(List<DailyValue> values, List<StageSegment> segments)
         {
             Map<String, StageStats> map = new LinkedHashMap<>();
             for (String stage : STAGES)
@@ -236,47 +393,37 @@ public class LifecycleDatabaseAnalysisServiceImpl implements ILifecycleDatabaseA
                 return map;
             }
 
-            long first = values.get(0).day;
-            long totalDays = Math.max(1L, lifecycleDays);
-            long base = totalDays / 4;
-            long remainder = totalDays % 4;
-            long startOffset = 0L;
-
-            for (int i = 0; i < STAGES.size(); i++)
+            for (StageSegment segment : segments)
             {
-                String stage = STAGES.get(i);
-                StageStats stats = map.get(stage);
-                long duration = base + (i < remainder ? 1 : 0);
-                long endOffset = startOffset + Math.max(1L, duration) - 1L;
-                stats.durationDays = (int) Math.max(1L, duration);
-                stats.startDate = dateFormat.format(new Date(first + TimeUnit.DAYS.toMillis(startOffset)));
-                stats.endDate = dateFormat.format(new Date(first + TimeUnit.DAYS.toMillis(Math.min(totalDays - 1L, endOffset))));
-                startOffset = endOffset + 1L;
+                StageStats stats = map.get(segment.stage);
+                if (stats == null)
+                {
+                    continue;
+                }
+                DailyValue start = values.get(segment.startIndex);
+                DailyValue end = values.get(segment.endIndex);
+                stats.startDate = dateFormat.format(new Date(start.day));
+                stats.endDate = dateFormat.format(new Date(end.day));
+                stats.durationDays = (int) Math.max(1L, TimeUnit.MILLISECONDS.toDays(end.day - start.day) + 1L);
             }
 
-            for (DailyValue value : values)
+            for (int i = 0; i < values.size(); i++)
             {
-                long offset = TimeUnit.MILLISECONDS.toDays(value.day - first);
-                int index = (int) Math.min(3L, Math.max(0L, offset * 4 / totalDays));
-                map.get(STAGES.get(index)).add(value);
+                map.get(stageForIndex(i, segments)).add(values.get(i));
             }
             return map;
         }
 
-        private List<String> buildStagesMap(List<DailyValue> values, long lifecycleDays)
+        private List<String> buildStagesMap(List<DailyValue> values, List<StageSegment> segments)
         {
             List<String> result = new ArrayList<>();
             if (values.isEmpty())
             {
                 return result;
             }
-            long first = values.get(0).day;
-            long totalDays = Math.max(1L, lifecycleDays);
-            for (DailyValue value : values)
+            for (int i = 0; i < values.size(); i++)
             {
-                long offset = TimeUnit.MILLISECONDS.toDays(value.day - first);
-                int index = (int) Math.min(3L, Math.max(0L, offset * 4 / totalDays));
-                result.add(STAGES.get(index));
+                result.add(stageForIndex(i, segments));
             }
             return result;
         }
@@ -302,21 +449,19 @@ public class LifecycleDatabaseAnalysisServiceImpl implements ILifecycleDatabaseA
             return result;
         }
 
-        private List<Map<String, Object>> buildStageBoundaries(List<DailyValue> values, long lifecycleDays)
+        private List<Map<String, Object>> buildStageBoundaries(List<DailyValue> values, List<StageSegment> segments)
         {
             List<Map<String, Object>> result = new ArrayList<>();
-            if (values.size() < 4)
+            if (values.size() < 4 || segments.size() < 2)
             {
                 return result;
             }
-            long first = values.get(0).day;
-            long totalDays = Math.max(1L, lifecycleDays);
-            for (int i = 1; i < STAGES.size(); i++)
+            for (int i = 1; i < segments.size(); i++)
             {
-                long boundaryOffset = totalDays * i / 4;
-                int index = findNearestIndex(values, first + TimeUnit.DAYS.toMillis(boundaryOffset));
+                StageSegment segment = segments.get(i);
+                int index = segment.startIndex;
                 Map<String, Object> item = new HashMap<>();
-                item.put("type", STAGES.get(i) + "开始");
+                item.put("type", segment.stage + "开始");
                 item.put("date", dateFormat.format(new Date(values.get(index).day)));
                 item.put("index", index);
                 result.add(item);
@@ -324,20 +469,16 @@ public class LifecycleDatabaseAnalysisServiceImpl implements ILifecycleDatabaseA
             return result;
         }
 
-        private int findNearestIndex(List<DailyValue> values, long targetDay)
+        private String stageForIndex(int index, List<StageSegment> segments)
         {
-            int index = 0;
-            long diff = Long.MAX_VALUE;
-            for (int i = 0; i < values.size(); i++)
+            for (StageSegment segment : segments)
             {
-                long currentDiff = Math.abs(values.get(i).day - targetDay);
-                if (currentDiff < diff)
+                if (index >= segment.startIndex && index <= segment.endIndex)
                 {
-                    diff = currentDiff;
-                    index = i;
+                    return segment.stage;
                 }
             }
-            return index;
+            return STAGES.get(STAGES.size() - 1);
         }
 
         private Map<String, CompletionHit> buildCompletion(List<DailyValue> values, Map<String, StageStats> stats, long lifecycleDays, int peakRevenueIdx, List<BigDecimal> revenue, List<Long> qty)
@@ -507,6 +648,22 @@ public class LifecycleDatabaseAnalysisServiceImpl implements ILifecycleDatabaseA
         }
     }
 
+    private static class StageSegment
+    {
+        private final String stage;
+
+        private final int startIndex;
+
+        private final int endIndex;
+
+        private StageSegment(String stage, int startIndex, int endIndex)
+        {
+            this.stage = stage;
+            this.startIndex = startIndex;
+            this.endIndex = endIndex;
+        }
+    }
+
     private static class CompletionHit
     {
         private final boolean hit;

+ 532 - 301
dtm-system/src/main/java/com/dtm/upload/service/PreparedDataUploadService.java

@@ -3,26 +3,18 @@ package com.dtm.upload.service;
 import com.dtm.common.utils.StringUtils;
 import com.dtm.storage.util.ExcelSheet;
 import com.dtm.storage.util.ExcelUtils;
-import com.dtm.upload.domain.DtmAssemblyRecord;
-import com.dtm.upload.domain.DtmBomList;
-import com.dtm.upload.domain.DtmOrderMain;
-import com.dtm.upload.domain.DtmProduct;
-import com.dtm.upload.domain.DtmPurchaseReceipt;
-import com.dtm.upload.domain.DtmSemiFinishedProduct;
-import com.dtm.upload.domain.DtmStore;
-import com.dtm.upload.domain.DtmSupplier;
 import org.apache.commons.csv.CSVFormat;
 import org.apache.commons.csv.CSVParser;
 import org.apache.commons.csv.CSVRecord;
+import org.springframework.jdbc.core.JdbcTemplate;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 import org.springframework.web.multipart.MultipartFile;
 
 import java.io.BufferedReader;
 import java.io.InputStreamReader;
 import java.math.BigDecimal;
 import java.nio.charset.Charset;
-import java.nio.charset.StandardCharsets;
-import java.security.MessageDigest;
 import java.sql.Date;
 import java.sql.Timestamp;
 import java.time.LocalDate;
@@ -31,42 +23,24 @@ import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Set;
 
 @Service
 public class PreparedDataUploadService {
     private static final Charset CSV_CHARSET = Charset.forName("GB18030");
     private static final List<String> SUPPORTED_EXTENSIONS = Arrays.asList(".csv", ".xls", ".xlsx");
 
-    private final IDtmSupplierService supplierService;
-    private final IDtmSemiFinishedProductService semiFinishedProductService;
-    private final IDtmProductService productService;
-    private final IDtmBomListService bomListService;
-    private final IDtmPurchaseReceiptService purchaseReceiptService;
-    private final IDtmOrderMainService orderMainService;
-    private final IDtmAssemblyRecordService assemblyRecordService;
-    private final IDtmStoreService storeService;
+    private final JdbcTemplate jdbcTemplate;
     private final Map<String, UploadConfig> configs;
 
-    public PreparedDataUploadService(IDtmSupplierService supplierService,
-                                     IDtmSemiFinishedProductService semiFinishedProductService,
-                                     IDtmProductService productService,
-                                     IDtmBomListService bomListService,
-                                     IDtmPurchaseReceiptService purchaseReceiptService,
-                                     IDtmOrderMainService orderMainService,
-                                     IDtmAssemblyRecordService assemblyRecordService,
-                                     IDtmStoreService storeService) {
-        this.supplierService = supplierService;
-        this.semiFinishedProductService = semiFinishedProductService;
-        this.productService = productService;
-        this.bomListService = bomListService;
-        this.purchaseReceiptService = purchaseReceiptService;
-        this.orderMainService = orderMainService;
-        this.assemblyRecordService = assemblyRecordService;
-        this.storeService = storeService;
+    public PreparedDataUploadService(JdbcTemplate jdbcTemplate) {
+        this.jdbcTemplate = jdbcTemplate;
         this.configs = buildConfigs();
     }
 
@@ -76,32 +50,40 @@ public class PreparedDataUploadService {
             Map<String, Object> item = new LinkedHashMap<>();
             item.put("key", config.key);
             item.put("label", config.label);
+            item.put("tableName", config.tableName);
+            item.put("order", config.order);
+            item.put("dependencies", config.dependencies);
             item.put("replaceBeforeInsert", config.replaceBeforeInsert);
+            item.put("databaseFields", buildDatabaseFields(config));
             items.add(item);
         }
         return items;
     }
 
-    public Map<String, Object> upload(String key, MultipartFile file) {
+    @Transactional
+    public Map<String, Object> upload(String key, MultipartFile file, Map<String, String> requestedMappings) {
         UploadConfig config = configs.get(key);
         validateFile(config, file);
 
         try {
             FileRows fileRows = readRows(file, resolveExtension(file.getOriginalFilename()));
-            List<FieldRequirement> missingFields = getMissingFields(config, fileRows.headers);
-            if (!missingFields.isEmpty()) {
-                throw new IllegalArgumentException("文件缺少必要内容:" + joinFieldLabels(missingFields));
+            MappingValidation validation = validateMappings(config, fileRows.headers, requestedMappings);
+            if (!validation.valid) {
+                throw new IllegalArgumentException("字段映射未完成:" + joinFieldLabels(validation.missingFields));
             }
 
-            int affected = writeRows(config, fileRows.rows);
+            List<Map<String, Object>> normalizedRows = normalizeRows(config, fileRows.rows, validation.mappings);
+            int affected = writeRows(config, normalizedRows, validation.mappings.keySet());
 
             Map<String, Object> result = new LinkedHashMap<>();
             result.put("key", key);
             result.put("label", config.label);
+            result.put("tableName", config.tableName);
             result.put("fileName", file.getOriginalFilename());
-            result.put("totalRows", fileRows.rows.size());
+            result.put("totalRows", normalizedRows.size());
             result.put("affectedRows", affected);
-            result.put("message", config.label + "处理完成,共读取 " + fileRows.rows.size() + " 行");
+            result.put("fieldMappings", validation.mappings);
+            result.put("message", config.label + "处理完成,共读取 " + normalizedRows.size() + " 行");
             return result;
         } catch (IllegalArgumentException e) {
             throw e;
@@ -110,23 +92,33 @@ public class PreparedDataUploadService {
         }
     }
 
-    public Map<String, Object> preview(String key, MultipartFile file) {
+    public Map<String, Object> upload(String key, MultipartFile file) {
+        return upload(key, file, null);
+    }
+
+    public Map<String, Object> preview(String key, MultipartFile file, Map<String, String> requestedMappings) {
         UploadConfig config = configs.get(key);
         validateFile(config, file);
 
         try {
             FileRows fileRows = readRows(file, resolveExtension(file.getOriginalFilename()));
-            List<FieldRequirement> missingFields = getMissingFields(config, fileRows.headers);
+            MappingValidation validation = validateMappings(config, fileRows.headers, requestedMappings);
+
             Map<String, Object> result = new LinkedHashMap<>();
             result.put("key", key);
             result.put("label", config.label);
+            result.put("tableName", config.tableName);
             result.put("fileName", file.getOriginalFilename());
             result.put("totalRows", fileRows.rows.size());
             result.put("headers", fileRows.headers);
             result.put("columns", buildPreviewColumns(fileRows.headers));
             result.put("sampleRows", fileRows.rows.subList(0, Math.min(fileRows.rows.size(), 20)));
-            result.put("missingFields", missingFields);
-            result.put("valid", missingFields.isEmpty());
+            result.put("databaseFields", buildDatabaseFields(config));
+            result.put("fieldMappings", validation.mappings);
+            result.put("fieldStatuses", validation.fieldStatuses);
+            result.put("missingFields", validation.missingFields);
+            result.put("duplicateHeaders", validation.duplicateHeaders);
+            result.put("valid", validation.valid);
             return result;
         } catch (IllegalArgumentException e) {
             throw e;
@@ -135,6 +127,10 @@ public class PreparedDataUploadService {
         }
     }
 
+    public Map<String, Object> preview(String key, MultipartFile file) {
+        return preview(key, file, null);
+    }
+
     private void validateFile(UploadConfig config, MultipartFile file) {
         if (config == null) {
             throw new IllegalArgumentException("这项资料暂未整理完成,暂不支持上传");
@@ -148,11 +144,121 @@ public class PreparedDataUploadService {
         }
     }
 
-    private int writeRows(UploadConfig config, List<Map<String, String>> rows) {
+    private int writeRows(UploadConfig config, List<Map<String, Object>> rows, Set<String> mappedFieldNames) {
+        if (jdbcTemplate == null) {
+            throw new IllegalStateException("数据源未初始化");
+        }
         if (rows.isEmpty()) {
             throw new IllegalArgumentException("文件中没有可导入的数据");
         }
-        return config.writer.write(rows);
+
+        List<FieldDefinition> insertFields = new ArrayList<>();
+        for (FieldDefinition field : config.fields) {
+            if (!field.autoIncrement && mappedFieldNames.contains(field.name)) {
+                insertFields.add(field);
+            }
+        }
+        if (insertFields.isEmpty()) {
+            throw new IllegalArgumentException("没有可写入的字段");
+        }
+
+        if (config.replaceBeforeInsert) {
+            jdbcTemplate.update("delete from `" + config.tableName + "`");
+        }
+
+        String sql = buildInsertSql(config, insertFields);
+        List<Object[]> args = new ArrayList<>();
+        for (Map<String, Object> row : rows) {
+            Object[] values = new Object[insertFields.size()];
+            for (int i = 0; i < insertFields.size(); i++) {
+                values[i] = row.get(insertFields.get(i).name);
+            }
+            args.add(values);
+        }
+
+        int affected = 0;
+        int[] results = jdbcTemplate.batchUpdate(sql, args);
+        for (int result : results) {
+            affected += Math.max(result, 0);
+        }
+        return affected;
+    }
+
+    private String buildInsertSql(UploadConfig config, List<FieldDefinition> fields) {
+        StringBuilder columns = new StringBuilder();
+        StringBuilder placeholders = new StringBuilder();
+        StringBuilder updates = new StringBuilder();
+
+        for (int i = 0; i < fields.size(); i++) {
+            FieldDefinition field = fields.get(i);
+            if (i > 0) {
+                columns.append(", ");
+                placeholders.append(", ");
+            }
+            columns.append("`").append(field.name).append("`");
+            placeholders.append("?");
+
+            if (!field.primaryKey) {
+                if (updates.length() > 0) {
+                    updates.append(", ");
+                }
+                updates.append("`").append(field.name).append("`=values(`").append(field.name).append("`)");
+            }
+        }
+
+        String sql = "insert into `" + config.tableName + "` (" + columns + ") values (" + placeholders + ")";
+        if (updates.length() > 0) {
+            sql += " on duplicate key update " + updates;
+        }
+        return sql;
+    }
+
+    private List<Map<String, Object>> normalizeRows(UploadConfig config,
+                                                    List<Map<String, String>> fileRows,
+                                                    Map<String, String> mappings) {
+        List<Map<String, Object>> rows = new ArrayList<>();
+        int rowNumber = 1;
+        for (Map<String, String> fileRow : fileRows) {
+            Map<String, Object> row = new LinkedHashMap<>();
+            for (FieldDefinition field : config.fields) {
+                String header = mappings.get(field.name);
+                if (StringUtils.isEmpty(header)) {
+                    continue;
+                }
+                String rawValue = clean(fileRow.get(normalizeHeader(header)));
+                if (StringUtils.isEmpty(rawValue) && !field.required) {
+                    row.put(field.name, null);
+                    continue;
+                }
+                if (StringUtils.isEmpty(rawValue) && field.required) {
+                    throw new IllegalArgumentException("第 " + rowNumber + " 行缺少必填内容: " + field.label);
+                }
+                row.put(field.name, convertValue(field, rawValue, rowNumber));
+            }
+            rows.add(row);
+            rowNumber++;
+        }
+        return rows;
+    }
+
+    private Object convertValue(FieldDefinition field, String rawValue, int rowNumber) {
+        try {
+            if ("int".equals(field.valueType) || "bigint".equals(field.valueType)) {
+                return new BigDecimal(rawValue.replace(",", "")).longValue();
+            }
+            if ("decimal".equals(field.valueType)) {
+                return new BigDecimal(rawValue.replace(",", ""));
+            }
+            if ("date".equals(field.valueType)) {
+                return Date.valueOf(parseDate(rawValue));
+            }
+            if ("datetime".equals(field.valueType)) {
+                return parseTimestamp(rawValue);
+            }
+            return limit(rawValue, field.maxLength);
+        } catch (Exception e) {
+            throw new IllegalArgumentException("第 " + rowNumber + " 行字段 " + field.label + " 格式不正确: " + rawValue);
+        }
     }
 
     private FileRows readRows(MultipartFile file, String extension) throws Exception {
@@ -218,54 +324,95 @@ public class PreparedDataUploadService {
         }
     }
 
-    private Map<String, UploadConfig> buildConfigs() {
-        Map<String, UploadConfig> map = new LinkedHashMap<>();
-        map.put("supplier", new UploadConfig("supplier", "供应商资料", false, fields(
-                field("supplier_id", "供应商编号"), field("supplier_name", "供应商名称")),
-                rows -> supplierService.batchSaveDtmSupplier(mapRows(rows, this::mapSupplierRow))));
-        map.put("semi_finished_product", new UploadConfig("semi_finished_product", "半成品资料", false, fields(
-                field("sku", "半成品编码"), field("semi_name", "半成品名称"), field("price", "单价")),
-                rows -> semiFinishedProductService.batchSaveDtmSemiFinishedProduct(mapRows(rows, this::mapSemiFinishedProductRow))));
-        map.put("product", new UploadConfig("product", "产品资料", false, fields(
-                field("sku", "产品编码"), field("product_name", "产品名称"), field("product_price", "产品价格")),
-                rows -> productService.batchSaveDtmProduct(mapRows(rows, this::mapProductRow))));
-        map.put("bom_list", new UploadConfig("bom_list", "组合清单资料", true, fields(
-                field("finished_sku", "成品编码"), field("semi_sku", "半成品编码"), field("quantity", "数量")),
-                rows -> bomListService.replaceDtmBomList(mapRows(rows, this::mapBomListRow))));
-        map.put("purchase_receipt", new UploadConfig("purchase_receipt", "采购入库资料", false, fields(
-                field("receipt_id", "入库单号"), field("product_code", "产品编码"), field("supplier_id", "供应商编号"),
-                field("quantity", "数量"), field("receipt_date", "入库日期")),
-                rows -> purchaseReceiptService.batchSaveDtmPurchaseReceipt(mapRows(rows, this::mapPurchaseReceiptRow))));
-        map.put("order_main", new UploadConfig("order_main", "订单资料", false, fields(
-                field("order_id", "订单编号"), field("price", "单价"), field("quantity", "数量")),
-                rows -> orderMainService.batchSaveDtmOrderMain(mapRows(rows, this::mapOrderMainRow))));
-        map.put("assembly_record", new UploadConfig("assembly_record", "组装记录资料", true, fields(
-                field("assembly_date", "组装日期"), field("product_code", "产品编码"), field("quantity", "数量")),
-                rows -> assemblyRecordService.replaceDtmAssemblyRecord(mapRows(rows, this::mapAssemblyRecordRow))));
-        map.put("store", new UploadConfig("store", "店铺资料", true, fields(
-                field("business_unit_name", "事业部"), field("channel_name", "渠道"), field("platform_name", "店铺名称"),
-                field("stat_year", "年份"), field("stat_month", "月份"), field("stat_day", "日期"),
-                field("quantity", "数量"), field("sales_amount", "销售金额")),
-                rows -> storeService.replaceDtmStore(mapRows(rows, this::mapStoreRow))));
-        return map;
+    private MappingValidation validateMappings(UploadConfig config, List<String> headers, Map<String, String> requestedMappings) {
+        Map<String, String> mappings = requestedMappings == null || requestedMappings.isEmpty()
+                ? recommendMappings(config, headers)
+                : normalizeRequestedMappings(config, headers, requestedMappings);
+        List<FieldRequirement> missingFields = new ArrayList<>();
+        List<Map<String, Object>> fieldStatuses = new ArrayList<>();
+        Set<String> seenHeaders = new HashSet<>();
+        Set<String> duplicateHeaders = new LinkedHashSet<>();
+
+        for (FieldDefinition field : config.fields) {
+            String header = mappings.get(field.name);
+            if (StringUtils.isNotEmpty(header) && !seenHeaders.add(header)) {
+                duplicateHeaders.add(header);
+            }
+        }
+
+        for (FieldDefinition field : config.fields) {
+            String header = mappings.get(field.name);
+            String type = "success";
+            String message = "匹配成功";
+            if (field.required && StringUtils.isEmpty(header)) {
+                missingFields.add(field.requirement());
+                type = "danger";
+                message = "必填未匹配";
+            } else if (StringUtils.isEmpty(header)) {
+                type = "info";
+                message = "选填未匹配";
+            } else if (duplicateHeaders.contains(header)) {
+                type = "danger";
+                message = "重复映射";
+            }
+
+            Map<String, Object> status = new LinkedHashMap<>();
+            status.put("name", field.name);
+            status.put("header", header);
+            status.put("type", type);
+            status.put("message", message);
+            fieldStatuses.add(status);
+        }
+
+        return new MappingValidation(mappings, missingFields, new ArrayList<>(duplicateHeaders), fieldStatuses);
     }
 
-    private List<FieldRequirement> getMissingFields(UploadConfig config, List<String> headers) {
-        List<FieldRequirement> missingFields = new ArrayList<>();
-        for (FieldRequirement field : config.requiredFields) {
-            if (!headers.contains(field.name)) {
-                missingFields.add(field);
+    private Map<String, String> recommendMappings(UploadConfig config, List<String> headers) {
+        Map<String, String> mappings = new LinkedHashMap<>();
+        Set<String> usedHeaders = new HashSet<>();
+        for (FieldDefinition field : config.fields) {
+            String matchedHeader = "";
+            for (String header : headers) {
+                if (usedHeaders.contains(header)) {
+                    continue;
+                }
+                if (field.matches(header)) {
+                    matchedHeader = header;
+                    usedHeaders.add(header);
+                    break;
+                }
+            }
+            if (StringUtils.isNotEmpty(matchedHeader)) {
+                mappings.put(field.name, matchedHeader);
             }
         }
-        return missingFields;
+        return mappings;
     }
 
-    private String joinFieldLabels(List<FieldRequirement> fields) {
-        List<String> labels = new ArrayList<>();
-        for (FieldRequirement field : fields) {
-            labels.add(field.label + "(" + field.name + ")");
+    private Map<String, String> normalizeRequestedMappings(UploadConfig config,
+                                                           List<String> headers,
+                                                           Map<String, String> requestedMappings) {
+        Set<String> allowedFields = new HashSet<>();
+        for (FieldDefinition field : config.fields) {
+            allowedFields.add(field.name);
         }
-        return String.join("、", labels);
+        Set<String> allowedHeaders = new HashSet<>(headers);
+        Map<String, String> mappings = new LinkedHashMap<>();
+        for (Map.Entry<String, String> entry : requestedMappings.entrySet()) {
+            String fieldName = normalizeHeader(entry.getKey());
+            String header = normalizeHeader(entry.getValue());
+            if (StringUtils.isEmpty(header)) {
+                continue;
+            }
+            if (!allowedFields.contains(fieldName)) {
+                throw new IllegalArgumentException("字段不属于当前数据表: " + entry.getKey());
+            }
+            if (!allowedHeaders.contains(header)) {
+                throw new IllegalArgumentException("文件中不存在表头: " + entry.getValue());
+            }
+            mappings.put(fieldName, header);
+        }
+        return mappings;
     }
 
     private List<Map<String, Object>> buildPreviewColumns(List<String> headers) {
@@ -280,234 +427,234 @@ public class PreparedDataUploadService {
         return columns;
     }
 
-    private static List<FieldRequirement> fields(FieldRequirement... fields) {
-        return Arrays.asList(fields);
-    }
-
-    private static FieldRequirement field(String name, String label) {
-        return new FieldRequirement(name, label);
-    }
-
-    private DtmSupplier mapSupplierRow(Map<String, String> row) {
-        DtmSupplier supplier = new DtmSupplier();
-        supplier.setSupplierId(text(row, "supplier_id", 32));
-        supplier.setSupplierName(text(row, "supplier_name", 100));
-        return supplier;
-    }
-
-    private DtmSemiFinishedProduct mapSemiFinishedProductRow(Map<String, String> row) {
-        DtmSemiFinishedProduct product = new DtmSemiFinishedProduct();
-        product.setSku(text(row, "sku", 64));
-        product.setSemiName(text(row, "semi_name", 100));
-        product.setPrice(decimal(row, "price"));
-        product.setBomLevelId(nullableText(row, "bom_level_id", 32));
-        return product;
-    }
-
-    private DtmProduct mapProductRow(Map<String, String> row) {
-        DtmProduct product = new DtmProduct();
-        product.setSku(text(row, "sku", 64));
-        product.setProductName(text(row, "product_name", 128));
-        product.setSpu(nullableText(row, "spu", 64));
-        product.setProductPrice(decimal(row, "product_price"));
-        product.setAttributes(nullableText(row, "attributes", 32));
-        product.setBomLevelId(nullableText(row, "bom_level_id", 32));
-        return product;
-    }
-
-    private DtmBomList mapBomListRow(Map<String, String> row) {
-        DtmBomList bom = new DtmBomList();
-        bom.setFinishedSku(text(row, "finished_sku", 64));
-        bom.setSemiSku(text(row, "semi_sku", 64));
-        bom.setQuantity(integer(row, "quantity"));
-        return bom;
-    }
-
-    private DtmPurchaseReceipt mapPurchaseReceiptRow(Map<String, String> row) {
-        DtmPurchaseReceipt receipt = new DtmPurchaseReceipt();
-        receipt.setReceiptId(text(row, "receipt_id", 32));
-        receipt.setSku(text(row, "product_code", 64));
-        receipt.setSupplierId(text(row, "supplier_id", 32));
-        receipt.setWarehouseCode(nullableText(row, "warehouse_code", 32));
-        receipt.setUnitprice(decimal(row, "unitprice"));
-        receipt.setActualAmount(decimal(row, "actual_amount"));
-        receipt.setQuantity(integer(row, "quantity"));
-        receipt.setReceiptDate(date(row, "receipt_date"));
-        receipt.setStatus(nullableText(row, "status", 16));
-        return receipt;
-    }
-
-    private DtmOrderMain mapOrderMainRow(Map<String, String> row) {
-        DtmOrderMain order = new DtmOrderMain();
-        order.setOrderId(text(row, "order_id", 64));
-        order.setTitle(nullableText(row, "title", 255));
-        order.setPrice(decimal(row, "price"));
-        order.setQuantity(integer(row, "quantity"));
-        order.setSku(nullableText(row, "sku", 32));
-        order.setAttrs(nullableText(row, "attrs", 500));
-        order.setOrderStatus(nullableText(row, "order_status", 32));
-        order.setPayNumber(nullableText(row, "pay_number", 32));
-        order.setPayAmount(decimal(row, "pay_amount"));
-        order.setPaidAmount(decimal(row, "paid_amount"));
-        order.setRefundStatus(nullableText(row, "refund_status", 32));
-        order.setRefundAmount(nullableText(row, "refund_amount", 32));
-        order.setCreateTime(timestamp(row, "create_time"));
-        order.setPayTime(timestamp(row, "pay_time"));
-        return order;
-    }
-
-    private DtmAssemblyRecord mapAssemblyRecordRow(Map<String, String> row) {
-        DtmAssemblyRecord record = new DtmAssemblyRecord();
-        record.setAssemblyDate(date(row, "assembly_date"));
-        record.setProductCode(text(row, "product_code", 64));
-        record.setQuantity(integer(row, "quantity"));
-        return record;
-    }
-
-    private DtmStore mapStoreRow(Map<String, String> row) {
-        String deptName = text(row, "business_unit_name", 32);
-        String channelName = text(row, "channel_name", 32);
-        String storeName = text(row, "platform_name", 32);
-        LocalDate date = dateFromParts(row);
-
-        DtmStore store = new DtmStore();
-        store.setStoreCode(buildStoreCode(row, deptName, channelName, storeName, date));
-        store.setStoreName(storeName);
-        store.setDeptName(deptName);
-        store.setChannelName(channelName);
-        store.setRecordStartDate(Date.valueOf(date));
-        store.setRecordEndDate(Date.valueOf(date));
-        store.setSalesQty(integer(row, "quantity"));
-        store.setSalesAmount(decimal(row, "sales_amount"));
-        return store;
-    }
-
-    private <T> List<T> mapRows(List<Map<String, String>> rows, RowMapper<T> mapper) {
-        List<T> list = new ArrayList<>();
-        for (Map<String, String> row : rows) {
-            T item = mapper.map(row);
-            if (item != null) {
-                list.add(item);
-            }
+    private List<Map<String, Object>> buildDatabaseFields(UploadConfig config) {
+        List<Map<String, Object>> fields = new ArrayList<>();
+        for (FieldDefinition field : config.fields) {
+            Map<String, Object> item = new LinkedHashMap<>();
+            item.put("name", field.name);
+            item.put("label", field.label);
+            item.put("type", field.sqlType);
+            item.put("required", field.required);
+            item.put("primaryKey", field.primaryKey);
+            item.put("autoIncrement", field.autoIncrement);
+            fields.add(item);
         }
-        return list;
+        return fields;
     }
 
-    private LocalDate dateFromParts(Map<String, String> row) {
-        int year = safeInt(value(row, "stat_year"), 1970);
-        int month = safeInt(value(row, "stat_month"), 1);
-        int day = safeInt(value(row, "stat_day"), 1);
-        return LocalDate.of(year, month, day);
+    private Map<String, UploadConfig> buildConfigs() {
+        Map<String, UploadConfig> map = new LinkedHashMap<>();
+        map.put("warehouse", config("warehouse", 1, "dtm_warehouse", "仓库资料", false,
+                fields(
+                        text("warehouse_code", "仓库编码", "varchar(32)", 32, true, true, false, "仓库编号"),
+                        text("warehouse_name", "仓库名称", "varchar(100)", 100, true, false, false),
+                        text("warehouse_type", "仓库类型", "varchar(32)", 32, false, false, false),
+                        text("warehouse_address", "仓库地址", "varchar(200)", 200, false, false, false)
+                )));
+        map.put("supplier", config("supplier", 2, "dtm_supplier", "供应商资料", false,
+                fields(
+                        text("supplier_id", "供应商编号", "varchar(32)", 32, true, true, false, "供应商ID", "供应商编码"),
+                        text("supplier_name", "供应商名称", "varchar(100)", 100, true, false, false)
+                )));
+        map.put("semi_finished_product", config("semi_finished_product", 3, "dtm_semi_finished_product", "半成品资料", false,
+                fields(
+                        text("sku", "半成品SKU", "varchar(64)", 64, true, true, false, "半成品编码"),
+                        text("semi_name", "半成品名称", "varchar(100)", 100, true, false, false),
+                        decimal("price", "半成品单价", "decimal(10,2)", true, false, false, "单价"),
+                        text("bom_level_id", "产品结构层级编号", "varchar(32)", 32, false, false, false)
+                )));
+        map.put("product", config("product", 4, "dtm_product", "产品资料", false,
+                fields(
+                        text("sku", "产品SKU", "varchar(64)", 64, true, true, false, "产品编码", "商品SKU"),
+                        text("spu", "SPU", "varchar(64)", 64, false, false, false),
+                        text("product_name", "产品名称", "varchar(128)", 128, true, false, false, "商品名称"),
+                        text("attributes", "产品属性", "varchar(32)", 32, false, false, false),
+                        text("bom_level_id", "产品结构层级编号", "varchar(32)", 32, false, false, false),
+                        decimal("product_price", "产品价格", "decimal(18,2)", false, false, false, "价格", "单价")
+                )));
+        map.put("product_structure", config("product_structure", 5, "dtm_product_structure", "产品分类资料", false,
+                fields(
+                        text("level_no", "层级编号", "varchar(32)", 32, true, true, false),
+                        text("parent_id", "父类ID", "varchar(32)", 32, false, false, false),
+                        text("parent_name", "父类分类名称", "varchar(100)", 100, false, false, false),
+                        text("category_id", "分类ID", "varchar(32)", 32, true, false, false),
+                        text("level_name", "层级名称", "varchar(100)", 100, true, false, false, "分类名称")
+                )));
+        map.put("store", config("store", 6, "dtm_store", "店铺资料", true,
+                fields(
+                        text("store_code", "店铺编码", "varchar(32)", 32, true, true, false, "店铺代码"),
+                        text("store_name", "店铺名称", "varchar(32)", 32, true, false, false),
+                        text("dept_name", "事业部名称", "varchar(32)", 32, true, false, false, "事业部", "部门"),
+                        text("channel_name", "渠道名称", "varchar(32)", 32, true, false, false, "渠道"),
+                        dateField("record_start_date", "记录开始日期", "date", false, false, false),
+                        dateField("record_end_date", "记录结束日期", "date", false, false, false),
+                        integer("sales_qty", "销量", "int", false, false, false, "销售数量", "数量"),
+                        decimal("sales_amount", "销售额", "decimal(10,2)", false, false, false, "销售金额")
+                )));
+        map.put("structure_rule_setting", config("structure_rule_setting", 7, "dtm_rule_setting", "编码规则资料", false,
+                fields(
+                        integer("rule_id", "规则ID", "bigint", false, true, true),
+                        text("rule_type", "规则对象", "varchar(32)", 32, true, false, false),
+                        text("parent_field", "父编码字段", "varchar(32)", 32, false, false, false),
+                        integer("parent_length", "父编码占用位数", "int", true, false, false),
+                        text("current_level", "当前层级名称", "varchar(32)", 32, true, false, false),
+                        integer("current_length", "当前层级追加位数", "int", true, false, false),
+                        integer("is_enabled", "是否启用", "tinyint", false, false, false),
+                        text("remark", "备注", "varchar(255)", 255, false, false, false)
+                )));
+        map.put("location", config("location", 8, "dtm_location", "库位资料", false,
+                fields(
+                        text("location_code", "库位编码", "varchar(32)", 32, true, true, false),
+                        text("warehouse_code", "仓库编码", "varchar(32)", 32, true, false, false),
+                        text("location_type", "库位类型", "varchar(32)", 32, false, false, false),
+                        integer("capacity_limit", "库位容量限制", "int", false, false, false),
+                        text("location_status", "库位状态", "varchar(20)", 20, false, false, false)
+                ), "warehouse"));
+        map.put("bom_list", config("bom_list", 9, "dtm_bom_list", "组合清单资料", true,
+                fields(
+                        integer("id", "主键ID", "bigint", false, true, true),
+                        text("finished_sku", "成品SKU", "varchar(64)", 64, true, false, false, "成品编码"),
+                        text("semi_sku", "半成品SKU", "varchar(64)", 64, true, false, false, "半成品编码"),
+                        integer("quantity", "数量", "int", true, false, false)
+                ), "product", "semi_finished_product"));
+        map.put("purchase_receipt", config("purchase_receipt", 10, "dtm_purchase_receipt", "采购入库资料", false,
+                fields(
+                        text("receipt_id", "入库单号", "varchar(32)", 32, true, true, false, "单据编号"),
+                        text("sku", "产品SKU", "varchar(64)", 64, true, false, false, "产品编码"),
+                        text("supplier_id", "供应商编号", "varchar(32)", 32, true, false, false),
+                        integer("quantity", "采购入库数量", "int", false, false, false, "数量"),
+                        dateField("receipt_date", "入库日期", "date", false, false, false),
+                        decimal("actual_amount", "实际金额", "decimal(18,2)", false, false, false),
+                        text("status", "状态", "varchar(16)", 16, false, false, false),
+                        text("warehouse_code", "仓库编码", "varchar(32)", 32, false, false, false),
+                        decimal("unitprice", "单价", "decimal(18,2)", false, false, false)
+                ), "product", "supplier"));
+        map.put("order_main", config("order_main", 11, "dtm_order_main", "订单资料", false,
+                fields(
+                        text("order_id", "订单编号", "varchar(64)", 64, true, true, false),
+                        text("title", "商品标题", "varchar(255)", 255, false, false, false),
+                        decimal("price", "商品原价", "decimal(10,2)", false, false, false, "单价"),
+                        integer("quantity", "购买数量", "int", false, false, false, "数量"),
+                        text("sku", "产品代码", "varchar(32)", 32, false, false, false, "产品编码"),
+                        text("attrs", "商品属性", "varchar(500)", 500, false, false, false),
+                        text("order_status", "订单状态", "varchar(32)", 32, false, false, false),
+                        text("pay_number", "支付流水号", "varchar(32)", 32, false, false, false),
+                        decimal("pay_amount", "应付金额", "decimal(10,2)", false, false, false),
+                        decimal("paid_amount", "实付金额", "decimal(10,2)", false, false, false),
+                        text("refund_status", "退款状态", "varchar(32)", 32, false, false, false),
+                        text("refund_amount", "退款金额", "varchar(32)", 32, false, false, false),
+                        datetime("create_time", "订单创建时间", "datetime", false, false, false),
+                        datetime("pay_time", "订单付款时间", "datetime", false, false, false)
+                )));
+        map.put("assembly_record", config("assembly_record", 12, "dtm_assembly_record", "组装记录资料", true,
+                fields(
+                        integer("id", "主键ID", "bigint", false, true, true),
+                        text("product_code", "产品SKU", "varchar(64)", 64, true, false, false, "产品编码"),
+                        dateField("assembly_date", "组装日期", "date", true, false, false),
+                        integer("quantity", "组装数量", "int", true, false, false, "数量")
+                ), "product"));
+        map.put("outbound_order", config("outbound_order", 13, "dtm_outbound_order", "出库资料", false,
+                fields(
+                        text("order_no", "出库单据编号", "varchar(32)", 32, true, true, false, "出库单号"),
+                        text("location_code", "库位编码", "varchar(32)", 32, true, false, false),
+                        text("sku", "产品SKU", "varchar(64)", 64, true, false, false, "产品编码"),
+                        dateField("outbound_date", "出库日期", "date", true, false, false),
+                        integer("quantity", "出库数量", "int", true, false, false, "数量")
+                ), "location", "product"));
+        map.put("inventory_detail", config("inventory_detail", 14, "dtm_stock_detail", "库存资料", false,
+                fields(
+                        text("location_code", "库位编码", "varchar(32)", 32, true, true, false),
+                        text("sku", "产品SKU", "varchar(64)", 64, true, true, false, "产品编码"),
+                        integer("stock_quantity", "可用库存数量", "int", false, false, false, "可用库存", "库存数量"),
+                        integer("lock_quantity", "锁定库存数量", "int", false, false, false, "锁定库存")
+                ), "location", "product"));
+        map.put("metrics", config("metrics", 15, "dtm_metrics", "经营指标资料", false,
+                fields(
+                        integer("metrics_id", "指标ID", "bigint", false, true, true),
+                        text("metrics_type", "指标类型", "varchar(32)", 32, true, false, false),
+                        text("biz_code", "业务编码", "varchar(64)", 64, true, false, false),
+                        dateField("stat_start_date", "统计开始日期", "date", true, false, false, "开始日期"),
+                        dateField("stat_end_date", "统计结束日期", "date", true, false, false, "结束日期"),
+                        integer("total_stock", "库存总量", "int", false, false, false),
+                        decimal("stock_holding_cost", "库存持有成本", "decimal(10,2)", false, false, false),
+                        decimal("stock_turnover_rate", "库存周转率", "decimal(10,4)", false, false, false),
+                        decimal("warehouse_operation_cost", "仓储作业成本", "decimal(10,2)", false, false, false),
+                        decimal("stock_accuracy", "库存准确率", "decimal(5,2)", false, false, false),
+                        decimal("stock_sales_ratio", "库销比", "decimal(10,4)", false, false, false),
+                        decimal("logistics_cost", "物流成本", "decimal(10,2)", false, false, false),
+                        integer("ap_days", "应付账期", "int", false, false, false),
+                        decimal("delivery_score", "交付能力评分", "decimal(5,2)", false, false, false),
+                        decimal("quality_loss", "品质损失费", "decimal(10,2)", false, false, false),
+                        decimal("inspection_fee", "品质检查费", "decimal(10,2)", false, false, false),
+                        decimal("cost_price", "成本价格", "decimal(10,2)", false, false, false),
+                        decimal("reverse_logistics", "逆向物流成本", "decimal(10,2)", false, false, false),
+                        decimal("return_cost", "退货处理成本", "decimal(10,2)", false, false, false),
+                        decimal("platform_value", "平台价值评分", "decimal(10,2)", false, false, false),
+                        decimal("sum_amount", "店铺总销售额", "decimal(10,2)", false, false, false),
+                        decimal("loss_rate", "漏损率", "decimal(5,4)", false, false, false),
+                        decimal("large_order_pct", "大额订单贡献率", "decimal(5,4)", false, false, false),
+                        decimal("funnel_conv", "支付决策漏斗转化率", "decimal(5,4)", false, false, false),
+                        integer("avg_pay_time", "平均支付时长", "int", false, false, false),
+                        text("remark", "备注", "varchar(255)", 255, false, false, false)
+                )));
+        return map;
     }
 
-    private String buildStoreCode(Map<String, String> row, String deptName, String channelName, String storeName, LocalDate date) {
-        String raw = deptName + "|" + channelName + "|" + storeName + "|" + date + "|"
-                + value(row, "product_code") + "|" + value(row, "quantity") + "|" + value(row, "sales_amount");
-        return "ST" + md5(raw).substring(0, 14).toUpperCase(Locale.ROOT);
+    private UploadConfig config(String key, int order, String tableName, String label, boolean replaceBeforeInsert,
+                                List<FieldDefinition> fields, String... dependencies) {
+        return new UploadConfig(key, order, tableName, label, replaceBeforeInsert, fields, Arrays.asList(dependencies));
     }
 
-    private String md5(String raw) {
-        try {
-            MessageDigest digest = MessageDigest.getInstance("MD5");
-            byte[] bytes = digest.digest(raw.getBytes(StandardCharsets.UTF_8));
-            StringBuilder builder = new StringBuilder();
-            for (byte b : bytes) {
-                builder.append(String.format("%02x", b));
-            }
-            return builder.toString();
-        } catch (Exception e) {
-            return Integer.toHexString(raw.hashCode());
-        }
-    }
-
-    private List<String> normalizeHeaders(List<String> headers) {
-        List<String> result = new ArrayList<>();
-        if (headers == null) {
-            return result;
-        }
-        for (String header : headers) {
-            result.add(clean(header).toLowerCase(Locale.ROOT));
-        }
-        return result;
+    private static List<FieldDefinition> fields(FieldDefinition... fields) {
+        return Arrays.asList(fields);
     }
 
-    private static String text(Map<String, String> row, String key, int maxLength) {
-        String value = value(row, key);
-        if (StringUtils.isEmpty(value)) {
-            throw new IllegalArgumentException("缺少必填内容: " + key);
-        }
-        return limit(value, maxLength);
+    private static FieldDefinition text(String name, String label, String sqlType, int maxLength,
+                                        boolean required, boolean primaryKey, boolean autoIncrement, String... aliases) {
+        return new FieldDefinition(name, label, sqlType, "text", maxLength, required, primaryKey, autoIncrement, aliases);
     }
 
-    private static String nullableText(Map<String, String> row, String key, int maxLength) {
-        String value = value(row, key);
-        return StringUtils.isEmpty(value) ? null : limit(value, maxLength);
+    private static FieldDefinition integer(String name, String label, String sqlType,
+                                           boolean required, boolean primaryKey, boolean autoIncrement, String... aliases) {
+        return new FieldDefinition(name, label, sqlType, "int", 0, required, primaryKey, autoIncrement, aliases);
     }
 
-    private static Integer integer(Map<String, String> row, String key) {
-        String value = value(row, key);
-        if (StringUtils.isEmpty(value)) {
-            return null;
-        }
-        return new BigDecimal(value.replace(",", "")).intValue();
+    private static FieldDefinition decimal(String name, String label, String sqlType,
+                                           boolean required, boolean primaryKey, boolean autoIncrement, String... aliases) {
+        return new FieldDefinition(name, label, sqlType, "decimal", 0, required, primaryKey, autoIncrement, aliases);
     }
 
-    private static int safeInt(String value, int fallback) {
-        if (StringUtils.isEmpty(value)) {
-            return fallback;
-        }
-        try {
-            return new BigDecimal(value.replace(",", "")).intValue();
-        } catch (Exception e) {
-            return fallback;
-        }
+    private static FieldDefinition dateField(String name, String label, String sqlType,
+                                             boolean required, boolean primaryKey, boolean autoIncrement, String... aliases) {
+        return new FieldDefinition(name, label, sqlType, "date", 0, required, primaryKey, autoIncrement, aliases);
     }
 
-    private static BigDecimal decimal(Map<String, String> row, String key) {
-        String value = value(row, key);
-        if (StringUtils.isEmpty(value) || "无退款金额".equals(value)) {
-            return null;
-        }
-        return new BigDecimal(value.replace(",", ""));
+    private static FieldDefinition datetime(String name, String label, String sqlType,
+                                            boolean required, boolean primaryKey, boolean autoIncrement, String... aliases) {
+        return new FieldDefinition(name, label, sqlType, "datetime", 0, required, primaryKey, autoIncrement, aliases);
     }
 
-    private static Date date(Map<String, String> row, String key) {
-        String value = value(row, key);
-        if (StringUtils.isEmpty(value)) {
-            return null;
+    private String joinFieldLabels(List<FieldRequirement> fields) {
+        List<String> labels = new ArrayList<>();
+        for (FieldRequirement field : fields) {
+            labels.add(field.label + "(" + field.name + ")");
         }
-        return Date.valueOf(parseDate(value));
+        return String.join("、", labels);
     }
 
-    private static Timestamp timestamp(Map<String, String> row, String key) {
-        String value = value(row, key);
-        if (StringUtils.isEmpty(value)) {
-            return null;
-        }
-        String normalized = value.trim().replace('/', '-');
-        if (normalized.length() <= 10) {
-            return Timestamp.valueOf(LocalDate.parse(normalized, DateTimeFormatter.ofPattern("yyyy-M-d")).atStartOfDay());
+    private List<String> normalizeHeaders(List<String> headers) {
+        List<String> result = new ArrayList<>();
+        if (headers == null) {
+            return result;
         }
-        List<DateTimeFormatter> formatters = Arrays.asList(
-                DateTimeFormatter.ofPattern("yyyy-M-d H:m:s"),
-                DateTimeFormatter.ofPattern("yyyy-M-d H:m")
-        );
-        for (DateTimeFormatter formatter : formatters) {
-            try {
-                return Timestamp.valueOf(LocalDateTime.parse(normalized, formatter));
-            } catch (Exception ignored) {
-            }
+        for (String header : headers) {
+            result.add(normalizeHeader(header));
         }
-        throw new IllegalArgumentException("无法解析时间: " + value);
-    }
-
-    private static LocalDate parseDate(String value) {
-        String normalized = value.trim().replace('/', '-');
-        return LocalDate.parse(normalized, DateTimeFormatter.ofPattern("yyyy-M-d"));
+        return result;
     }
 
-    private static String value(Map<String, String> row, String key) {
-        return clean(row.get(key.toLowerCase(Locale.ROOT)));
+    private static String normalizeHeader(String header) {
+        return clean(header).toLowerCase(Locale.ROOT);
     }
 
     private static String clean(Object value) {
@@ -522,7 +669,7 @@ public class PreparedDataUploadService {
     }
 
     private static String limit(String value, int maxLength) {
-        if (value == null || value.length() <= maxLength) {
+        if (value == null || maxLength <= 0 || value.length() <= maxLength) {
             return value;
         }
         return value.substring(0, maxLength);
@@ -536,12 +683,27 @@ public class PreparedDataUploadService {
         return idx < 0 ? "" : fileName.substring(idx).toLowerCase(Locale.ROOT);
     }
 
-    private interface UploadWriter {
-        int write(List<Map<String, String>> rows);
+    private static LocalDate parseDate(String value) {
+        String normalized = value.trim().replace('/', '-');
+        return LocalDate.parse(normalized, DateTimeFormatter.ofPattern("yyyy-M-d"));
     }
 
-    private interface RowMapper<T> {
-        T map(Map<String, String> row);
+    private static Timestamp parseTimestamp(String value) {
+        String normalized = value.trim().replace('/', '-');
+        if (normalized.length() <= 10) {
+            return Timestamp.valueOf(LocalDate.parse(normalized, DateTimeFormatter.ofPattern("yyyy-M-d")).atStartOfDay());
+        }
+        List<DateTimeFormatter> formatters = Arrays.asList(
+                DateTimeFormatter.ofPattern("yyyy-M-d H:m:s"),
+                DateTimeFormatter.ofPattern("yyyy-M-d H:m")
+        );
+        for (DateTimeFormatter formatter : formatters) {
+            try {
+                return Timestamp.valueOf(LocalDateTime.parse(normalized, formatter));
+            } catch (Exception ignored) {
+            }
+        }
+        throw new IllegalArgumentException("无法解析时间: " + value);
     }
 
     private static class FileRows {
@@ -572,20 +734,89 @@ public class PreparedDataUploadService {
         }
     }
 
+    private static class FieldDefinition {
+        private final String name;
+        private final String label;
+        private final String sqlType;
+        private final String valueType;
+        private final int maxLength;
+        private final boolean required;
+        private final boolean primaryKey;
+        private final boolean autoIncrement;
+        private final List<String> aliases;
+
+        private FieldDefinition(String name, String label, String sqlType, String valueType, int maxLength,
+                                boolean required, boolean primaryKey, boolean autoIncrement, String... aliases) {
+            this.name = name;
+            this.label = label;
+            this.sqlType = sqlType;
+            this.valueType = valueType;
+            this.maxLength = maxLength;
+            this.required = required;
+            this.primaryKey = primaryKey;
+            this.autoIncrement = autoIncrement;
+            this.aliases = new ArrayList<>();
+            this.aliases.add(name);
+            this.aliases.add(label);
+            this.aliases.addAll(Arrays.asList(aliases));
+        }
+
+        private boolean matches(String header) {
+            String normalizedHeader = normalizeMatchText(header);
+            for (String alias : aliases) {
+                if (normalizeMatchText(alias).equals(normalizedHeader)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        private FieldRequirement requirement() {
+            return new FieldRequirement(name, label);
+        }
+
+        private static String normalizeMatchText(String value) {
+            return clean(value).toLowerCase(Locale.ROOT).replace("_", "").replace("-", "").replace(" ", "");
+        }
+    }
+
     private static class UploadConfig {
         private final String key;
+        private final int order;
+        private final String tableName;
         private final String label;
         private final boolean replaceBeforeInsert;
-        private final List<FieldRequirement> requiredFields;
-        private final UploadWriter writer;
+        private final List<FieldDefinition> fields;
+        private final List<String> dependencies;
 
-        private UploadConfig(String key, String label, boolean replaceBeforeInsert,
-                             List<FieldRequirement> requiredFields, UploadWriter writer) {
+        private UploadConfig(String key, int order, String tableName, String label, boolean replaceBeforeInsert,
+                             List<FieldDefinition> fields, List<String> dependencies) {
             this.key = key;
+            this.order = order;
+            this.tableName = tableName;
             this.label = label;
             this.replaceBeforeInsert = replaceBeforeInsert;
-            this.requiredFields = requiredFields;
-            this.writer = writer;
+            this.fields = fields;
+            this.dependencies = dependencies;
+        }
+    }
+
+    private static class MappingValidation {
+        private final Map<String, String> mappings;
+        private final List<FieldRequirement> missingFields;
+        private final List<String> duplicateHeaders;
+        private final List<Map<String, Object>> fieldStatuses;
+        private final boolean valid;
+
+        private MappingValidation(Map<String, String> mappings,
+                                  List<FieldRequirement> missingFields,
+                                  List<String> duplicateHeaders,
+                                  List<Map<String, Object>> fieldStatuses) {
+            this.mappings = mappings;
+            this.missingFields = missingFields;
+            this.duplicateHeaders = duplicateHeaders;
+            this.fieldStatuses = fieldStatuses;
+            this.valid = missingFields.isEmpty() && duplicateHeaders.isEmpty();
         }
     }
 }

BIN
tmpcheck/BOOT-INF/lib/dtm-storage-3.9.0.jar


BIN
tmpcheck2/BOOT-INF/lib/dtm-storage-3.9.0.jar


BIN
tmpcheck3/BOOT-INF/lib/dtm-storage-3.9.0.jar


BIN
tmpcheck4/BOOT-INF/lib/dtm-storage-3.9.0.jar