昨天 d5bbd17a1428811da046ec3be3c0cc943a7ae059
ai数据自动生成
已修改10个文件
1727 ■■■■ 文件已修改
docs/mock_data_check.md 22 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/common/utils/DateUtils.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/mock/prompt/MockDataPrompt.java 229 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/mock/service/impl/DataCheckServiceImpl.java 136 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/mock/service/impl/DataGenerateServiceImpl.java 1205 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/system/domain/SysUser.java 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/system/mapper/SysUserMapper.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/impl/SalesLedgerProductServiceImpl.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application-dev.yml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/system/SysUserMapper.xml 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/mock_data_check.md
@@ -14,11 +14,11 @@
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| modules | List\<String\> | 是 | 要检测的模块列表,可选值:sales(销售)、purchase(采购)、quality(质量) |
| modules | List\<String\> | 是 | 要检测的模块列表,可选值:sales(销售)、purchase(采购)、quality(质量)、production(生产) |
请求体示例:
```json
{ "modules": ["sales", "purchase", "quality"] }
{ "modules": ["sales", "purchase", "quality", "production"] }
```
**响应:**
@@ -68,6 +68,20 @@
| 供应商数据 | 1 | 缺少供应商数据,请先在【基础数据-供应商管理】中添加至少1条供应商 |
| 采购审批模板 | 1 | 缺少采购审批模板,请先在【系统管理-审批模板】中创建采购审批模板 |
### 生产模块 (production)
| 检测项 | 最低数量 | 未通过提示 |
|--------|----------|------------|
| 产品数据 | 1 | 缺少产品数据,请先在【基础数据-产品管理】中添加至少1条产品 |
| 产品规格 | 1 | 缺少产品规格,请先在【基础数据-产品管理】中为产品添加规格型号 |
| 工序 | 1 | 缺少工序,请先在【工艺设计-工序管理】中创建工序 |
| BOM | 1 | 缺少BOM,请先在【工艺设计-BOM管理】中创建BOM |
| BOM产品结构 | 1 | 缺少BOM产品结构,请先在【工艺设计-BOM管理】中为BOM添加产品结构节点 |
| 工艺路线 | 1 | 缺少工艺路线,请先在【工艺设计-工艺路线】中创建工艺路线 |
| 工艺路线工序 | 1 | 缺少工艺路线工序,请先在【工艺设计-工艺路线】中为工艺路线添加工序 |
| 产品-工艺路线关联 | ≥1 | 部分产品规格未关联工艺路线,请先在【工艺设计-工艺路线】中为产品规格创建工艺路线 |
| 工艺路线-工序关联 | ≥1 | 部分工艺路线未添加工序,请先在【工艺设计-工艺路线】中为工艺路线添加工序 |
| BOM-产品结构关联 | ≥1 | 部分BOM未添加产品结构,请先在【工艺设计-BOM管理】中为BOM添加产品结构节点 |
### 质量模块 (quality)
| 检测项 | 最低数量 | 未通过提示 |
|--------|----------|------------|
@@ -91,6 +105,7 @@
        <el-checkbox label="sales">销售模块</el-checkbox>
        <el-checkbox label="purchase">采购模块</el-checkbox>
        <el-checkbox label="quality">质量模块</el-checkbox>
        <el-checkbox label="production">生产模块</el-checkbox>
      </el-checkbox-group>
      <el-button type="primary" @click="handleCheck" :loading="checking">
        开始检测
@@ -128,7 +143,7 @@
```js
data() {
  return {
    selectedModules: ['sales', 'purchase', 'quality'],
    selectedModules: ['sales', 'purchase', 'quality', 'production'],
    checking: false,
    checkResult: null,
  }
@@ -165,3 +180,4 @@
- 检测仅做只读查询,不写入任何数据
- 模块参数为空数组时返回空检测列表
- 建议在数据模拟开始前先调用此接口确认基础数据就绪
- 生产模块除了检测数据量,还会检测关联性(产品-工艺路线、工艺路线-工序、BOM-产品结构),确保工艺设计数据完整
src/main/java/com/ruoyi/common/utils/DateUtils.java
@@ -3,17 +3,13 @@
import java.lang.management.ManagementFactory;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.*;
import java.util.Date;
import org.apache.commons.lang3.time.DateFormatUtils;
/**
 * 时间工具类
 *
 *
 * @author ruoyi
 */
public class DateUtils extends org.apache.commons.lang3.time.DateUtils
@@ -29,13 +25,13 @@
    public static String YYYY_MM_DD_HH_MM_SS = "yyyy-MM-dd HH:mm:ss";
    private static String[] parsePatterns = {
            "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy-MM",
            "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy-MM",
            "yyyy/MM/dd", "yyyy/MM/dd HH:mm:ss", "yyyy/MM/dd HH:mm", "yyyy/MM",
            "yyyy.MM.dd", "yyyy.MM.dd HH:mm:ss", "yyyy.MM.dd HH:mm", "yyyy.MM"};
    /**
     * 获取当前Date型日期
     *
     *
     * @return Date() 当前日期
     */
    public static Date getNowDate()
@@ -45,16 +41,15 @@
    public static LocalDate toLocalDate(Date date){
        if(date == null) return LocalDate.now();
        // 2. 指定固定时区转换(如东八区UTC+8、UTC零时区)
        return date.toInstant()
                .atZone(ZoneId.of("Asia/Shanghai")) // 东八区(北京/上海时区)
                // .atZone(ZoneId.of("UTC")) // 可选:UTC零时区
        // java.sql.Date.toInstant() 会抛 UnsupportedOperationException,需先转为 java.util.Date
        Instant instant = new java.util.Date(date.getTime()).toInstant();
        return instant.atZone(ZoneId.of("Asia/Shanghai"))
                .toLocalDate();
    }
    /**
     * 获取当前日期, 默认格式为yyyy-MM-dd
     *
     *
     * @return String
     */
    public static String getDate()
src/main/java/com/ruoyi/mock/prompt/MockDataPrompt.java
@@ -1,6 +1,9 @@
package com.ruoyi.mock.prompt;
import com.ruoyi.project.system.domain.SysUser;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
@@ -12,18 +15,17 @@
    public static String buildSystemPrompt() {
        return """
            你是一个企业ERP系统的数据模拟专家。你需要根据用户提供的行业、数量、时间范围等信息,
            生成符合业务逻辑的模拟数据。
            你是一个企业ERP系统的数据模拟专家。
            要求:
            核心规则(必须严格遵守):
            1. 输出必须是纯JSON数组,不要用markdown代码块包裹,不要有任何其他文字
            2. 每个JSON对象必须包含 "entity" 字段,值为实体类型名
            3. 数据内容要符合指定行业的特征(公司名称、产品名称、联系人等要像该行业的)
            4. 所有日期字段必须在指定的时间范围内,严禁使用范围之外的日期
            5. 金额、数量等数值字段要合理
            6. 同一模块内的实体之间要有引用关系(如销售台账引用客户名称)
            7. 所有字符串字段不要使用emoji或特殊unicode字符
            8. 合同编号、批号等包含日期的字段,必须使用时间范围内的日期,不要使用示例中的具体日期
            3. 数据内容要符合指定行业的特征
            4. 金额、数量等数值字段要合理
            5. 同一模块内的实体之间要有引用关系
            6. 不要使用emoji或特殊unicode字符
            7. 最重要:日期只从"指定时间范围"中取,绝对不要用其他年份!
            8. 人员字段(如entryPerson、salesman等)必须从"系统用户列表"中选取,根据角色和部门匹配对应模块,不要编造人名!
            """;
    }
@@ -31,29 +33,68 @@
                                           int countMin, int countMax,
                                           String dateStart, String dateEnd,
                                           String additionalInfo,
                                           List<Long> productIds,
                                           List<Long> productModelIds) {
                                           Map<Long, String> productModelIdToCategory,
                                           List<SysUser> systemUsers,
                                           List<String> existingCustomerNames,
                                           List<String> existingSupplierNames) {
        StringBuilder sb = new StringBuilder();
        sb.append("请为以下行业生成模拟ERP业务数据:\n");
        sb.append("- 行业: ").append(String.join("、", industries)).append("\n");
        sb.append("- 数据条数: 每种实体 ").append(countMin).append("-").append(countMax).append(" 条\n");
        sb.append("- 时间范围: ").append(dateStart).append(" ~ ").append(dateEnd).append("\n");
        sb.append("指定行业: ").append(String.join("、", industries)).append("\n");
        sb.append("每种实体生成: ").append(countMin).append("-").append(countMax).append(" 条\n");
        sb.append("指定时间范围: ").append(dateStart).append(" 到 ").append(dateEnd);
        sb.append("(所有日期字段必须在这个范围内!禁止使用2023、2024、2025等任何超出范围的年份!)\n");
        if (additionalInfo != null && !additionalInfo.isBlank()) {
            sb.append("- 补充信息: ").append(additionalInfo).append("\n");
            sb.append("补充信息: ").append(additionalInfo).append("\n");
        }
        sb.append("\n");
        sb.append("可用的产品ID列表: ").append(productIds.stream().map(String::valueOf).collect(Collectors.joining(","))).append("\n");
        sb.append("可用的产品规格ID列表: ").append(productModelIds.stream().map(String::valueOf).collect(Collectors.joining(","))).append("\n");
        sb.append("\n");
        // 按顶级产品分类(成品/原材料/半成品)分组列出产品规格
        Map<String, List<Map.Entry<Long, String>>> grouped = productModelIdToCategory.entrySet().stream()
                .collect(Collectors.groupingBy(
                        e -> categorize(e.getValue()),
                        java.util.LinkedHashMap::new,
                        Collectors.toList()));
        sb.append("需要生成的模块: ").append(String.join("、", modules)).append("\n\n");
        sb.append("=== 产品规格列表 ===\n");
        for (Map.Entry<String, List<Map.Entry<Long, String>>> entry : grouped.entrySet()) {
            String label = entry.getKey();
            List<Map.Entry<Long, String>> items = entry.getValue();
            sb.append("【").append(label).append("】: ");
            sb.append(items.stream()
                    .map(e -> e.getKey() + "(" + e.getValue() + ")")
                    .collect(Collectors.joining(", "))).append("\n");
        }
        sb.append("销售台账的productData只能选【成品类】规格,采购台账只能选【原材料类】规格。\n\n");
        // 系统用户列表(含角色和部门)
        sb.append("=== 系统用户列表(人员字段必须从中选取,不要编造人名)===\n");
        for (SysUser u : systemUsers) {
            String roleNames = u.getRoleNames() != null && !u.getRoleNames().isBlank() ? u.getRoleNames() : "无角色";
            String deptNames = u.getDeptNames() != null && !u.getDeptNames().isBlank() ? u.getDeptNames() : "无部门";
            sb.append(u.getNickName()).append("(角色: ").append(roleNames).append(", 部门: ").append(deptNames).append(")\n");
        }
        sb.append("选择规则:\n");
        sb.append("- 销售模块的人员(entryPerson、salesman)选角色或部门含\"销售\"的用户\n");
        sb.append("- 采购模块的人员选角色或部门含\"采购\"的用户\n");
        sb.append("- 生产模块的人员选角色或部门含\"生产\"的用户\n");
        sb.append("- 质量模块的人员选角色或部门含\"质量\"或\"质检\"的用户\n");
        sb.append("- 仓库模块的人员选角色或部门含\"仓库\"或\"库存\"的用户\n\n");
        if (!existingCustomerNames.isEmpty()) {
            sb.append("=== 已有客户(只能引用,不要生成新customer实体)===\n");
            sb.append(String.join("、", existingCustomerNames)).append("\n\n");
        }
        if (!existingSupplierNames.isEmpty()) {
            sb.append("=== 已有供应商(只能引用,不要生成新supplier实体)===\n");
            sb.append(String.join("、", existingSupplierNames)).append("\n\n");
        }
        sb.append("数据模块: ").append(String.join("、", modules)).append("\n\n");
        if (modules.contains("sales")) {
            sb.append(buildSalesPrompt(countMin, countMax, dateStart, dateEnd));
            sb.append(buildSalesPrompt(countMin, countMax, dateStart, dateEnd, existingCustomerNames));
        }
        if (modules.contains("purchase")) {
            sb.append(buildPurchasePrompt(countMin, countMax, dateStart, dateEnd));
            sb.append(buildPurchasePrompt(countMin, countMax, dateStart, dateEnd, existingSupplierNames));
        }
        if (modules.contains("quality")) {
            sb.append(buildQualityPrompt(countMin, countMax, dateStart));
@@ -65,130 +106,105 @@
            sb.append(buildStockPrompt(countMin, countMax, dateStart));
        }
        sb.append("\n请直接输出JSON数组,不要有任何其他内容:");
        sb.append("\n请直接输出JSON数组:");
        return sb.toString();
    }
    private static String buildSalesPrompt(int min, int max, String dateStart, String dateEnd) {
    /**
     * 将顶级产品名称映射为分类标签
     */
    private static String categorize(String rootProductName) {
        if (rootProductName == null) return "其他类";
        if (rootProductName.contains("成品") || rootProductName.contains("产成品")) return "成品类-销售优先选这些";
        if (rootProductName.contains("原料") || rootProductName.contains("原材料") || rootProductName.contains("材料")) return "原材料类-采购优先选这些";
        if (rootProductName.contains("半成品")) return "半成品类";
        return "其他类";
    }
    private static String buildSalesPrompt(int min, int max, String dateStart, String dateEnd,
                                            List<String> existingCustomerNames) {
        String namesHint = existingCustomerNames.isEmpty() ? ""
                : "customerName必须从以下已有客户中选择: " + String.join("、", existingCustomerNames) + "\n";
        return """
            销售模块 - 按以下格式生成(注意:示例中的日期仅作格式参考,实际日期必须在 %s ~ %s 范围内):
            {
              "entity": "customer",
              "customerName": "XX科技有限公司",
              "customerType": "企业客户",
              "contactPerson": "张三",
              "contactPhone": "13800138000",
              "companyAddress": "XX省XX市XX区XX路XX号",
              "companyPhone": "0513-XXXXXXXX",
              "taxpayerIdentificationNumber": "91110108XXXXXXXXXX",
              "maintainer": "李四",
              "maintenanceTime": "%s",
              "bankAccount": "622202XXXXXXXXXXXX",
              "basicBankAccount": "XX银行XX支行",
              "bankCode": "308100XXXXXX"
            }
            销售模块格式(所有日期必须在 %s ~ %s 范围内):
            %s注意:不要生成customer实体,只生成salesLedger!
            productData中的productModelId必须选【成品类】规格ID!
            entryPerson和salesman必须从系统用户列表中角色或部门含"销售"的用户中选取!
            {
              "entity": "salesLedger",
              "customerName": "引用上面生成的客户名称",
              "salesContractNo": "XS-年月日-序号",
              "salesContractNo": "XS-YYYYMMDD-001",
              "projectName": "XX项目",
              "entryDate": "%s",
              "entryPerson": "张三",
              "salesman": "销售员姓名",
              "entryPerson": "从用户列表中选",
              "salesman": "从用户列表中选",
              "contractAmount": 50000.00,
              "paymentMethod": "月结30天",
              "executionDate": "%s",
              "deliveryDate": "%s",
              "type": 1,
              "productData": [
                {
                  "productId": 1,
                  "productModelId": 1,
                  "quantity": 100,
                  "taxInclusiveUnitPrice": 500.00,
                  "taxInclusiveTotalPrice": 50000.00,
                  "taxExclusiveTotalPrice": 44247.79,
                  "taxRate": 13.00,
                  "unit": "件",
                  "type": 1
                }
              ]
              "productData": [{
                "productId": 1, "productModelId": 1, "quantity": 100,
                "taxInclusiveUnitPrice": 500.00, "taxInclusiveTotalPrice": 50000.00,
                "taxExclusiveTotalPrice": 44247.79, "taxRate": 13.00, "unit": "件", "type": 1
              }]
            }
            客户名称要像指定行业的公司,合同编号格式XS-年月日-序号(年月日取entryDate的日期),金额合理。
            注意 taxExclusiveTotalPrice 是不含税总价,需要根据含税总价和税率计算(=含税总价/(1+税率/100))。
            日期范围: %s ~ %s,每种实体生成%d-%d条。
            """.formatted(dateStart, dateEnd, dateStart, dateStart, dateStart, dateEnd, dateStart, dateEnd, min, max);
            salesContractNo中YYYYMMDD替换为entryDate的日期。%d-%d条。
            """.formatted(dateStart, dateEnd, namesHint, dateStart, dateStart, dateEnd, min, max);
    }
    private static String buildPurchasePrompt(int min, int max, String dateStart, String dateEnd) {
    private static String buildPurchasePrompt(int min, int max, String dateStart, String dateEnd,
                                              List<String> existingSupplierNames) {
        String namesHint = existingSupplierNames.isEmpty() ? ""
                : "supplierName必须从以下已有供应商中选择: " + String.join("、", existingSupplierNames) + "\n";
        return """
            采购模块 - 按以下格式生成(注意:示例中的日期仅作格式参考,实际日期必须在 %s ~ %s 范围内):
            {
              "entity": "supplier",
              "supplierName": "XX原材料有限公司",
              "supplierType": "原材料供应商",
              "contactUserName": "李四",
              "contactUserPhone": "13900139000",
              "companyAddress": "XX省XX市XX区XX路XX号",
              "companyPhone": "0513-XXXXXXXX",
              "taxpayerIdentificationNum": "91110108XXXXXXXXXX",
              "bankAccountName": "XX银行XX支行",
              "bankAccountNum": "622202XXXXXXXXXXXX",
              "isWhite": 0
            }
            采购模块格式(所有日期必须在 %s ~ %s 范围内):
            %s注意:不要生成supplier实体,只生成purchaseLedger!
            productData中的productModelId必须选【原材料类】规格ID!
            人员字段必须从系统用户列表中角色或部门含"采购"的用户中选取!
            {
              "entity": "purchaseLedger",
              "supplierName": "引用上面生成的供应商名称",
              "purchaseContractNumber": "CG-年月日-序号",
              "supplierName": "从已有供应商中选择",
              "purchaseContractNumber": "CG-YYYYMMDD-001",
              "projectName": "XX采购项目",
              "entryDate": "%s",
              "contractAmount": 30000.00,
              "paymentMethod": "货到付款",
              "executionDate": "%s",
              "productData": [
                {
                  "productId": 1,
                  "productModelId": 1,
                  "quantity": 50,
                  "taxInclusiveUnitPrice": 600.00,
                  "taxInclusiveTotalPrice": 30000.00,
                  "taxExclusiveTotalPrice": 26548.67,
                  "taxRate": 13.00,
                  "unit": "件",
                  "type": 2
                }
              ]
              "productData": [{
                "productId": 1, "productModelId": 1, "quantity": 50,
                "taxInclusiveUnitPrice": 600.00, "taxInclusiveTotalPrice": 30000.00,
                "taxExclusiveTotalPrice": 26548.67, "taxRate": 13.00, "unit": "件", "type": 2
              }]
            }
            供应商名称要像指定行业的供应商,合同编号格式CG-年月日-序号(年月日取entryDate的日期)。
            注意 taxExclusiveTotalPrice 是不含税总价,需要根据含税总价和税率计算(=含税总价/(1+税率/100))。
            日期范围: %s ~ %s,每种实体生成%d-%d条。
            """.formatted(dateStart, dateEnd, dateStart, dateStart, dateStart, dateEnd, min, max);
            purchaseContractNumber中YYYYMMDD替换为entryDate的日期。%d-%d条。
            """.formatted(dateStart, dateEnd, namesHint, dateStart, dateStart, min, max);
    }
    private static String buildQualityPrompt(int min, int max, String dateStart) {
        return """
            质量模块 - 按以下格式生成:
            质量模块格式:
            {
              "entity": "qualityTestStandard",
              "standardNo": "QTS-年月日-序号",
              "standardNo": "QTS-YYYYMMDD-001",
              "standardName": "XX产品检验标准",
              "inspectType": 0,
              "remark": "适用于XX行业的质量检验标准"
            }
            inspectType: 0=原材料检验, 1=过程检验, 2=出厂检验。三种类型都要覆盖。
            standardNo中的年月日使用 %s 这个日期。
            standardNo中YYYYMMDD替换为 %s。%d-%d条。
            {
              "entity": "qualityTestStandardBinding",
              "productId": 1,
              "testStandardId": 1
            }
            每种实体生成%d-%d条。
            """.formatted(dateStart, min, max);
            %d-%d条。
            """.formatted(dateStart.replace("-", ""), min, max, min, max);
    }
    private static String buildProductionPrompt(int min, int max, String dateStart, String dateEnd) {
        return """
            生产模块 - 按以下格式生成(注意:示例中的日期仅作格式参考,实际日期必须在 %s ~ %s 范围内):
            生产模块格式(所有日期必须在 %s ~ %s 范围内):
            {
              "entity": "productionPlan",
              "productModelId": 1,
@@ -205,23 +221,22 @@
              "planCompleteTime": "%s",
              "remark": "根据生产计划XX生成"
            }
            productionPlan的source可选"销售"或"内部"。
            日期范围: %s ~ %s,每种实体生成%d-%d条。
            """.formatted(dateStart, dateEnd, dateStart, dateEnd, dateEnd, dateStart, dateEnd, min, max);
            source可选"销售"或"内部"。%d-%d条。
            """.formatted(dateStart, dateEnd, dateStart, dateEnd, dateEnd, min, max);
    }
    private static String buildStockPrompt(int min, int max, String dateStart) {
        return """
            库存模块 - 按以下格式生成:
            库存模块格式:
            {
              "entity": "stockInventory",
              "productModelId": 1,
              "qualitity": 500,
              "batchNo": "BATCH-年月日-序号",
              "batchNo": "BATCH-YYYYMMDD-001",
              "warnNum": 50,
              "remark": "安全库存"
            }
            每种实体生成%d-%d条。batchNo格式BATCH-年月日-序号,年月日使用 %s 这个日期。
            """.formatted(min, max, dateStart);
            batchNo中YYYYMMDD替换为 %s。%d-%d条。
            """.formatted(dateStart.replace("-", ""), min, max);
    }
}
src/main/java/com/ruoyi/mock/service/impl/DataCheckServiceImpl.java
@@ -5,14 +5,26 @@
import com.ruoyi.approve.mapper.ApprovalTemplateMapper;
import com.ruoyi.basic.mapper.CustomerMapper;
import com.ruoyi.basic.mapper.ProductMapper;
import com.ruoyi.basic.mapper.ProductModelMapper;
import com.ruoyi.basic.mapper.SupplierManageMapper;
import com.ruoyi.common.enums.TypeEnums;
import com.ruoyi.mock.dto.DataCheckRequest;
import com.ruoyi.mock.service.DataCheckService;
import com.ruoyi.mock.vo.DataCheckResult;
import com.ruoyi.mock.vo.DataCheckResult.CheckItem;
import com.ruoyi.project.system.domain.SysUser;
import com.ruoyi.project.system.mapper.SysUserMapper;
import com.ruoyi.quality.mapper.QualityTestStandardBindingMapper;
import com.ruoyi.quality.mapper.QualityTestStandardMapper;
import com.ruoyi.technology.mapper.TechnologyBomMapper;
import com.ruoyi.technology.mapper.TechnologyBomStructureMapper;
import com.ruoyi.technology.mapper.TechnologyOperationMapper;
import com.ruoyi.technology.mapper.TechnologyRoutingMapper;
import com.ruoyi.technology.mapper.TechnologyRoutingOperationMapper;
import com.ruoyi.technology.pojo.TechnologyBom;
import com.ruoyi.technology.pojo.TechnologyBomStructure;
import com.ruoyi.technology.pojo.TechnologyRouting;
import com.ruoyi.technology.pojo.TechnologyRoutingOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@@ -26,11 +38,18 @@
public class DataCheckServiceImpl implements DataCheckService {
    private final ProductMapper productMapper;
    private final ProductModelMapper productModelMapper;
    private final CustomerMapper customerMapper;
    private final SupplierManageMapper supplierManageMapper;
    private final ApprovalTemplateMapper approvalTemplateMapper;
    private final QualityTestStandardMapper qualityTestStandardMapper;
    private final QualityTestStandardBindingMapper qualityTestStandardBindingMapper;
    private final TechnologyRoutingMapper technologyRoutingMapper;
    private final TechnologyRoutingOperationMapper technologyRoutingOperationMapper;
    private final TechnologyOperationMapper technologyOperationMapper;
    private final TechnologyBomMapper technologyBomMapper;
    private final TechnologyBomStructureMapper technologyBomStructureMapper;
    private final SysUserMapper sysUserMapper;
    @Override
    public DataCheckResult check(DataCheckRequest request) {
@@ -43,6 +62,7 @@
        if (!modules.isEmpty()) {
            addProductCheck(itemMap);
            addUserChecks(itemMap);
        }
        for (String module : modules) {
@@ -50,6 +70,7 @@
                case "sales" -> addSalesChecks(itemMap);
                case "purchase" -> addPurchaseChecks(itemMap);
                case "quality" -> addQualityChecks(itemMap);
                case "production" -> addProductionChecks(itemMap);
            }
        }
@@ -70,6 +91,34 @@
        long count = productMapper.selectCount(null);
        itemMap.put(key, buildItem("common", "产品数据", 1, (int) count,
                "缺少产品数据,请先在【基础数据-产品管理】中添加至少1条产品"));
    }
    private void addUserChecks(Map<String, CheckItem> itemMap) {
        // 1. 系统用户
        String userKey = "common:系统用户";
        if (!itemMap.containsKey(userKey)) {
            List<SysUser> users = sysUserMapper.selectUserListWithDetail();
            itemMap.put(userKey, buildItem("common", "系统用户", 1, (int) users.size(),
                    "缺少系统用户,请先在【系统管理-用户管理】中添加用户"));
        }
        // 2. 有角色的用户
        String userWithRoleKey = "common:用户角色配置";
        if (!itemMap.containsKey(userWithRoleKey)) {
            List<SysUser> users = sysUserMapper.selectUserListWithDetail();
            long withRole = users.stream().filter(u -> u.getRoleNames() != null && !u.getRoleNames().isBlank()).count();
            itemMap.put(userWithRoleKey, buildItem("common", "用户角色配置", 1, (int) withRole,
                    "没有用户分配角色,请先在【系统管理-用户管理】中为用户分配角色"));
        }
        // 3. 有部门的用户
        String userWithDeptKey = "common:用户部门配置";
        if (!itemMap.containsKey(userWithDeptKey)) {
            List<SysUser> users = sysUserMapper.selectUserListWithDetail();
            long withDept = users.stream().filter(u -> u.getDeptNames() != null && !u.getDeptNames().isBlank()).count();
            itemMap.put(userWithDeptKey, buildItem("common", "用户部门配置", 1, (int) withDept,
                    "没有用户分配部门,请先在【系统管理-用户管理】中为用户分配部门"));
        }
    }
    private void addSalesChecks(Map<String, CheckItem> itemMap) {
@@ -133,6 +182,93 @@
        }
    }
    private void addProductionChecks(Map<String, CheckItem> itemMap) {
        // 1. 产品规格
        String modelKey = "production:产品规格";
        if (!itemMap.containsKey(modelKey)) {
            long count = productModelMapper.selectCount(null);
            itemMap.put(modelKey, buildItem("production", "产品规格", 1, (int) count,
                    "缺少产品规格,请先在【基础数据-产品管理】中为产品添加规格型号"));
        }
        // 2. 工序
        String operationKey = "production:工序";
        if (!itemMap.containsKey(operationKey)) {
            long count = technologyOperationMapper.selectCount(null);
            itemMap.put(operationKey, buildItem("production", "工序", 1, (int) count,
                    "缺少工序,请先在【工艺设计-工序管理】中创建工序"));
        }
        // 3. BOM
        String bomKey = "production:BOM";
        if (!itemMap.containsKey(bomKey)) {
            long count = technologyBomMapper.selectCount(null);
            itemMap.put(bomKey, buildItem("production", "BOM", 1, (int) count,
                    "缺少BOM,请先在【工艺设计-BOM管理】中创建BOM"));
        }
        // 4. BOM产品结构
        String bomStructureKey = "production:BOM产品结构";
        if (!itemMap.containsKey(bomStructureKey)) {
            long count = technologyBomStructureMapper.selectCount(null);
            itemMap.put(bomStructureKey, buildItem("production", "BOM产品结构", 1, (int) count,
                    "缺少BOM产品结构,请先在【工艺设计-BOM管理】中为BOM添加产品结构节点"));
        }
        // 5. 工艺路线
        String routingKey = "production:工艺路线";
        if (!itemMap.containsKey(routingKey)) {
            long count = technologyRoutingMapper.selectCount(null);
            itemMap.put(routingKey, buildItem("production", "工艺路线", 1, (int) count,
                    "缺少工艺路线,请先在【工艺设计-工艺路线】中创建工艺路线"));
        }
        // 6. 工艺路线工序
        String routingOpKey = "production:工艺路线工序";
        if (!itemMap.containsKey(routingOpKey)) {
            long count = technologyRoutingOperationMapper.selectCount(null);
            itemMap.put(routingOpKey, buildItem("production", "工艺路线工序", 1, (int) count,
                    "缺少工艺路线工序,请先在【工艺设计-工艺路线】中为工艺路线添加工序"));
        }
        // 7. 关联性检测:有产品规格但无工艺路线的
        String linkageKey = "production:产品-工艺路线关联";
        if (!itemMap.containsKey(linkageKey)) {
            long modelCount = productModelMapper.selectCount(null);
            long routedCount = technologyRoutingMapper.selectCount(
                    new LambdaQueryWrapper<TechnologyRouting>()
                            .isNotNull(TechnologyRouting::getProductModelId)
                            .gt(TechnologyRouting::getProductModelId, 0));
            itemMap.put(linkageKey, buildItem("production", "产品-工艺路线关联",
                    (int) Math.min(modelCount, 1), (int) routedCount,
                    "部分产品规格未关联工艺路线,请先在【工艺设计-工艺路线】中为产品规格创建工艺路线"));
        }
        // 8. 关联性检测:有工艺路线但无工序的
        String routingOpLinkKey = "production:工艺路线-工序关联";
        if (!itemMap.containsKey(routingOpLinkKey)) {
            long routingCount = technologyRoutingMapper.selectCount(null);
            long routedOpCount = technologyRoutingOperationMapper.selectCount(
                    new LambdaQueryWrapper<TechnologyRoutingOperation>()
                            .gt(TechnologyRoutingOperation::getTechnologyRoutingId, 0));
            itemMap.put(routingOpLinkKey, buildItem("production", "工艺路线-工序关联",
                    (int) Math.min(routingCount, 1), (int) routedOpCount,
                    "部分工艺路线未添加工序,请先在【工艺设计-工艺路线】中为工艺路线添加工序"));
        }
        // 9. 关联性检测:有BOM但无产品结构的
        String bomStructureLinkKey = "production:BOM-产品结构关联";
        if (!itemMap.containsKey(bomStructureLinkKey)) {
            long bomCount = technologyBomMapper.selectCount(null);
            long structureCount = technologyBomStructureMapper.selectCount(
                    new LambdaQueryWrapper<TechnologyBomStructure>()
                            .gt(TechnologyBomStructure::getBomId, 0));
            itemMap.put(bomStructureLinkKey, buildItem("production", "BOM-产品结构关联",
                    (int) Math.min(bomCount, 1), (int) structureCount,
                    "部分BOM未添加产品结构,请先在【工艺设计-BOM管理】中为BOM添加产品结构节点"));
        }
    }
    private CheckItem buildItem(String module, String itemName, int minRequired, int currentCount, String failMessage) {
        CheckItem item = new CheckItem();
        item.setModule(module);
src/main/java/com/ruoyi/mock/service/impl/DataGenerateServiceImpl.java
@@ -5,27 +5,51 @@
import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.ai.assistant.Assistant;
import com.ruoyi.approve.mapper.ApprovalInstanceMapper;
import com.ruoyi.approve.mapper.ApprovalInstanceNodeMapper;
import com.ruoyi.approve.mapper.ApprovalRecordMapper;
import com.ruoyi.approve.mapper.ApprovalTaskMapper;
import com.ruoyi.approve.pojo.ApprovalInstance;
import com.ruoyi.approve.pojo.ApprovalInstanceNode;
import com.ruoyi.approve.pojo.ApprovalRecord;
import com.ruoyi.approve.pojo.ApprovalTask;
import com.ruoyi.approve.service.ApprovalInstanceService;
import com.ruoyi.basic.mapper.ProductMapper;
import com.ruoyi.basic.mapper.ProductModelMapper;
import com.ruoyi.basic.pojo.Customer;
import com.ruoyi.basic.pojo.Product;
import com.ruoyi.basic.pojo.ProductModel;
import com.ruoyi.basic.pojo.SupplierManage;
import com.ruoyi.basic.service.ICustomerService;
import com.ruoyi.basic.service.ISupplierService;
import com.ruoyi.common.enums.ReviewStatusEnum;
import com.ruoyi.common.enums.StockInQualifiedRecordTypeEnum;
import com.ruoyi.common.enums.StockOutQualifiedRecordTypeEnum;
import com.ruoyi.common.utils.OrderUtils;
import com.ruoyi.mock.dto.DataGenerateRequest;
import com.ruoyi.mock.prompt.MockDataPrompt;
import com.ruoyi.mock.service.DataGenerateService;
import com.ruoyi.mock.vo.DataGenerateResult;
import com.ruoyi.mock.vo.DataGenerateResult.ModuleSummary;
import com.ruoyi.production.pojo.ProductionOrder;
import com.ruoyi.production.pojo.ProductionPlan;
import com.ruoyi.procurementrecord.utils.StockUtils;
import com.ruoyi.production.bean.dto.ProductionOrderPickDto;
import com.ruoyi.production.bean.dto.ProductionPlanDto;
import com.ruoyi.production.bean.dto.ProductionProductMainDto;
import com.ruoyi.production.bean.vo.ProductionBomStructureVo;
import com.ruoyi.production.mapper.*;
import com.ruoyi.production.pojo.*;
import com.ruoyi.production.service.ProductionOrderPickService;
import com.ruoyi.production.service.ProductionOrderService;
import com.ruoyi.production.service.ProductionPlanService;
import com.ruoyi.production.service.ProductionProductMainService;
import com.ruoyi.purchase.dto.PurchaseLedgerDto;
import com.ruoyi.purchase.pojo.PurchaseLedger;
import com.ruoyi.purchase.service.IPurchaseLedgerService;
import com.ruoyi.project.system.domain.SysDept;
import com.ruoyi.project.system.domain.SysUser;
import com.ruoyi.project.system.mapper.SysDeptMapper;
import com.ruoyi.project.system.mapper.SysUserDeptMapper;
import com.ruoyi.project.system.mapper.SysUserMapper;
import com.ruoyi.quality.mapper.QualityInspectMapper;
import com.ruoyi.quality.pojo.QualityInspect;
import com.ruoyi.quality.pojo.QualityTestStandard;
import com.ruoyi.quality.pojo.QualityTestStandardBinding;
@@ -33,11 +57,29 @@
import com.ruoyi.quality.service.IQualityTestStandardService;
import com.ruoyi.quality.service.QualityTestStandardBindingService;
import com.ruoyi.sales.dto.SalesLedgerDto;
import com.ruoyi.sales.dto.ShippingInfoDto;
import com.ruoyi.sales.mapper.SalesLedgerMapper;
import com.ruoyi.sales.mapper.SalesLedgerProductMapper;
import com.ruoyi.sales.mapper.ShippingInfoMapper;
import com.ruoyi.sales.mapper.ShippingProductDetailMapper;
import com.ruoyi.sales.pojo.SalesLedger;
import com.ruoyi.sales.pojo.SalesLedgerProduct;
import com.ruoyi.sales.pojo.ShippingInfo;
import com.ruoyi.sales.pojo.ShippingProductDetail;
import com.ruoyi.sales.service.ISalesLedgerService;
import com.ruoyi.sales.service.ShippingInfoService;
import com.ruoyi.stock.mapper.StockInRecordMapper;
import com.ruoyi.stock.mapper.StockInventoryMapper;
import com.ruoyi.stock.mapper.StockOutRecordMapper;
import com.ruoyi.stock.pojo.StockInventory;
import com.ruoyi.stock.pojo.StockOutRecord;
import com.ruoyi.stock.dto.StockInventoryDto;
import com.ruoyi.stock.service.StockInRecordService;
import com.ruoyi.stock.service.StockOutRecordService;
import com.ruoyi.stock.service.StockInventoryService;
import com.ruoyi.technology.mapper.TechnologyBomStructureMapper;
import com.ruoyi.technology.mapper.TechnologyRoutingMapper;
import com.ruoyi.technology.mapper.TechnologyRoutingOperationMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@@ -46,11 +88,15 @@
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;
@@ -76,8 +122,43 @@
    // 采购完整流程需要的service
    private final ApprovalInstanceService approvalInstanceService;
    private final ApprovalInstanceMapper approvalInstanceMapper;
    private final ApprovalInstanceNodeMapper approvalInstanceNodeMapper;
    private final ApprovalTaskMapper approvalTaskMapper;
    private final ApprovalRecordMapper approvalRecordMapper;
    private final IQualityInspectService qualityInspectService;
    private final QualityInspectMapper qualityInspectMapper;
    private final StockInRecordService stockInRecordService;
    private final StockInRecordMapper stockInRecordMapper;
    // 销售完整流程需要的service/mapper
    private final SalesLedgerMapper salesLedgerMapper;
    private final SalesLedgerProductMapper salesLedgerProductMapper;
    private final ProductionPlanMapper productionPlanMapper;
    private final ProductionOrderMapper productionOrderMapper;
    private final ProductionOperationTaskMapper productionOperationTaskMapper;
    private final ProductionProductMainService productionProductMainService;
    private final ShippingInfoService shippingInfoService;
    private final ShippingInfoMapper shippingInfoMapper;
    private final ShippingProductDetailMapper shippingProductDetailMapper;
    private final StockUtils stockUtils;
    private final StockOutRecordMapper stockOutRecordMapper;
    private final StockOutRecordService stockOutRecordService;
    private final StockInventoryMapper stockInventoryMapper;
    // 工艺数据检测
    private final TechnologyRoutingMapper technologyRoutingMapper;
    private final TechnologyRoutingOperationMapper technologyRoutingOperationMapper;
    private final TechnologyBomStructureMapper technologyBomStructureMapper;
    // 领料
    private final ProductionOrderPickService productionOrderPickService;
    private final ProductionOrderBomMapper productionOrderBomMapper;
    private final ProductionBomStructureMapper productionBomStructureMapper;
    // 用户/角色查询
    private final SysUserMapper sysUserMapper;
    private final SysDeptMapper sysDeptMapper;
    private final SysUserDeptMapper sysUserDeptMapper;
    @Override
    public DataGenerateResult generate(DataGenerateRequest request) {
@@ -87,10 +168,18 @@
        int totalGenerated = 0;
        try {
            List<Long> productIds = productMapper.selectList(null).stream()
                    .map(p -> p.getId()).collect(Collectors.toList());
            List<Long> productModelIds = productModelMapper.selectList(null).stream()
                    .map(m -> m.getId()).collect(Collectors.toList());
            // 构建产品规格分类:通过Product树根节点判断成品/原材料
            Map<Long, String> productModelIdToCategory = buildProductModelCategoryMap();
            List<Long> productModelIds = new ArrayList<>(productModelIdToCategory.keySet());
            // 加载已有客户/供应商(不新增,使用已有数据)
            Map<String, Long> customerNameToId = loadExistingCustomers();
            Map<String, Long> supplierNameToId = loadExistingSuppliers();
            List<String> existingCustomerNames = new ArrayList<>(customerNameToId.keySet());
            List<String> existingSupplierNames = new ArrayList<>(supplierNameToId.keySet());
            // 加载系统用户信息(昵称、角色、部门)
            List<SysUser> systemUsers = loadSystemUsersWithDetail();
            String systemPrompt = MockDataPrompt.buildSystemPrompt();
            String userMessage = MockDataPrompt.buildUserMessage(
@@ -98,7 +187,9 @@
                    request.getCountMin(), request.getCountMax(),
                    request.getDateStart(), request.getDateEnd(),
                    request.getAdditionalInfo(),
                    productIds, productModelIds);
                    productModelIdToCategory,
                    systemUsers,
                    existingCustomerNames, existingSupplierNames);
            String fullPrompt = systemPrompt + "\n\n" + userMessage;
            log.info("调用AI生成模拟数据, modules={}, industries={}", request.getModules(), request.getIndustries());
@@ -120,24 +211,17 @@
                return result;
            }
            // 修正AI生成的日期到指定时间范围内
            String dateStart = request.getDateStart();
            String dateEnd = request.getDateEnd();
            if (dateStart != null && dateEnd != null) {
                fixDatesInRange(entities, dateStart, dateEnd);
            }
            Map<String, List<JSONObject>> grouped = entities.stream()
                    .collect(Collectors.groupingBy(e -> e.getString("entity")));
            // 名称→ID 映射表,用于创建业务单据时回填外键
            Map<String, Long> customerNameToId = new HashMap<>();
            Map<String, Long> supplierNameToId = new HashMap<>();
            // Tier 0: 基础数据
            if (grouped.containsKey("customer")) {
                ModuleSummary s = createCustomers(grouped.get("customer"), customerNameToId);
                summaries.add(s);
                totalGenerated += s.getSuccessCount();
            }
            if (grouped.containsKey("supplier")) {
                ModuleSummary s = createSuppliers(grouped.get("supplier"), supplierNameToId);
                summaries.add(s);
                totalGenerated += s.getSuccessCount();
            }
            // Tier 0: 基础数据(客户和供应商不自动生成,使用已有数据)
            if (grouped.containsKey("qualityTestStandard")) {
                ModuleSummary s = createQualityStandards(grouped.get("qualityTestStandard"));
                summaries.add(s);
@@ -146,13 +230,13 @@
            // Tier 1: 业务单据
            if (grouped.containsKey("salesLedger")) {
                ModuleSummary s = createSalesLedgers(grouped.get("salesLedger"), customerNameToId);
                ModuleSummary s = createSalesLedgers(grouped.get("salesLedger"), customerNameToId, request.getDateStart());
                summaries.add(s);
                totalGenerated += s.getSuccessCount();
            }
            if (grouped.containsKey("purchaseLedger")) {
                ModuleSummary s = createPurchaseLedgers(grouped.get("purchaseLedger"), supplierNameToId,
                        request.getAdditionalInfo(), request.getDateEnd());
                        request.getAdditionalInfo(), request.getDateStart(), request.getDateEnd());
                summaries.add(s);
                totalGenerated += s.getSuccessCount();
            }
@@ -216,7 +300,154 @@
        }
    }
    /**
     * 修正AI生成的日期到指定时间范围内。
     * 遍历所有实体的所有字段,如果发现日期字符串(yyyy-MM-dd格式)超出范围,替换为范围内的随机日期。
     */
    private void fixDatesInRange(List<JSONObject> entities, String dateStart, String dateEnd) {
        LocalDate start;
        LocalDate end;
        try {
            start = LocalDate.parse(dateStart);
            end = LocalDate.parse(dateEnd);
        } catch (Exception e) {
            log.warn("日期范围解析失败: {} ~ {}", dateStart, dateEnd);
            return;
        }
        long daysBetween = ChronoUnit.DAYS.between(start, end);
        if (daysBetween <= 0) {
            daysBetween = 1;
        }
        // 所有已知的日期字段名
        java.util.Set<String> dateFields = java.util.Set.of(
                "entryDate", "executionDate", "deliveryDate", "requiredDate",
                "promisedDeliveryDate", "planCompleteTime", "maintenanceTime",
                "checkTime", "registerDate"
        );
        for (JSONObject entity : entities) {
            for (String key : dateFields) {
                String val = entity.getString(key);
                if (val != null && val.matches("\\d{4}-\\d{2}-\\d{2}")) {
                    try {
                        LocalDate d = LocalDate.parse(val);
                        if (d.isBefore(start) || d.isAfter(end)) {
                            LocalDate fixed = start.plusDays(ThreadLocalRandom.current().nextLong(daysBetween + 1));
                            entity.put(key, fixed.toString());
                            log.debug("修正日期 {}: {} -> {}", key, val, fixed);
                        }
                    } catch (Exception ignored) {
                    }
                } else if (val != null && !val.matches("\\d{4}-\\d{2}-\\d{2}")) {
                    // AI返回了无效日期格式,直接替换为范围内的随机日期
                    LocalDate fixed = start.plusDays(ThreadLocalRandom.current().nextLong(daysBetween + 1));
                    entity.put(key, fixed.toString());
                    log.debug("修正无效日期 {}: {} -> {}", key, val, fixed);
                }
            }
            // 修正合同编号中的日期: CG-20230601-001 -> CG-YYYYMMDD-001
            for (String key : java.util.Set.of("purchaseContractNumber", "salesContractNo", "standardNo", "batchNo")) {
                String val = entity.getString(key);
                if (val != null && val.matches(".*\\d{8}.*")) {
                    String datePart = val.replaceAll(".*?(\\d{8}).*", "$1");
                    try {
                        // 验证提取到的是有效日期
                        String dateStr = datePart.substring(0, 4) + "-" + datePart.substring(4, 6) + "-" + datePart.substring(6, 8);
                        LocalDate d = LocalDate.parse(dateStr);
                        if (d.isBefore(start) || d.isAfter(end)) {
                            LocalDate fixed = start.plusDays(ThreadLocalRandom.current().nextLong(daysBetween + 1));
                            String newDatePart = fixed.format(java.time.format.DateTimeFormatter.BASIC_ISO_DATE);
                            String newVal = val.replace(datePart, newDatePart);
                            entity.put(key, newVal);
                            log.debug("修正编号 {}: {} -> {}", key, val, newVal);
                        }
                    } catch (Exception ignored) {
                    }
                }
            }
        }
    }
    // ---- Tier 0: 基础数据 ----
    /**
     * 从数据库加载已有客户名称→ID映射
     */
    private Map<String, Long> loadExistingCustomers() {
        Map<String, Long> map = new HashMap<>();
        List<Customer> customers = customerService.list();
        for (Customer c : customers) {
            if (c.getCustomerName() != null) {
                map.put(c.getCustomerName(), c.getId());
            }
        }
        return map;
    }
    /**
     * 从数据库加载已有供应商名称→ID映射
     */
    private Map<String, Long> loadExistingSuppliers() {
        Map<String, Long> map = new HashMap<>();
        List<SupplierManage> suppliers = supplierService.list();
        for (SupplierManage s : suppliers) {
            if (s.getSupplierName() != null) {
                map.put(s.getSupplierName(), s.getId());
            }
        }
        return map;
    }
    /**
     * 加载系统用户信息(含角色和部门名称)
     */
    private List<SysUser> loadSystemUsersWithDetail() {
        return sysUserMapper.selectUserListWithDetail();
    }
    /**
     * 构建产品规格ID→顶级产品分类名称的映射
     * 通过Product树向上找到根节点,判断"成品"/"原材料"/"半成品"
     */
    private Map<Long, String> buildProductModelCategoryMap() {
        // 加载所有Product节点
        List<Product> allProducts = productMapper.selectList(null);
        Map<Long, Product> productMap = allProducts.stream()
                .collect(Collectors.toMap(Product::getId, p -> p, (a, b) -> a));
        // 对每个Product,沿parentId向上找到根节点名称
        Map<Long, String> productIdToRootName = new HashMap<>();
        for (Product p : allProducts) {
            if (productIdToRootName.containsKey(p.getId())) continue;
            // 向上遍历找根
            List<Long> chain = new ArrayList<>();
            Long currentId = p.getId();
            while (currentId != null) {
                chain.add(currentId);
                Product current = productMap.get(currentId);
                if (current == null || current.getParentId() == null) break;
                currentId = current.getParentId();
            }
            // 根节点的productName就是分类名
            Product root = productMap.get(chain.get(chain.size() - 1));
            String rootName = root != null ? root.getProductName() : "其他";
            for (Long id : chain) {
                productIdToRootName.put(id, rootName);
            }
        }
        // 对每个ProductModel,通过productId找到其所属分类
        List<ProductModel> allModels = productModelMapper.selectList(null);
        Map<Long, String> result = new HashMap<>();
        for (ProductModel m : allModels) {
            String category = m.getProductId() != null
                    ? productIdToRootName.getOrDefault(m.getProductId(), "其他")
                    : "其他";
            result.put(m.getId(), category);
        }
        return result;
    }
    private ModuleSummary createCustomers(List<JSONObject> items, Map<String, Long> nameToId) {
        int success = 0, fail = 0;
@@ -232,7 +463,10 @@
                c.setTaxpayerIdentificationNumber(item.getString("taxpayerIdentificationNumber"));
                c.setMaintainer(item.getString("maintainer"));
                if (item.containsKey("maintenanceTime")) {
                    c.setMaintenanceTime(java.sql.Date.valueOf(item.getString("maintenanceTime")));
                    String mtStr = item.getString("maintenanceTime");
                    if (mtStr != null && mtStr.matches("\\d{4}-\\d{2}-\\d{2}")) {
                        c.setMaintenanceTime(java.sql.Date.valueOf(mtStr));
                    }
                } else {
                    c.setMaintenanceTime(new java.sql.Date(System.currentTimeMillis()));
                }
@@ -309,7 +543,7 @@
    // ---- Tier 1: 业务单据 ----
    private ModuleSummary createSalesLedgers(List<JSONObject> items, Map<String, Long> customerNameToId) {
    private ModuleSummary createSalesLedgers(List<JSONObject> items, Map<String, Long> customerNameToId, String dateStart) {
        int success = 0, fail = 0;
        for (JSONObject item : items) {
            try {
@@ -323,20 +557,56 @@
                dto.setSalesman(item.getString("salesman"));
                dto.setPaymentMethod(item.getString("paymentMethod"));
                dto.setType(item.getInteger("type"));
                // 录入人:优先使用AI提供的数据,否则用当前登录用户
                if (item.containsKey("entryPerson")) {
                    dto.setEntryPerson(item.getString("entryPerson"));
                // 录入人:优先用AI返回的,否则从"销售"角色/部门随机选
                String entryPerson = item.getString("entryPerson");
                if (entryPerson == null || entryPerson.isBlank()) {
                    SysUser salesUser = randomUserByKeyword("销售");
                    entryPerson = salesUser != null ? salesUser.getNickName() : "系统";
                }
                dto.setEntryPerson(entryPerson);
                // 业务员:优先用AI返回的,否则从"销售"角色/部门随机选
                String salesman = dto.getSalesman();
                if (salesman == null || salesman.isBlank()) {
                    SysUser salesUser = randomUserByKeyword("销售");
                    salesman = salesUser != null ? salesUser.getNickName() : "系统";
                    dto.setSalesman(salesman);
                }
                // customerId为null会导致"客户不存在"异常,跳过
                if (customerId == null) {
                    log.warn("跳过销售台账,客户[{}]不存在", customerName);
                    fail++;
                    continue;
                }
                LocalDate entryDate = null;
                if (item.containsKey("entryDate")) {
                    dto.setEntryDate(java.sql.Date.valueOf(item.getString("entryDate")));
                    String entryDateStr = item.getString("entryDate");
                    if (entryDateStr != null && entryDateStr.matches("\\d{4}-\\d{2}-\\d{2}")) {
                        entryDate = LocalDate.parse(entryDateStr);
                        dto.setEntryDate(java.sql.Date.valueOf(entryDateStr));
                    } else {
                        dto.setEntryDate(java.sql.Date.valueOf(dateStart));
                    }
                }
                if (item.containsKey("executionDate")) {
                    dto.setExecutionDate(LocalDate.parse(item.getString("executionDate")));
                    String execDateStr = item.getString("executionDate");
                    if (execDateStr != null && execDateStr.matches("\\d{4}-\\d{2}-\\d{2}")) {
                        dto.setExecutionDate(LocalDate.parse(execDateStr));
                    } else {
                        dto.setExecutionDate(LocalDate.parse(dateStart));
                    }
                }
                if (item.containsKey("deliveryDate")) {
                    dto.setDeliveryDate(LocalDate.parse(item.getString("deliveryDate")));
                    String delDateStr = item.getString("deliveryDate");
                    if (delDateStr != null && delDateStr.matches("\\d{4}-\\d{2}-\\d{2}")) {
                        dto.setDeliveryDate(LocalDate.parse(delDateStr));
                    } else {
                        dto.setDeliveryDate(LocalDate.parse(dateStart));
                    }
                }
                JSONArray productData = item.getJSONArray("productData");
                boolean hasProduction = false;
                if (productData != null) {
                    List<SalesLedgerProduct> products = new ArrayList<>();
                    for (int i = 0; i < productData.size(); i++) {
@@ -353,7 +623,6 @@
                        if (pd.containsKey("taxExclusiveTotalPrice")) {
                            slp.setTaxExclusiveTotalPrice(pd.getBigDecimal("taxExclusiveTotalPrice"));
                        } else if (pd.getBigDecimal("taxInclusiveTotalPrice") != null && pd.getBigDecimal("taxRate") != null) {
                            // 不含税总价 = 含税总价 / (1 + 税率/100)
                            slp.setTaxExclusiveTotalPrice(
                                pd.getBigDecimal("taxInclusiveTotalPrice").divide(
                                    BigDecimal.ONE.add(pd.getBigDecimal("taxRate").divide(new BigDecimal("100"), 4, RoundingMode.HALF_UP)),
@@ -361,22 +630,603 @@
                        } else {
                            slp.setTaxExclusiveTotalPrice(BigDecimal.ZERO);
                        }
                        slp.setIsProduction(true);
                        products.add(slp);
                        hasProduction = true;
                    }
                    dto.setProductData(products);
                }
                salesLedgerService.addOrUpdateSalesLedger(dto);
                // addOrUpdateSalesLedger内部会自动生成合同号并insert,但dto上没有回写
                // 直接用dto的entryDate和customerId查最新创建的记录
                SalesLedger savedLedger = salesLedgerMapper.selectOne(
                        new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<SalesLedger>()
                                .eq(SalesLedger::getCustomerId, dto.getCustomerId())
                                .orderByDesc(SalesLedger::getId)
                                .last("limit 1"));
                if (savedLedger != null && hasProduction) {
                    processSalesFullFlow(savedLedger, entryDate);
                }
                success++;
            } catch (Exception e) {
                log.warn("创建销售台账失败: {}", e.getMessage());
                log.warn("创建销售台账失败: {}", e.getMessage(), e);
                fail++;
            }
        }
        return summary("sales", "销售台账", items.size(), success, fail);
    }
    // ==================== 销售完整流程 ====================
    /**
     * 销售完整流程: 生产计划合并下发 → 领料 → 报工 → 质检(可选) → 入库 → 发货 → 出库
     * 时间以销售录入日期为基准,各环节递推0-3天
     */
    private void processSalesFullFlow(SalesLedger salesLedger, LocalDate entryDate) {
        try {
            LocalDate baseDate = entryDate != null ? entryDate : LocalDate.now();
            // 1. 查询销售台账产品(含生产计划)
            List<SalesLedgerProduct> products = salesLedgerProductMapper.selectList(
                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<SalesLedgerProduct>()
                            .eq(SalesLedgerProduct::getSalesLedgerId, salesLedger.getId())
                            .eq(SalesLedgerProduct::getIsProduction, true));
            if (products.isEmpty()) {
                log.info("销售台账[{}]无需生产,跳过生产流程", salesLedger.getSalesContractNo());
                return;
            }
            // 检查产品是否具备完整的工艺数据(工艺路线、工序、BOM)
            List<Long> missingTechProductIds = checkTechnologyDataReadiness(products);
            if (!missingTechProductIds.isEmpty()) {
                log.warn("销售台账[{}]以下产品缺少工艺数据(工艺路线/工序/BOM),跳过生产流程: {}",
                        salesLedger.getSalesContractNo(), missingTechProductIds);
                return;
            }
            // 2. 生产计划合并下发 → 生产订单
            LocalDate planDate = baseDate.plusDays(ThreadLocalRandom.current().nextInt(0, 4));
            processProductionCombine(salesLedger, planDate);
            // 3. 领料(物料不足自动入库后再领)
            LocalDate pickDate = planDate.plusDays(ThreadLocalRandom.current().nextInt(0, 3));
            processMaterialPick(salesLedger, pickDate);
            // 4. 生产报工
            LocalDate reportDate = pickDate.plusDays(ThreadLocalRandom.current().nextInt(1, 4));
            processProductionReport(salesLedger, reportDate);
            // 5. 质检提交 + 入库审批
            LocalDate qualityDate = reportDate.plusDays(ThreadLocalRandom.current().nextInt(0, 4));
            processSalesQualityAndStockIn(salesLedger, qualityDate);
            // 6. 发货 + 发货审批 + 出库审批
            LocalDate shipDate = qualityDate.plusDays(ThreadLocalRandom.current().nextInt(0, 4));
            processSalesShipping(salesLedger, shipDate);
            log.info("销售台账[{}]完整流程处理完毕", salesLedger.getSalesContractNo());
        } catch (Exception e) {
            log.warn("销售完整流程处理失败[{}]: {}", salesLedger.getSalesContractNo(), e.getMessage());
        }
    }
    /**
     * 查询角色或部门含指定关键字的用户,随机返回一个
     */
    private SysUser randomUserByKeyword(String keyword) {
        try {
            List<Long> userIds = new ArrayList<>(sysUserMapper.getUserByRole(keyword));
            List<SysDept> depts = sysDeptMapper.selectList(
                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<SysDept>()
                            .like(SysDept::getDeptName, keyword)
                            .eq(SysDept::getDelFlag, "0")
                            .eq(SysDept::getStatus, "0"));
            if (!depts.isEmpty()) {
                List<Long> deptIds = depts.stream().map(SysDept::getDeptId).collect(Collectors.toList());
                List<Long> deptUserIds = sysUserDeptMapper.selectDistinctUserIdsByDeptIds(deptIds);
                userIds.addAll(deptUserIds);
            }
            if (userIds.isEmpty()) {
                return null;
            }
            List<Long> distinctIds = userIds.stream().distinct().collect(Collectors.toList());
            Long randomId = distinctIds.get(ThreadLocalRandom.current().nextInt(distinctIds.size()));
            return sysUserMapper.selectUserById(randomId);
        } catch (Exception e) {
            log.warn("查询[{}]角色/部门用户失败: {}", keyword, e.getMessage());
            return null;
        }
    }
    /**
     * 查询角色或部门含"生产"的用户,随机返回一个
     */
    private SysUser randomProductionUser() {
        return randomUserByKeyword("生产");
    }
    /**
     * 检查产品是否具备完整的工艺数据(工艺路线、工序、BOM)
     * @return 缺少工艺数据的产品规格ID列表(空列表表示全部就绪)
     */
    private List<Long> checkTechnologyDataReadiness(List<SalesLedgerProduct> products) {
        List<Long> missingIds = new ArrayList<>();
        for (SalesLedgerProduct slp : products) {
            Long productModelId = slp.getProductModelId();
            if (productModelId == null) continue;
            // 1. 检查工艺路线
            List<com.ruoyi.technology.pojo.TechnologyRouting> routings =
                    technologyRoutingMapper.selectList(
                            new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<com.ruoyi.technology.pojo.TechnologyRouting>()
                                    .eq(com.ruoyi.technology.pojo.TechnologyRouting::getProductModelId, productModelId));
            if (routings.isEmpty()) {
                log.warn("产品规格[{}]缺少工艺路线", productModelId);
                missingIds.add(productModelId);
                continue;
            }
            Long routingId = routings.get(0).getId();
            // 2. 检查工艺路线是否有关联工序
            List<com.ruoyi.technology.pojo.TechnologyRoutingOperation> routingOps =
                    technologyRoutingOperationMapper.selectList(
                            new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<com.ruoyi.technology.pojo.TechnologyRoutingOperation>()
                                    .eq(com.ruoyi.technology.pojo.TechnologyRoutingOperation::getTechnologyRoutingId, routingId));
            if (routingOps.isEmpty()) {
                log.warn("产品规格[{}]工艺路线[{}]缺少工序", productModelId, routingId);
                missingIds.add(productModelId);
                continue;
            }
            // 3. 检查BOM(可选但有更好)
            com.ruoyi.technology.pojo.TechnologyRouting routing = routings.get(0);
            if (routing.getBomId() != null) {
                List<com.ruoyi.technology.pojo.TechnologyBomStructure> bomStructures =
                        technologyBomStructureMapper.selectList(
                                new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<com.ruoyi.technology.pojo.TechnologyBomStructure>()
                                        .eq(com.ruoyi.technology.pojo.TechnologyBomStructure::getBomId, routing.getBomId()));
                if (bomStructures.isEmpty()) {
                    log.warn("产品规格[{}]BOM[{}]缺少产品结构", productModelId, routing.getBomId());
                    missingIds.add(productModelId);
                }
            }
        }
        return missingIds;
    }
    /**
     * 生产计划合并下发 → 生产订单(按productModelId分组,同型号合并下发)
     */
    private void processProductionCombine(SalesLedger salesLedger, LocalDate planDate) {
        try {
            List<ProductionPlan> plans = productionPlanMapper.selectList(
                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<ProductionPlan>()
                            .eq(ProductionPlan::getSalesLedgerId, salesLedger.getId())
                            .and(w -> w.eq(ProductionPlan::getStatus, 0).or().isNull(ProductionPlan::getStatus)));
            if (plans.isEmpty()) {
                log.info("销售台账[{}]无待下发生产计划", salesLedger.getSalesContractNo());
                return;
            }
            // 按productModelId分组,同型号才能合并下发;productModelId为null的单独逐个下发
            Map<Long, List<ProductionPlan>> grouped = plans.stream()
                    .filter(p -> p.getProductModelId() != null)
                    .collect(Collectors.groupingBy(ProductionPlan::getProductModelId));
            for (Map.Entry<Long, List<ProductionPlan>> entry : grouped.entrySet()) {
                try {
                    List<ProductionPlan> sameModelPlans = entry.getValue();
                    BigDecimal totalQty = sameModelPlans.stream()
                            .map(p -> Optional.ofNullable(p.getQtyRequired()).orElse(BigDecimal.ZERO))
                            .reduce(BigDecimal.ZERO, BigDecimal::add);
                    ProductionPlanDto combineDto = new ProductionPlanDto();
                    combineDto.setIds(sameModelPlans.stream().map(ProductionPlan::getId).collect(Collectors.toList()));
                    combineDto.setTotalAssignedQuantity(totalQty);
                    combineDto.setPlanCompleteTime(planDate.plusDays(ThreadLocalRandom.current().nextInt(3, 7)));
                    productionPlanService.combine(combineDto);
                    // 修正生产订单的planCompleteTime
                    List<ProductionOrder> orders = productionOrderMapper.selectList(
                            new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<ProductionOrder>()
                                    .like(ProductionOrder::getProductionPlanIds, String.valueOf(sameModelPlans.get(0).getId())));
                    for (ProductionOrder order : orders) {
                        order.setPlanCompleteTime(planDate.plusDays(ThreadLocalRandom.current().nextInt(3, 7)));
                        order.setStartTime(planDate.atStartOfDay());
                        productionOrderMapper.updateById(order);
                    }
                    log.info("销售台账[{}]生产计划下发成功, productModelId={}, 计划数: {}",
                            salesLedger.getSalesContractNo(), entry.getKey(), sameModelPlans.size());
                } catch (Exception e) {
                    log.warn("销售台账[{}]生产计划下发失败, productModelId={}: {}",
                            salesLedger.getSalesContractNo(), entry.getKey(), e.getMessage());
                }
            }
        } catch (Exception e) {
            log.warn("生产计划下发失败[{}]: {}", salesLedger.getSalesContractNo(), e.getMessage());
        }
    }
    /**
     * 领料: 查询生产订单的BOM领料清单,检查库存,不足则先自动入库审批后再领料
     */
    private void processMaterialPick(SalesLedger salesLedger, LocalDate pickDate) {
        try {
            List<ProductionOrder> orders = findOrdersForSalesLedger(salesLedger);
            if (orders.isEmpty()) {
                log.info("销售台账[{}]无生产订单,跳过领料", salesLedger.getSalesContractNo());
                return;
            }
            for (ProductionOrder order : orders) {
                try {
                    // 查询订单BOM领料清单
                    List<com.ruoyi.production.bean.vo.ProductionOrderPickVo> pickList = productionOrderService.pick(order.getId());
                    if (pickList == null || pickList.isEmpty()) {
                        log.info("生产订单[{}]无领料清单", order.getId());
                        continue;
                    }
                    // 检查库存不足的物料,先自动入库
                    ensureMaterialStock(pickList, pickDate);
                    // 重新获取领料清单(入库后批号和库存量已更新)
                    pickList = productionOrderService.pick(order.getId());
                    if (pickList == null || pickList.isEmpty()) {
                        log.info("生产订单[{}]重新获取领料清单为空", order.getId());
                        continue;
                    }
                    // 执行领料
                    List<ProductionOrderPickDto> pickDtoList = new ArrayList<>();
                    for (com.ruoyi.production.bean.vo.ProductionOrderPickVo pickVo : pickList) {
                        ProductionOrderPickDto pickDto = new ProductionOrderPickDto();
                        pickDto.setProductionOrderId(order.getId());
                        pickDto.setProductModelId(pickVo.getProductModelId());
                        pickDto.setPickQuantity(pickVo.getDemandedQuantity());
                        pickDto.setPickType((byte) 1);
                        pickDto.setOperationName(pickVo.getOperationName());
                        pickDto.setTechnologyOperationId(pickVo.getTechnologyOperationId());
                        pickDto.setDemandedQuantity(pickVo.getDemandedQuantity());
                        pickDto.setBom(pickVo.getBom());
                        // 设置批号(用更新后的批号列表)
                        if (pickVo.getBatchNoList() != null && !pickVo.getBatchNoList().isEmpty()) {
                            pickDto.setBatchNoList(pickVo.getBatchNoList());
                            pickDto.setBatchNo(pickVo.getBatchNoList().get(0));
                        }
                        pickDtoList.add(pickDto);
                    }
                    if (!pickDtoList.isEmpty()) {
                        ProductionOrderPickDto batchDto = new ProductionOrderPickDto();
                        batchDto.setPickList(pickDtoList);
                        productionOrderPickService.savePick(batchDto);
                        log.info("生产订单[{}]领料成功, 领料项数: {}", order.getId(), pickDtoList.size());
                    }
                } catch (Exception e) {
                    log.warn("生产订单[{}]领料失败: {}", order.getId(), e.getMessage());
                }
            }
        } catch (Exception e) {
            log.warn("领料流程失败[{}]: {}", salesLedger.getSalesContractNo(), e.getMessage());
        }
    }
    /**
     * 确保物料库存充足,不足则先自动入库审批
     */
    private void ensureMaterialStock(List<com.ruoyi.production.bean.vo.ProductionOrderPickVo> pickList, LocalDate pickDate) {
        LocalDateTime pickDateTime = pickDate.atTime(9, 0, 0).plusMinutes(ThreadLocalRandom.current().nextInt(0, 480));
        String pickDateStr = pickDate.format(DateTimeFormatter.BASIC_ISO_DATE);
        for (com.ruoyi.production.bean.vo.ProductionOrderPickVo pickVo : pickList) {
            BigDecimal demanded = pickVo.getDemandedQuantity() != null ? pickVo.getDemandedQuantity() : BigDecimal.ZERO;
            BigDecimal stockQty = pickVo.getStockQuantity() != null ? pickVo.getStockQuantity() : BigDecimal.ZERO;
            BigDecimal shortage = demanded.subtract(stockQty);
            if (shortage.compareTo(BigDecimal.ZERO) <= 0) {
                continue; // 库存充足
            }
            // 库存不足,自动入库补充
            try {
                StockInventoryDto dto = new StockInventoryDto();
                dto.setProductModelId(pickVo.getProductModelId());
                dto.setQualitity(shortage);
                dto.setRecordType(StockInQualifiedRecordTypeEnum.CUSTOMIZATION_STOCK_IN.getCode());
                dto.setRecordId(0L);
                stockInventoryService.addStockInRecordOnly(dto);
                // 查找刚创建的入库记录
                List<com.ruoyi.stock.pojo.StockInRecord> records = stockInRecordService.list(
                        new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<com.ruoyi.stock.pojo.StockInRecord>()
                                .eq(com.ruoyi.stock.pojo.StockInRecord::getProductModelId, pickVo.getProductModelId())
                                .eq(com.ruoyi.stock.pojo.StockInRecord::getApprovalStatus, 0)
                                .orderByDesc(com.ruoyi.stock.pojo.StockInRecord::getId)
                                .last("limit 1"));
                if (!records.isEmpty()) {
                    com.ruoyi.stock.pojo.StockInRecord record = records.get(0);
                    record.setCreateTime(pickDateTime);
                    record.setBatchNo(fixBatchNoDate(record.getBatchNo(), pickDateStr));
                    record.setInboundBatches(fixBatchNoDate(record.getInboundBatches(), pickDateStr));
                    stockInRecordMapper.updateById(record);
                    // 审批通过
                    stockInRecordService.batchApprove(List.of(record.getId()), ReviewStatusEnum.APPROVED.getCode());
                    log.info("物料[{}]自动入库审批完成, 入库数量: {}", pickVo.getProductModelId(), shortage);
                }
            } catch (Exception e) {
                log.warn("物料[{}]自动入库失败: {}", pickVo.getProductModelId(), e.getMessage());
            }
        }
    }
    /**
     * 生产报工: 遍历工单逐个报工,从生产角色用户中随机选取报工人
     */
    private void processProductionReport(SalesLedger salesLedger, LocalDate reportDate) {
        try {
            List<ProductionOrder> orders = findOrdersForSalesLedger(salesLedger);
            if (orders.isEmpty()) {
                log.info("销售台账[{}]无生产订单", salesLedger.getSalesContractNo());
                return;
            }
            for (ProductionOrder order : orders) {
                List<ProductionOperationTask> tasks = productionOperationTaskMapper.selectList(
                        new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<ProductionOperationTask>()
                                .eq(ProductionOperationTask::getProductionOrderId, order.getId())
                                .orderByAsc(ProductionOperationTask::getId));
                for (ProductionOperationTask task : tasks) {
                    if (task.getStatus() != null && task.getStatus() >= 3) continue;
                    // 每次报工随机选一个生产用户
                    SysUser productionUser = randomProductionUser();
                    Long userId = productionUser != null ? productionUser.getUserId() : 1L;
                    String userName = productionUser != null ? productionUser.getNickName() : "系统";
                    // 报工
                    ProductionProductMainDto reportDto = new ProductionProductMainDto();
                    reportDto.setProductionOperationTaskId(task.getId());
                    reportDto.setQuantity(task.getPlanQuantity());
                    reportDto.setScrapQty(BigDecimal.ZERO);
                    reportDto.setUserId(userId);
                    reportDto.setUserName(userName);
                    productionProductMainService.addProductMain(reportDto);
                    // 修正报工时间(重新从DB读取,避免覆盖addProductMain更新的completeQuantity)
                    fixProductionReportTimes(task.getId(), reportDate);
                }
                // 修正生产订单时间(重新从DB读取,避免覆盖addProductMain更新的completeQuantity)
                fixProductionOrderTimes(order.getId(), reportDate);
            }
            log.info("销售台账[{}]生产报工完成, 订单数: {}", salesLedger.getSalesContractNo(), orders.size());
        } catch (Exception e) {
            log.warn("生产报工失败[{}]: {}", salesLedger.getSalesContractNo(), e.getMessage());
        }
    }
    /**
     * 查找销售台账关联的生产订单
     */
    private List<ProductionOrder> findOrdersForSalesLedger(SalesLedger salesLedger) {
        List<ProductionOrder> orders = new ArrayList<>();
        List<ProductionPlan> plans = productionPlanMapper.selectList(
                new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<ProductionPlan>()
                        .eq(ProductionPlan::getSalesLedgerId, salesLedger.getId()));
        for (ProductionPlan plan : plans) {
            List<ProductionOrder> po = productionOrderMapper.selectList(
                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<ProductionOrder>()
                            .like(ProductionOrder::getProductionPlanIds, String.valueOf(plan.getId())));
            orders.addAll(po);
        }
        // 去重
        return orders.stream()
                .collect(Collectors.toMap(ProductionOrder::getId, o -> o, (a, b) -> a))
                .values().stream().collect(Collectors.toList());
    }
    /**
     * 修正生产报工相关时间(重新从DB读取task,避免覆盖addProductMain已更新的completeQuantity)
     */
    private void fixProductionReportTimes(Long taskId, LocalDate reportDate) {
        try {
            ProductionOperationTask freshTask = productionOperationTaskMapper.selectById(taskId);
            if (freshTask != null) {
                freshTask.setActualStartTime(reportDate);
                freshTask.setActualEndTime(reportDate);
                freshTask.setStatus(4);
                productionOperationTaskMapper.updateById(freshTask);
            }
        } catch (Exception e) {
            log.warn("修正生产报工时间失败: {}", e.getMessage());
        }
    }
    /**
     * 修正生产订单时间(重新从DB读取order,避免覆盖addProductMain已更新的completeQuantity)
     */
    private void fixProductionOrderTimes(Long orderId, LocalDate reportDate) {
        try {
            ProductionOrder freshOrder = productionOrderMapper.selectById(orderId);
            if (freshOrder != null) {
                freshOrder.setStartTime(reportDate.minusDays(1).atStartOfDay());
                freshOrder.setEndTime(reportDate.plusDays(1).atStartOfDay());
                freshOrder.setStatus(3);
                productionOrderMapper.updateById(freshOrder);
            }
        } catch (Exception e) {
            log.warn("修正生产订单时间失败: {}", e.getMessage());
        }
    }
    /**
     * 销售质检提交 + 入库审批
     */
    private void processSalesQualityAndStockIn(SalesLedger salesLedger, LocalDate qualityDate) {
        try {
            LocalDateTime qualityDateTime = qualityDate.atTime(9, 0, 0)
                    .plusMinutes(ThreadLocalRandom.current().nextInt(0, 480));
            // 查所有未提交的质检单(inspectType=1过程检验, 2=出厂检验,由生产报工生成)
            List<QualityInspect> allUnsubmitted = qualityInspectService.list(
                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<QualityInspect>()
                            .eq(QualityInspect::getInspectState, 0)
                            .in(QualityInspect::getInspectType, java.util.Arrays.asList(1, 2)));
            for (QualityInspect qi : allUnsubmitted) {
                qualityInspectService.autoSubmit(qi.getId());
                qi.setCreateTime(qualityDateTime);
                qualityInspectMapper.updateById(qi);
            }
            log.info("销售台账[{}]质检提交完成, 质检单数: {}", salesLedger.getSalesContractNo(), allUnsubmitted.size());
            // 入库审批:审批所有待审批的入库记录
            LocalDate stockDate = qualityDate.plusDays(ThreadLocalRandom.current().nextInt(0, 3));
            LocalDateTime stockDateTime = stockDate.atTime(9, 0, 0)
                    .plusMinutes(ThreadLocalRandom.current().nextInt(0, 480));
            String stockDateStr = stockDate.format(DateTimeFormatter.BASIC_ISO_DATE);
            List<com.ruoyi.stock.pojo.StockInRecord> stockRecords = stockInRecordService.list(
                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<com.ruoyi.stock.pojo.StockInRecord>()
                            .eq(com.ruoyi.stock.pojo.StockInRecord::getApprovalStatus, 0));
            List<Long> recordIds = stockRecords.stream()
                    .map(com.ruoyi.stock.pojo.StockInRecord::getId)
                    .collect(Collectors.toList());
            if (!recordIds.isEmpty()) {
                for (com.ruoyi.stock.pojo.StockInRecord sr : stockRecords) {
                    sr.setCreateTime(stockDateTime);
                    sr.setBatchNo(fixBatchNoDate(sr.getBatchNo(), stockDateStr));
                    sr.setInboundBatches(fixBatchNoDate(sr.getInboundBatches(), stockDateStr));
                    stockInRecordMapper.updateById(sr);
                }
                stockInRecordService.batchApprove(recordIds, ReviewStatusEnum.APPROVED.getCode());
                for (com.ruoyi.stock.pojo.StockInRecord sr : stockRecords) {
                    StockInventory si = stockInventoryMapper.selectOne(
                            new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<StockInventory>()
                                    .eq(StockInventory::getProductModelId, sr.getProductModelId())
                                    .eq(StockInventory::getBatchNo, sr.getBatchNo()));
                    if (si != null) {
                        si.setBatchNo(fixBatchNoDate(si.getBatchNo(), stockDateStr));
                        stockInventoryMapper.updateById(si);
                    }
                }
                log.info("销售台账[{}]入库审批完成, 入库记录数: {}", salesLedger.getSalesContractNo(), recordIds.size());
            }
        } catch (Exception e) {
            log.warn("销售质检入库失败[{}]: {}", salesLedger.getSalesContractNo(), e.getMessage());
        }
    }
    /**
     * 发货 + 发货审批通过 + 出库审批
     */
    private void processSalesShipping(SalesLedger salesLedger, LocalDate shipDate) {
        try {
            LocalDateTime shipDateTime = shipDate.atTime(9, 0, 0)
                    .plusMinutes(ThreadLocalRandom.current().nextInt(0, 480));
            String shipDateStr = shipDate.format(DateTimeFormatter.BASIC_ISO_DATE);
            // 查找有库存的产品规格
            List<SalesLedgerProduct> products = salesLedgerProductMapper.selectList(
                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<SalesLedgerProduct>()
                            .eq(SalesLedgerProduct::getSalesLedgerId, salesLedger.getId()));
            List<ShippingProductDetail> details = new ArrayList<>();
            for (SalesLedgerProduct slp : products) {
                StockInventory si = stockInventoryMapper.selectOne(
                        new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<StockInventory>()
                                .eq(StockInventory::getProductModelId, slp.getProductModelId())
                                .gt(StockInventory::getQualitity, BigDecimal.ZERO)
                                .orderByDesc(StockInventory::getId)
                                .last("limit 1"));
                if (si != null) {
                    ShippingProductDetail detail = new ShippingProductDetail();
                    detail.setStockInventoryId(si.getId());
                    detail.setProductModelId(slp.getProductModelId());
                    detail.setBatchNo(si.getBatchNo());
                    detail.setQuantity(slp.getQuantity());
                    details.add(detail);
                }
            }
            if (details.isEmpty()) {
                log.info("销售台账[{}]无可用库存,跳过发货", salesLedger.getSalesContractNo());
                return;
            }
            // 创建发货单
            ShippingInfoDto shipDto = new ShippingInfoDto();
            shipDto.setSalesLedgerId(salesLedger.getId());
            shipDto.setSalesLedgerProductId(products.get(0).getId());
            shipDto.setCreateTime(shipDateTime);
            shipDto.setBatchNoDetailList(details);
            shippingInfoService.addReq(shipDto);
            // 找到发货单和审批实例
            ShippingInfo shippingInfo = shippingInfoMapper.selectOne(
                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<ShippingInfo>()
                            .eq(ShippingInfo::getShippingNo, shipDto.getShippingNo())
                            .last("limit 1"));
            if (shippingInfo != null) {
                shippingInfo.setCreateTime(shipDateTime);
                shippingInfo.setShippingDate(java.sql.Date.valueOf(shipDate));
                // 根据客户地址生成发货车牌号
                shippingInfo.setShippingCarNumber(generateCarNumber(salesLedger.getCustomerName()));
                shippingInfoMapper.updateById(shippingInfo);
                // 发货审批自动通过
                ApprovalInstance shipApproval = approvalInstanceMapper.selectOne(
                        new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<ApprovalInstance>()
                                .eq(ApprovalInstance::getBusinessId, shippingInfo.getId())
                                .eq(ApprovalInstance::getBusinessType, 7L)
                                .eq(ApprovalInstance::getDeleted, 0)
                                .orderByDesc(ApprovalInstance::getId)
                                .last("limit 1"));
                if (shipApproval != null) {
                    LocalDate shipApproveDate = shipDate.plusDays(ThreadLocalRandom.current().nextInt(0, 3));
                    LocalDateTime shipApproveDateTime = shipApproveDate.atTime(9, 0, 0)
                            .plusMinutes(ThreadLocalRandom.current().nextInt(0, 480));
                    approvalInstanceService.autoApprove(shipApproval.getId());
                    fixApprovalTimes(shipApproval.getId(), shipApproveDateTime);
                    shippingInfo.setStatus("已发货");
                    shippingInfo.setShippingDate(java.sql.Date.valueOf(shipApproveDate));
                    shippingInfoMapper.updateById(shippingInfo);
                }
                // 出库审批
                LocalDate outDate = shipDate.plusDays(ThreadLocalRandom.current().nextInt(0, 3));
                LocalDateTime outDateTime = outDate.atTime(9, 0, 0)
                        .plusMinutes(ThreadLocalRandom.current().nextInt(0, 480));
                String outDateStr = outDate.format(DateTimeFormatter.BASIC_ISO_DATE);
                List<StockOutRecord> outRecords = stockOutRecordMapper.selectList(
                        new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<StockOutRecord>()
                                .eq(StockOutRecord::getRecordId, shippingInfo.getId())
                                .eq(StockOutRecord::getRecordType,
                                        String.valueOf(StockOutQualifiedRecordTypeEnum.SALE_SHIP_STOCK_OUT.getCode())));
                List<Long> outIds = new ArrayList<>();
                for (StockOutRecord sr : outRecords) {
                    if (sr.getApprovalStatus() == null || sr.getApprovalStatus() == 0 || sr.getApprovalStatus() == 3) {
                        sr.setCreateTime(outDateTime);
                        sr.setOutboundBatches(fixBatchNoDate(sr.getOutboundBatches(), outDateStr));
                        sr.setBatchNo(fixBatchNoDate(sr.getBatchNo(), outDateStr));
                        stockOutRecordMapper.updateById(sr);
                        outIds.add(sr.getId());
                    }
                }
                if (!outIds.isEmpty()) {
                    // 出库审批通过(走正式审批流程,内部会扣减库存)
                    stockOutRecordService.batchApprove(outIds, ReviewStatusEnum.APPROVED.getCode());
                    log.info("销售台账[{}]出库审批完成, 出库记录数: {}",
                            salesLedger.getSalesContractNo(), outIds.size());
                }
            }
            log.info("销售台账[{}]发货流程完成", salesLedger.getSalesContractNo());
        } catch (Exception e) {
            log.warn("销售发货流程失败[{}]: {}", salesLedger.getSalesContractNo(), e.getMessage());
        }
    }
    private ModuleSummary createPurchaseLedgers(List<JSONObject> items, Map<String, Long> supplierNameToId,
                                                 String additionalInfo, String dateEnd) {
                                                 String additionalInfo, String dateStart, String dateEnd) {
        // 是否需要质检,从补充信息判断,默认不需要
        boolean needQualityInspect = additionalInfo != null
                && (additionalInfo.contains("质检") || additionalInfo.contains("需要检验"));
@@ -396,12 +1246,19 @@
                String entryDateStr = item.getString("entryDate");
                LocalDate entryDate = null;
                if (item.containsKey("entryDate")) {
                if (item.containsKey("entryDate") && entryDateStr != null && entryDateStr.matches("\\d{4}-\\d{2}-\\d{2}")) {
                    entryDate = LocalDate.parse(entryDateStr);
                    dto.setEntryDate(java.sql.Date.valueOf(entryDateStr));
                } else {
                    dto.setEntryDate(java.sql.Date.valueOf(dateStart));
                }
                if (item.containsKey("executionDate")) {
                    dto.setExecutionDate(java.sql.Date.valueOf(item.getString("executionDate")));
                    String execDateStr = item.getString("executionDate");
                    if (execDateStr != null && execDateStr.matches("\\d{4}-\\d{2}-\\d{2}")) {
                        dto.setExecutionDate(java.sql.Date.valueOf(execDateStr));
                    } else {
                        dto.setExecutionDate(java.sql.Date.valueOf(dateStart));
                    }
                }
                JSONArray productData = item.getJSONArray("productData");
                if (productData != null) {
@@ -445,7 +1302,7 @@
                }
                success++;
            } catch (Exception e) {
                log.warn("创建采购台账失败: {}", e.getMessage());
                log.warn("创建采购台账失败: {}", e.getMessage(), e);
                fail++;
            }
        }
@@ -454,10 +1311,18 @@
    /**
     * 采购完整流程: 审核通过 → 质检(可选) → 入库审核通过
     * 各环节时间以采购录入日期为基准,偶尔推迟1-3天
     */
    private void processPurchaseFullFlow(PurchaseLedger purchaseLedger, boolean needQualityInspect,
                                          LocalDate entryDate, String dateEnd) {
        try {
            // 基准日期:采购录入日期,为空则用当天
            LocalDate baseDate = entryDate != null ? entryDate : LocalDate.now();
            // 审批时间:基于录入日期,随机推0-3天
            LocalDate approveDate = baseDate.plusDays(ThreadLocalRandom.current().nextInt(0, 4));
            LocalDateTime approveDateTime = approveDate.atTime(9, 0, 0)
                    .plusMinutes(ThreadLocalRandom.current().nextInt(0, 480));
            // 1. 审批自动通过
            ApprovalInstance approvalInstance = approvalInstanceMapper.selectOne(
                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<ApprovalInstance>()
@@ -467,21 +1332,25 @@
                            .orderByDesc(ApprovalInstance::getId)
                            .last("limit 1"));
            if (approvalInstance != null) {
                // 结算审批开始时间:基于录入日期,随机推0-3天
                LocalDate baseDate = entryDate != null ? entryDate : LocalDate.now();
                LocalDate approveDate = baseDate.plusDays(ThreadLocalRandom.current().nextInt(0, 4));
                // 使用autoApprove完成审批
                approvalInstanceService.autoApprove(approvalInstance.getId());
                // 修正审批相关时间为采购录入日期范围
                fixApprovalTimes(approvalInstance.getId(), approveDateTime);
                log.info("采购台账[{}]审批通过, 审批日期: {}", purchaseLedger.getPurchaseContractNumber(), approveDate);
            }
            // 2. 质检流程(如果需要质检)
            if (needQualityInspect) {
                processQualityInspect(purchaseLedger, entryDate, dateEnd);
                // 质检时间:审批之后再推0-3天
                LocalDate inspectDate = approveDate.plusDays(ThreadLocalRandom.current().nextInt(0, 4));
                processQualityInspect(purchaseLedger, inspectDate);
            }
            // 3. 入库审批通过
            processStockInApprove(purchaseLedger, dateEnd);
            // 入库时间:质检时间(有质检)或审批时间之后再推0-3天
            LocalDate stockBaseDate = needQualityInspect
                    ? approveDate.plusDays(ThreadLocalRandom.current().nextInt(1, 4))
                    : approveDate.plusDays(ThreadLocalRandom.current().nextInt(0, 4));
            processStockInApprove(purchaseLedger, stockBaseDate);
        } catch (Exception e) {
            log.warn("采购完整流程处理失败[{}]: {}", purchaseLedger.getPurchaseContractNumber(), e.getMessage());
@@ -489,17 +1358,68 @@
    }
    /**
     * 修正审批实例及关联节点、任务、记录的时间
     */
    private void fixApprovalTimes(Long instanceId, LocalDateTime dateTime) {
        try {
            // 更新审批实例的finishTime和createTime
            ApprovalInstance instance = approvalInstanceMapper.selectById(instanceId);
            if (instance != null) {
                instance.setFinishTime(dateTime);
                instance.setCreateTime(dateTime.minusHours(1));
                approvalInstanceMapper.updateById(instance);
            }
            // 更新审批实例节点的finishTime
            List<ApprovalInstanceNode> nodes = approvalInstanceNodeMapper.selectList(
                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<ApprovalInstanceNode>()
                            .eq(ApprovalInstanceNode::getInstanceId, instanceId));
            for (ApprovalInstanceNode node : nodes) {
                node.setFinishTime(dateTime);
                node.setCreateTime(dateTime.minusHours(1));
                approvalInstanceNodeMapper.updateById(node);
            }
            // 更新审批任务的createTime
            List<ApprovalTask> tasks = approvalTaskMapper.selectList(
                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<ApprovalTask>()
                            .eq(ApprovalTask::getInstanceId, instanceId));
            for (ApprovalTask task : tasks) {
                task.setCreateTime(dateTime.minusMinutes(30));
                approvalTaskMapper.updateById(task);
            }
            // 更新审批记录的createTime
            List<ApprovalRecord> records = approvalRecordMapper.selectList(
                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<ApprovalRecord>()
                            .eq(ApprovalRecord::getInstanceId, instanceId));
            for (ApprovalRecord record : records) {
                record.setCreateTime(dateTime.minusMinutes(10));
                approvalRecordMapper.updateById(record);
            }
        } catch (Exception e) {
            log.warn("修正审批时间失败: {}", e.getMessage());
        }
    }
    /**
     * 质检: 找到采购关联的质检单,自动提交为合格
     */
    private void processQualityInspect(PurchaseLedger purchaseLedger, LocalDate entryDate, String dateEnd) {
    private void processQualityInspect(PurchaseLedger purchaseLedger, LocalDate inspectDate) {
        try {
            LocalDateTime inspectDateTime = inspectDate.atTime(9, 0, 0)
                    .plusMinutes(ThreadLocalRandom.current().nextInt(0, 480));
            List<QualityInspect> inspectList = qualityInspectService.list(
                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<QualityInspect>()
                            .eq(QualityInspect::getPurchaseLedgerId, purchaseLedger.getId()));
            for (QualityInspect qi : inspectList) {
                if (qi.getInspectState() == null || qi.getInspectState() == 0) {
                    qualityInspectService.autoSubmit(qi.getId());
                    log.info("采购台账[{}]质检单[{}]自动提交合格", purchaseLedger.getPurchaseContractNumber(), qi.getId());
                    // 修正质检时间
                    qi.setCreateTime(inspectDateTime);
                    qualityInspectMapper.updateById(qi);
                    log.info("采购台账[{}]质检单[{}]自动提交合格, 质检日期: {}",
                            purchaseLedger.getPurchaseContractNumber(), qi.getId(), inspectDate);
                }
            }
        } catch (Exception e) {
@@ -508,16 +1428,19 @@
    }
    /**
     * 入库审核: 找到入库记录并审批通过
     * 入库审核: 找到入库记录并审批通过,并修正时间与批号日期
     */
    private void processStockInApprove(PurchaseLedger purchaseLedger, String dateEnd) {
    private void processStockInApprove(PurchaseLedger purchaseLedger, LocalDate stockDate) {
        try {
            LocalDateTime stockDateTime = stockDate.atTime(9, 0, 0)
                    .plusMinutes(ThreadLocalRandom.current().nextInt(0, 480));
            String stockDateStr = stockDate.format(java.time.format.DateTimeFormatter.BASIC_ISO_DATE);
            List<com.ruoyi.stock.pojo.StockInRecord> stockRecords = stockInRecordService.list(
                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<com.ruoyi.stock.pojo.StockInRecord>()
                            .eq(com.ruoyi.stock.pojo.StockInRecord::getRecordId, purchaseLedger.getId())
                            .eq(com.ruoyi.stock.pojo.StockInRecord::getRecordType,
                                    String.valueOf(com.ruoyi.common.enums.StockInQualifiedRecordTypeEnum.PURCHASE_STOCK_IN.getCode())));
            // 如果按PURCHASE_STOCK_IN找不到,尝试CUSTOMIZATION_UNSTOCK_OUT(质检合格入库)
            if (stockRecords.isEmpty()) {
                stockRecords = stockInRecordService.list(
                        new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<com.ruoyi.stock.pojo.StockInRecord>()
@@ -525,7 +1448,6 @@
                                .eq(com.ruoyi.stock.pojo.StockInRecord::getRecordType,
                                        String.valueOf(com.ruoyi.common.enums.StockInQualifiedRecordTypeEnum.CUSTOMIZATION_UNSTOCK_OUT.getCode())));
            }
            // 也尝试按质检单ID查找(质检单的recordId是质检单ID)
            for (QualityInspect qi : qualityInspectService.list(
                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<QualityInspect>()
                            .eq(QualityInspect::getPurchaseLedgerId, purchaseLedger.getId()))) {
@@ -534,7 +1456,6 @@
                                .eq(com.ruoyi.stock.pojo.StockInRecord::getRecordId, qi.getId()));
                stockRecords.addAll(qiRecords);
            }
            // 去重
            stockRecords = stockRecords.stream()
                    .collect(Collectors.toMap(com.ruoyi.stock.pojo.StockInRecord::getId, r -> r, (a, b) -> a))
                    .values().stream().collect(Collectors.toList());
@@ -545,8 +1466,20 @@
                        .map(com.ruoyi.stock.pojo.StockInRecord::getId)
                        .collect(Collectors.toList());
                if (!recordIds.isEmpty()) {
                    // 先修正入库记录的createTime和批号日期(batchApprove内部会用batchNo创建StockInventory)
                    for (Long recordId : recordIds) {
                        com.ruoyi.stock.pojo.StockInRecord sr = stockInRecordMapper.selectById(recordId);
                        if (sr != null) {
                            sr.setCreateTime(stockDateTime);
                            sr.setBatchNo(fixBatchNoDate(sr.getBatchNo(), stockDateStr));
                            String s = OrderUtils.countTodayByCreateTime(stockInRecordMapper, "RK", "inbound_batches", stockDateTime);
                            sr.setInboundBatches(s);
                            stockInRecordMapper.updateById(sr);
                        }
                    }
                    stockInRecordService.batchApprove(recordIds, ReviewStatusEnum.APPROVED.getCode());
                    log.info("采购台账[{}]入库审批通过, 入库记录数: {}", purchaseLedger.getPurchaseContractNumber(), recordIds.size());
                    log.info("采购台账[{}]入库审批通过, 入库日期: {}, 入库记录数: {}",
                            purchaseLedger.getPurchaseContractNumber(), stockDate, recordIds.size());
                }
            }
        } catch (Exception e) {
@@ -554,8 +1487,137 @@
        }
    }
    /**
     * 替换批号中的日期部分(yyyyMMdd → 指定日期)
     */
    /**
     * 根据客户名称查地址,推断省份简称生成车牌号
     * 格式: 省份简称 + 字母 + 5位随机数字字母, 如 苏A12345
     */
    private String generateCarNumber(String customerName) {
        // 省份简称到城市字母的常见映射
        Map<String, String[]> provinceCityMap = new HashMap<>();
        provinceCityMap.put("京", new String[]{"A", "B", "C", "E", "F", "G"});
        provinceCityMap.put("沪", new String[]{"A", "B", "D", "E", "F"});
        provinceCityMap.put("粤", new String[]{"A", "B", "E", "F", "G", "H", "J", "K"});
        provinceCityMap.put("苏", new String[]{"A", "B", "E", "F", "G", "H", "J", "K", "L", "M"});
        provinceCityMap.put("浙", new String[]{"A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "L"});
        provinceCityMap.put("鲁", new String[]{"A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "L", "M", "N", "P", "Q", "R", "S", "T", "U", "V", "W", "Y"});
        provinceCityMap.put("豫", new String[]{"A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "L", "M", "N", "P", "Q", "R", "S", "U"});
        provinceCityMap.put("川", new String[]{"A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "L", "M", "N", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"});
        provinceCityMap.put("渝", new String[]{"A", "B", "C", "D"});
        provinceCityMap.put("鄂", new String[]{"A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "L", "M", "N", "P", "Q", "R", "S"});
        provinceCityMap.put("湘", new String[]{"A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "L", "M", "N", "S"});
        provinceCityMap.put("闽", new String[]{"A", "B", "C", "D", "E", "F", "G", "H", "J", "K"});
        provinceCityMap.put("赣", new String[]{"A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "L", "M"});
        provinceCityMap.put("皖", new String[]{"A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "L", "M", "N", "P", "R", "S"});
        provinceCityMap.put("冀", new String[]{"A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "R", "T"});
        provinceCityMap.put("辽", new String[]{"A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "L", "M", "N", "P"});
        provinceCityMap.put("吉", new String[]{"A", "B", "C", "D", "E", "F", "G", "H", "J", "K"});
        provinceCityMap.put("黑", new String[]{"A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "L", "M", "N", "P", "R"});
        provinceCityMap.put("晋", new String[]{"A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "L", "M"});
        provinceCityMap.put("陕", new String[]{"A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "V"});
        provinceCityMap.put("桂", new String[]{"A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "L", "M", "N", "P", "R"});
        provinceCityMap.put("云", new String[]{"A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "L", "M", "N", "P", "Q", "R", "S"});
        provinceCityMap.put("贵", new String[]{"A", "B", "C", "D", "E", "F", "G", "H", "J"});
        provinceCityMap.put("甘", new String[]{"A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "L", "M", "N", "P"});
        provinceCityMap.put("琼", new String[]{"A", "B", "C", "D", "E", "F"});
        provinceCityMap.put("宁", new String[]{"A", "B", "C", "D", "E"});
        provinceCityMap.put("青", new String[]{"A", "B", "C", "D", "E", "F", "G", "H"});
        provinceCityMap.put("藏", new String[]{"A", "B", "C", "D", "E", "F", "G", "H", "J"});
        provinceCityMap.put("蒙", new String[]{"A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "L", "M"});
        provinceCityMap.put("新", new String[]{"A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "L", "M", "N", "P", "Q", "R"});
        provinceCityMap.put("津", new String[]{"A", "B", "C", "E", "F", "G", "H", "J", "K", "L", "M", "N", "Q", "R"});
        // 地址关键词到省份简称的映射
        Map<String, String> addressToProvince = new HashMap<>();
        addressToProvince.put("北京", "京"); addressToProvince.put("上海", "沪");
        addressToProvince.put("广州", "粤"); addressToProvince.put("深圳", "粤");
        addressToProvince.put("东莞", "粤"); addressToProvince.put("佛山", "粤");
        addressToProvince.put("南京", "苏"); addressToProvince.put("苏州", "苏");
        addressToProvince.put("无锡", "苏"); addressToProvince.put("南通", "苏");
        addressToProvince.put("常州", "苏"); addressToProvince.put("徐州", "苏");
        addressToProvince.put("杭州", "浙"); addressToProvince.put("宁波", "浙");
        addressToProvince.put("温州", "浙"); addressToProvince.put("嘉兴", "浙");
        addressToProvince.put("济南", "鲁"); addressToProvince.put("青岛", "鲁");
        addressToProvince.put("郑州", "豫"); addressToProvince.put("成都", "川");
        addressToProvince.put("重庆", "渝"); addressToProvince.put("武汉", "鄂");
        addressToProvince.put("长沙", "湘"); addressToProvince.put("福州", "闽");
        addressToProvince.put("厦门", "闽"); addressToProvince.put("南昌", "赣");
        addressToProvince.put("合肥", "皖"); addressToProvince.put("石家庄", "冀");
        addressToProvince.put("沈阳", "辽"); addressToProvince.put("大连", "辽");
        addressToProvince.put("长春", "吉"); addressToProvince.put("哈尔滨", "黑");
        addressToProvince.put("太原", "晋"); addressToProvince.put("西安", "陕");
        addressToProvince.put("南宁", "桂"); addressToProvince.put("昆明", "云");
        addressToProvince.put("贵阳", "贵"); addressToProvince.put("兰州", "甘");
        addressToProvince.put("海口", "琼"); addressToProvince.put("银川", "宁");
        addressToProvince.put("西宁", "青"); addressToProvince.put("拉萨", "藏");
        addressToProvince.put("呼和浩特", "蒙"); addressToProvince.put("乌鲁木齐", "新");
        addressToProvince.put("天津", "津");
        // 省名映射
        addressToProvince.put("江苏", "苏"); addressToProvince.put("浙江", "浙");
        addressToProvince.put("山东", "鲁"); addressToProvince.put("河南", "豫");
        addressToProvince.put("四川", "川"); addressToProvince.put("湖北", "鄂");
        addressToProvince.put("湖南", "湘"); addressToProvince.put("福建", "闽");
        addressToProvince.put("江西", "赣"); addressToProvince.put("安徽", "皖");
        addressToProvince.put("河北", "冀"); addressToProvince.put("辽宁", "辽");
        addressToProvince.put("吉林", "吉"); addressToProvince.put("黑龙江", "黑");
        addressToProvince.put("山西", "晋"); addressToProvince.put("陕西", "陕");
        addressToProvince.put("广东", "粤"); addressToProvince.put("广西", "桂");
        addressToProvince.put("云南", "云"); addressToProvince.put("贵州", "贵");
        addressToProvince.put("甘肃", "甘"); addressToProvince.put("海南", "琼");
        addressToProvince.put("宁夏", "宁"); addressToProvince.put("青海", "青");
        addressToProvince.put("西藏", "藏"); addressToProvince.put("内蒙古", "蒙");
        addressToProvince.put("新疆", "新");
        String province = "苏"; // 默认江苏
        // 尝试从客户地址推断省份
        if (customerName != null) {
            try {
                Customer customer = customerService.getOne(
                        new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Customer>()
                                .eq(Customer::getCustomerName, customerName)
                                .last("limit 1"));
                if (customer != null && customer.getCompanyAddress() != null) {
                    String addr = customer.getCompanyAddress();
                    for (Map.Entry<String, String> e : addressToProvince.entrySet()) {
                        if (addr.contains(e.getKey())) {
                            province = e.getValue();
                            break;
                        }
                    }
                }
            } catch (Exception e) {
                log.debug("查客户地址失败: {}", e.getMessage());
            }
        }
        // 生成车牌号: 省份简称 + 城市字母 + 5位随机字符(数字+大写字母)
        String[] cities = provinceCityMap.getOrDefault(province, new String[]{"A", "B", "C"});
        String city = cities[ThreadLocalRandom.current().nextInt(cities.length)];
        String chars = "ABCDEFGHJKLMNPQRSTUVWXYZ0123456789";
        StringBuilder suffix = new StringBuilder();
        for (int i = 0; i < 5; i++) {
            suffix.append(chars.charAt(ThreadLocalRandom.current().nextInt(chars.length())));
        }
        return province + city + suffix;
    }
    private String fixBatchNoDate(String batchNo, String newDate) {
        if (batchNo == null || batchNo.length() < 8) {
            return batchNo;
        }
        // 批号格式: yyyyMMdd-产品编码-NNN
        String prefix = batchNo.substring(0, 8);
        if (prefix.matches("\\d{8}")) {
            return newDate + batchNo.substring(8);
        }
        return batchNo;
    }
    private ModuleSummary createProductionPlans(List<JSONObject> items) {
        int success = 0, fail = 0;
        List<ProductionPlan> createdPlans = new ArrayList<>();
        for (JSONObject item : items) {
            try {
                ProductionPlan plan = new ProductionPlan();
@@ -564,18 +1626,48 @@
                plan.setSource(item.getString("source"));
                plan.setRemark(item.getString("remark"));
                if (item.containsKey("requiredDate")) {
                    plan.setRequiredDate(LocalDate.parse(item.getString("requiredDate")));
                    String dateStr = item.getString("requiredDate");
                    if (dateStr != null && dateStr.matches("\\d{4}-\\d{2}-\\d{2}")) {
                        plan.setRequiredDate(LocalDate.parse(dateStr));
                    }
                }
                if (item.containsKey("promisedDeliveryDate")) {
                    plan.setPromisedDeliveryDate(LocalDate.parse(item.getString("promisedDeliveryDate")));
                    String dateStr = item.getString("promisedDeliveryDate");
                    if (dateStr != null && dateStr.matches("\\d{4}-\\d{2}-\\d{2}")) {
                        plan.setPromisedDeliveryDate(LocalDate.parse(dateStr));
                    }
                }
                productionPlanService.save(plan);
                createdPlans.add(plan);
                success++;
            } catch (Exception e) {
                log.warn("创建生产计划失败: {}", e.getMessage());
                fail++;
            }
        }
        // 按productModelId分组,自动合并下发
        Map<Long, List<ProductionPlan>> grouped = createdPlans.stream()
                .filter(p -> p.getProductModelId() != null)
                .collect(Collectors.groupingBy(ProductionPlan::getProductModelId));
        for (Map.Entry<Long, List<ProductionPlan>> entry : grouped.entrySet()) {
            try {
                List<Long> planIds = entry.getValue().stream().map(ProductionPlan::getId).collect(Collectors.toList());
                BigDecimal totalQty = entry.getValue().stream()
                        .map(ProductionPlan::getQtyRequired)
                        .reduce(BigDecimal.ZERO, BigDecimal::add);
                ProductionPlanDto combineDto = new ProductionPlanDto();
                combineDto.setIds(planIds);
                combineDto.setTotalAssignedQuantity(totalQty);
                combineDto.setPlanCompleteTime(LocalDate.now().plusDays(ThreadLocalRandom.current().nextInt(3, 10)));
                productionPlanService.combine(combineDto);
                log.info("生产计划自动下发成功, productModelId={}, 计划数: {}, 下发数量: {}",
                        entry.getKey(), planIds.size(), totalQty);
            } catch (Exception e) {
                log.warn("生产计划自动下发失败, productModelId={}: {}", entry.getKey(), e.getMessage());
            }
        }
        return summary("production", "生产计划", items.size(), success, fail);
    }
@@ -589,7 +1681,10 @@
                order.setProductModelId(item.getLong("productModelId"));
                order.setQuantity(item.getBigDecimal("quantity"));
                if (item.containsKey("planCompleteTime")) {
                    order.setPlanCompleteTime(LocalDate.parse(item.getString("planCompleteTime")));
                    String dateStr = item.getString("planCompleteTime");
                    if (dateStr != null && dateStr.matches("\\d{4}-\\d{2}-\\d{2}")) {
                        order.setPlanCompleteTime(LocalDate.parse(dateStr));
                    }
                }
                productionOrderService.saveProductionOrder(order);
                success++;
src/main/java/com/ruoyi/project/system/domain/SysUser.java
@@ -1,8 +1,8 @@
package com.ruoyi.project.system.domain;
import com.baomidou.mybatisplus.annotation.TableField;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.ruoyi.common.xss.Xss;
import com.baomidou.mybatisplus.annotation.TableField;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.ruoyi.common.xss.Xss;
import com.ruoyi.framework.aspectj.lang.annotation.Excel;
import com.ruoyi.framework.aspectj.lang.annotation.Excel.ColumnType;
import com.ruoyi.framework.aspectj.lang.annotation.Excel.Type;
@@ -60,13 +60,13 @@
    @Excel(name = "账号状态", readConverterExp = "0=正常,1=停用")
    private String status;
    /** 删除标志(0代表存在 2代表删除) */
    private String delFlag;
    /** 是否开通AI功能(0否 1是) */
    @JsonIgnore
    private Integer aiEnabled;
    /** 删除标志(0代表存在 2代表删除) */
    private String delFlag;
    /** 是否开通AI功能(0否 1是) */
    @JsonIgnore
    private Integer aiEnabled;
    /** 最后登录IP */
    @Excel(name = "最后登录IP", type = Type.EXPORT)
    private String loginIp;
@@ -128,6 +128,12 @@
     */
    @TableField(exist = false)
    private String deptNames;
    /**
     * 角色名称
     */
    @TableField(exist = false)
    private String roleNames;
    public Long getCurrentDeptId() {
        return currentDeptId;
@@ -255,25 +261,25 @@
        this.status = status;
    }
    public String getDelFlag()
    {
        return delFlag;
    }
    public String getDelFlag()
    {
        return delFlag;
    }
    public void setDelFlag(String delFlag)
    {
        this.delFlag = delFlag;
    }
    public Integer getAiEnabled()
    {
        return aiEnabled;
    }
    public void setAiEnabled(Integer aiEnabled)
    {
        this.aiEnabled = aiEnabled;
    }
    public void setDelFlag(String delFlag)
    {
        this.delFlag = delFlag;
    }
    public Integer getAiEnabled()
    {
        return aiEnabled;
    }
    public void setAiEnabled(Integer aiEnabled)
    {
        this.aiEnabled = aiEnabled;
    }
    public String getLoginIp()
    {
@@ -369,6 +375,14 @@
        this.deptNames = deptNames;
    }
    public String getRoleNames() {
        return roleNames;
    }
    public void setRoleNames(String roleNames) {
        this.roleNames = roleNames;
    }
    public Long getDeptId() {
        return deptId;
    }
@@ -388,10 +402,10 @@
            .append("sex", getSex())
            .append("avatar", getAvatar())
            .append("password", getPassword())
            .append("status", getStatus())
            .append("delFlag", getDelFlag())
            .append("aiEnabled", getAiEnabled())
            .append("loginIp", getLoginIp())
            .append("status", getStatus())
            .append("delFlag", getDelFlag())
            .append("aiEnabled", getAiEnabled())
            .append("loginIp", getLoginIp())
            .append("loginDate", getLoginDate())
            .append("createBy", getCreateBy())
            .append("createTime", getCreateTime())
src/main/java/com/ruoyi/project/system/mapper/SysUserMapper.java
@@ -155,5 +155,10 @@
    List<Long> getUserByRole(@Param("role") String role);
    /**
     * 查询所有活跃用户(含部门名称和角色名称)
     */
    List<SysUser> selectUserListWithDetail();
    List<Long> getUserByPerms(@Param("perms") List<String> perms);
}
src/main/java/com/ruoyi/sales/service/impl/SalesLedgerProductServiceImpl.java
@@ -223,9 +223,8 @@
        ProductionPlan productionPlan = new ProductionPlan();
        productionPlan.setSalesLedgerId(salesLedgerProduct.getSalesLedgerId());
        productionPlan.setSalesLedgerProductId(salesLedgerProduct.getId());
        productionPlan.setMpsNo(generateNextPlanNo(salesLedger.getEntryDate().toInstant()
                .atZone(ZoneId.systemDefault())
                .toLocalDate().format(DateTimeFormatter.ofPattern("yyyyMMdd"))));
        productionPlan.setMpsNo(generateNextPlanNo(com.ruoyi.common.utils.DateUtils.toLocalDate(salesLedger.getEntryDate())
                .format(DateTimeFormatter.ofPattern("yyyyMMdd"))));
        productionPlan.setProductModelId(salesLedgerProduct.getProductModelId());
        productionPlan.setQtyRequired(salesLedgerProduct.getQuantity());
        productionPlan.setSource("销售");
src/main/resources/application-dev.yml
@@ -74,7 +74,7 @@
    druid:
      # 主库数据源
      master:
        url: jdbc:mysql://localhost:3306/product-inventory-management-new-pro?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
        url: jdbc:mysql://localhost:3306/product-inventory-management-bdpro?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
        username: root
        password: 123456
      # 从库数据源
src/main/resources/mapper/system/SysUserMapper.xml
@@ -25,6 +25,7 @@
        <result property="remark"       column="remark"       />
        <result property="deptNames"       column="dept_names"       />
        <result property="tenantId"       column="tenant_id"       />
        <result property="roleNames"       column="role_names"       />
        <association property="dept"    javaType="com.ruoyi.project.system.domain.SysDept"         resultMap="deptResult" />
        <collection  property="roles"   javaType="java.util.List"  resultMap="RoleResult" />
    </resultMap>
@@ -290,6 +291,29 @@
          and su.del_flag = '0'
        ORDER BY su.create_time DESC
    </select>
    <select id="selectUserListWithDetail" resultMap="SysUserResult">
        select u.user_id, u.nick_name, u.user_name, u.status, u.del_flag,
               dept_agg.dept_names,
               role_agg.role_names
        from sys_user u
        left join (
            SELECT ud.user_id, GROUP_CONCAT(d.dept_name SEPARATOR ', ') AS dept_names
            FROM sys_user_dept ud
            LEFT JOIN sys_dept d ON ud.dept_id = d.dept_id
            WHERE d.del_flag = '0' AND d.status = '0'
            GROUP BY ud.user_id
        ) dept_agg on dept_agg.user_id = u.user_id
        left join (
            SELECT ur.user_id, GROUP_CONCAT(r.role_name SEPARATOR ', ') AS role_names
            FROM sys_user_role ur
            LEFT JOIN sys_role r ON ur.role_id = r.role_id
            WHERE r.del_flag = '0' AND r.status = '0'
            GROUP BY ur.user_id
        ) role_agg on role_agg.user_id = u.user_id
        where u.del_flag = '0' AND u.status = '0'
    </select>
    <select id="getUserByPerms" resultType="java.lang.Long">
        select distinct t5.user_id
        from sys_role_menu t1