gongchunyi
2026-07-01 38f618f789b4062a51f1c8c608bb734d9e47b5ba
Merge branch 'dev_New_pro' into dev_平遥县盛达铸造厂
已添加1个文件
已修改64个文件
3030 ■■■■ 文件已修改
docs/mock_data_check.md 22 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/vo/sales/SalesOutboundVo.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/controller/sales/AccountInvoiceApplicationController.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/mapper/sales/AccountInvoiceApplicationMapper.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/impl/sales/AccountInvoiceApplicationServiceImpl.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/impl/sales/AccountSalesCollectionServiceImpl.java 18 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/sales/AccountInvoiceApplicationService.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/ApprovalInstanceServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/ApprovalTemplateServiceImpl.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/service/impl/CustomerServiceImpl.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/vo/CustomerVo.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/common/enums/StockOutQualifiedRecordTypeEnum.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/common/utils/DateUtils.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/home/controller/HomeController.java 38 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/home/service/impl/HomeServiceImpl.java 92 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/mock/prompt/MockDataPrompt.java 251 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/mock/service/impl/DataCheckServiceImpl.java 136 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/mock/service/impl/DataGenerateServiceImpl.java 1318 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/service/impl/ReturnManagementServiceImpl.java 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/utils/StockUtils.java 27 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/bean/dto/ProductionPlanImportDto.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/enums/ProductOrderStatusEnum.java 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionOrderRoutingOperationParamServiceImpl.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionOrderServiceImpl.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionPlanServiceImpl.java 2 ●●● 补丁 | 查看 | 原始文档 | 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/purchase/dto/PurchaseLedgerDto.java 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/pojo/PurchaseLedger.java 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/service/impl/PurchaseLedgerServiceImpl.java 33 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/service/impl/PurchaseReturnOrdersServiceImpl.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/pojo/QualityInspect.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/pojo/QualityUnqualified.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/controller/SalesLedgerProductController.java 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/dto/SalesLedgerDto.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/pojo/SalesLedger.java 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/pojo/SalesLedgerProduct.java 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/impl/SalesLedgerProductServiceImpl.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/impl/SalesLedgerServiceImpl.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/impl/SalesQuotationServiceImpl.java 26 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/impl/ShippingInfoServiceImpl.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/staff/service/impl/SchemeApplicableStaffServiceImpl.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/controller/StockOutRecordController.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/service/StockOutRecordService.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/service/impl/StockInRecordServiceImpl.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/service/impl/StockInventoryServiceImpl.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/service/impl/StockOutRecordServiceImpl.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/technology/controller/TechnologyOperationController.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/technology/service/TechnologyOperationService.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/technology/service/impl/TechnologyOperationServiceImpl.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application-dev.yml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application-myhb-pro.yml 268 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/account/sales/AccountInvoiceApplicationMapper.xml 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/account/sales/AccountSalesCollectionMapper.xml 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/basic/CustomerMapper.xml 22 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/basic/SupplierManageMapper.xml 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductionOrderMapper.xml 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/purchase/PurchaseLedgerMapper.xml 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/sales/SalesLedgerMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/sales/SalesLedgerProductMapper.xml 33 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/sales/ShippingInfoMapper.xml 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/staff/PersonalShiftMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/stock/StockInRecordMapper.xml 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/system/SysDeptMapper.xml 3 ●●●● 补丁 | 查看 | 原始文档 | 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/account/bean/vo/sales/SalesOutboundVo.java
@@ -1,5 +1,6 @@
package com.ruoyi.account.bean.vo.sales;
import com.alibaba.excel.annotation.ExcelIgnore;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.framework.aspectj.lang.annotation.Excel;
@@ -52,4 +53,8 @@
    @Schema(description = "销售订单号")
    @Excel(name = "销售订单号")
    private String salesContractNo;
    @Schema(description = "已收款金额")
    @ExcelIgnore
    private BigDecimal amountReceived;
}
src/main/java/com/ruoyi/account/controller/sales/AccountInvoiceApplicationController.java
@@ -16,6 +16,7 @@
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
import java.util.List;
/**
 * <p>
@@ -41,6 +42,13 @@
        return R.ok(listPage);
    }
    @GetMapping("/getAccountInvoiceApplicationList")
    @Log(title = "根据客户id查询开票申请台账", businessType = BusinessType.OTHER)
    @Operation(summary = "财务管理--开票申请台账")
    public R<List<AccountInvoiceApplicationVo>> getAccountInvoiceApplicationList(@RequestParam Integer customerId){
        return R.ok(accountInvoiceApplicationService.getAccountInvoiceApplicationList(customerId));
    }
    @GetMapping("/getOutboundBatchesByCustomer")
    @Log(title = "根据客户查询出库单号(开票申请)", businessType = BusinessType.OTHER)
    @Operation(summary = "财务管理--根据客户查询出库单号(开票申请)")
src/main/java/com/ruoyi/account/mapper/sales/AccountInvoiceApplicationMapper.java
@@ -29,4 +29,6 @@
    //判断该出库记录是否有开票申请
    boolean existsByStockOutRecordId(@Param("stockOutRecordIds") List<Long> stockOutRecordIds);
    List<AccountInvoiceApplicationVo> selectAccountInvoiceApplicationList(@Param("customerId") Integer customerId);
}
src/main/java/com/ruoyi/account/service/impl/sales/AccountInvoiceApplicationServiceImpl.java
@@ -91,6 +91,11 @@
        return removeBatchByIds(ids);
    }
    @Override
    public List<AccountInvoiceApplicationVo> getAccountInvoiceApplicationList(Integer customerId) {
        return accountInvoiceApplicationMapper.selectAccountInvoiceApplicationList(customerId);
    }
    private String genInvoiceApplicationNo() {
        return "KP" + LocalDateTime.now().format(CODE_TIME_FORMATTER) + new Random().nextInt(10);
    }
src/main/java/com/ruoyi/account/service/impl/sales/AccountSalesCollectionServiceImpl.java
@@ -53,15 +53,15 @@
        if (StringUtils.isEmpty(accountSalesCollection.getCollectionNumber())) {
            accountSalesCollection.setCollectionNumber(genAccountSalesCollectionNo());
        }
        String stockOutRecordIds = accountSalesCollection.getStockOutRecordIds();
        if (stockOutRecordIds != null && !stockOutRecordIds.isEmpty()) {
            List<Long> ids = Arrays.stream(stockOutRecordIds.split(","))
                    .map(Long::valueOf)
                    .toList();
            if (accountSalesCollectionMapper.existsByStockOutRecordId(ids)){
                throw new ServiceException("存在重复的出库单");
            }
        }
//        String stockOutRecordIds = accountSalesCollection.getStockOutRecordIds();
//        if (stockOutRecordIds != null && !stockOutRecordIds.isEmpty()) {
//            List<Long> ids = Arrays.stream(stockOutRecordIds.split(","))
//                    .map(Long::valueOf)
//                    .toList();
//            if (accountSalesCollectionMapper.existsByStockOutRecordId(ids)){
//                throw new ServiceException("存在重复的出库单");
//            }
//        }
        return save(accountSalesCollection);
    }
src/main/java/com/ruoyi/account/service/sales/AccountInvoiceApplicationService.java
@@ -30,4 +30,6 @@
    void exportAccountInvoiceApplication(HttpServletResponse response, AccountInvoiceApplicationDto accountInvoiceApplicationDto);
    boolean deleteAccountInvoiceApplication(List<Long> ids);
    List<AccountInvoiceApplicationVo> getAccountInvoiceApplicationList(Integer customerId);
}
src/main/java/com/ruoyi/approve/service/impl/ApprovalInstanceServiceImpl.java
@@ -111,7 +111,6 @@
                if (TypeEnums.PURCHASE_APPROVAL.getCode().equals(vo.getBusinessType())) {
                    // é‡‡è´­å®¡æ‰¹ - æŸ¥è¯¢é‡‡è´­å•号
                    PurchaseLedger purchaseLedger = purchaseLedgerMapper.selectById(vo.getBusinessId());
                    System.out.println("业务类型:" + purchaseLedger.getPurchaseContractNumber());
                    if (purchaseLedger != null) {
                        vo.setPurchaseContractNumber(purchaseLedger.getPurchaseContractNumber());
                    }
src/main/java/com/ruoyi/approve/service/impl/ApprovalTemplateServiceImpl.java
@@ -13,6 +13,8 @@
import com.ruoyi.approve.bean.vo.ApprovalTemplateVo;
import com.ruoyi.approve.mapper.ApprovalTemplateMapper;
import com.ruoyi.approve.mapper.ApprovalTemplateNodeApproverMapper;
import com.ruoyi.approve.mapper.ApprovalInstanceMapper;
import com.ruoyi.approve.pojo.ApprovalInstance;
import com.ruoyi.approve.pojo.ApprovalTemplate;
import com.ruoyi.approve.pojo.ApprovalTemplateNode;
import com.ruoyi.approve.pojo.ApprovalTemplateNodeApprover;
@@ -48,6 +50,7 @@
    private final ApprovalTemplateMapper approvalTemplateMapper;
    private final ApprovalTemplateNodeService approvalTemplateNodeService;
    private final ApprovalTemplateNodeApproverMapper approvalTemplateNodeApproverMapper;
    private final ApprovalInstanceMapper approvalInstanceMapper;
    private final FileUtil fileUtil;
    @Override
@@ -82,6 +85,10 @@
    @Override
    public Boolean updateApprovalTemplateDto(ApprovalTemplateDto approvalTemplateDto) {
        // æ£€æŸ¥æ¨¡æ¿æ˜¯å¦è¢«å®¡æ‰¹å®žä¾‹å¼•用
        if (isTemplateReferenced(approvalTemplateDto.getId())) {
            throw new RuntimeException("该审批模板已被审批流程引用,无法编辑");
        }
        approvalTemplateMapper.updateById(approvalTemplateDto);
        approvalTemplateNodeService.remove(
                new LambdaQueryWrapper<ApprovalTemplateNode>()
@@ -111,6 +118,12 @@
    public Boolean delete(List<Long> ids) {
        if (ids == null || ids.isEmpty()) {
            return false;
        }
        // æ£€æŸ¥æ¨¡æ¿æ˜¯å¦è¢«å®¡æ‰¹å®žä¾‹å¼•用
        for (Long id : ids) {
            if (isTemplateReferenced(id)) {
                throw new RuntimeException("审批模板(ID: " + id + ")已被审批流程引用,无法删除");
            }
        }
        ApprovalTemplate updateEntity = new ApprovalTemplate();
        updateEntity.setDeleted(1);
@@ -275,4 +288,21 @@
        BeanUtils.copyProperties(approver, approverVo);
        return approverVo;
    }
    /**
     * æ£€æŸ¥å®¡æ‰¹æ¨¡æ¿æ˜¯å¦è¢«å®¡æ‰¹å®žä¾‹å¼•用
     * @param templateId æ¨¡æ¿ID
     * @return true è¡¨ç¤ºè¢«å¼•用,false è¡¨ç¤ºæœªè¢«å¼•用
     */
    private boolean isTemplateReferenced(Long templateId) {
        if (templateId == null) {
            return false;
        }
        Long count = approvalInstanceMapper.selectCount(
                new LambdaQueryWrapper<ApprovalInstance>()
                        .eq(ApprovalInstance::getTemplateId, templateId)
                        .eq(ApprovalInstance::getDeleted, 0)
        );
        return count != null && count > 0;
    }
}
src/main/java/com/ruoyi/basic/service/impl/CustomerServiceImpl.java
@@ -212,6 +212,7 @@
    public int updateCustomer(Customer customer) {
        LoginUser loginUser = SecurityUtils.getLoginUser();
        Long tenantId = loginUser.getTenantId();
        validateCustomerEditPermission(customer.getId());
        customer.setTenantId(tenantId);
        return customerMapper.updateById(customer);
    }
@@ -341,6 +342,7 @@
    public void recycleCustomer(CustomerDto customerDto) {
        Customer customer = customerMapper.selectById(customerDto.getId());
        if (customer.getType() == 1 && customer.getIsAssigned() == 1) {  // å…¬æµ·ä¸”已分配
            validatePublicSeaRecyclePermission(customer.getId());
            customer.setIsAssigned(0);
            customer.setUsageStatus(0L);
            customer.setUsageUser(0L);
@@ -403,6 +405,44 @@
        return this.updateById(customer);
    }
    private void validateCustomerEditPermission(Long customerId) {
        Customer customer = customerMapper.selectById(customerId);
        if (customer == null) {
            throw new RuntimeException("客户档案不存在");
        }
        if (!hasSalesModuleReference(customerId)) {
            return;
        }
        if (Objects.equals(customer.getType(), 0)) {
            throw new RuntimeException("已被销售模块引用的私海客户不能编辑");
        }
        if (Objects.equals(customer.getType(), 1) && Objects.equals(customer.getIsAssigned(), 1)) {
            throw new RuntimeException("已领用并且被销售模块引用的公海客户不能编辑");
        }
    }
    private void validatePublicSeaRecyclePermission(Long customerId) {
        if (hasSalesModuleReference(customerId)) {
            throw new RuntimeException("已领用并且被销售模块引用的公海客户不能回收");
        }
    }
    private boolean hasSalesModuleReference(Long customerId) {
        Long salesLedgerCount = salesLedgerMapper.selectCount(new QueryWrapper<SalesLedger>().lambda()
                .eq(SalesLedger::getCustomerId, customerId));
        if (salesLedgerCount != null && salesLedgerCount > 0) {
            return true;
        }
        Long salesQuotationCount = salesQuotationMapper.selectCount(new QueryWrapper<SalesQuotation>().lambda()
                .eq(SalesQuotation::getCustomerId, customerId));
        if (salesQuotationCount != null && salesQuotationCount > 0) {
            return true;
        }
        Long returnManagementCount = returnManagementMapper.selectCount(new QueryWrapper<ReturnManagement>().lambda()
                .eq(ReturnManagement::getCustomerId, customerId));
        return returnManagementCount != null && returnManagementCount > 0;
    }
    @Override
    public IPage<CustomerTransactionsVo> customewTransactions(Page page, String customerName) {
        return customerMapper.customewTransactions(page, customerName);
src/main/java/com/ruoyi/basic/vo/CustomerVo.java
@@ -25,4 +25,9 @@
     * å…±äº«ç”¨æˆ·ID字符串(SQL查询返回,用于转换为List)
     */
    private String userIdsStr;
    /**
     * æ˜¯å¦è¢«é”€å”®æ¨¡å—引用:0-否,1-是
     */
    private Integer salesReferenceFlag;
}
src/main/java/com/ruoyi/common/enums/StockOutQualifiedRecordTypeEnum.java
@@ -7,7 +7,6 @@
public enum StockOutQualifiedRecordTypeEnum implements BaseEnum<String> {
    CUSTOMIZATION_STOCK_OUT("1", "合格自定义出库"),
    PRODUCTION_REPORT_STOCK_OUT("3", "生产报工-出库"),
    SALE_STOCK_OUT("8", "销售-出库"),
    PURCHASE_RETURN_STOCK_OUT("9", "采购退货"),
    CUSTOMIZATION_UNSTOCK_OUT("10", "不合格自定义出库"),
    SALE_SHIP_STOCK_OUT("13", "销售-发货出库"),
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/home/controller/HomeController.java
@@ -61,7 +61,8 @@
    private static final Integer ORDER_STATUS_WAIT = 1;
    private static final Integer ORDER_STATUS_RUNNING = 2;
    private static final Integer ORDER_STATUS_COMPLETED = 3;
    private static final Integer ORDER_STATUS_PAUSED = 4;
    //private static final Integer ORDER_STATUS_END = 4;  // å·²å–消
    private static final Integer ORDER_STATUS_END = 5;
    /********************************************************基础类*****************************************************/
    @GetMapping("/todos")
@@ -260,13 +261,14 @@
        }
        Integer statusFromParam = parseOrderStatus(status);
        if (!isBlank(status) && statusFromParam == null && !"all".equalsIgnoreCase(status.trim())) {
            return R.fail("status参数不合法,可选值:all/waiting/inProgress/completed/paused æˆ– 1/2/3/4");
            return R.fail("status参数不合法,可选值:all/waiting/inProgress/completed/end æˆ– 1/2/3/5");
        }
        Integer queryStatus = resolveOrderStatus(status, tab);
        long safePageNum = pageNum == null || pageNum < 1 ? 1 : pageNum;
        long safePageSize = pageSize == null || pageSize < 1 ? 10 : Math.min(pageSize, 50);
        long offset = (safePageNum - 1) * safePageSize;
        boolean queryAll = pageSize != null && pageSize == -1;
        long safePageSize = queryAll ? -1 : (pageSize == null || pageSize < 1 ? 10 : Math.min(pageSize, 50));
        long offset = queryAll ? 0 : (safePageNum - 1) * safePageSize;
        LocalDateTime startTime = queryDate == null ? null : queryDate.atStartOfDay();
        LocalDateTime endTime = queryDate == null ? null : queryDate.plusDays(1).atStartOfDay();
@@ -281,7 +283,7 @@
        long waitingCount = 0L;
        long inProgressCount = 0L;
        long completedCount = 0L;
        long pausedCount = 0L;
        long endCount = 0L;
        List<Map<String, Object>> statusCountRows = productionOrderMapper.countHomeOrderProgressByStatus(startTime, endTime);
        if (statusCountRows != null) {
            for (Map<String, Object> countRow : statusCountRows) {
@@ -293,8 +295,8 @@
                    inProgressCount = cnt;
                } else if (Objects.equals(statusKey, ORDER_STATUS_COMPLETED)) {
                    completedCount = cnt;
                } else if (Objects.equals(statusKey, ORDER_STATUS_PAUSED)) {
                    pausedCount = cnt;
                } else if (Objects.equals(statusKey, ORDER_STATUS_END)) {
                    endCount = cnt;
                }
            }
        }
@@ -303,13 +305,13 @@
        result.put("tab", mapOrderTab(queryStatus));
        result.put("status", mapOrderStatus(queryStatus));
        result.put("bizDate", queryDate == null ? null : queryDate.format(DATE_FORMATTER));
        result.put("total", toLong(productionOrderMapper.countHomeOrderProgress(queryStatus, startTime, endTime)));
        result.put("total", toLong(productionOrderMapper.countHomeOrderProgress(null, startTime, endTime)));
        result.put("pageNum", safePageNum);
        result.put("pageSize", safePageSize);
        result.put("waitingCount", waitingCount);
        result.put("inProgressCount", inProgressCount);
        result.put("completedCount", completedCount);
        result.put("pausedCount", pausedCount);
        result.put("endCount", endCount);
        result.put("records", records);
        return R.ok(result);
    }
@@ -529,8 +531,8 @@
        if ("3".equals(normalized) || "completed".equals(normalized)) {
            return ORDER_STATUS_COMPLETED;
        }
        if ("4".equals(normalized) || "paused".equals(normalized)) {
            return ORDER_STATUS_PAUSED;
        if ("5".equals(normalized) || "end".equals(normalized)) {
            return ORDER_STATUS_END;
        }
        return null;
    }
@@ -542,8 +544,8 @@
        if (Objects.equals(status, ORDER_STATUS_COMPLETED)) {
            return "completed";
        }
        if (Objects.equals(status, ORDER_STATUS_PAUSED)) {
            return "paused";
        if (Objects.equals(status, ORDER_STATUS_END)) {
            return "end";
        }
        if (Objects.equals(status, ORDER_STATUS_WAIT)) {
            return "waiting";
@@ -561,8 +563,8 @@
        if (Objects.equals(status, ORDER_STATUS_COMPLETED)) {
            return "completed";
        }
        if (Objects.equals(status, ORDER_STATUS_PAUSED)) {
            return "paused";
        if (Objects.equals(status, ORDER_STATUS_END)) {
            return "end";
        }
        return "all";
    }
@@ -577,8 +579,8 @@
        if (Objects.equals(status, ORDER_STATUS_COMPLETED)) {
            return "已完成";
        }
        if (Objects.equals(status, ORDER_STATUS_PAUSED)) {
            return "已暂停";
        if (Objects.equals(status, ORDER_STATUS_END)) {
            return "已结束";
        }
        return "未知";
    }
@@ -635,7 +637,7 @@
                .select(ProductionOrder::getQuantity, ProductionOrder::getCompleteQuantity)
                .ge(ProductionOrder::getCreateTime, day.atStartOfDay())
                .lt(ProductionOrder::getCreateTime, day.plusDays(1).atStartOfDay())
                .ne(ProductionOrder::getStatus, ORDER_STATUS_PAUSED));
                .ne(ProductionOrder::getStatus, ORDER_STATUS_END));
        BigDecimal totalQuantity = BigDecimal.ZERO;
        BigDecimal totalCompleteQuantity = BigDecimal.ZERO;
src/main/java/com/ruoyi/home/service/impl/HomeServiceImpl.java
@@ -18,7 +18,11 @@
import com.ruoyi.account.pojo.purchase.AccountPurchasePayment;
import com.ruoyi.account.pojo.sales.AccountSalesCollection;
import com.ruoyi.approve.mapper.ApproveProcessMapper;
import com.ruoyi.approve.mapper.ApprovalInstanceMapper;
import com.ruoyi.approve.mapper.ApprovalTaskMapper;
import com.ruoyi.approve.pojo.ApproveProcess;
import com.ruoyi.approve.pojo.ApprovalInstance;
import com.ruoyi.approve.pojo.ApprovalTask;
import com.ruoyi.basic.mapper.CustomerMapper;
import com.ruoyi.basic.mapper.ProductMapper;
import com.ruoyi.basic.mapper.SupplierManageMapper;
@@ -28,6 +32,7 @@
import com.ruoyi.collaborativeApproval.mapper.NoticeMapper;
import com.ruoyi.collaborativeApproval.pojo.Notice;
import com.ruoyi.common.enums.ApproveTypeEnum;
import com.ruoyi.common.enums.TypeEnums;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.device.mapper.DeviceRepairMapper;
@@ -97,6 +102,10 @@
    private final QualityInspectMapper qualityStatisticsMapper;
    private final ApproveProcessMapper approveProcessMapper;
    private final ApprovalTaskMapper approvalTaskMapper;
    private final ApprovalInstanceMapper approvalInstanceMapper;
    private final SysDeptMapper sysDeptMapper;
@@ -384,40 +393,58 @@
    @Override
    public List<ApproveProcess> todos() throws ParseException {
        LoginUser loginUser = SecurityUtils.getLoginUser();
        List<ApproveProcess> approveProcesses = new ArrayList<>();
        // æŸ¥è¯¢æ—§å®¡æ‰¹è¡¨ï¼ˆapprove_process)
        LambdaQueryWrapper<ApproveProcess> approveProcessLambdaQueryWrapper = new LambdaQueryWrapper<>();
        approveProcessLambdaQueryWrapper.eq(ApproveProcess::getApproveDelete, 0)
                .eq(ApproveProcess::getApproveUserCurrentId, loginUser.getUserId())
                .ne(ApproveProcess::getApproveStatus, 2);
        // .eq(ApproveProcess::getTenantId, loginUser.getTenantId());
        List<ApproveProcess> approveProcesses = approveProcessMapper.selectList(approveProcessLambdaQueryWrapper);
        if (CollectionUtils.isEmpty(approveProcesses)) {
            approveProcesses = new ArrayList<>();
        List<ApproveProcess> oldList = approveProcessMapper.selectList(approveProcessLambdaQueryWrapper);
        if (!CollectionUtils.isEmpty(oldList)) {
            oldList.forEach(a -> {
                if (a.getApproveType() != null) {
                    a.setApproveTypeName(ApproveTypeEnum.getNameByCode(a.getApproveType()));
                }
            });
            approveProcesses.addAll(oldList);
        }
        approveProcesses.forEach(a -> {
            if (a.getApproveType() != null) {
                a.setApproveTypeName(ApproveTypeEnum.getNameByCode(a.getApproveType()));
        // æŸ¥è¯¢æ–°å®¡æ‰¹è¡¨ï¼ˆapproval_task + approval_instance)
        List<ApprovalTask> pendingTasks = approvalTaskMapper.selectList(new LambdaQueryWrapper<ApprovalTask>()
                .eq(ApprovalTask::getApproverId, loginUser.getUserId())
                .eq(ApprovalTask::getTaskStatus, "PENDING")
                .eq(ApprovalTask::getDeleted, 0));
        if (!CollectionUtils.isEmpty(pendingTasks)) {
            List<Long> instanceIds = pendingTasks.stream()
                    .map(ApprovalTask::getInstanceId)
                    .distinct()
                    .collect(Collectors.toList());
            List<ApprovalInstance> instances = approvalInstanceMapper.selectList(new LambdaQueryWrapper<ApprovalInstance>()
                    .in(ApprovalInstance::getId, instanceIds)
                    .eq(ApprovalInstance::getDeleted, 0));
            Map<Long, ApprovalInstance> instanceMap = instances.stream()
                    .collect(Collectors.toMap(ApprovalInstance::getId, i -> i, (a, b) -> a));
            for (ApprovalTask task : pendingTasks) {
                ApprovalInstance instance = instanceMap.get(task.getInstanceId());
                if (instance == null) {
                    continue;
                }
                ApproveProcess ap = new ApproveProcess();
                ap.setId(instance.getId());
                ap.setApproveId(instance.getInstanceNo());
                ap.setApproveReason(instance.getTitle());
                ap.setApproveTime(instance.getApplyTime() != null ? java.sql.Timestamp.valueOf(instance.getApplyTime()) : null);
                ap.setApproveUserName(instance.getApplicantName());
                ap.setApproveDeptName(instance.getTemplateName());
                if (instance.getBusinessType() != null) {
                    ap.setApproveType(instance.getBusinessType().intValue());
                    ap.setApproveTypeName(TypeEnums.getLabelByValue(instance.getBusinessType()));
                }
                approveProcesses.add(ap);
            }
        });
        // // æŸ¥è¯¢æœªé¢†ç”¨åŠ³ä¿è®°å½•
        // SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        //
        // LaborIssue laborIssue1 = new LaborIssue();
        // laborIssue1.setAdoptedDate(new Date());
        // laborIssue1.setIssueDate(sdf.parse(sdf.format(new Date())));
        // List<LaborIssue> laborIssues = lavorIssueMapper.list(laborIssue1);
        // //staff_join_leave_record表被删除
        // if(!CollectionUtils.isEmpty(laborIssues)){
        // for (LaborIssue laborIssue : laborIssues) {
        // ApproveProcess approveProcess = new ApproveProcess();
        // approveProcess.setApproveId(laborIssue.getOrderNo());
        // approveProcess.setApproveDeptName(sysDeptMapper.selectDeptById(loginUser.getTenantId()).getDeptName());
        // approveProcess.setApproveTime(laborIssue.getIssueDate());
        // approveProcess.setApproveReason(laborIssue.getDictTypeName() + "-" +
        // laborIssue.getDictName() + "超时未领取");
        // approveProcesses.add(approveProcess);
        // }
        // }
        }
        return approveProcesses;
    }
@@ -470,18 +497,23 @@
    @Override
    public Map<String, Object> approveAndDeviceTodos() {
        // å®¡æ‰¹ååŒå¾…办
        Long aLong = approveProcessMapper.selectCount(new LambdaQueryWrapper<ApproveProcess>()
        // æ—§å®¡æ‰¹è¡¨å¾…办
        Long oldCount = approveProcessMapper.selectCount(new LambdaQueryWrapper<ApproveProcess>()
                .eq(ApproveProcess::getApproveUserCurrentId, SecurityUtils.getUserId())
                .eq(ApproveProcess::getApproveDelete, 0)
                .in(ApproveProcess::getApproveStatus, 0, 1, 3));
        // æ–°å®¡æ‰¹è¡¨å¾…办
        Long newCount = approvalTaskMapper.selectCount(new LambdaQueryWrapper<ApprovalTask>()
                .eq(ApprovalTask::getApproverId, SecurityUtils.getUserId())
                .eq(ApprovalTask::getTaskStatus, "PENDING")
                .eq(ApprovalTask::getDeleted, 0));
        // è®¾å¤‡æŠ¥ä¿®å¾…办
        Long aLong1 = deviceRepairMapper.selectCount(new LambdaQueryWrapper<DeviceRepair>()
                .eq(DeviceRepair::getStatus, 0)
                .eq(DeviceRepair::getRepairName, SecurityUtils.getLoginUser().getNickName()));
        return new HashMap<String, Object>() {
            {
                put("approveTodo", aLong);
                put("approveTodo", oldCount + newCount);
                put("deviceRepairTodo", aLong1);
            }
        };
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,17 +15,17 @@
    public static String buildSystemPrompt() {
        return """
            ä½ æ˜¯ä¸€ä¸ªä¼ä¸šERP系统的数据模拟专家。你需要根据用户提供的行业、数量、时间范围等信息,
            ç”Ÿæˆç¬¦åˆä¸šåŠ¡é€»è¾‘çš„æ¨¡æ‹Ÿæ•°æ®ã€‚
            ä½ æ˜¯ä¸€ä¸ªä¼ä¸šERP系统的数据模拟专家。
            è¦æ±‚:
            æ ¸å¿ƒè§„则(必须严格遵守):
            1. è¾“出必须是纯JSON数组,不要用markdown代码块包裹,不要有任何其他文字
            2. æ¯ä¸ªJSON对象必须包含 "entity" å­—段,值为实体类型名
            3. æ•°æ®å†…容要符合指定行业的特征(公司名称、产品名称、联系人等要像该行业的)
            4. æ—¥æœŸå­—段在指定的时间范围内随机分布
            5. é‡‘额、数量等数值字段要合理
            6. åŒä¸€æ¨¡å—内的实体之间要有引用关系(如销售台账引用客户名称)
            7. æ‰€æœ‰å­—符串字段不要使用emoji或特殊unicode字符
            3. æ•°æ®å†…容要符合指定行业的特征
            4. é‡‘额、数量等数值字段要合理
            5. åŒä¸€æ¨¡å—内的实体之间要有引用关系
            6. ä¸è¦ä½¿ç”¨emoji或特殊unicode字符
            7. æœ€é‡è¦ï¼šæ—¥æœŸåªä»Ž"指定时间范围"中取,绝对不要用其他年份!
            8. äººå‘˜å­—段(如entryPerson、salesman等)必须从"系统用户列表"中选取,根据角色和部门匹配对应模块,不要编造人名!
            """;
    }
@@ -30,196 +33,210 @@
                                           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));
            sb.append(buildQualityPrompt(countMin, countMax, dateStart));
        }
        if (modules.contains("production")) {
            sb.append(buildProductionPrompt(countMin, countMax, dateStart, dateEnd));
        }
        if (modules.contains("stock")) {
            sb.append(buildStockPrompt(countMin, countMax));
            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 """
            é”€å”®æ¨¡å— - æŒ‰ä»¥ä¸‹æ ¼å¼ç”Ÿæˆ:
            {
              "entity": "customer",
              "customerName": "XX科技有限公司",
              "customerType": "企业客户",
              "contactPerson": "张三",
              "contactPhone": "13800138000",
              "companyAddress": "XX省XX市XX区XXè·¯XX号",
              "companyPhone": "0513-XXXXXXXX",
              "taxpayerIdentificationNumber": "91110108XXXXXXXXXX",
              "maintainer": "李四",
              "maintenanceTime": "2026-06-01",
              "bankAccount": "622202XXXXXXXXXXXX",
              "basicBankAccount": "XX银行XX支行",
              "bankCode": "308100XXXXXX"
            }
            é”€å”®æ¨¡å—格式(所有日期必须在 %s ~ %s èŒƒå›´å†…):
            %s注意:不要生成customer实体,只生成salesLedger!
            productData中的productModelId必须选【成品类】规格ID!
            entryPerson和salesman必须从系统用户列表中角色或部门含"销售"的用户中选取!
            {
              "entity": "salesLedger",
              "customerName": "引用上面生成的客户名称",
              "salesContractNo": "XS-20260601-001",
              "salesContractNo": "XS-YYYYMMDD-001",
              "projectName": "XX项目",
              "entryDate": "2026-06-01",
              "entryPerson": "张三",
              "salesman": "销售员姓名",
              "entryDate": "%s",
              "entryPerson": "从用户列表中选",
              "salesman": "从用户列表中选",
              "contractAmount": 50000.00,
              "paymentMethod": "月结30天",
              "executionDate": "2026-06-01",
              "deliveryDate": "2026-07-01",
              "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-年月日-序号,金额合理。
            æ³¨æ„ taxExclusiveTotalPrice æ˜¯ä¸å«ç¨Žæ€»ä»·ï¼Œéœ€è¦æ ¹æ®å«ç¨Žæ€»ä»·å’Œç¨ŽçŽ‡è®¡ç®—ï¼ˆ=含税总价/(1+税率/100))。
            æ—¥æœŸèŒƒå›´: %s ~ %s,每种实体生成%d-%d条。
            """.formatted(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 """
            é‡‡è´­æ¨¡å— - æŒ‰ä»¥ä¸‹æ ¼å¼ç”Ÿæˆ:
            {
              "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-20260601-001",
              "supplierName": "从已有供应商中选择",
              "purchaseContractNumber": "CG-YYYYMMDD-001",
              "projectName": "XX采购项目",
              "entryDate": "2026-06-01",
              "entryDate": "%s",
              "contractAmount": 30000.00,
              "paymentMethod": "货到付款",
              "executionDate": "2026-06-01",
              "productData": [
                {
                  "productId": 1,
                  "productModelId": 1,
                  "quantity": 50,
                  "taxInclusiveUnitPrice": 600.00,
                  "taxInclusiveTotalPrice": 30000.00,
                  "taxExclusiveTotalPrice": 26548.67,
                  "taxRate": 13.00,
                  "unit": "ä»¶",
                  "type": 2
                }
              ]
              "executionDate": "%s",
              "productData": [{
                "productId": 1, "productModelId": 1, "quantity": 50,
                "taxInclusiveUnitPrice": 600.00, "taxInclusiveTotalPrice": 30000.00,
                "taxExclusiveTotalPrice": 26548.67, "taxRate": 13.00, "unit": "ä»¶", "type": 2
              }]
            }
            ä¾›åº”商名称要像指定行业的供应商,合同编号格式CG-年月日-序号。
            æ³¨æ„ taxExclusiveTotalPrice æ˜¯ä¸å«ç¨Žæ€»ä»·ï¼Œéœ€è¦æ ¹æ®å«ç¨Žæ€»ä»·å’Œç¨ŽçŽ‡è®¡ç®—ï¼ˆ=含税总价/(1+税率/100))。
            æ—¥æœŸèŒƒå›´: %s ~ %s,每种实体生成%d-%d条。
            """.formatted(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) {
    private static String buildQualityPrompt(int min, int max, String dateStart) {
        return """
            è´¨é‡æ¨¡å— - æŒ‰ä»¥ä¸‹æ ¼å¼ç”Ÿæˆ:
            è´¨é‡æ¨¡å—格式:
            {
              "entity": "qualityTestStandard",
              "standardNo": "QTS-20260601-001",
              "standardNo": "QTS-YYYYMMDD-001",
              "standardName": "XX产品检验标准",
              "inspectType": 0,
              "remark": "适用于XX行业的质量检验标准"
            }
            inspectType: 0=原材料检验, 1=过程检验, 2=出厂检验。三种类型都要覆盖。
            standardNo中YYYYMMDD替换为 %s。%d-%d条。
            {
              "entity": "qualityTestStandardBinding",
              "productId": 1,
              "testStandardId": 1
            }
            æ¯ç§å®žä½“生成%d-%d条。
            """.formatted(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 èŒƒå›´å†…):
            {
              "entity": "productionPlan",
              "productModelId": 1,
              "qtyRequired": 200,
              "requiredDate": "2026-06-15",
              "requiredDate": "%s",
              "source": "销售",
              "promisedDeliveryDate": "2026-07-01",
              "promisedDeliveryDate": "%s",
              "remark": "XX客户订单需求"
            }
            {
              "entity": "productionOrder",
              "productModelId": 1,
              "quantity": 200,
              "planCompleteTime": "2026-06-30",
              "planCompleteTime": "%s",
              "remark": "根据生产计划XX生成"
            }
            productionPlan的source可选"销售"或"内部"。
            æ—¥æœŸèŒƒå›´: %s ~ %s,每种实体生成%d-%d条。
            """.formatted(dateStart, dateEnd, min, max);
            source可选"销售"或"内部"。%d-%d条。
            """.formatted(dateStart, dateEnd, dateStart, dateEnd, dateEnd, min, max);
    }
    private static String buildStockPrompt(int min, int max) {
    private static String buildStockPrompt(int min, int max, String dateStart) {
        return """
            åº“存模块 - æŒ‰ä»¥ä¸‹æ ¼å¼ç”Ÿæˆ:
            åº“存模块格式:
            {
              "entity": "stockInventory",
              "productModelId": 1,
              "qualitity": 500,
              "batchNo": "BATCH-20260601-001",
              "batchNo": "BATCH-YYYYMMDD-001",
              "warnNum": 50,
              "remark": "安全库存"
            }
            æ¯ç§å®žä½“生成%d-%d条。batchNo格式BATCH-年月日-序号。
            """.formatted(min, max);
            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
@@ -4,32 +4,82 @@
import com.alibaba.fastjson2.JSONArray;
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;
import com.ruoyi.quality.service.IQualityInspectService;
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;
@@ -37,10 +87,17 @@
import java.math.BigDecimal;
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;
@Slf4j
@@ -62,6 +119,47 @@
    private final ProductionOrderService productionOrderService;
    private final StockInventoryService stockInventoryService;
    // é‡‡è´­å®Œæ•´æµç¨‹éœ€è¦çš„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) {
        DataGenerateResult result = new DataGenerateResult();
@@ -70,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(
@@ -81,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());
@@ -103,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);
@@ -129,12 +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);
                ModuleSummary s = createPurchaseLedgers(grouped.get("purchaseLedger"), supplierNameToId,
                        request.getAdditionalInfo(), request.getDateStart(), request.getDateEnd());
                summaries.add(s);
                totalGenerated += s.getSuccessCount();
            }
@@ -198,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;
@@ -214,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()));
                }
@@ -291,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 {
@@ -305,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++) {
@@ -335,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)),
@@ -343,21 +630,606 @@
                        } 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);
    }
    private ModuleSummary createPurchaseLedgers(List<JSONObject> items, Map<String, Long> supplierNameToId) {
    // ==================== é”€å”®å®Œæ•´æµç¨‹ ====================
    /**
     * é”€å”®å®Œæ•´æµç¨‹: ç”Ÿäº§è®¡åˆ’合并下发 â†’ é¢†æ–™ â†’ æŠ¥å·¥ â†’ è´¨æ£€(可选) â†’ å…¥åº“ â†’ å‘è´§ â†’ å‡ºåº“
     * æ—¶é—´ä»¥é”€å”®å½•入日期为基准,各环节递推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 dateStart, String dateEnd) {
        // æ˜¯å¦éœ€è¦è´¨æ£€ï¼Œä»Žè¡¥å……信息判断,默认不需要
        boolean needQualityInspect = additionalInfo != null
                && (additionalInfo.contains("质检") || additionalInfo.contains("需要检验"));
        int success = 0, fail = 0;
        for (JSONObject item : items) {
            try {
@@ -371,11 +1243,22 @@
                dto.setProjectName(item.getString("projectName"));
                dto.setContractAmount(item.getBigDecimal("contractAmount"));
                dto.setPaymentMethod(item.getString("paymentMethod"));
                if (item.containsKey("entryDate")) {
                    dto.setEntryDate(java.sql.Date.valueOf(item.getString("entryDate")));
                String entryDateStr = item.getString("entryDate");
                LocalDate entryDate = null;
                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) {
@@ -391,6 +1274,8 @@
                        slp.setTaxRate(pd.getBigDecimal("taxRate"));
                        slp.setUnit(pd.getString("unit"));
                        slp.setType(2);
                        // æ˜¯å¦è´¨æ£€ï¼šä»Žè¡¥å……信息判断,默认不需要
                        slp.setIsChecked(needQualityInspect);
                        if (pd.containsKey("taxExclusiveTotalPrice")) {
                            slp.setTaxExclusiveTotalPrice(pd.getBigDecimal("taxExclusiveTotalPrice"));
                        } else if (pd.getBigDecimal("taxInclusiveTotalPrice") != null && pd.getBigDecimal("taxRate") != null) {
@@ -406,17 +1291,333 @@
                    dto.setProductData(products);
                }
                purchaseLedgerService.addOrEditPurchase(dto);
                // é€šè¿‡åˆåŒå·æ‰¾åˆ°åˆšåˆ›å»ºçš„采购台账
                PurchaseLedger savedLedger = purchaseLedgerService.getOne(
                        new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<PurchaseLedger>()
                                .eq(PurchaseLedger::getPurchaseContractNumber, dto.getPurchaseContractNumber())
                                .last("limit 1"));
                if (savedLedger != null) {
                    // èµ°å®Œæ•´æµç¨‹ï¼šå®¡æ ¸é€šè¿‡ â†’ è´¨æ£€(可选) â†’ å…¥åº“ + å…¥åº“审核通过
                    processPurchaseFullFlow(savedLedger, needQualityInspect, entryDate, dateEnd);
                }
                success++;
            } catch (Exception e) {
                log.warn("创建采购台账失败: {}", e.getMessage());
                log.warn("创建采购台账失败: {}", e.getMessage(), e);
                fail++;
            }
        }
        return summary("purchase", "采购台账", items.size(), success, fail);
    }
    /**
     * é‡‡è´­å®Œæ•´æµç¨‹: å®¡æ ¸é€šè¿‡ â†’ è´¨æ£€(可选) â†’ å…¥åº“审核通过
     * å„环节时间以采购录入日期为基准,偶尔推迟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>()
                            .eq(ApprovalInstance::getBusinessId, purchaseLedger.getId())
                            .eq(ApprovalInstance::getBusinessType, 5L)
                            .eq(ApprovalInstance::getDeleted, 0)
                            .orderByDesc(ApprovalInstance::getId)
                            .last("limit 1"));
            if (approvalInstance != null) {
                approvalInstanceService.autoApprove(approvalInstance.getId());
                // ä¿®æ­£å®¡æ‰¹ç›¸å…³æ—¶é—´ä¸ºé‡‡è´­å½•入日期范围
                fixApprovalTimes(approvalInstance.getId(), approveDateTime);
                log.info("采购台账[{}]审批通过, å®¡æ‰¹æ—¥æœŸ: {}", purchaseLedger.getPurchaseContractNumber(), approveDate);
            }
            // 2. è´¨æ£€æµç¨‹ï¼ˆå¦‚果需要质检)
            if (needQualityInspect) {
                // è´¨æ£€æ—¶é—´ï¼šå®¡æ‰¹ä¹‹åŽå†æŽ¨0-3天
                LocalDate inspectDate = approveDate.plusDays(ThreadLocalRandom.current().nextInt(0, 4));
                processQualityInspect(purchaseLedger, inspectDate);
            }
            // 3. å…¥åº“审批通过
            // å…¥åº“时间:质检时间(有质检)或审批时间之后再推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());
        }
    }
    /**
     * ä¿®æ­£å®¡æ‰¹å®žä¾‹åŠå…³è”节点、任务、记录的时间
     */
    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 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());
                    // ä¿®æ­£è´¨æ£€æ—¶é—´
                    qi.setCreateTime(inspectDateTime);
                    qualityInspectMapper.updateById(qi);
                    log.info("采购台账[{}]质检单[{}]自动提交合格, è´¨æ£€æ—¥æœŸ: {}",
                            purchaseLedger.getPurchaseContractNumber(), qi.getId(), inspectDate);
                }
            }
        } catch (Exception e) {
            log.warn("质检流程处理失败[{}]: {}", purchaseLedger.getPurchaseContractNumber(), e.getMessage());
        }
    }
    /**
     * å…¥åº“审核: æ‰¾åˆ°å…¥åº“记录并审批通过,并修正时间与批号日期
     */
    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())));
            if (stockRecords.isEmpty()) {
                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.CUSTOMIZATION_UNSTOCK_OUT.getCode())));
            }
            for (QualityInspect qi : qualityInspectService.list(
                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<QualityInspect>()
                            .eq(QualityInspect::getPurchaseLedgerId, purchaseLedger.getId()))) {
                List<com.ruoyi.stock.pojo.StockInRecord> qiRecords = stockInRecordService.list(
                        new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<com.ruoyi.stock.pojo.StockInRecord>()
                                .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());
            if (!stockRecords.isEmpty()) {
                List<Long> recordIds = stockRecords.stream()
                        .filter(r -> r.getApprovalStatus() == null || r.getApprovalStatus() == 0)
                        .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(), stockDate, recordIds.size());
                }
            }
        } catch (Exception e) {
            log.warn("入库审批失败[{}]: {}", purchaseLedger.getPurchaseContractNumber(), e.getMessage());
        }
    }
    /**
     * æ›¿æ¢æ‰¹å·ä¸­çš„æ—¥æœŸéƒ¨åˆ†ï¼ˆ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();
@@ -425,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);
    }
@@ -450,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/procurementrecord/service/impl/ReturnManagementServiceImpl.java
@@ -21,6 +21,7 @@
import com.ruoyi.procurementrecord.service.ReturnSaleProductService;
import com.ruoyi.procurementrecord.utils.StockUtils;
import com.ruoyi.sales.mapper.SalesLedgerMapper;
import com.ruoyi.sales.pojo.SalesLedger;
import com.ruoyi.sales.pojo.ShippingInfo;
import com.ruoyi.sales.service.ShippingInfoService;
import lombok.RequiredArgsConstructor;
@@ -100,31 +101,42 @@
    public boolean handle(Long returnManagementId) {
        ReturnManagement byId = this.getById(returnManagementId);
        List<ReturnSaleProductDto> list = returnSaleProductService.listReturnSaleProduct(returnManagementId);
        byId.setStatus(1);
        byId.setSettler(SecurityUtils.getLoginUser().getNickName());
        updateById(byId);
        SalesRefundAmountOrderDto salesRefundAmountOrder = new SalesRefundAmountOrderDto();
        salesRefundAmountOrder.setReturnManagementId(returnManagementId);
        salesRefundAmountOrder.setStatus(0);
        salesRefundAmountOrder.setCreateTime(byId.getCreateTime());
        salesRefundAmountOrder.setCreateUserId(SecurityUtils.getUserId());
        BigDecimal bigDecimal = new BigDecimal(0);
        // 1. è®¡ç®—正确的退款总额(金额=单价×数量)
        BigDecimal totalRefund = BigDecimal.ZERO;
        for (ReturnSaleProductDto returnSaleProduct : list) {
            bigDecimal = bigDecimal.add(returnSaleProduct.getPrice());
            salesRefundAmountOrder.setRefundedAmount(new BigDecimal(0));
            BigDecimal itemAmount = returnSaleProduct.getAmount() != null
                    ? returnSaleProduct.getAmount()
                    : BigDecimal.ZERO;
            totalRefund = totalRefund.add(itemAmount);
            // æ˜¯å¦æœ‰è´¨é‡é—®é¢˜
            if (returnSaleProduct.getIsQuality() == 1) {
                // æœ‰è´¨é‡é—®é¢˜ï¼Œå…¥ä¸åˆæ ¼åº“(带批次)
                stockUtils.addUnStockWithBatchNo(returnSaleProduct.getProductModelId(),returnSaleProduct.getNum(), StockInQualifiedRecordTypeEnum.RETURN_UNSTOCK_IN.getCode(),returnSaleProduct.getId(),returnSaleProduct.getBatchNo());
            }else{
                // æ— è´¨é‡é—®é¢˜ï¼Œå…¥åˆæ ¼åº“(带批次)
                stockUtils.addStockWithBatchNo(returnSaleProduct.getProductModelId(),returnSaleProduct.getNum(), StockInQualifiedRecordTypeEnum.RETURN_HE_IN.getCode(),returnSaleProduct.getId(),returnSaleProduct.getBatchNo());
                stockUtils.addUnStockWithBatchNo(returnSaleProduct.getProductModelId(), returnSaleProduct.getNum(), StockInQualifiedRecordTypeEnum.RETURN_UNSTOCK_IN.getCode(), returnSaleProduct.getId(), returnSaleProduct.getBatchNo());
            } else {
                stockUtils.addStockWithBatchNo(returnSaleProduct.getProductModelId(), returnSaleProduct.getNum(), StockInQualifiedRecordTypeEnum.RETURN_HE_IN.getCode(), returnSaleProduct.getId(), returnSaleProduct.getBatchNo());
            }
        }
        salesRefundAmountOrder.setRefundAmount(bigDecimal);
        salesRefundAmountOrder.setNotRefundedAmount(salesRefundAmountOrder.getRefundedAmount());
        // åˆ†æ‰¹é€€æ¬¾
//        salesRefundAmountOrderService.addSalesRefundAmountOrderDto(salesRefundAmountOrder);
        // 2. æ›´æ–°é€€è´§å•状态和退款总额
        byId.setStatus(1);
        byId.setSettler(SecurityUtils.getLoginUser().getNickName());
        byId.setRefundAmount(totalRefund);
        updateById(byId);
        // 3. æ›´æ–°é”€å”®å°è´¦çš„实际合同金额
        if (byId.getShippingId() != null) {
            ShippingInfo shippingInfo = shippingInfoService.getById(byId.getShippingId());
            if (shippingInfo != null && shippingInfo.getSalesLedgerId() != null) {
                SalesLedger salesLedger = salesLedgerMapper.selectById(shippingInfo.getSalesLedgerId());
                if (salesLedger != null && salesLedger.getContractAmount() != null) {
                    BigDecimal baseAmount = salesLedger.getNetContractAmount() != null
                            ? salesLedger.getNetContractAmount()
                            : salesLedger.getContractAmount();
                    salesLedger.setNetContractAmount(baseAmount.subtract(totalRefund));
                    salesLedgerMapper.updateById(salesLedger);
                }
            }
        }
        return true;
    }
src/main/java/com/ruoyi/procurementrecord/utils/StockUtils.java
@@ -20,6 +20,7 @@
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
@Component
@RequiredArgsConstructor
@@ -183,15 +184,25 @@
    //删除出库记录
    public void deleteStockOutRecord(Long recordId, String recordType) {
        StockOutRecord one = stockOutRecordService.getOne(new QueryWrapper<StockOutRecord>()
        List<StockOutRecord> records = stockOutRecordService.list(new QueryWrapper<StockOutRecord>()
                .lambda().eq(StockOutRecord::getRecordId, recordId)
                .eq(StockOutRecord::getRecordType, recordType), false);
        if (ObjectUtils.isNotEmpty(one)) {
            if (ReviewStatusEnum.APPROVED.getCode().equals(one.getApprovalStatus())) {
                stockOutRecordService.batchDelete(Collections.singletonList(one.getId()));
            } else {
                stockOutRecordService.removeById(one.getId());
            }
                .eq(StockOutRecord::getRecordType, recordType));
        if (ObjectUtils.isEmpty(records)) {
            return;
        }
        List<Long> approvedIds = records.stream()
                .filter(one -> ReviewStatusEnum.APPROVED.getCode().equals(one.getApprovalStatus()))
                .map(StockOutRecord::getId)
                .toList();
        List<Long> pendingIds = records.stream()
                .filter(one -> !ReviewStatusEnum.APPROVED.getCode().equals(one.getApprovalStatus()))
                .map(StockOutRecord::getId)
                .toList();
        if (!approvedIds.isEmpty()) {
            stockOutRecordService.batchDelete(approvedIds);
        }
        if (!pendingIds.isEmpty()) {
            stockOutRecordService.removeByIds(pendingIds);
        }
    }
src/main/java/com/ruoyi/production/bean/dto/ProductionPlanImportDto.java
@@ -36,10 +36,6 @@
    @Excel(name = "需求数量")
    private BigDecimal qtyRequired;
    @Schema(description = "来源 é”€å”®/内部")
    @Excel(name = "来源 é”€å”®/内部")
    private String source;
    @Schema(description = "承诺日期")
    @Excel(name = "承诺日期")
    private LocalDate promisedDeliveryDate;
src/main/java/com/ruoyi/production/enums/ProductOrderStatusEnum.java
@@ -17,7 +17,8 @@
    WAIT(1, "待开始"),
    RUNNING(2, "进行中"),
    FINISHED(3, "已完成"),
    CANCEL(4, "已取消");
    //CANCEL(4, "已取消"),
    END(5, "已结束");
    private final Integer code;
    private final String desc;
@@ -43,10 +44,10 @@
    }
    /**
     * åˆ¤æ–­æ˜¯å¦å…è®¸åˆ é™¤ï¼ˆ4)
     * åˆ¤æ–­æ˜¯å¦å…è®¸åˆ é™¤ï¼ˆ1)
     */
    public static boolean canDelete(Integer code) {
        return WAIT.getCode().equals(code) || CANCEL.getCode().equals(code);
        return WAIT.getCode().equals(code);
    }
    /**
@@ -57,9 +58,9 @@
    }
    /**
     * åˆ¤æ–­æ˜¯å¦å·²å¼€å§‹ç”Ÿäº§ï¼ˆ2、3)
     * åˆ¤æ–­æ˜¯å¦å·²å¼€å§‹ç”Ÿäº§ï¼ˆ2、3、5)
     */
    public static boolean isStarted(Integer code) {
        return RUNNING.getCode().equals(code) || FINISHED.getCode().equals(code);
        return RUNNING.getCode().equals(code) || FINISHED.getCode().equals(code) || END.getCode().equals(code);
    }
}
src/main/java/com/ruoyi/production/service/impl/ProductionOrderRoutingOperationParamServiceImpl.java
@@ -161,11 +161,21 @@
        // æ£€æŸ¥æ•°æ®æ˜¯å¦é‡å¤ï¼Œé¿å…é‡å¤ä¿å­˜
        boolean duplicate = productionOrderRoutingOperationParamMapper.selectCount(
                Wrappers.<ProductionOrderRoutingOperationParam>lambdaQuery()
                        .eq(ProductionOrderRoutingOperationParam::getProductionOrderId, item.getProductionOrderId())
                        .isNull(ProductionOrderRoutingOperationParam::getProductionProductMainId)
                        .eq(ProductionOrderRoutingOperationParam::getProductionOrderRoutingOperationId, item.getProductionOrderRoutingOperationId())
                        .eq(item.getTechnologyRoutingOperationParamId() != null,
                                ProductionOrderRoutingOperationParam::getTechnologyRoutingOperationParamId, item.getTechnologyRoutingOperationParamId())
                        .eq(item.getTechnologyRoutingOperationParamId() == null && item.getParamCode() != null,
                        .eq(item.getTechnologyRoutingOperationParamId() == null && item.getTechnologyOperationParamId() != null,
                                ProductionOrderRoutingOperationParam::getTechnologyOperationParamId, item.getTechnologyOperationParamId())
                        .eq(item.getTechnologyRoutingOperationParamId() == null
                                        && item.getTechnologyOperationParamId() == null
                                        && item.getParamId() != null,
                                ProductionOrderRoutingOperationParam::getParamId, item.getParamId())
                        .eq(item.getTechnologyRoutingOperationParamId() == null
                                        && item.getTechnologyOperationParamId() == null
                                        && item.getParamId() == null
                                        && item.getParamCode() != null,
                                ProductionOrderRoutingOperationParam::getParamCode, item.getParamCode())
                        .ne(item.getId() != null, ProductionOrderRoutingOperationParam::getId, item.getId())
        ) > 0;
src/main/java/com/ruoyi/production/service/impl/ProductionOrderServiceImpl.java
@@ -1134,7 +1134,7 @@
    @Override
    public int updateOrder(ProductionOrderDto productionOrderDto) {
        // æ›´æ–°ç”Ÿäº§è®¢å•主数据
        productionOrderDto.setStatus(5);
        productionOrderDto.setStatus(ProductOrderStatusEnum.END.getCode());
        return baseMapper.updateById(productionOrderDto);
    }
}
src/main/java/com/ruoyi/production/service/impl/ProductionPlanServiceImpl.java
@@ -311,7 +311,7 @@
            BeanUtils.copyProperties(dto, entity);
            entity.setProductModelId(resolveProductModelId(dto, i + 2, allModels, productNameById));
            entity.setStatus(PLAN_STATUS_WAIT);
            entity.setSource(StringUtils.isNotEmpty(dto.getSource()) ? StringUtils.trim(dto.getSource()) : "内部");
            entity.setSource("内部");
            entity.setQuantityIssued(BigDecimal.ZERO);
            entity.setCreateTime(now);
            entity.setUpdateTime(now);
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/purchase/dto/PurchaseLedgerDto.java
@@ -88,7 +88,7 @@
    /**
     * å½•入日期
     */
    @JsonFormat(pattern = "yyyy-MM-dd")
    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
    @Excel(name = "录入日期", width = 30, dateFormat = "yyyy-MM-dd")
    private Date entryDate;
@@ -175,6 +175,12 @@
    @Excel(name = "合同金额")
    private BigDecimal contractAmount = BigDecimal.ZERO;
    /**
     * å®žé™…合同金额(扣减退货后)
     */
    @Schema(description = "实际合同金额")
    private BigDecimal netContractAmount = BigDecimal.ZERO;
    @Schema(description = "文件类型  å– 4")
    @TableField(exist = false)
src/main/java/com/ruoyi/purchase/pojo/PurchaseLedger.java
@@ -69,7 +69,7 @@
    /**
     * å½•入日期
     */
    @JsonFormat(pattern = "yyyy-MM-dd")
    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
    @Excel(name = "录入日期", width = 30, dateFormat = "yyyy-MM-dd")
    private Date entryDate;
@@ -119,6 +119,12 @@
     * åˆåŒé‡‘额(产品含税总价)
     */
    private BigDecimal contractAmount;
    /**
     * å®žé™…合同金额(扣减退货后)
     */
    @Schema(description = "实际合同金额")
    private BigDecimal netContractAmount;
//
//    /**
//     * ä¸šåŠ¡å‘˜
src/main/java/com/ruoyi/purchase/service/impl/PurchaseLedgerServiceImpl.java
@@ -129,6 +129,9 @@
        if (purchaseLedger.getApprovalStatus() != null) {
            queryWrapper.eq(PurchaseLedger::getApprovalStatus, purchaseLedger.getApprovalStatus());
        }
        // åªæŸ¥è¯¢å…¥åº“已审批通过的采购台账
        queryWrapper.inSql(PurchaseLedger::getId,
                "SELECT DISTINCT record_id FROM stock_in_record WHERE approval_status = 1");
        return purchaseLedgerMapper.selectList(queryWrapper);
    }
@@ -162,6 +165,8 @@
        purchaseLedger.setRecorderId(purchaseLedgerDto.getRecorderId());
        purchaseLedger.setApprovalStatus(1);
        // å®žé™…合同金额始终等于合同金额
        purchaseLedger.setNetContractAmount(purchaseLedger.getContractAmount());
        // 3. æ–°å¢žæˆ–更新主表
        if (purchaseLedger.getId() == null) {
            if (!StringUtils.hasText(purchaseLedger.getPurchaseContractNumber())) {
@@ -754,13 +759,30 @@
        for (Long id : ids) {
            PurchaseLedger purchaseLedger = purchaseLedgerMapper.selectById(id);
            if(purchaseLedger != null){
                ApproveProcess one = approveProcessService.getOne(new LambdaQueryWrapper<ApproveProcess>()
                List<ApprovalInstance> approvalInstances = approvalInstanceService.list(new LambdaQueryWrapper<ApprovalInstance>()
                        .eq(ApprovalInstance::getBusinessId, purchaseLedger.getId())
                        .eq(ApprovalInstance::getBusinessType, 5L)
                        .eq(ApprovalInstance::getDeleted, 0));
                if (CollectionUtils.isNotEmpty(approvalInstances)) {
                    approvalInstanceService.delete(
                            approvalInstances.stream()
                                    .map(ApprovalInstance::getId)
                                    .filter(Objects::nonNull)
                                    .collect(Collectors.toList())
                    );
                }
                List<ApproveProcess> approveProcesses = approveProcessService.list(new LambdaQueryWrapper<ApproveProcess>()
                        .eq(ApproveProcess::getApproveType, 5)
                        .eq(ApproveProcess::getApproveDelete, 0)
                        .eq(ApproveProcess::getApproveReason, purchaseLedger.getPurchaseContractNumber())
                        .last("limit 1"));
                if (one != null) {
                    approveProcessService.delByIds(Collections.singletonList(one.getId()));
                        .eq(ApproveProcess::getApproveReason, purchaseLedger.getPurchaseContractNumber()));
                if (CollectionUtils.isNotEmpty(approveProcesses)) {
                    approveProcessService.delByIds(
                            approveProcesses.stream()
                                    .map(ApproveProcess::getId)
                                    .filter(Objects::nonNull)
                                    .collect(Collectors.toList())
                    );
                }
            }
        }
@@ -941,6 +963,7 @@
                salesLedger.setContractAmount(salesLedgerProductImportDtos.stream()
                        .map(PurchaseLedgerProductImportDto::getTaxInclusiveTotalPrice)
                        .reduce(BigDecimal.ZERO,BigDecimal::add));
                salesLedger.setNetContractAmount(salesLedger.getContractAmount());
                // é€šè¿‡é”€å”®å•号绑定销售
                SalesLedger salesLedger1 = salesLedgerMapper.selectOne(new LambdaQueryWrapper<SalesLedger>()
                        .eq(SalesLedger::getSalesContractNo, salesLedger.getSalesContractNo())
src/main/java/com/ruoyi/purchase/service/impl/PurchaseReturnOrdersServiceImpl.java
@@ -14,6 +14,7 @@
import com.ruoyi.purchase.mapper.PurchaseLedgerMapper;
import com.ruoyi.purchase.mapper.PurchaseReturnOrderProductsMapper;
import com.ruoyi.purchase.mapper.PurchaseReturnOrdersMapper;
import com.ruoyi.purchase.pojo.PurchaseLedger;
import com.ruoyi.purchase.pojo.PurchaseReturnOrderProducts;
import com.ruoyi.purchase.pojo.PurchaseReturnOrders;
import com.ruoyi.purchase.service.PurchaseReturnOrdersService;
@@ -28,6 +29,7 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.List;
/**
@@ -76,6 +78,18 @@
        }else {
            throw new RuntimeException("请选择退货商品");
        }
        // æ›´æ–°é‡‡è´­å°è´¦çš„实际合同金额
        if (purchaseReturnOrderDto.getPurchaseLedgerId() != null && purchaseReturnOrderDto.getTotalAmount() != null) {
            PurchaseLedger purchaseLedger = purchaseLedgerMapper.selectById(purchaseReturnOrderDto.getPurchaseLedgerId());
            if (purchaseLedger != null) {
                BigDecimal baseAmount = purchaseLedger.getNetContractAmount() != null
                        ? purchaseLedger.getNetContractAmount()
                        : purchaseLedger.getContractAmount();
                purchaseLedger.setNetContractAmount(baseAmount.subtract(purchaseReturnOrderDto.getTotalAmount()));
                purchaseLedgerMapper.updateById(purchaseLedger);
            }
        }
        return true;
    }
src/main/java/com/ruoyi/quality/pojo/QualityInspect.java
@@ -38,7 +38,7 @@
    /**
     * æ£€æµ‹æ—¥æœŸ
     */
    @JsonFormat(pattern = "yyyy-MM-dd")
    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
    @Excel(name = "检测日期", width = 30, dateFormat = "yyyy-MM-dd")
    private Date checkTime;
src/main/java/com/ruoyi/quality/pojo/QualityUnqualified.java
@@ -47,7 +47,7 @@
    /**
     * æ£€æµ‹æ—¥æœŸ
     */
    @JsonFormat(pattern = "yyyy-MM-dd")
    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
    @Excel(name = "检测日期", width = 30, dateFormat = "yyyy-MM-dd")
    private Date checkTime;
@@ -107,7 +107,7 @@
    /**
     * å¤„理日期
     */
    @JsonFormat(pattern = "yyyy-MM-dd")
    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
    @Excel(name = "处理日期", width = 30, dateFormat = "yyyy-MM-dd")
    private Date dealTime;
src/main/java/com/ruoyi/sales/controller/SalesLedgerProductController.java
@@ -7,20 +7,13 @@
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.procurementrecord.service.ProcurementRecordService;
import com.ruoyi.procurementrecord.utils.StockUtils;
import com.ruoyi.purchase.dto.SimpleReturnOrderGroupDto;
import com.ruoyi.purchase.mapper.PurchaseReturnOrderProductsMapper;
import com.ruoyi.sales.pojo.SalesLedgerProduct;
import com.ruoyi.sales.service.ISalesLedgerProductService;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
 * äº§å“ä¿¡æ¯Controller
@@ -34,9 +27,6 @@
public class SalesLedgerProductController extends BaseController
{
    private final ISalesLedgerProductService salesLedgerProductService;
    private final ProcurementRecordService procurementRecordService;
    private final StockUtils stockUtils;
    private final PurchaseReturnOrderProductsMapper purchaseReturnOrderProductsMapper;
    /**
@@ -45,26 +35,25 @@
    @GetMapping("/list")
    public AjaxResult list(SalesLedgerProduct salesLedgerProduct) {
        List<SalesLedgerProduct> list = salesLedgerProductService.selectSalesLedgerProductList(salesLedgerProduct);
        if (CollUtil.isEmpty(list)) {
            return AjaxResult.success(list);
        }
        List<Long> productIds = list.stream().map(SalesLedgerProduct::getProductModelId).collect(Collectors.toList());
        List<SimpleReturnOrderGroupDto> groupListByProductIds = purchaseReturnOrderProductsMapper.getReturnOrderGroupListByProductIds(productIds);
        Map<Long, BigDecimal> returnOrderGroupDtoMap = groupListByProductIds.stream().collect(Collectors.toMap(SimpleReturnOrderGroupDto::getProductModelId, item -> item.getSumReturnQuantity()));
        list.forEach(item -> {
            if (item.getApproveStatus() != 2) {
                if (item.getHasSufficientStock() == 0) {
                    item.setApproveStatus(0);
                } else {
                    item.setApproveStatus(1);
        if (CollUtil.isNotEmpty(list)) {
            list.forEach(item -> {
                if (item.getApproveStatus() != 2) {
                    if (item.getHasSufficientStock() == 0) {
                        item.setApproveStatus(0);
                    } else {
                        item.setApproveStatus(1);
                    }
                }
            }
            // ç»Ÿè®¡é€€è´§æ•°é‡
            BigDecimal returnQuality = returnOrderGroupDtoMap.getOrDefault(item.getProductModelId(), BigDecimal.ZERO);
            item.setReturnQuality(returnQuality);
            item.setAvailableQuality(item.getQuantity().subtract(returnQuality));
        });
                java.math.BigDecimal returnQuantity = item.getReturnQuantity() == null
                        ? java.math.BigDecimal.ZERO
                        : item.getReturnQuantity();
                java.math.BigDecimal quantity = item.getQuantity() == null
                        ? java.math.BigDecimal.ZERO
                        : item.getQuantity();
                item.setReturnQuantity(returnQuantity);
                item.setAvailableQuality(quantity.subtract(returnQuantity));
            });
        }
        return AjaxResult.success(list);
    }
src/main/java/com/ruoyi/sales/dto/SalesLedgerDto.java
@@ -19,7 +19,7 @@
    private String salesContractNo;
    private String customerContractNo;
    private String projectName;
    @JsonFormat(pattern = "yyyy-MM-dd")
    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
    private Date entryDate;
    private String entryDateStart;
src/main/java/com/ruoyi/sales/pojo/SalesLedger.java
@@ -50,7 +50,7 @@
    /**
     * å½•入日期
     */
    @JsonFormat(pattern = "yyyy-MM-dd")
    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
    @Excel(name = "录入日期", width = 30, dateFormat = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date entryDate;
@@ -109,6 +109,9 @@
    @Schema(description = "付款方式")
    private String paymentMethod;
    @Schema(description = "实际合同金额")
    private BigDecimal netContractAmount;
    @TableField(exist = false)
    @Schema(description = "生产状态")
    private String productionStatus = "未开始";
src/main/java/com/ruoyi/sales/pojo/SalesLedgerProduct.java
@@ -164,11 +164,7 @@
    @TableField(exist = false)
    private Integer hasSufficientStock;
    // é€€è´§æ•°é‡
    @TableField(exist = false)
    private BigDecimal returnQuality;
    // å¯ç”¨æ•°é‡  quantity - returnQuality
    // å¯ç”¨æ•°é‡  quantity - returnQuantity
    @TableField(exist = false)
    private BigDecimal availableQuality;
    @Schema(description = "创建用户")
@@ -187,6 +183,10 @@
    private BigDecimal noQuantity;
    @TableField(exist = false)
    @Schema(description = "退货数量")
    private BigDecimal returnQuantity;
    @TableField(exist = false)
    @Schema(description = "审批中数量")
    private BigDecimal pendingApprovalQuantity;
}
src/main/java/com/ruoyi/sales/service/impl/SalesLedgerProductServiceImpl.java
@@ -98,16 +98,29 @@
        List<SalesLedgerProduct> salesLedgerProducts = salesLedgerProductMapper.selectSalesLedgerProductList(salesLedgerProduct);
        if(!CollectionUtils.isEmpty(salesLedgerProducts)){
            salesLedgerProducts.forEach(item -> {
                // å‘货信息
                ShippingInfo shippingInfo = shippingInfoMapper.selectOne(new LambdaQueryWrapper<ShippingInfo>()
                // å‘货信息(取最新一条,只查询审核通过或已发货的记录)
                ShippingInfo latestShippingInfo = shippingInfoMapper.selectOne(new LambdaQueryWrapper<ShippingInfo>()
                        .eq(ShippingInfo::getSalesLedgerProductId, item.getId())
                        .in(ShippingInfo::getStatus, "审核通过", "已发货")
                        .orderByDesc(ShippingInfo::getCreateTime)
                        .last("limit 1"));
                if(shippingInfo != null){
                    item.setShippingDate(shippingInfo.getShippingDate());
                    item.setShippingCarNumber(shippingInfo.getShippingCarNumber());
                    item.setExpressCompany(shippingInfo.getExpressCompany());
                    item.setExpressNumber(shippingInfo.getExpressNumber());
                if(latestShippingInfo != null){
                    item.setShippingDate(latestShippingInfo.getShippingDate());
                    item.setExpressCompany(latestShippingInfo.getExpressCompany());
                    item.setExpressNumber(latestShippingInfo.getExpressNumber());
                }
                // è½¦ç‰Œå·ï¼šå–所有审核通过或已发货记录的车牌号,逗号拼接
                List<ShippingInfo> allShippingInfos = shippingInfoMapper.selectList(new LambdaQueryWrapper<ShippingInfo>()
                        .eq(ShippingInfo::getSalesLedgerProductId, item.getId())
                        .in(ShippingInfo::getStatus, "审核通过", "已发货")
                        .orderByAsc(ShippingInfo::getCreateTime));
                if(!CollectionUtils.isEmpty(allShippingInfos)){
                    String carNumbers = allShippingInfos.stream()
                            .map(ShippingInfo::getShippingCarNumber)
                            .filter(carNo -> carNo != null && !carNo.trim().isEmpty())
                            .distinct()
                            .collect(Collectors.joining(","));
                    item.setShippingCarNumber(carNumbers);
                }
            });
        }
@@ -223,9 +236,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/java/com/ruoyi/sales/service/impl/SalesLedgerServiceImpl.java
@@ -38,8 +38,6 @@
import com.ruoyi.project.system.domain.SysUser;
import com.ruoyi.project.system.mapper.SysDeptMapper;
import com.ruoyi.project.system.mapper.SysUserMapper;
import com.ruoyi.purchase.dto.SimpleReturnOrderGroupDto;
import com.ruoyi.purchase.mapper.PurchaseReturnOrderProductsMapper;
import com.ruoyi.quality.mapper.QualityInspectMapper;
import com.ruoyi.sales.dto.*;
import com.ruoyi.sales.mapper.CommonFileMapper;
@@ -121,9 +119,6 @@
    @Autowired
    private ProductionProductMainService productionProductMainService;
    @Autowired
    private PurchaseReturnOrderProductsMapper purchaseReturnOrderProductsMapper;
    ;
    @Autowired
    private SysUserMapper sysUserMapper;
    @Override
@@ -148,17 +143,11 @@
        productWrapper.eq(SalesLedgerProduct::getType, type.getCode());
        List<SalesLedgerProduct> salesLedgerProducts = salesLedgerProductMapper.selectList(productWrapper);
        if (type.equals(SaleEnum.PURCHASE)) {
            // æŸ¥è¯¢é€€è´§ä¿¡æ¯
            List<Long> productIds = salesLedgerProducts.stream().map(SalesLedgerProduct::getProductModelId).collect(Collectors.toList());
            List<SimpleReturnOrderGroupDto> groupListByProductIds = new ArrayList<>();
            if(CollectionUtils.isNotEmpty(productIds)){
                groupListByProductIds = purchaseReturnOrderProductsMapper.getReturnOrderGroupListByProductIds(productIds);
            }
            Map<Long, BigDecimal> returnOrderGroupDtoMap = groupListByProductIds.stream().collect(Collectors.toMap(SimpleReturnOrderGroupDto::getProductModelId, SimpleReturnOrderGroupDto::getSumReturnQuantity));
            salesLedgerProducts.forEach(item -> {
                BigDecimal returnQuality = returnOrderGroupDtoMap.getOrDefault(item.getProductModelId(), BigDecimal.ZERO);
                item.setReturnQuality(returnQuality);
                item.setAvailableQuality(item.getQuantity().subtract(returnQuality));
                BigDecimal returnQuantity = item.getReturnQuantity() == null ? BigDecimal.ZERO : item.getReturnQuantity();
                item.setReturnQuantity(returnQuantity);
                BigDecimal quantity = item.getQuantity() == null ? BigDecimal.ZERO : item.getQuantity();
                item.setAvailableQuality(quantity.subtract(returnQuantity));
            });
        }
        return salesLedgerProducts;
@@ -396,6 +385,7 @@
                salesLedger.setContractAmount(salesLedgerProductImportDtos.stream()
                        .map(SalesLedgerProductImportDto::getTaxInclusiveTotalPrice)
                        .reduce(BigDecimal.ZERO, BigDecimal::add));
                salesLedger.setNetContractAmount(salesLedger.getContractAmount());
                salesLedgerMapper.insert(salesLedger);
@@ -601,7 +591,18 @@
            );
        }
        // 5. ä¿å­˜é”€å”®å°è´¦é™„ä»¶
        // 5. å…œåº•:确保实际合同金额有初始值等于合同金额
        if (salesLedger.getNetContractAmount() == null) {
            SalesLedger dbLedger = salesLedgerMapper.selectById(salesLedger.getId());
            if (dbLedger != null && dbLedger.getNetContractAmount() == null && dbLedger.getContractAmount() != null) {
                SalesLedger updateEntity = new SalesLedger();
                updateEntity.setId(salesLedger.getId());
                updateEntity.setNetContractAmount(dbLedger.getContractAmount());
                salesLedgerMapper.updateById(updateEntity);
            }
        }
        // 6. ä¿å­˜é”€å”®å°è´¦é™„ä»¶
        fileUtil.saveStorageAttachment(ApplicationTypeEnum.FILE, RecordTypeEnum.SALES_LEDGER, salesLedger.getId(), salesLedgerDto.getStorageBlobDTOs());
        return 1;
@@ -739,6 +740,15 @@
            amountField.setAccessible(true);
            amountField.set(entity, totalAmount);
            // å®žé™…合同金额初始值等于合同金额
            try {
                Field netAmountField = mainEntityClass.getDeclaredField("netContractAmount");
                netAmountField.setAccessible(true);
                netAmountField.set(entity, totalAmount);
            } catch (NoSuchFieldException ignored) {
                // å­ç±»å¯èƒ½æ²¡æœ‰è¯¥å­—段,忽略
            }
            mainMapper.updateById(entity);
        } catch (Exception e) {
            throw new RuntimeException("动态更新主表金额失败", e);
src/main/java/com/ruoyi/sales/service/impl/SalesQuotationServiceImpl.java
@@ -194,13 +194,31 @@
        if(salesQuotation==null) return false;
        salesQuotationMapper.deleteById(id);
        salesQuotationProductMapper.delete(new LambdaQueryWrapper<SalesQuotationProduct>().eq(SalesQuotationProduct::getSalesQuotationId, id));
        // åˆ é™¤æŠ¥ä»·å®¡æ‰¹
        ApproveProcess one = approveProcessService.getOne(new LambdaQueryWrapper<ApproveProcess>()
        // åˆ é™¤è¯¥æŠ¥ä»·å¯¹åº”的全部审批实例(可能存在多次提交/重审)
        List<ApprovalInstance> approvalInstances = approvalInstanceService.list(new LambdaQueryWrapper<ApprovalInstance>()
                .eq(ApprovalInstance::getBusinessId, id)
                .eq(ApprovalInstance::getBusinessType, 6L)
                .eq(ApprovalInstance::getDeleted, 0));
        if(!approvalInstances.isEmpty()){
            approvalInstanceService.delete(
                    approvalInstances.stream()
                            .map(ApprovalInstance::getId)
                            .filter(Objects::nonNull)
                            .collect(Collectors.toList())
            );
        }
        // åˆ é™¤è¯¥æŠ¥ä»·å¯¹åº”的全部审批流程(同一报价可能反复发起)
        List<ApproveProcess> approveProcesses = approveProcessService.list(new LambdaQueryWrapper<ApproveProcess>()
                .eq(ApproveProcess::getApproveType, 6)
                .eq(ApproveProcess::getApproveDelete, IsDeleteEnum.NOT_DELETED)
                .eq(ApproveProcess::getApproveReason, salesQuotation.getQuotationNo()));
        if(one != null){
            approveProcessService.delByIds(Collections.singletonList(one.getId()));
        if(!approveProcesses.isEmpty()){
            approveProcessService.delByIds(
                    approveProcesses.stream()
                            .map(ApproveProcess::getId)
                            .filter(Objects::nonNull)
                            .collect(Collectors.toList())
            );
        }
        return true;
    }
src/main/java/com/ruoyi/sales/service/impl/ShippingInfoServiceImpl.java
@@ -172,6 +172,17 @@
    @Override
    public boolean addReq(ShippingInfoDto req) {
        // æ ¡éªŒè¯¥äº§å“æ˜¯å¦å·²æœ‰å¾…审核/审核中的发货记录,防止重复提交
        Long salesLedgerProductId = req.getSalesLedgerProductId();
        if (salesLedgerProductId != null) {
            long pendingCount = this.count(new LambdaQueryWrapper<ShippingInfo>()
                    .eq(ShippingInfo::getSalesLedgerProductId, salesLedgerProductId)
                    .in(ShippingInfo::getStatus, "待审核", "审核中"));
            if (pendingCount > 0) {
                throw new RuntimeException("该产品已有待审核的发货记录,请勿重复提交");
            }
        }
        LoginUser loginUser = SecurityUtils.getLoginUser();
        String sh = OrderUtils.countTodayByCreateTime(shippingInfoMapper, "SH","shipping_no",req.getCreateTime());
        // å…ˆä¿å­˜å‘货单,再发起审批;无审核人自动通过时需要按发货编号回写发货状态。
@@ -200,6 +211,9 @@
        approvalInstance.setApplicantName(loginUser.getNickName());
        approvalInstance.setApplyTime(LocalDateTime.now());
        approvalInstanceService.add(approvalInstance);
        // åˆ›å»ºå®¡æ‰¹å®žä¾‹åŽï¼Œæ›´æ–°å‘货状态为"审核中",使前端正确显示审批状态
        req.setStatus("审核中");
        this.updateById(req);
        return true;
    }
}
src/main/java/com/ruoyi/staff/service/impl/SchemeApplicableStaffServiceImpl.java
@@ -172,8 +172,8 @@
                : calculateLatestDeptSocialSupplementAmount(staffOnJobDto);
        BigDecimal pieceSalary = normalizeMoney(getBigDecimal(map, "pieceSalary"));
        BigDecimal hourlySalary = normalizeMoney(getBigDecimal(map, "hourlySalary"));
        BigDecimal socialPersonal = normalizeMoney(getBigDecimal(map, "socialPersonal"));
        BigDecimal fundPersonal = normalizeMoney(getBigDecimal(map, "fundPersonal"));
        BigDecimal socialPersonal = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
        BigDecimal fundPersonal = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
        UserProductionAccountingDto userProductionAccountingDto = new UserProductionAccountingDto();
        userProductionAccountingDto.setUserId(getUidByStaffId(staffId));
@@ -194,9 +194,9 @@
                }
                for (SchemeInsuranceDetail detail : detailList) {
                    if("公积金".equals(detail.getInsuranceType())){
                        fundPersonal = fundPersonal.add(calculateByEmployeeIdType(detail.getInsuranceType(), fundPersonal, staffOnJobDto, detail));
                        fundPersonal = calculateByEmployeeIdType(detail.getInsuranceType(), fundPersonal, basicSalary, staffOnJobDto, detail);
                    }else{
                        socialPersonal = socialPersonal.add(calculateByEmployeeIdType(detail.getInsuranceType(), socialPersonal, staffOnJobDto, detail));
                        socialPersonal = calculateByEmployeeIdType(detail.getInsuranceType(), socialPersonal, basicSalary, staffOnJobDto, detail);
                    }
                }
            }
@@ -263,14 +263,14 @@
     * @param detail
     * @return
     */
    public BigDecimal calculateByEmployeeIdType(String type,BigDecimal bigDecimal, StaffOnJob staffOnJobDto,SchemeInsuranceDetail detail) {
    public BigDecimal calculateByEmployeeIdType(String type,BigDecimal bigDecimal, BigDecimal basicSalary, StaffOnJob staffOnJobDto,SchemeInsuranceDetail detail) {
        // åˆ¤æ–­æ˜¯å¦è°ƒç”¨åŸºæœ¬å·¥èµ„
        if (detail.getUseBasicSalary() == 1) {
            BigDecimal divide = detail.getPaymentBase().multiply(detail.getPersonalRatio()).divide(new BigDecimal("100"), 2);
            bigDecimal = bigDecimal.add(divide);
        }else{
            // è°ƒç”¨åŸºæœ¬å·¥èµ„
            BigDecimal multiply = staffOnJobDto.getBasicSalary().multiply(detail.getPersonalRatio().divide(new BigDecimal("100"), 2));
            // è°ƒç”¨å½“前工资表中的基本工资
            BigDecimal multiply = basicSalary.multiply(detail.getPersonalRatio().divide(new BigDecimal("100"), 2));
            bigDecimal = bigDecimal.add(multiply);
        }
        bigDecimal = bigDecimal.add(detail.getPersonalFixed());
@@ -292,8 +292,12 @@
        BigDecimal socialSupplementAmount = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
        for (SchemeInsuranceDetail detail : detailList) {
            if (!"公积金".equals(detail.getInsuranceType())) {
                socialSupplementAmount = socialSupplementAmount.add(
                        calculateByEmployeeIdType(detail.getInsuranceType(), socialSupplementAmount, staffOnJobDto, detail)
                socialSupplementAmount = calculateByEmployeeIdType(
                        detail.getInsuranceType(),
                        socialSupplementAmount,
                        normalizeMoney(staffOnJobDto.getBasicSalary()),
                        staffOnJobDto,
                        detail
                );
            }
        }
src/main/java/com/ruoyi/stock/controller/StockOutRecordController.java
@@ -87,4 +87,15 @@
        return AjaxResult.success();
    }
    @PostMapping("/reAudit")
    @Log(title = "出库管理-反审出库", businessType = BusinessType.UPDATE)
    @Operation(summary = "批量反审出库记录")
    public AjaxResult reAudit(@RequestBody StockOutRecordDto approveDto) {
        if(CollectionUtils.isEmpty(approveDto.getIds())){
            return AjaxResult.error("请选择至少一条数据");
        }
        stockOutRecordService.batchReAudit(approveDto.getIds());
        return AjaxResult.success();
    }
}
src/main/java/com/ruoyi/stock/service/StockOutRecordService.java
@@ -31,4 +31,6 @@
    void exportStockOutRecord(HttpServletResponse response, StockOutRecordDto stockOutRecordDto);
    int batchApprove(List<Long> ids, Integer approvalStatus);
    int batchReAudit(List<Long> ids);
}
src/main/java/com/ruoyi/stock/service/impl/StockInRecordServiceImpl.java
@@ -246,14 +246,18 @@
                    stockInventoryDto.setQualitity(stockInRecord.getStockInNum());
                    stockInventoryDto.setRemark(stockInRecord.getRemark());
                    if (stockInventory == null) {
                        stockInventoryMapper.insert(new StockInventory() {{
                            setProductModelId(stockInRecord.getProductModelId());
                            setQualitity(stockInRecord.getStockInNum());
                            setBatchNo(stockInRecord.getBatchNo());
                            setRemark(stockInRecord.getRemark());
                            setWarnNum(stockInRecord.getWarnNum());
                            setVersion(1);
                        }});
                        try {
                            stockInventoryMapper.insert(new StockInventory() {{
                                setProductModelId(stockInRecord.getProductModelId());
                                setQualitity(stockInRecord.getStockInNum());
                                setBatchNo(stockInRecord.getBatchNo());
                                setRemark(stockInRecord.getRemark());
                                setWarnNum(stockInRecord.getWarnNum());
                                setVersion(1);
                            }});
                        } catch (org.springframework.dao.DuplicateKeyException e) {
                            stockInventoryMapper.updateAddStockInventory(stockInventoryDto);
                        }
                    } else {
                        stockInventoryMapper.updateAddStockInventory(stockInventoryDto);
                    }
@@ -266,13 +270,17 @@
                    stockUninventoryDto.setQualitity(stockInRecord.getStockInNum());
                    stockUninventoryDto.setRemark(stockInRecord.getRemark());
                    if (stockUninventory == null) {
                        stockUninventoryMapper.insert(new StockUninventory() {{
                            setProductModelId(stockInRecord.getProductModelId());
                            setQualitity(stockInRecord.getStockInNum());
                            setBatchNo(stockInRecord.getBatchNo());
                            setRemark(stockInRecord.getRemark());
                            setVersion(1);
                        }});
                        try {
                            stockUninventoryMapper.insert(new StockUninventory() {{
                                setProductModelId(stockInRecord.getProductModelId());
                                setQualitity(stockInRecord.getStockInNum());
                                setBatchNo(stockInRecord.getBatchNo());
                                setRemark(stockInRecord.getRemark());
                                setVersion(1);
                            }});
                        } catch (org.springframework.dao.DuplicateKeyException e) {
                            stockUninventoryMapper.updateAddStockUnInventory(stockUninventoryDto);
                        }
                    } else {
                        stockUninventoryMapper.updateAddStockUnInventory(stockUninventoryDto);
                    }
src/main/java/com/ruoyi/stock/service/impl/StockInventoryServiceImpl.java
@@ -105,7 +105,11 @@
            newStockInventory.setBatchNo(stockInventoryDto.getBatchNo());
            newStockInventory.setLockedQuantity(stockInventoryDto.getLockedQuantity());
            newStockInventory.setWarnNum(stockInventoryDto.getWarnNum());
            stockInventoryMapper.insert(newStockInventory);
            try {
                stockInventoryMapper.insert(newStockInventory);
            } catch (org.springframework.dao.DuplicateKeyException e) {
                stockInventoryMapper.updateAddStockInventory(stockInventoryDto);
            }
        } else {
            stockInventoryMapper.updateAddStockInventory(stockInventoryDto);
        }
src/main/java/com/ruoyi/stock/service/impl/StockOutRecordServiceImpl.java
@@ -215,6 +215,26 @@
        return ids.size();
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int batchReAudit(List<Long> ids) {
        if (CollectionUtils.isEmpty(ids)) {
            throw new BaseException("请选择至少一条数据");
        }
        for (Long id : ids) {
            StockOutRecord stockOutRecord = stockOutRecordMapper.selectById(id);
            if (stockOutRecord == null) {
                throw new BaseException("出库记录不存在,无法重新审核!!!");
            }
            if (!ReviewStatusEnum.REJECTED.getCode().equals(stockOutRecord.getApprovalStatus())) {
                throw new BaseException("只有驳回状态的记录才能重新审核,出库批次:" + stockOutRecord.getOutboundBatches());
            }
            stockOutRecord.setApprovalStatus(ReviewStatusEnum.PENDING_REVIEW.getCode());
            stockOutRecordMapper.updateById(stockOutRecord);
        }
        return ids.size();
    }
    private StockInventory getStockInventory(Long productModelId, String batchNo) {
        LambdaQueryWrapper<StockInventory> eq = new LambdaQueryWrapper<>();
        eq.eq(StockInventory::getProductModelId, productModelId);
src/main/java/com/ruoyi/technology/controller/TechnologyOperationController.java
@@ -41,7 +41,7 @@
    @Log(title = "工序更新", businessType = BusinessType.UPDATE)
    @Operation(summary = "修改工序")
    public R update(@RequestBody com.ruoyi.technology.pojo.TechnologyOperation technologyOperation) {
        return R.ok(technologyOperationService.updateById(technologyOperation));
        return technologyOperationService.update(technologyOperation);
    }
    @DeleteMapping("/batchDelete")
src/main/java/com/ruoyi/technology/service/TechnologyOperationService.java
@@ -16,6 +16,8 @@
    R add(TechnologyOperationDto technologyOperationDto);
    R update(TechnologyOperation technologyOperation);
    String batchDelete(List<Long> ids);
    List<TechnologyOperationVo> listVo();
src/main/java/com/ruoyi/technology/service/impl/TechnologyOperationServiceImpl.java
@@ -57,6 +57,27 @@
        return R.ok();
    }
    @Override
    public R update(TechnologyOperation technologyOperation) {
        if (technologyOperation.getId() == null) {
            return R.fail("工序ID不能为空");
        }
        boolean updated = technologyOperationMapper.update(
                null,
                Wrappers.<TechnologyOperation>lambdaUpdate()
                        .eq(TechnologyOperation::getId, technologyOperation.getId())
                        .set(TechnologyOperation::getName, technologyOperation.getName())
                        .set(TechnologyOperation::getNo, technologyOperation.getNo())
                        .set(TechnologyOperation::getRemark, technologyOperation.getRemark())
                        .set(TechnologyOperation::getSalaryQuota, technologyOperation.getSalaryQuota())
                        .set(TechnologyOperation::getIsQuality, technologyOperation.getIsQuality())
                        .set(TechnologyOperation::getIsProduction, technologyOperation.getIsProduction())
                        .set(TechnologyOperation::getType, technologyOperation.getType())
                        .set(TechnologyOperation::getDeviceLedgerId, technologyOperation.getDeviceLedgerId())
        ) > 0;
        return updated ? R.ok() : R.fail("修改失败");
    }
    /**
     * åˆ é™¤å·¥åºå‰æ ¡éªŒæ˜¯å¦å·²è¢«BOM结构或工艺路线引用。
     */
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/application-myhb-pro.yml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,268 @@
# é¡¹ç›®ç›¸å…³é…ç½®
ruoyi:
  # åç§°
  name: RuoYi
  # ç‰ˆæœ¬
  version: 3.8.9
  # ç‰ˆæƒå¹´ä»½
  copyrightYear: 2025
  # æ–‡ä»¶è·¯å¾„ ç¤ºä¾‹ï¼ˆ Windows配置D:/ruoyi/uploadPath,Linux配置 /home/ruoyi/uploadPath)
  profile: /javaWork/product-inventory-management/file
  # èŽ·å–ip地址开关
  addressEnabled: false
  # éªŒè¯ç ç±»åž‹ math æ•°å­—计算 char å­—符验证
  captchaType: math
  # ååŒå®¡æ‰¹ç¼–号前缀(配置文件后缀命名)
  approvalNumberPrefix: NEW
  # ä¸ªæŽ¨ Unipush é…ç½®
  getui:
    appId: PfjyAAE0FK64FaO1w2CMb1
    appKey: zTMb831OEL6J4GK1uE3Ob4
    masterSecret: K1GFtsv42v61tXGnF7SGE5
    domain: https://restapi.getui.cn/v2/
    # ç¦»çº¿æŽ¨é€ä½¿ç”¨çš„包名/组件名
    intentComponent: uni.app.UNI099A590/io.dcloud.PandoraEntry
# å¼€å‘环境配置
server:
  # æœåŠ¡å™¨çš„HTTP端口,默认为8080
  port: 9110
  servlet:
    # åº”用的访问路径
    context-path: /
  tomcat:
    # tomcat的URI编码
    uri-encoding: UTF-8
    # è¿žæŽ¥æ•°æ»¡åŽçš„æŽ’队数,默认为100
    accept-count: 1000
    threads:
      # tomcat最大线程数,默认为200
      max: 800
      # Tomcat启动初始化的线程数,默认值10
      min-spare: 100
# æ—¥å¿—配置
logging:
  level:
    com.ruoyi: warn
    org.springframework: warn
minio:
  endpoint: http://114.132.189.42/
  port: 7019
  secure: false
  accessKey: admin
  secretKey: 12345678
  preview-expiry: 24 # é¢„览地址默认24小时
  default-bucket: jxc
# ç”¨æˆ·é…ç½®
user:
  password:
    # å¯†ç æœ€å¤§é”™è¯¯æ¬¡æ•°
    maxRetryCount: 5
    # å¯†ç é”å®šæ—¶é—´ï¼ˆé»˜è®¤10分钟)
    lockTime: 10
# Spring配置
spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    druid:
      # ä¸»åº“数据源
      master:
        url: jdbc:mysql://172.17.0.1:3306/product-inventory-management-myhb?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
        username: root
        password: xd@123456..
      # ä»Žåº“数据源
      slave:
        # ä»Žæ•°æ®æºå¼€å…³/默认关闭
        enabled: false
        url:
        username:
        password:
      # åˆå§‹è¿žæŽ¥æ•°
      initialSize: 5
      # æœ€å°è¿žæŽ¥æ± æ•°é‡
      minIdle: 10
      # æœ€å¤§è¿žæŽ¥æ± æ•°é‡
      maxActive: 20
      # é…ç½®èŽ·å–è¿žæŽ¥ç­‰å¾…è¶…æ—¶çš„æ—¶é—´
      maxWait: 60000
      # é…ç½®è¿žæŽ¥è¶…æ—¶æ—¶é—´
      connectTimeout: 30000
      # é…ç½®ç½‘络超时时间
      socketTimeout: 60000
      # é…ç½®é—´éš”多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
      timeBetweenEvictionRunsMillis: 60000
      # é…ç½®ä¸€ä¸ªè¿žæŽ¥åœ¨æ± ä¸­æœ€å°ç”Ÿå­˜çš„æ—¶é—´ï¼Œå•位是毫秒
      minEvictableIdleTimeMillis: 300000
      # é…ç½®ä¸€ä¸ªè¿žæŽ¥åœ¨æ± ä¸­æœ€å¤§ç”Ÿå­˜çš„æ—¶é—´ï¼Œå•位是毫秒
      maxEvictableIdleTimeMillis: 900000
      # é…ç½®æ£€æµ‹è¿žæŽ¥æ˜¯å¦æœ‰æ•ˆ
      validationQuery: SELECT 1 FROM DUAL
      testWhileIdle: true
      testOnBorrow: false
      testOnReturn: false
      webStatFilter:
        enabled: true
      statViewServlet:
        enabled: true
        # è®¾ç½®ç™½åå•,不填则允许所有访问
        allow:
        url-pattern: /druid/*
        # æŽ§åˆ¶å°ç®¡ç†ç”¨æˆ·åå’Œå¯†ç 
        login-username: ruoyi
        login-password: 123456
      filter:
        stat:
          enabled: true
          # æ…¢SQL记录
          log-slow-sql: true
          slow-sql-millis: 1000
          merge-sql: true
        wall:
          config:
            multi-statement-allow: true
  # èµ„源信息
  messages:
    # å›½é™…化资源文件路径
    basename: i18n/messages
  # æ–‡ä»¶ä¸Šä¼ 
  servlet:
    multipart:
      # å•个文件大小
      max-file-size: 1GB
      # è®¾ç½®æ€»ä¸Šä¼ çš„æ–‡ä»¶å¤§å°
      max-request-size: 2GB
  # æœåŠ¡æ¨¡å—
  devtools:
    restart:
      # çƒ­éƒ¨ç½²å¼€å…³
      enabled: false
  # redis é…ç½®
  data:
    mongodb:
      uri: mongodb://114.132.189.42:9028/chat_memory_db_qlmc
    # redis é…ç½®
    redis:
      # åœ°å€
      #      host: 127.0.0.1
      host: 172.17.0.1
      # ç«¯å£ï¼Œé»˜è®¤ä¸º6379
      port: 6379
      # æ•°æ®åº“索引
      database: 0
      # å¯†ç 
      #    password: root2022!
      password:
      # è¿žæŽ¥è¶…æ—¶æ—¶é—´
      timeout: 10s
      lettuce:
        pool:
          # è¿žæŽ¥æ± ä¸­çš„æœ€å°ç©ºé—²è¿žæŽ¥
          min-idle: 0
          # è¿žæŽ¥æ± ä¸­çš„æœ€å¤§ç©ºé—²è¿žæŽ¥
          max-idle: 8
          # è¿žæŽ¥æ± çš„æœ€å¤§æ•°æ®åº“连接数
          max-active: 8
          # #连接池最大阻塞等待时间(使用负值表示没有限制)
          max-wait: -1ms
  # Quartz定时任务配置(新增部分)
  quartz:
    job-store-type: jdbc  # ä½¿ç”¨æ•°æ®åº“存储
    jdbc:
      initialize-schema: never  # é¦–次运行时自动创建表结构,成功后改为never
      schema: classpath:org/quartz/impl/jdbcjobstore/tables_mysql_innodb.sql  # MySQL表结构脚本
    properties:
      org:
        quartz:
          scheduler:
            instanceName: RuoYiScheduler
            instanceId: AUTO
          jobStore:
            class: org.quartz.impl.jdbcjobstore.JobStoreTX
            driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate  # MySQL适配
            tablePrefix: qrtz_  # è¡¨åå‰ç¼€ï¼Œä¸Žè„šæœ¬ä¸€è‡´
            isClustered: false  # å•节点模式(集群需改为true)
            clusterCheckinInterval: 10000
            txIsolationLevelSerializable: true
          threadPool:
            class: org.quartz.simpl.SimpleThreadPool
            threadCount: 10  # çº¿ç¨‹æ± å¤§å°
            threadPriority: 5
            makeThreadsDaemons: true
          updateCheck: false  # å…³é—­ç‰ˆæœ¬æ£€æŸ¥
# token配置
token:
  # ä»¤ç‰Œè‡ªå®šä¹‰æ ‡è¯†
  header: Authorization
  # ä»¤ç‰Œå¯†é’¥
  secret: xpAVjhCjQDaDB7mjPAzMDSbQWXNu2zYkTdDNUsPMS5Xx8QMmQVYN7n74eZrYJxDJ
  # ä»¤ç‰Œæœ‰æ•ˆæœŸï¼ˆé»˜è®¤30分钟)
  expireTime: 450
# MyBatis Plus配置
mybatis-plus:
  # æœç´¢æŒ‡å®šåŒ…别名   æ ¹æ®è‡ªå·±çš„项目来
  typeAliasesPackage: com.ruoyi.**.pojo
  # é…ç½®mapper的扫描,找到所有的mapper.xml映射文件
  mapperLocations: classpath*:mapper/**/*Mapper.xml
  # åŠ è½½å…¨å±€çš„é…ç½®æ–‡ä»¶
  configLocation: classpath:mybatis/mybatis-config.xml
  global-config:
    enable-sql-runner: true
    db-config:
      id-type: auto
# PageHelper分页插件
pagehelper:
  helperDialect: mysql
  supportMethodsArguments: true
  params: count=countSql
# Swagger配置
swagger:
  # æ˜¯å¦å¼€å¯swagger
  enabled: true
  # è¯·æ±‚前缀
  pathMapping: /dev-api
# é˜²æ­¢XSS攻击
xss:
  # è¿‡æ»¤å¼€å…³
  enabled: true
  # æŽ’除链接(多个用逗号分隔)
  excludes: /system/notice
  # åŒ¹é…é“¾æŽ¥
  urlPatterns: /system/*,/monitor/*,/tool/*
# ä»£ç ç”Ÿæˆ
gen:
  # ä½œè€…
  author: ruoyi
  # é»˜è®¤ç”ŸæˆåŒ…路径 system éœ€æ”¹æˆè‡ªå·±çš„æ¨¡å—名称 å¦‚ system monitor tool
  packageName: com.ruoyi.project.system
  # è‡ªåŠ¨åŽ»é™¤è¡¨å‰ç¼€ï¼Œé»˜è®¤æ˜¯true
  autoRemovePre: false
  # è¡¨å‰ç¼€ï¼ˆç”Ÿæˆç±»åä¸ä¼šåŒ…含表前缀,多个用逗号分隔)
  tablePrefix: sys_
  # æ˜¯å¦å…è®¸ç”Ÿæˆæ–‡ä»¶è¦†ç›–到本地(自定义路径),默认不允许
  allowOverwrite: false
# æ–‡ä»¶ä¸Šä¼ é…ç½®
file:
  temp-dir: /javaWork/product-inventory-management/file/temp/uploads   # ä¸´æ—¶ç›®å½•
  upload-dir: /javaWork/product-inventory-management/file/prod/uploads # æ­£å¼ç›®å½•
  path: /javaWork/product-inventory-management/file # ä¸Šä¼ ç›®å½•
  urlPrefix: /prod-api/common # é“¾æŽ¥å‰ç¼€
  domain: http://1.15.17.182:9095 # åŸŸåå‰ç¼€
  expired: 120 # è¿‡æœŸæ—¶é—´(单位:分钟)
  useLimit: 10 # ä½¿ç”¨æ¬¡æ•°
  compress: true # æ˜¯å¦åŽ‹ç¼©
  needCompressSize: 10MB # åŽ‹ç¼©é˜ˆå€¼
  compressQuality: 0.5 # åŽ‹ç¼©è´¨é‡(0.0-1.0)
src/main/resources/mapper/account/sales/AccountInvoiceApplicationMapper.xml
@@ -72,5 +72,26 @@
            </foreach>
        )
    </select>
    <select id="selectAccountInvoiceApplicationList"
            resultType="com.ruoyi.account.bean.vo.sales.AccountInvoiceApplicationVo">
        select
            *
        from (
            select aia.*,
            asi.id AS sales_invoice_id,
            asi.status AS sales_invoice_status,
            c.customer_name,
            GROUP_CONCAT(sour.outbound_batches SEPARATOR ',') AS outboundBatches
            from account_invoice_application aia
            left join customer c on aia.customer_id = c.id
            left join stock_out_record sour on FIND_IN_SET(sour.id, aia.stock_out_record_ids) > 0
            left join account_sales_invoice asi on aia.id = asi.account_invoice_application_id
            GROUP BY aia.id
        )A
        WHERE A.customer_id = #{customerId}
        AND (COALESCE(A.sales_invoice_id,'') = '' OR A.sales_invoice_status = 1)
        AND A.status = 1
        order by A.id desc
    </select>
</mapper>
src/main/resources/mapper/account/sales/AccountSalesCollectionMapper.xml
@@ -58,36 +58,51 @@
    <select id="getOutboundBatchesByCustomer"
            resultType="com.ruoyi.account.bean.vo.sales.SalesOutboundVo">
        SELECT
        sor.id,
        sor.outbound_batches,
        sl.customer_name,
        sor.create_time as shippingDate,
        p.product_name,
        pm.model as specification_model,
        slp.tax_rate,
        sor.stock_out_num * slp.tax_inclusive_unit_price as outboundAmount,
        s.shipping_no,
        sl.sales_contract_no
        FROM stock_out_record sor
        left join shipping_info s on sor.record_id = s.id
        LEFT JOIN sales_ledger sl ON s.sales_ledger_id = sl.id
        LEFT JOIN sales_ledger_product slp ON s.sales_ledger_product_id = slp.id and slp.type = 1
        left join product_model pm on slp.product_model_id = pm.id
        left join product p on pm.product_id = p.id
        WHERE sor.record_type='13' and sor.approval_status=1
        and sl.customer_id=#{customerId}
        and sor.id NOT IN (
        SELECT DISTINCT SUBSTRING_INDEX(SUBSTRING_INDEX(a.stock_out_record_ids, ',', n.n), ',', -1)
        FROM account_sales_collection a
        CROSS JOIN (
            SELECT 1 n UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL
            SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6
        ) n
        WHERE n.n &lt;= CHAR_LENGTH(a.stock_out_record_ids) - CHAR_LENGTH(REPLACE(a.stock_out_record_ids, ',', '')) + 1
        )
        order by sor.id DESC
            id,
            outbound_batches,
            customer_name,
            shipping_date,
            product_name,
            specification_model,
            tax_rate,
            outbound_amount,
            shipping_no,
            sales_contract_no,
            amount_received
        FROM
            (
                SELECT
                    sor.id,
                    sor.outbound_batches,
                    sl.customer_name,
                    sor.create_time AS shipping_date,
                    p.product_name,
                    pm.model AS specification_model,
                    slp.tax_rate,
                    sor.stock_out_num * slp.tax_inclusive_unit_price AS outbound_amount,
                    s.shipping_no,
                    sl.sales_contract_no,
                    COALESCE ( SUM( acsc.collection_amount ), 0 ) AS amount_received
                FROM
                    stock_out_record sor
                        LEFT JOIN shipping_info s ON sor.record_id = s.id
                        LEFT JOIN sales_ledger sl ON s.sales_ledger_id = sl.id
                        LEFT JOIN sales_ledger_product slp ON s.sales_ledger_product_id = slp.id
                        AND slp.type = 1
                        LEFT JOIN product_model pm ON slp.product_model_id = pm.id
                        LEFT JOIN product p ON pm.product_id = p.id
                        LEFT JOIN account_sales_collection acsc ON acsc.stock_out_record_ids = sor.id
                WHERE
                    sor.record_type = '13'
                  AND sor.approval_status = 1
                  AND sl.customer_id = #{customerId}
                GROUP BY
                    sor.outbound_batches
                ORDER BY
                    sor.id
            ) tempA
        WHERE
            amount_received &lt; outbound_amount
    </select>
    <select id="selectIncomeStats" resultType="com.ruoyi.home.dto.IncomeExpenseAnalysisDto">
         SELECT DATE_FORMAT(collection_date, #{dateFormat}) AS dateStr,
src/main/resources/mapper/basic/CustomerMapper.xml
@@ -22,7 +22,14 @@
        from customer_user cu
        where cu.customer_id = c.id
        and cu.user_id != c.usage_user
        ) as user_ids_str
        ) as user_ids_str,
        CASE
            WHEN EXISTS (SELECT 1 FROM sales_ledger sl WHERE sl.customer_id = c.id)
              OR EXISTS (SELECT 1 FROM sales_quotation sq WHERE sq.customer_id = c.id)
              OR EXISTS (SELECT 1 FROM return_management rm WHERE rm.customer_id = c.id)
            THEN 1
            ELSE 0
        END AS sales_reference_flag
        from customer c
        left join sys_user u on c.usage_user = u.user_id
        <where>
@@ -71,7 +78,14 @@
        from customer_user cu
        where cu.customer_id = c.id
        and cu.user_id != c.usage_user
        ) as user_ids_str
        ) as user_ids_str,
        CASE
            WHEN EXISTS (SELECT 1 FROM sales_ledger sl WHERE sl.customer_id = c.id)
              OR EXISTS (SELECT 1 FROM sales_quotation sq WHERE sq.customer_id = c.id)
              OR EXISTS (SELECT 1 FROM return_management rm WHERE rm.customer_id = c.id)
            THEN 1
            ELSE 0
        END AS sales_reference_flag
        from customer c
        left join sys_user u on c.usage_user = u.user_id
        <where>
@@ -112,7 +126,7 @@
    <select id="customewTransactions" resultType="com.ruoyi.sales.vo.CustomerTransactionsVo">
        select T1.customer_id,
               c.customer_name,
               T1.contractAmounts,
               T1.contractAmounts - IFNULL(T4.returnAmount, 0) AS contractAmounts,
               IFNULL(T2.receiptPaymentAmount, 0) AS receiptPaymentAmount,
               IFNULL(T3.outboundAmount, 0) - IFNULL(T4.returnAmount, 0) AS receiptableAmount
        from (select customer_id, sum(contract_amount) as contractAmounts from sales_ledger group by customer_id) T1
@@ -153,7 +167,7 @@
        select sl.id salesLedgerId,
               sl.sales_contract_no,
               sl.execution_date,
               sl.contract_amount,
               sl.contract_amount - IFNULL(T3.returnAmount, 0) AS contract_amount,
               IFNULL(T1.receiptPaymentAmount, 0) AS receiptPaymentAmount,
               IFNULL(T2.outboundAmount, 0) - IFNULL(T3.returnAmount, 0) AS receiptableAmount
        from sales_ledger sl
src/main/resources/mapper/basic/SupplierManageMapper.xml
@@ -75,7 +75,7 @@
               T1.contractAmounts,
               IFNULL(T2.paymentAmount, 0) AS paymentAmount,
               IFNULL(T3.InboundAmount, 0) - IFNULL(T4.returnAmount, 0) AS payableAmount
        from (select supplier_id, sum(contract_amount) as contractAmounts from purchase_ledger group by supplier_id) T1
        from (select supplier_id, sum(COALESCE(net_contract_amount, contract_amount)) as contractAmounts from purchase_ledger group by supplier_id) T1
        left join (select supplier_id, sum(payment_amount) as paymentAmount from account_purchase_payment group by supplier_id) T2 on T1.supplier_id = T2.supplier_id
        left join (
            SELECT
@@ -114,7 +114,7 @@
       select pl.id  purchaseLedgerId,
              pl.purchase_contract_number,
              pl.execution_date,
              pl.contract_amount,
              COALESCE(pl.net_contract_amount, pl.contract_amount) AS contract_amount,
              IFNULL(T1.paymentAmount, 0) AS paymentAmount,
              IFNULL(T2.InboundAmount, 0) - IFNULL(T3.returnAmount, 0) AS payableAmount
       from purchase_ledger pl
src/main/resources/mapper/production/ProductionOrderMapper.xml
@@ -233,7 +233,9 @@
            </if>
        </where>
        order by po.id desc
        limit #{offset}, #{size}
        <if test="size != null and size &gt;= 0">
            limit #{offset}, #{size}
        </if>
    </select>
    <select id="countHomeOrderProgress" resultType="java.lang.Long">
src/main/resources/mapper/purchase/PurchaseLedgerMapper.xml
@@ -6,7 +6,8 @@
    <update id="updateContractAmountById">
        UPDATE purchase_ledger
        SET contract_amount = #{totalTaxInclusiveAmount}
        SET contract_amount = #{totalTaxInclusiveAmount},
            net_contract_amount = #{totalTaxInclusiveAmount}
        WHERE id = #{id}
    </update>
    <select id="selectPurchaseStats" resultType="com.ruoyi.home.dto.IncomeExpenseAnalysisDto">
@@ -27,6 +28,7 @@
                pl.supplier_name,
                pl.project_name,
                pl.contract_amount,
                pl.net_contract_amount,
                pl.entry_date,
                pl.execution_date,
                pl.recorder_id,
src/main/resources/mapper/sales/SalesLedgerMapper.xml
@@ -56,6 +56,7 @@
        T1.attachment_materials,
        T1.tenant_id,
        T1.contract_amount,
        T1.net_contract_amount,
        T1.contract_amount AS noInvoiceAmountTotal,
        T1.execution_date,
        T2.nick_name AS entry_person_name,
src/main/resources/mapper/sales/SalesLedgerProductMapper.xml
@@ -33,7 +33,8 @@
        WHEN (IFNULL(t2.qualitity, 0) - IFNULL(t2.locked_quantity, 0)) >0 THEN 1
        ELSE 0
        END as has_sufficient_stock,
        (IFNULL(T1.quantity, 0) - IFNULL(t3.shipped_quantity, 0)) as no_quantity,
        (IFNULL(T1.quantity, 0) - IFNULL(t3.shipped_quantity, 0) - IFNULL(t5.pending_approval_quantity, 0)) as no_quantity,
        IFNULL(t6.return_quantity, 0) as return_quantity,
        IFNULL(t5.pending_approval_quantity, 0) as pending_approval_quantity,
        CASE
         WHEN IFNULL(t3.shipped_quantity, 0) = 0 AND IFNULL(t5.pending_approval_quantity, 0) = 0 THEN '待发货'
@@ -55,11 +56,17 @@
        GROUP BY product_model_id
        ) t2 ON T1.product_model_id = t2.product_model_id
        LEFT JOIN (
        SELECT sales_ledger_product_id, IFNULL(SUM(spd.quantity), 0) as shipped_quantity
        SELECT si.sales_ledger_product_id, IFNULL(SUM(spd.quantity), 0) as shipped_quantity
        FROM shipping_info si
        LEFT JOIN shipping_product_detail spd ON si.id = spd.shipping_info_id
        where si.status = '审核通过' OR si.status = '已发货'
        GROUP BY sales_ledger_product_id
        INNER JOIN shipping_product_detail spd ON si.id = spd.shipping_info_id
        WHERE si.status IN ('审核通过', '已发货')
          AND EXISTS (
            SELECT 1 FROM stock_out_record sor
            WHERE sor.record_id = si.id
              AND TRIM(sor.record_type) = '13'
              AND sor.approval_status = 1
          )
        GROUP BY si.sales_ledger_product_id
        ) t3 ON t3.sales_ledger_product_id = T1.id
        LEFT JOIN (
        SELECT rel.sales_ledger_product_id,
@@ -95,12 +102,24 @@
        GROUP BY rel.sales_ledger_product_id
        ) t4 ON t4.sales_ledger_product_id = T1.id
        LEFT JOIN (
        SELECT sales_ledger_product_id, IFNULL(SUM(spd.quantity), 0) as pending_approval_quantity
        SELECT si.sales_ledger_product_id, IFNULL(SUM(spd.quantity), 0) as pending_approval_quantity
        FROM shipping_info si
        LEFT JOIN shipping_product_detail spd ON si.id = spd.shipping_info_id
        WHERE si.status IN ('待审核', '审核中')
        GROUP BY sales_ledger_product_id
           OR (si.status = '审核通过' AND NOT EXISTS (
                SELECT 1 FROM stock_out_record sor
                WHERE sor.record_id = si.id
                  AND TRIM(sor.record_type) = '13'
                  AND sor.approval_status = 1
           ))
        GROUP BY si.sales_ledger_product_id
        ) t5 ON t5.sales_ledger_product_id = T1.id
        LEFT JOIN (
        SELECT prop.sales_ledger_product_id, IFNULL(SUM(prop.return_quantity), 0) AS return_quantity
        FROM purchase_return_order_products prop
        INNER JOIN purchase_return_orders pro ON prop.purchase_return_order_id = pro.id
        GROUP BY prop.sales_ledger_product_id
        ) t6 ON t6.sales_ledger_product_id = T1.id
        left join product_model pm ON T1.product_model_id = pm.id
        left join product p ON pm.product_id = p.id
        <where>
src/main/resources/mapper/sales/ShippingInfoMapper.xml
@@ -101,8 +101,9 @@
        order by sor.id
    </select>
    <select id="getShippingInfoByCustomerName" resultType="com.ruoyi.sales.pojo.ShippingInfo">
        select * from shipping_info si
        select distinct si.* from shipping_info si
        left join sales_ledger sl on si.sales_ledger_id = sl.id
        where  sl.customer_name = #{customerName}
        inner join stock_out_record sor on sor.record_id = si.id and sor.record_type = '13' and sor.approval_status = 1
        where sl.customer_name = #{customerName} and si.status = '审核通过'
    </select>
</mapper>
src/main/resources/mapper/staff/PersonalShiftMapper.xml
@@ -20,6 +20,7 @@
        LEFT JOIN staff_on_job u ON u.id = s.staff_on_job_id
        LEFT JOIN personal_attendance_location_config palc ON palc.id = s.personal_attendance_location_config_id
        <where>
            u.id IS NOT NULL
            <if test="sysDeptId != null and sysDeptId != ''">
                and u.sys_dept_id = #{sysDeptId}
            </if>
src/main/resources/mapper/stock/StockInRecordMapper.xml
@@ -21,9 +21,9 @@
        pm.unit,
        u.nick_name as createBy,
        CASE
            WHEN #{params.topParentProductId} = 278 AND TRIM(sir.record_type) = '7' THEN
            WHEN TRIM(sir.record_type) = '7' THEN
                COALESCE(pl_by_product.purchase_contract_number, pl_direct.purchase_contract_number)
            WHEN #{params.topParentProductId} = 278 AND TRIM(sir.record_type) = '10' THEN
            WHEN TRIM(sir.record_type) = '10' THEN
                pl_by_quality.purchase_contract_number
            WHEN #{params.topParentProductId} = 276 THEN
                CASE
src/main/resources/mapper/system/SysDeptMapper.xml
@@ -26,7 +26,6 @@
    <sql id="selectDeptVo">
        select d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.phone, d.email, d.status, d.del_flag, d.create_by, d.create_time
        from sys_dept d
        order by d.create_time desc
    </sql>
    <select id="selectDeptList" parameterType="com.ruoyi.project.system.domain.SysDept" resultMap="SysDeptResult">
@@ -92,7 +91,7 @@
    <select id="checkDeptNameUnique" resultMap="SysDeptResult">
        <include refid="selectDeptVo"/>
        where dept_name=#{deptName} and parent_id = #{parentId} and del_flag = '0' limit 1
        where dept_name=#{deptName} and parent_id = #{parentId} and del_flag = '0' order by d.create_time desc limit 1
    </select>
    <insert id="insertDept" parameterType="com.ruoyi.project.system.domain.SysDept">
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