2026-05-07 031399b2893306a010cb4a6b0261441deb0d6625
Merge branch 'dev_New_pro' into dev_山西_黎城县胜德建材
已添加8个文件
已修改46个文件
已删除14个文件
2569 ■■■■ 文件已修改
doc/20260507_采购多文件分析确认接口传参类型约束.md 166 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/create_table_customer_user.sql 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/tools/ApproveTodoTools.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/ApproveNodeServiceImpl.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/ApproveProcessServiceImpl.java 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/controller/CustomerController.java 79 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/controller/CustomerFollowUpController.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/controller/CustomerPrivateController.java 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/controller/CustomerPrivatePoolController.java 85 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/dto/CustomerDto.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/dto/CustomerPrivateDto.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/dto/CustomerPrivatePoolDto.java 124 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/mapper/CustomerMapper.java 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/mapper/CustomerPrivateMapper.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/mapper/CustomerPrivatePoolMapper.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/mapper/CustomerUserMapper.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/pojo/Customer.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/pojo/CustomerFollowUp.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/pojo/CustomerPrivate.java 114 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/pojo/CustomerPrivatePool.java 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/pojo/CustomerReturnVisit.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/pojo/CustomerUser.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/service/CustomerPrivatePoolService.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/service/CustomerPrivateService.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/service/CustomerUserService.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/service/ICustomerService.java 25 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/service/impl/CustomerFollowUpServiceImpl.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/service/impl/CustomerPrivatePoolServiceImpl.java 225 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/service/impl/CustomerPrivateServiceImpl.java 121 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/service/impl/CustomerReturnVisitServiceImpl.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/service/impl/CustomerServiceImpl.java 211 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/service/impl/CustomerUserServiceImpl.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/task/ReturnVisitReminderTask.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/vo/CustomerVo.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/utils/StockUtils.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/bean/dto/ProductionOperationTaskDto.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/bean/vo/ProductionOperationTaskVo.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/bean/vo/ProductionOrderVo.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/bean/vo/ProductionOrderWorkOrderDetailVo.java 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/controller/ProductionOperationTaskController.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/controller/ProductionOrderController.java 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/mapper/ProductionOperationTaskMapper.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/ProductionOperationTaskService.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/ProductionOrderService.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionOperationTaskServiceImpl.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionOrderPickServiceImpl.java 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionOrderServiceImpl.java 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/system/service/impl/UnipushService.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/dto/PurchaseLedgerDto.java 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/service/impl/PurchaseLedgerServiceImpl.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/controller/ShippingInfoController.java 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/dto/ShippingProductDetailDto.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/mapper/ShippingInfoMapper.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/mapper/ShippingProductDetailMapper.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/pojo/SalesQuotation.java 35 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/ShippingInfoService.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/impl/SalesLedgerServiceImpl.java 194 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/impl/SalesQuotationServiceImpl.java 15 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/impl/ShippingInfoServiceImpl.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/staff/controller/PersonalAttendanceLocationConfigController.java 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application-demo.yml 115 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application-new-pro.yml 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/basic/CustomerMapper.xml 93 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/basic/CustomerPrivateMapper.xml 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/basic/CustomerPrivatePoolMapper.xml 101 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductionOperationTaskMapper.xml 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductionOrderMapper.xml 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/sales/ShippingProductDetailMapper.xml 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260507_²É¹º¶àÎļþ·ÖÎöÈ·ÈϽӿڴ«²ÎÀàÐÍÔ¼Êø.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,166 @@
# é‡‡è´­å¤šæ–‡ä»¶åˆ†æžç¡®è®¤æŽ¥å£ä¼ å‚类型约束(`purchase_ledger`)
## 1. é€‚用接口
- `POST /purchase-ai/analyze-files/confirm`
- `businessType = purchase_ledger`
> æœ¬æ–‡ç”¨äºŽçº¦æŸå‰ç«¯æäº¤åˆ°ç¡®è®¤æŽ¥å£çš„ `payload` ç±»åž‹ï¼Œé¿å… `Cannot deserialize ...` è¿™ç±»ååºåˆ—化异常。
## 2. é¡¶å±‚请求体
```json
{
  "businessType": "purchase_ledger",
  "payload": {
    "purchaseLedgers": []
  }
}
```
## 3. payload ç»“æž„
推荐统一使用批量结构(即使只有 1 æ¡ï¼‰ï¼š
```json
{
  "purchaseLedgers": [
    {
      "purchaseContractNumber": "CG-2026-001",
      "supplierId": 10001,
      "entryDate": "2026-05-07",
      "type": 2,
      "approvalStatus": 1,
      "productData": [
        {
          "productCategory": "钢材",
          "specificationModel": "Q235-A",
          "unit": "吨",
          "quantity": 10,
          "taxInclusiveUnitPrice": 1200,
          "taxInclusiveTotalPrice": 12000,
          "taxRate": 13,
          "type": 2
        }
      ]
    }
  ]
}
```
后端也兼容“单条直传”(`payload` ç›´æŽ¥æ˜¯ä¸€æ¡ `PurchaseLedgerDto`),但不建议新前端继续使用。
## 4. Java ç±»åž‹åˆ° JSON ç±»åž‹æ˜ å°„规则
- `Long` / `Integer`:传 **number**(整数),不要传 `"pending"`、`"1级"` è¿™ç±»å­—符串。
- `BigDecimal`:传 **number**(可小数),不要传带逗号、单位的字符串(如 `"12,000元"`)。
- `Date`:传 **string**,格式固定 `yyyy-MM-dd`。
- `Boolean`:传 `true/false`,不要传 `"true"`、`"是"`。
- `List<T>`:传数组 `[]`。
## 5. `PurchaseLedgerDto` å­—段类型约束(确认接口可识别字段)
| å­—段 | Java ç±»åž‹ | JSON ç±»åž‹ | è¯´æ˜Ž |
| --- | --- | --- | --- |
| entryDateStart | String | string | æŸ¥è¯¢åŒºé—´å¼€å§‹æ—¥æœŸï¼ˆ`yyyy-MM-dd`) |
| entryDateEnd | String | string | æŸ¥è¯¢åŒºé—´ç»“束日期(`yyyy-MM-dd`) |
| id | Long | number(integer) | å°è´¦ID |
| purchaseContractNumber | String | string | **必填**(主业务单号) |
| supplierId | Long | number(integer) | `supplierId` ä¸Ž `supplierName` äºŒé€‰ä¸€å¿…å¡« |
| supplierName | String | string | `supplierId` ä¸Ž `supplierName` äºŒé€‰ä¸€å¿…å¡« |
| recorderId | Long | number(integer) | å½•入人ID |
| recorderName | String | string | å½•入人名称 |
| salesContractNo | String | string | é”€å”®åˆåŒå· |
| salesContractNoId | Long | number(integer) | é”€å”®åˆåŒID |
| projectName | String | string | é¡¹ç›®åç§° |
| entryDate | Date | string(`yyyy-MM-dd`) | å½•入日期;缺省时后端会补当天 |
| executionDate | Date | string(`yyyy-MM-dd`) | ç­¾è®¢æ—¥æœŸ |
| remarks | String | string | å¤‡æ³¨ |
| attachmentMaterials | String | string | é™„件说明/路径 |
| createdAt | Date | string(`yyyy-MM-dd`) | åˆ›å»ºæ—¥æœŸ |
| updatedAt | Date | string(`yyyy-MM-dd`) | æ›´æ–°æ—¥æœŸ |
| salesLedgerId | Long | number(integer) | é”€å”®å°è´¦ID |
| hasChildren | Boolean | boolean | æ˜¯å¦æœ‰å­é¡¹ |
| Type | Integer | number(integer) | åŽ†å²å­—æ®µï¼ˆä¸æŽ¨èæ–°å‰ç«¯ä½¿ç”¨ï¼‰ |
| productData | List<SalesLedgerProduct> | array | äº§å“æ˜Žç»†ï¼Œè§ä¸‹èŠ‚ |
| tempFileIds | List<String> | array[string] | ä¸´æ—¶æ–‡ä»¶ID列表 |
| SalesLedgerFiles | List<CommonFile> | array[object] | åŽ†å²å…¼å®¹å­—æ®µ |
| phoneNumber | String | string | ä¸šåŠ¡å‘˜æ‰‹æœºå· |
| businessPersonId | Long | number(integer) | ä¸šåŠ¡å‘˜ID |
| productId | Long | number(integer) | äº§å“ID |
| productModelId | Long | number(integer) | äº§å“è§„æ ¼ID |
| invoiceNumber | String | string | å‘票号 |
| invoiceAmount | BigDecimal | number | å‘票金额 |
| ticketRegistrationId | Long | number(integer) | æ¥ç¥¨ç™»è®°ID |
| contractAmount | BigDecimal | number | åˆåŒé‡‘额 |
| receiptPaymentAmount | BigDecimal | number | æ¥ç¥¨é‡‘额 |
| unReceiptPaymentAmount | BigDecimal | number | æœªæ¥ç¥¨é‡‘额 |
| type | Integer | number(integer) | å°è´¦ç±»åž‹ï¼Œé‡‡è´­å›ºå®š `2`(缺省时后端补 `2`) |
| paymentMethod | String | string | ä»˜æ¬¾æ–¹å¼ |
| approvalStatus | Integer | number(integer) | å®¡æ‰¹çŠ¶æ€ï¼Œ**严禁传字符串**(如 `"pending"`) |
| templateName | String | string | æ¨¡æ¿åç§° |
### å®¡æ‰¹çŠ¶æ€å»ºè®®å€¼ï¼ˆ`approvalStatus`)
建议按数字传值:
- `1`:待审核
- `2`:审核中
- `3`:审核通过
- `4`:审核拒绝/失败
- `5`:模板数据(历史定义)
## 6. `productData`(`SalesLedgerProduct`)建议字段及类型
| å­—段 | Java ç±»åž‹ | JSON ç±»åž‹ | è¯´æ˜Ž |
| --- | --- | --- | --- |
| productCategory | String | string | **必填**,产品大类/名称 |
| specificationModel | String | string | **必填**,规格型号 |
| unit | String | string | **必填**,单位 |
| quantity | BigDecimal | number | **必填**,数量 |
| taxRate | BigDecimal | number | ç¨ŽçŽ‡ï¼ˆå¦‚ `13`) |
| taxInclusiveUnitPrice | BigDecimal | number | **必填**,含税单价 |
| taxInclusiveTotalPrice | BigDecimal | number | **必填**,含税总价 |
| taxExclusiveTotalPrice | BigDecimal | number | ä¸å«ç¨Žæ€»ä»·ï¼ˆå¯ä¸ä¼ ï¼ŒåŽç«¯å¯æŽ¨å¯¼ï¼‰ |
| invoiceType | String | string | å‘票类型 |
| productId | Long | number(integer) | äº§å“ID |
| productModelId | Long | number(integer) | äº§å“åž‹å·ID |
| isChecked | Boolean | boolean | æ˜¯å¦è´¨æ£€ |
| type | Integer | number(integer) | é‡‡è´­äº§å“å›ºå®š `2`(建议传 `2`) |
## 7. å¿…填与后端默认行为
- å°è´¦ä¸»è¡¨å¿…填:`purchaseContractNumber`,以及 `supplierId` / `supplierName` äºŒé€‰ä¸€ã€‚
- äº§å“æ˜Žç»†è‹¥ä¼ äº† `productData`,则每条产品必填:`productCategory`、`specificationModel`、`unit`、`quantity`、`taxInclusiveUnitPrice`、`taxInclusiveTotalPrice`。
- `entryDate` ä¸ºç©ºæ—¶ï¼ŒåŽç«¯è¡¥å½“天日期。
- `type` ä¸ºç©ºæ—¶ï¼ŒåŽç«¯è¡¥ `2`。
## 8. å‰ç«¯é«˜é¢‘错误示例
错误(会触发反序列化异常):
```json
{
  "approvalStatus": "pending",
  "type": "采购",
  "supplierId": "供应商A"
}
```
正确:
```json
{
  "approvalStatus": 1,
  "type": 2,
  "supplierId": 10001
}
```
## 9. æäº¤å‰è‡ªæ£€æ¸…单
1. æ‰€æœ‰ `Long/Integer/BigDecimal` å­—段都为数字,不是业务词字符串。
2. æ‰€æœ‰æ—¥æœŸå­—段都是 `yyyy-MM-dd`。
3. `approvalStatus` ä»…传数字状态码。
4. `supplierId` ä¸Ž `supplierName` è‡³å°‘有一个有效值。
5. `productData` ä¸­å¿…填列齐全且为正确类型。
doc/create_table_customer_user.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,10 @@
drop table if exists customer_user;
create table customer_user
(
    id          bigint auto_increment
        primary key,
    customer_id bigint not null default 0 comment '客户id',
    user_id     bigint not null default 0 comment '用户id',
    create_time datetime null comment '录入时间',
    tenant_id   bigint not null default 0 comment '租户id'
);
src/main/java/com/ruoyi/ai/tools/ApproveTodoTools.java
@@ -707,6 +707,7 @@
            case 6 -> "报价审批";
            case 7 -> "发货审批";
            case 8 -> "危险作业审批";
            case 9 -> "办公用品审批";
            default -> "类型" + type;
        };
    }
src/main/java/com/ruoyi/approve/service/impl/ApproveNodeServiceImpl.java
@@ -301,6 +301,8 @@
                return "发货审批";
            case 8:
                return "危险作业审批";
            case 9:
                return "办公用品审批";
        }
        return null;
    }
@@ -317,12 +319,12 @@
        qualityInspect.setUnit(saleProduct.getUnit());
        qualityInspect.setQuantity(saleProduct.getQuantity());
        qualityInspectMapper.insert(qualityInspect);
        List<QualityTestStandard> qualityTestStandard = qualityTestStandardMapper.getQualityTestStandardByProductId(saleProduct.getProductId(), 0,null);
        if (qualityTestStandard.size()>0){
        List<QualityTestStandard> qualityTestStandard = qualityTestStandardMapper.getQualityTestStandardByProductId(saleProduct.getProductId(), 0, null);
        if (qualityTestStandard.size() > 0) {
            qualityInspect.setTestStandardId(qualityTestStandard.get(0).getId());
            qualityInspectMapper.updateById(qualityInspect);
            qualityTestStandardParamMapper.selectList(Wrappers.<QualityTestStandardParam>lambdaQuery()
                            .eq(QualityTestStandardParam::getTestStandardId,qualityTestStandard.get(0).getId()))
                            .eq(QualityTestStandardParam::getTestStandardId, qualityTestStandard.get(0).getId()))
                    .forEach(qualityTestStandardParam -> {
                        QualityInspectParam param = new QualityInspectParam();
                        com.ruoyi.common.utils.bean.BeanUtils.copyProperties(qualityTestStandardParam, param);
src/main/java/com/ruoyi/approve/service/impl/ApproveProcessServiceImpl.java
@@ -80,11 +80,9 @@
                .map(ApproveProcessConfigNodeVo::getApproverId)
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
        if (list.isEmpty()) {
            throw new RuntimeException("流程不存在");
        }
        // æ— å®¡æ ¸äººé€»è¾‘添加
        if (CollectionUtils.isEmpty(nodeIds)) {
            autoPassPurchaseApproveIfNoApprover(approveProcessVO);
            autoPassPurchaseApproveIfNoApprover(approveProcessVO); // é‡‡è´­å•无审核人逻辑
            return;
        }
        List<SysUser> sysUsers = sysUserMapper.selectUserByIds(nodeIds);
@@ -451,6 +449,8 @@
                return "发货审批";
            case 8:
                return "危险作业审批";
            case 9:
                return "办公用品审批";
        }
        return null;
    }
src/main/java/com/ruoyi/basic/controller/CustomerController.java
@@ -5,17 +5,17 @@
import com.ruoyi.basic.dto.CustomerDto;
import com.ruoyi.basic.pojo.Customer;
import com.ruoyi.basic.service.ICustomerService;
import com.ruoyi.basic.vo.CustomerVo;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.framework.web.domain.R;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import jakarta.servlet.http.HttpServletResponse;
import java.util.List;
/**
@@ -35,7 +35,7 @@
     */
    @GetMapping("/list")
    public R list(Page<CustomerDto> page, CustomerDto customer) {
        IPage<CustomerDto> customerDtoIPage = customerService.selectCustomerList(page, customer);
        IPage<CustomerVo> customerDtoIPage = customerService.selectCustomerList(page, customer);
        return R.ok(customerDtoIPage);
    }
@@ -44,16 +44,9 @@
     */
    @Log(title = "客户档案", businessType = BusinessType.EXPORT)
    @PostMapping("/export")
    public void export(HttpServletResponse response, Customer customer) {
        Long[] ids = customer.getIds();
        List<Customer> list;
        if (ids != null && ids.length > 0) {
            list = customerService.selectCustomerListByIds(ids);
        } else {
            list = customerService.selectCustomerLists(customer);
        }
        ExcelUtil<Customer> util = new ExcelUtil<Customer>(Customer.class);
        util.exportExcel(response, list, "客户档案数据");
    public void export(HttpServletResponse response, CustomerDto customer) {
        ExcelUtil<CustomerVo> util = new ExcelUtil<CustomerVo>(CustomerVo.class);
        util.exportExcel(response, customerService.selectCustomerLists(customer), "客户档案数据");
    }
    @PostMapping("/downloadTemplate")
@@ -69,17 +62,17 @@
     */
    @Log(title = "客户档案", businessType = BusinessType.IMPORT)
    @PostMapping("/importData")
    public AjaxResult importData(MultipartFile file) throws Exception {
    public R importData(MultipartFile file, Integer type) throws Exception {
        return customerService.importData(file);
        return customerService.importData(file, type);
    }
    /**
     * èŽ·å–å®¢æˆ·æ¡£æ¡ˆè¯¦ç»†ä¿¡æ¯
     */
    @GetMapping(value = "/{id}")
    public AjaxResult getInfo(@PathVariable("id") Long id) {
        return success(customerService.selectCustomerDetailById(id));
    public R getInfo(@PathVariable("id") Long id) {
        return R.ok(customerService.selectCustomerDetailById(id));
    }
    /**
@@ -87,8 +80,8 @@
     */
    @Log(title = "客户档案", businessType = BusinessType.INSERT)
    @PostMapping("/addCustomer")
    public AjaxResult add(@RequestBody Customer customer) {
        return toAjax(customerService.insertCustomer(customer));
    public R add(@RequestBody Customer customer) {
        return R.ok(customerService.insertCustomer(customer));
    }
    /**
@@ -96,8 +89,8 @@
     */
    @Log(title = "客户档案", businessType = BusinessType.UPDATE)
    @PostMapping("/updateCustomer")
    public AjaxResult edit(@RequestBody Customer customer) {
        return toAjax(customerService.updateCustomer(customer));
    public R edit(@RequestBody Customer customer) {
        return R.ok(customerService.updateCustomer(customer));
    }
    /**
@@ -105,11 +98,11 @@
     */
    @Log(title = "客户档案", businessType = BusinessType.DELETE)
    @DeleteMapping("/delCustomer")
    public AjaxResult remove(@RequestBody Long[] ids) {
    public R remove(@RequestBody Long[] ids) {
        if (ids == null || ids.length == 0) {
            return AjaxResult.error("请传入要删除的ID");
            return R.fail("请传入要删除的ID");
        }
        return toAjax(customerService.deleteCustomerByIds(ids));
        return R.ok(customerService.deleteCustomerByIds(ids));
    }
    /**
@@ -121,4 +114,42 @@
    }
    /**
     * åˆ†é…å®¢æˆ·
     */
    @Log(title = "客户档案", businessType = BusinessType.OTHER)
    @PostMapping("/assignCustomer")
    public R assignCustomer(@RequestBody CustomerDto customer) {
        customerService.assignCustomer(customer);
        return R.ok();
    }
    /**
     * å›žæ”¶å®¢æˆ·
     */
    @Log(title = "客户档案", businessType = BusinessType.OTHER)
    @PostMapping("/recycleCustomer")
    public R recycleCustomer(@RequestBody CustomerDto customer) {
        customerService.recycleCustomer(customer);
        return R.ok();
    }
    /**
     * å…±äº«å®¢æˆ·
     */
    @Log(title = "客户档案", businessType = BusinessType.OTHER)
    @PostMapping("/together")
    public R together(@RequestBody CustomerDto customer) {
        customerService.together(customer);
        return R.ok();
    }
    /**
     * ç§æµ·å®¢æˆ·æµå›žå…¬æµ·
     */
    @Log(title = "客户档案", businessType = BusinessType.OTHER)
    @PostMapping("/back")
    public R back(Long id) {
        return R.ok(customerService.back(id));
    }
}
src/main/java/com/ruoyi/basic/controller/CustomerFollowUpController.java
@@ -44,7 +44,7 @@
    @Operation(summary = "查询客户跟进列表")
    public IPage<CustomerFollowUp> list(Page<CustomerFollowUp> page, CustomerFollowUp customerFollowUp) {
        LambdaQueryWrapper<CustomerFollowUp> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(customerFollowUp.getCustomerPrivatePoolId() != null, CustomerFollowUp::getCustomerPrivatePoolId, customerFollowUp.getCustomerPrivatePoolId())
        queryWrapper.eq(customerFollowUp.getCustomerId() != null, CustomerFollowUp::getCustomerId, customerFollowUp.getCustomerId())
                .like(customerFollowUp.getFollowerUserName() != null, CustomerFollowUp::getFollowerUserName, customerFollowUp.getFollowerUserName())
                .orderByDesc(CustomerFollowUp::getFollowUpTime);
        return customerFollowUpService.page(page, queryWrapper);
src/main/java/com/ruoyi/basic/controller/CustomerPrivateController.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/basic/controller/CustomerPrivatePoolController.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/basic/dto/CustomerDto.java
@@ -25,4 +25,8 @@
    private String togetherUserNames;
}
    /**
     * å…±äº«ç”¨æˆ·ID列表
     */
    private List<Long> userIds;
}
src/main/java/com/ruoyi/basic/dto/CustomerPrivateDto.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/basic/dto/CustomerPrivatePoolDto.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/basic/mapper/CustomerMapper.java
@@ -5,6 +5,7 @@
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.basic.dto.CustomerDto;
import com.ruoyi.basic.pojo.Customer;
import com.ruoyi.basic.vo.CustomerVo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@@ -67,5 +68,7 @@
     */
    int deleteCustomerByIds(Long[] ids);
    IPage<CustomerDto> listPage(Page<CustomerDto> page, @Param("c") CustomerDto customer);
}
    IPage<CustomerVo> listPage(Page<CustomerDto> page, @Param("c") CustomerDto customer, @Param("loginUserId") Long loginUserId);
    List<CustomerVo> list(@Param("c") CustomerDto customer, @Param("loginUserId") Long loginUserId);
}
src/main/java/com/ruoyi/basic/mapper/CustomerPrivateMapper.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/basic/mapper/CustomerPrivatePoolMapper.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/basic/mapper/CustomerUserMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,15 @@
package com.ruoyi.basic.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.basic.pojo.CustomerUser;
import org.apache.ibatis.annotations.Mapper;
/**
 * å®¢æˆ·å…±äº«Mapper接口
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @date 2026-04-29
 */
@Mapper
public interface CustomerUserMapper extends BaseMapper<CustomerUser> {
}
src/main/java/com/ruoyi/basic/pojo/Customer.java
@@ -129,4 +129,9 @@
    @Schema(description = "使用状态")
    private Long usageStatus;
    @Schema(description = "类型 0 ç§æµ·å®¢æˆ· 1 å…¬æµ·å®¢æˆ·")
    private Integer type;
    @Schema(description = "是否被分配:0-未分配,1-已分配")
    private Integer isAssigned;
}
src/main/java/com/ruoyi/basic/pojo/CustomerFollowUp.java
@@ -37,7 +37,7 @@
    /**
     * å…³è”的私海id
     */
    private Long customerPrivatePoolId;
    private Long customerId;
    /**
     * è·Ÿè¿›æ–¹å¼
src/main/java/com/ruoyi/basic/pojo/CustomerPrivate.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/basic/pojo/CustomerPrivatePool.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/basic/pojo/CustomerReturnVisit.java
@@ -36,7 +36,7 @@
    /**
     * å…³è”客户ID
     */
    private Integer customerPrivatePoolId;
    private Long customerId;
    /**
     * æé†’开关 (0:关闭, 1:开启)
src/main/java/com/ruoyi/basic/pojo/CustomerUser.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,43 @@
package com.ruoyi.basic.pojo;
import com.baomidou.mybatisplus.annotation.*;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
@TableName(value = "customer_user")
@Data
public class CustomerUser implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * åºå·
     */
    @TableId(type = IdType.AUTO)
    private Long id;
    /**
     * å®¢æˆ·id
     */
    private Long customerId;
    /**
     * ç”¨æˆ·id
     */
    private Long userId;
    /**
     * ç§Ÿæˆ·id
     */
    @TableField(fill = FieldFill.INSERT)
    private Long tenantId;
    /**
     * å½•入时间
     */
    @ApiModelProperty(value = "创建时间")
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
}
src/main/java/com/ruoyi/basic/service/CustomerPrivatePoolService.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/basic/service/CustomerPrivateService.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/basic/service/CustomerUserService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,13 @@
package com.ruoyi.basic.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.basic.pojo.CustomerUser;
/**
 * å®¢æˆ·å…±äº«Service接口
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @date 2026-04-29
 */
public interface CustomerUserService extends IService<CustomerUser> {
}
src/main/java/com/ruoyi/basic/service/ICustomerService.java
@@ -4,9 +4,9 @@
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.basic.dto.CustomerDto;
import com.ruoyi.basic.dto.CustomerPrivatePoolDto;
import com.ruoyi.basic.pojo.Customer;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.basic.vo.CustomerVo;
import com.ruoyi.framework.web.domain.R;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
@@ -33,7 +33,7 @@
     * @param id å®¢æˆ·æ¡£æ¡ˆä¸»é”®
     * @return å®¢æˆ·è¯¦æƒ…DTO
     */
    Customer selectCustomerDetailById(Long id);
    CustomerVo selectCustomerDetailById(Long id);
    /**
     * æŸ¥è¯¢å®¢æˆ·æ¡£æ¡ˆåˆ—表
@@ -75,9 +75,22 @@
     */
    List<Map<String, Object>> customerList(Customer customer);
    List<Customer> selectCustomerLists(Customer customer);
    List<CustomerVo> selectCustomerLists(CustomerDto customer);
    AjaxResult importData(MultipartFile file);
    R importData(MultipartFile file, Integer type);
    IPage<CustomerDto> selectCustomerList(Page<CustomerDto> page, CustomerDto customer);
    IPage<CustomerVo> selectCustomerList(Page<CustomerDto> page, CustomerDto customer);
    void assignCustomer(CustomerDto customer);
    void recycleCustomer(CustomerDto customer);
    /**
     * å…±äº«å®¢æˆ·ç»™å…¶ä»–用户
     *
     * @param customerDto å®¢æˆ·DTO(包含客户ID和共享用户ID列表)
     */
    void together(CustomerDto customerDto);
    Boolean back(Long id);
}
src/main/java/com/ruoyi/basic/service/impl/CustomerFollowUpServiceImpl.java
@@ -130,7 +130,7 @@
        }
        List<CustomerFollowUp> followUps = list(new LambdaQueryWrapper<CustomerFollowUp>()
                .eq(CustomerFollowUp::getCustomerPrivatePoolId, customerId));
                .eq(CustomerFollowUp::getCustomerId, customerId));
        if (followUps != null && !followUps.isEmpty()) {
            for (CustomerFollowUp followUp : followUps) {
src/main/java/com/ruoyi/basic/service/impl/CustomerPrivatePoolServiceImpl.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/basic/service/impl/CustomerPrivateServiceImpl.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/basic/service/impl/CustomerReturnVisitServiceImpl.java
@@ -75,7 +75,7 @@
            throw new ServiceException("客户ID不能为空");
        }
        LambdaQueryWrapper<CustomerReturnVisit> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(CustomerReturnVisit::getCustomerPrivatePoolId, customerId);
        queryWrapper.eq(CustomerReturnVisit::getCustomerId, customerId);
        CustomerReturnVisit returnVisit = baseMapper.selectOne(queryWrapper);
        if (returnVisit == null) {
@@ -94,7 +94,7 @@
            throw new ServiceException("客户ID不能为空");
        }
        LambdaQueryWrapper<CustomerReturnVisit> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(CustomerReturnVisit::getCustomerPrivatePoolId, customerId);
        queryWrapper.eq(CustomerReturnVisit::getCustomerId, customerId);
        List<CustomerReturnVisit> returnVisits = baseMapper.selectList(queryWrapper);
        for (CustomerReturnVisit returnVisit : returnVisits) {
@@ -124,7 +124,7 @@
        if (returnVisit == null) {
            throw new ServiceException("回访提醒数据不能为空");
        }
        if (returnVisit.getCustomerPrivatePoolId() == null) {
        if (returnVisit.getCustomerId() == null) {
            throw new ServiceException("客户ID不能为空");
        }
        if (returnVisit.getReminderTime() == null) {
src/main/java/com/ruoyi/basic/service/impl/CustomerServiceImpl.java
@@ -8,23 +8,25 @@
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.basic.dto.CustomerDto;
import com.ruoyi.basic.dto.CustomerFollowUpDto;
import com.ruoyi.basic.mapper.CustomerMapper;
import com.ruoyi.basic.mapper.CustomerPrivatePoolMapper;
import com.ruoyi.basic.pojo.Customer;
import com.ruoyi.basic.pojo.CustomerFollowUp;
import com.ruoyi.basic.pojo.CustomerPrivatePool;
import com.ruoyi.basic.service.CustomerFollowUpService;
import com.ruoyi.basic.service.CustomerReturnVisitService;
import com.ruoyi.basic.service.ICustomerService;
import com.ruoyi.basic.pojo.CustomerFollowUpFile;
import com.ruoyi.basic.pojo.CustomerUser;
import com.ruoyi.basic.service.*;
import com.ruoyi.basic.vo.CustomerVo;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.security.LoginUser;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.sales.mapper.SalesLedgerMapper;
import com.ruoyi.sales.pojo.SalesLedger;
import lombok.RequiredArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
@@ -42,15 +44,22 @@
 * @date 2025-05-07
 */
@Service
@RequiredArgsConstructor
@AllArgsConstructor
@Slf4j
public class CustomerServiceImpl extends ServiceImpl<CustomerMapper, Customer> implements ICustomerService {
    private final SalesLedgerMapper salesLedgerMapper;
    private final CustomerPrivatePoolMapper customerPrivatePoolMapper;
    private final CustomerMapper customerMapper;
    @Autowired
    private  SalesLedgerMapper salesLedgerMapper;
    @Autowired
    private CustomerMapper customerMapper;
    private final CustomerFollowUpService customerFollowUpService;
    private final CustomerReturnVisitService customerReturnVisitService;
    @Autowired
    private CustomerFollowUpService customerFollowUpService;
    @Autowired
    private CustomerFollowUpFileService customerFollowUpFileService;
    @Autowired
    private CustomerReturnVisitService customerReturnVisitService;
    @Autowired
    private CustomerUserService customerUserService;
    /**
     * æŸ¥è¯¢å®¢æˆ·æ¡£æ¡ˆ
@@ -70,8 +79,35 @@
     * @return å®¢æˆ·è¯¦æƒ…DTO
     */
    @Override
    public Customer selectCustomerDetailById(Long id) {
        return this.getById( id);
    public CustomerVo selectCustomerDetailById(Long id) {
        CustomerVo customerVo = new CustomerVo();
        BeanUtils.copyProperties(this.getById(id), customerVo);
        // æŸ¥è¯¢è·Ÿè¿›è®°å½•
        List<CustomerFollowUp> followUpList = customerFollowUpService.list(
                new LambdaQueryWrapper<CustomerFollowUp>()
                        .eq(CustomerFollowUp::getCustomerId, id)
                        .orderByDesc(CustomerFollowUp::getFollowUpTime)
        );
        if (!com.baomidou.mybatisplus.core.toolkit.CollectionUtils.isEmpty(followUpList)) {
            List<CustomerFollowUpDto> followUpDtoList = followUpList.stream().map(followUp -> {
                CustomerFollowUpDto followUpDto = new CustomerFollowUpDto();
                BeanUtils.copyProperties(followUp, followUpDto);
                // æŸ¥è¯¢é™„ä»¶
                List<CustomerFollowUpFile> fileList = customerFollowUpFileService.list(
                        new LambdaQueryWrapper<CustomerFollowUpFile>()
                                .eq(CustomerFollowUpFile::getFollowUpId, followUp.getId())
                );
                followUpDto.setFileList(fileList);
                return followUpDto;
            }).collect(Collectors.toList());
            customerVo.setFollowUpList(followUpDtoList);
        }
        return customerVo;
    }
    /**
@@ -81,16 +117,18 @@
     * @return å®¢æˆ·æ¡£æ¡ˆ
     */
    @Override
    public IPage<CustomerDto> selectCustomerList(Page<CustomerDto> page, CustomerDto customer) {
        IPage<CustomerDto> customerPage = customerMapper.listPage(page, customer);
    public IPage<CustomerVo> selectCustomerList(Page<CustomerDto> page, CustomerDto customer) {
        LoginUser loginUser = SecurityUtils.getLoginUser();
        Long loginUserId = loginUser.getUserId();
        IPage<CustomerVo> customerPage = customerMapper.listPage(page, customer, loginUserId);
        List<CustomerDto> records = customerPage.getRecords();
        List<CustomerVo> records = customerPage.getRecords();
        if (CollectionUtils.isEmpty(records)) {
            return customerPage;
        }
        List<Long> customerIds = records.stream()
                .map(CustomerDto::getId)
                .map(CustomerVo::getId)
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
@@ -109,6 +147,16 @@
                            followUp.getFollowUpTime().atZone(ZoneId.systemDefault()).toInstant()
                    ));
                }
                // è½¬æ¢å…±äº«ç”¨æˆ·ID字符串为List<Long>
                String userIdsStr = c.getUserIdsStr();
                if (StringUtils.isNotEmpty(userIdsStr)) {
                    List<Long> userIds = Arrays.stream(userIdsStr.split(","))
                            .map(String::trim)
                            .map(Long::parseLong)
                            .collect(Collectors.toList());
                    c.setUserIds(userIds);
                }
            });
        }
@@ -118,13 +166,13 @@
    private Map<Long, CustomerFollowUp> getLatestFollowUpMap(List<Long> customerIds) {
        List<CustomerFollowUp> followUps = customerFollowUpService.list(
                new LambdaQueryWrapper<CustomerFollowUp>()
                        .in(CustomerFollowUp::getCustomerPrivatePoolId, customerIds)
                        .in(CustomerFollowUp::getCustomerId, customerIds)
                        .orderByDesc(CustomerFollowUp::getFollowUpTime)
        );
        return followUps.stream()
                .collect(Collectors.toMap(
                        CustomerFollowUp::getCustomerPrivatePoolId,
                        CustomerFollowUp::getCustomerId,
                        followUp -> followUp,
                        (existing, replacement) -> existing
                ));
@@ -172,16 +220,28 @@
        if (!salesLedgers.isEmpty()) {
            throw new RuntimeException("客户档案下有销售合同,请先删除销售合同");
        }
        List<CustomerPrivatePool> customerPrivatePools = customerPrivatePoolMapper.selectList(new QueryWrapper<CustomerPrivatePool>().lambda().in(CustomerPrivatePool::getCustomerId, idList));
        if (!customerPrivatePools.isEmpty()) {
            throw new RuntimeException("客户档案下有客户私海,请先收回私海数据");
        // æŸ¥è¯¢æ˜¯å¦æœ‰å·²åˆ†é…çš„公海客户
        List<Customer> assignedPools = customerMapper.selectList(
                new QueryWrapper<Customer>().lambda()
                        .in(Customer::getId, idList)
                        .eq(Customer::getType, 1).
                        eq(Customer::getIsAssigned, 1)  // å…¬æµ·å®¢æˆ·
        );
        if (!assignedPools.isEmpty()) {
            throw new RuntimeException("客户档案下有已分配的公海客户,请先收回");
        }
        //  åˆ é™¤å®¢æˆ·çš„同时也需要删除对应的客户跟随、附件和回访提醒
        // åˆ é™¤å®¢æˆ·çš„同时也需要删除对应的客户跟随、附件和回访提醒
        for (Long id : ids) {
            customerFollowUpService.deleteByCustomerId(id);
            customerReturnVisitService.deleteByCustomerId(id);
            // åˆ é™¤å®¢æˆ·çš„共享关系
            customerUserService.remove(
                new QueryWrapper<CustomerUser>().lambda()
                    .eq(CustomerUser::getCustomerId, id)
            );
        }
        // åˆ é™¤å®¢æˆ·ä¸»è¡¨æ•°æ®
        return customerMapper.deleteBatchIds(idList);
    }
@@ -193,23 +253,32 @@
    }
    @Override
    public List<Customer> selectCustomerLists(Customer customer) {
        return customerMapper.selectList(null);
    public List<CustomerVo> selectCustomerLists(CustomerDto customer) {
        LoginUser loginUser = SecurityUtils.getLoginUser();
        Long loginUserId = loginUser.getUserId();
        return customerMapper.list(customer, loginUserId);
    }
    @Override
    public AjaxResult importData(MultipartFile file) {
    public R importData(MultipartFile file, Integer type) {
        try {
            ExcelUtil<Customer> util = new ExcelUtil<Customer>(Customer.class);
            List<Customer> userList = util.importExcel(file.getInputStream());
            if (CollectionUtils.isEmpty(userList)) {
                return AjaxResult.warn("模板错误或导入数据为空");
                return R.fail("模板错误或导入数据为空");
            }
            // æ ¹æ® type å‚数设置客户类型(私海/公海)
            if (type != null) {
                userList.forEach(customer -> {
                    customer.setType(type);
                });
            }
            this.saveOrUpdateBatch(userList);
            return AjaxResult.success(true);
            return R.ok(true);
        } catch (Exception e) {
            e.printStackTrace();
            return AjaxResult.error("导入失败");
            return R.fail("导入失败");
        }
    }
@@ -230,6 +299,86 @@
        ).collect(Collectors.toList());
    }
    // åˆ†é…å…¬æµ·å®¢æˆ·ç»™ç§æµ·
    @Override
    public void assignCustomer(CustomerDto customerDto) {
        Customer customer = customerMapper.selectById(customerDto.getId());
        if (customer.getType() == 1 && customer.getIsAssigned() == 0) {  // å…¬æµ·ä¸”可分配
            customer.setIsAssigned(1);
            customer.setUsageStatus(1L);
            customer.setUsageUser(customerDto.getUsageUser());
            customerMapper.updateById(customer);
        }
    }
    // å›žæ”¶ç§æµ·å®¢æˆ·åˆ°å…¬æµ·
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void recycleCustomer(CustomerDto customerDto) {
        Customer customer = customerMapper.selectById(customerDto.getId());
        if (customer.getType() == 1 && customer.getIsAssigned() == 1) {  // å…¬æµ·ä¸”已分配
            customer.setIsAssigned(0);
            customer.setUsageStatus(0L);
            customer.setUsageUser(0L);
            customerMapper.updateById(customer);
            // åˆ é™¤è¯¥å®¢æˆ·çš„æ‰€æœ‰å…±äº«å…³ç³»
            customerUserService.remove(
                new QueryWrapper<CustomerUser>().lambda()
                    .eq(CustomerUser::getCustomerId, customerDto.getId())
            );
        }
    }
    // å®¢æˆ·å…±äº«
    @Override
    public void together(CustomerDto customerDto) {
        // æŸ¥è¯¢çŽ°æœ‰çš„å…±äº«è®°å½•
        List<CustomerUser> existingUsers = customerUserService.list(
                new QueryWrapper<CustomerUser>().lambda().eq(CustomerUser::getCustomerId, customerDto.getId())
        );
        // èŽ·å–å·²å­˜åœ¨çš„ç”¨æˆ·ID列表
        List<Long> existingUserIds = existingUsers.stream()
                .map(CustomerUser::getUserId)
                .collect(Collectors.toList());
        // è¿‡æ»¤æŽ‰å·²å­˜åœ¨çš„用户,只保留新用户
        List<Long> newUserIds = customerDto.getUserIds().stream()
                .filter(userId -> !existingUserIds.contains(userId))
                .collect(Collectors.toList());
        if (CollectionUtils.isEmpty(newUserIds)) {
            return;
        }
        // èŽ·å–å½“å‰ç§Ÿæˆ·ID
        LoginUser loginUser = SecurityUtils.getLoginUser();
        Long tenantId = loginUser.getTenantId();
        // æ‰¹é‡ä¿å­˜æ–°çš„共享记录
        List<CustomerUser> customerUsers = newUserIds.stream()
                .map(userId -> {
                    CustomerUser customerUser = new CustomerUser();
                    customerUser.setCustomerId(customerDto.getId());
                    customerUser.setUserId(userId);
                    customerUser.setTenantId(tenantId);
                    return customerUser;
                })
                .collect(Collectors.toList());
        customerUserService.saveBatch(customerUsers);
    }
    @Override
    public Boolean back(Long id) {
        //将客户的type改为1 ä¸”直接分配给当前用户
        Customer customer = customerMapper.selectById(id);
        customer.setType(1);
        customer.setIsAssigned(1);
        return this.updateById(customer);
    }
    /**
     * ä¸‹åˆ’线命名转驼峰命名
     */
src/main/java/com/ruoyi/basic/service/impl/CustomerUserServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,19 @@
package com.ruoyi.basic.service.impl;
import com.ruoyi.basic.mapper.CustomerUserMapper;
import com.ruoyi.basic.pojo.CustomerUser;
import com.ruoyi.basic.service.CustomerUserService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
 * å®¢æˆ·å…±äº«Service实现类
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @date 2026-04-29
 */
@Service
@Transactional(rollbackFor = Exception.class)
public class CustomerUserServiceImpl extends ServiceImpl<CustomerUserMapper, CustomerUser> implements CustomerUserService {
}
src/main/java/com/ruoyi/basic/task/ReturnVisitReminderTask.java
@@ -72,7 +72,7 @@
        }
        try {
            unipushService.sendReturnVisitReminder(returnVisitId, client.getCid(), returnVisit.getContent(), returnVisit.getCustomerPrivatePoolId());
            unipushService.sendReturnVisitReminder(returnVisitId, client.getCid(), returnVisit.getContent(), returnVisit.getCustomerId());
            CustomerReturnVisit updateObj = new CustomerReturnVisit();
            updateObj.setId(returnVisitId);
            updateObj.setIsCompleted(1);
src/main/java/com/ruoyi/basic/vo/CustomerVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
package com.ruoyi.basic.vo;
import com.ruoyi.basic.dto.CustomerFollowUpDto;
import com.ruoyi.basic.pojo.Customer;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.List;
@Data
public class CustomerVo extends Customer {
    @ApiModelProperty(value = "跟进记录")
    private List<CustomerFollowUpDto> followUpList;
    private String usageUserName;
    private String togetherUserNames;
    /**
     * å…±äº«ç”¨æˆ·ID列表
     */
    private List<Long> userIds;
    /**
     * å…±äº«ç”¨æˆ·ID字符串(SQL查询返回,用于转换为List)
     */
    private String userIdsStr;
}
src/main/java/com/ruoyi/procurementrecord/utils/StockUtils.java
@@ -44,7 +44,7 @@
        stockUninventoryDto.setRecordType(String.valueOf(recordType));
        stockUninventoryDto.setQualitity(quantity);
        stockUninventoryDto.setProductModelId(productModelId);
        stockUninventoryService.addStockUninventory(stockUninventoryDto);
        stockUninventoryService.addStockInRecordOnly(stockUninventoryDto);
    }
    /**
@@ -77,7 +77,7 @@
        stockInventoryDto.setRecordType(String.valueOf(recordType));
        stockInventoryDto.setQualitity(quantity);
        stockInventoryDto.setProductModelId(productModelId);
        stockInventoryService.addstockInventory(stockInventoryDto);
        stockInventoryService.addStockInRecordOnly(stockInventoryDto);
    }
    /**
src/main/java/com/ruoyi/production/bean/dto/ProductionOperationTaskDto.java
@@ -4,8 +4,10 @@
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.format.annotation.DateTimeFormat;
import java.math.BigDecimal;
import java.time.LocalDate;
@EqualsAndHashCode(callSuper = true)
@Data
@@ -31,4 +33,15 @@
    @Schema(description = "完成进度")
    private BigDecimal completionStatus;
    @Schema(description = "订单号")
    private String npsNo;
    @Schema(description = "开始日期")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate startDate;
    @Schema(description = "结束日期")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate endDate;
}
src/main/java/com/ruoyi/production/bean/vo/ProductionOperationTaskVo.java
@@ -29,6 +29,15 @@
    @Schema(description = "工单类型 æ­£å¸¸/返工返修")
    private String workOrderType;
    @Schema(description = "生产任务数")
    private Long productionTaskCount;
    @Schema(description = "良品数")
    private BigDecimal goodQuantity;
    @Schema(description = "报废数量")
    private BigDecimal scrapQty;
    @Schema(description = "完成进度")
    private BigDecimal completionStatus;
src/main/java/com/ruoyi/production/bean/vo/ProductionOrderVo.java
@@ -36,4 +36,7 @@
    @Schema(description = "完成进度")
    private BigDecimal completionStatus;
    @Schema(description = "是否已退料")
    private Boolean returned;
}
src/main/java/com/ruoyi/production/bean/vo/ProductionOrderWorkOrderDetailVo.java
@@ -1,6 +1,5 @@
package com.ruoyi.production.bean.vo;
import com.ruoyi.production.pojo.ProductionOperationTask;
import com.ruoyi.production.pojo.ProductionOrderRoutingOperationParam;
import com.ruoyi.production.pojo.ProductionProductMain;
import com.ruoyi.production.pojo.ProductionProductOutput;
@@ -13,54 +12,63 @@
import java.util.List;
@Data
@Schema(name = "ProductionOrderWorkOrderDetailVo", description = "Production order work order/report/inspect detail")
@Schema(name = "ProductionOrderWorkOrderDetailVo", description = "生产追溯返回对象")
public class ProductionOrderWorkOrderDetailVo {
    @Schema(description = "Production order info")
    @Schema(description = "订单")
    private ProductionOrderVo productionOrder;
    @Schema(description = "Work order list")
    @Schema(description = "工单明细列表")
    private List<WorkOrderDetail> workOrderList;
    @Data
    @Schema(name = "WorkOrderDetail", description = "Work order detail")
    @Schema(name = "WorkOrderDetail", description = "工单明细")
    public static class WorkOrderDetail {
        @Schema(description = "Work order info")
        private ProductionOperationTask workOrder;
        @Schema(description = "工单信息")
        private ProductionOperationTaskVo workOrder;
        @Schema(description = "Report list under current work order")
        @Schema(description = "报工详情列表")
        private List<ReportDetail> reportList;
    }
    @Data
    @Schema(name = "ReportDetail", description = "Production report detail")
    public static class ReportDetail {
        @Schema(description = "Report main info")
        private ProductionProductMain reportMain;
        @Schema(description = "Report output list")
        private List<ProductionProductOutput> reportOutputList;
        @Schema(description = "Report process param list")
        private List<ProductionOrderRoutingOperationParam> reportParamList;
        @Schema(description = "Inspect list under current report")
        @Schema(description = "质检详情列表")
        private List<InspectDetail> inspectList;
    }
    @Data
    @Schema(name = "InspectDetail", description = "Quality inspect detail")
    @Schema(name = "ReportDetail", description = "报工详情")
    public static class ReportDetail {
        @Schema(description = "报工主信息")
        private ProductionProductMain reportMain;
        @Schema(description = "报工产出明细")
        private List<ProductionProductOutput> reportOutputList;
        @Schema(description = "报工工序参数")
        private List<ProductionOrderRoutingOperationParam> reportParamList;
    }
    @Data
    @Schema(name = "InspectDetail", description = "质检详情")
    public static class InspectDetail {
        @Schema(description = "Inspect main info")
        @Schema(description = "报工ID")
        private Long reportId;
        @Schema(description = "报工单号")
        private String reportNo;
        @Schema(description = "报工主信息")
        private ProductionProductMain reportMain;
        @Schema(description = "质检主信息")
        private QualityInspect inspect;
        @Schema(description = "Inspect param list")
        @Schema(description = "质检指标明细")
        private List<QualityInspectParam> inspectParamList;
        @Schema(description = "Inspect attachment list")
        @Schema(description = "质检附件列表")
        private List<QualityInspectFile> inspectFileList;
    }
}
src/main/java/com/ruoyi/production/controller/ProductionOperationTaskController.java
@@ -70,14 +70,16 @@
        return R.ok(productionOperationTaskService.assign(dto));
    }
    /**
     * å·¥å•流转卡下载
     * @param response
     * @param dto
     */
    @PostMapping("/down")
    @Operation(summary = "工单流转卡下载")
    public void down(HttpServletResponse response, @RequestBody ProductionOperationTaskDto dto) {
        productionOperationTaskService.down(response, dto);
    }
    @GetMapping("/getOperation")
    @Operation(summary = "工序详情查询")
    public R<List<ProductionOperationTaskVo>> getOperation(ProductionOperationTaskDto dto) {
        return R.ok(productionOperationTaskService.getOperation(dto));
    }
}
src/main/java/com/ruoyi/production/controller/ProductionOrderController.java
@@ -84,10 +84,10 @@
        return R.ok(productionOrderService.pick(productionOrderId));
    }
    @GetMapping("/workOrder/detail/{productionOrderId}")
    @Operation(summary = "Query work orders/reports/inspects by production order id")
    public R<ProductionOrderWorkOrderDetailVo> getWorkOrderReportInspectDetail(@PathVariable Long productionOrderId) {
        return R.ok(productionOrderService.getWorkOrderReportInspectDetail(productionOrderId));
    @GetMapping("/ordeDetail")
    @Operation(summary = "生产追溯")
    public R<ProductionOrderWorkOrderDetailVo> getWorkOrderReportInspectDetail(ProductionOrderDto productionOrderDto) {
        return R.ok(productionOrderService.getWorkOrderReportInspectDetail(productionOrderDto));
    }
    @Operation(summary = "更新订单状态")
src/main/java/com/ruoyi/production/mapper/ProductionOperationTaskMapper.java
@@ -41,4 +41,6 @@
                                                                           @Param("processIds") List<Long> processIds);
    ProductionOperationTaskDto getProductWorkOrderFlowCard(@Param("id") Long id);
    List<ProductionOperationTaskVo> getOperation(@Param("c") ProductionOperationTaskDto dto);
}
src/main/java/com/ruoyi/production/service/ProductionOperationTaskService.java
@@ -28,4 +28,6 @@
    boolean assign(ProductionOperationTaskDto dto);
    void down(HttpServletResponse response, ProductionOperationTaskDto dto);
    List<ProductionOperationTaskVo> getOperation(ProductionOperationTaskDto dto);
}
src/main/java/com/ruoyi/production/service/ProductionOrderService.java
@@ -32,7 +32,7 @@
    List<ProductionOrderPickVo> pick(Long productionOrderId);
    ProductionOrderWorkOrderDetailVo getWorkOrderReportInspectDetail(Long productionOrderId);
    ProductionOrderWorkOrderDetailVo getWorkOrderReportInspectDetail(ProductionOrderDto productionOrderDto);
    int updateOrder(ProductionOrderDto productionOrderDto);
}
src/main/java/com/ruoyi/production/service/impl/ProductionOperationTaskServiceImpl.java
@@ -347,4 +347,9 @@
                return null;
        }
    }
    @Override
    public List<ProductionOperationTaskVo> getOperation(ProductionOperationTaskDto dto) {
        return baseMapper.getOperation(dto);
    }
}
src/main/java/com/ruoyi/production/service/impl/ProductionOrderPickServiceImpl.java
@@ -673,8 +673,8 @@
        if (dto.getProductModelId() == null) {
            throw new ServiceException("第" + rowNo + "条产品规格ID不能为空");
        }
        if (dto.getPickQuantity() == null || dto.getPickQuantity().compareTo(BigDecimal.ZERO) <= 0) {
            throw new ServiceException("第" + rowNo + "条领料数量必须大于0");
        if (dto.getPickQuantity() == null || dto.getPickQuantity().compareTo(BigDecimal.ZERO) < 0) {
            throw new ServiceException("第" + rowNo + "条领料数量不能小于0");
        }
        if (dto.getPickType() != null && dto.getPickType() != PICK_TYPE_NORMAL && dto.getPickType() != PICK_TYPE_FEEDING) {
            throw new ServiceException("第" + rowNo + "条领料类型只能是1或2");
@@ -688,8 +688,8 @@
        if (dto.getId() == null) {
            throw new ServiceException("第" + rowNo + "条领料ID不能为空");
        }
        if (dto.getFeedingQuantity() == null || dto.getFeedingQuantity().compareTo(BigDecimal.ZERO) <= 0) {
            throw new ServiceException("第" + rowNo + "条本次补料数量必须大于0");
        if (dto.getFeedingQuantity() == null || dto.getFeedingQuantity().compareTo(BigDecimal.ZERO) < 0) {
            throw new ServiceException("第" + rowNo + "条本次补料数量不能小于0");
        }
        if (!isFeedingPick(dto)) {
            throw new ServiceException("第" + rowNo + "条补料类型必须为2");
src/main/java/com/ruoyi/production/service/impl/ProductionOrderServiceImpl.java
@@ -14,8 +14,10 @@
import com.ruoyi.basic.utils.FileUtil;
import com.ruoyi.common.constant.StorageAttachmentConstants;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.production.bean.dto.ProductionOperationTaskDto;
import com.ruoyi.production.bean.dto.ProductionOrderDto;
import com.ruoyi.production.bean.vo.ProductionBomStructureVo;
import com.ruoyi.production.bean.vo.ProductionOperationTaskVo;
import com.ruoyi.production.bean.vo.ProductionOrderPickVo;
import com.ruoyi.production.bean.vo.ProductionOrderVo;
import com.ruoyi.production.bean.vo.ProductionPlanVo;
@@ -681,29 +683,33 @@
    }
    @Override
    public ProductionOrderWorkOrderDetailVo getWorkOrderReportInspectDetail(Long productionOrderId) {
        if (productionOrderId == null) {
            throw new ServiceException("productionOrderId can not be null");
        }
    public ProductionOrderWorkOrderDetailVo getWorkOrderReportInspectDetail(ProductionOrderDto dto) {
        Long productionOrderId = resolveProductionOrderId(dto);
        ProductionOrderVo orderInfo = getProductionOrderInfo(productionOrderId);
        if (orderInfo == null) {
            throw new ServiceException("production order not found");
            throw new ServiceException("生产订单不存在");
        }
        ProductionOrderWorkOrderDetailVo detailVo = new ProductionOrderWorkOrderDetailVo();
        detailVo.setProductionOrder(orderInfo);
        List<ProductionOperationTask> workOrderList = productionOperationTaskMapper.selectList(
                Wrappers.<ProductionOperationTask>lambdaQuery()
                        .eq(ProductionOperationTask::getProductionOrderId, productionOrderId)
                        .orderByAsc(ProductionOperationTask::getId));
        ProductionOperationTaskDto taskQuery = new ProductionOperationTaskDto();
        taskQuery.setProductionOrderId(productionOrderId);
        IPage<ProductionOperationTaskVo> workOrderPage = productionOperationTaskMapper.pageProductionOperationTask(
                new Page<ProductionOperationTaskVo>(1, -1), taskQuery);
        List<ProductionOperationTaskVo> workOrderList = workOrderPage == null || workOrderPage.getRecords() == null
                ? Collections.emptyList()
                : workOrderPage.getRecords().stream()
                .filter(Objects::nonNull)
                .sorted(Comparator.comparing(ProductionOperationTaskVo::getId, Comparator.nullsLast(Comparator.naturalOrder())))
                .collect(Collectors.toList());
        if (workOrderList == null || workOrderList.isEmpty()) {
            detailVo.setWorkOrderList(Collections.emptyList());
            return detailVo;
        }
        List<Long> workOrderIdList = workOrderList.stream()
                .map(ProductionOperationTask::getId)
                .map(ProductionOperationTaskVo::getId)
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
        List<ProductionProductMain> reportMainList = workOrderIdList.isEmpty()
@@ -712,12 +718,12 @@
                Wrappers.<ProductionProductMain>lambdaQuery()
                        .in(ProductionProductMain::getProductionOperationTaskId, workOrderIdList)
                        .orderByAsc(ProductionProductMain::getId));
        Map<Long, List<ProductionProductMain>> reportMainMap = new LinkedHashMap<>();
        Map<Long, List<ProductionProductMain>> reportMainByWorkOrderMap = new LinkedHashMap<>();
        for (ProductionProductMain reportMain : reportMainList) {
            if (reportMain == null || reportMain.getProductionOperationTaskId() == null) {
                continue;
            }
            reportMainMap.computeIfAbsent(reportMain.getProductionOperationTaskId(), k -> new ArrayList<>()).add(reportMain);
            reportMainByWorkOrderMap.computeIfAbsent(reportMain.getProductionOperationTaskId(), key -> new ArrayList<>()).add(reportMain);
        }
        List<Long> reportMainIdList = reportMainList.stream()
@@ -766,7 +772,7 @@
                if (inspect == null || inspect.getProductMainId() == null) {
                    continue;
                }
                inspectMap.computeIfAbsent(inspect.getProductMainId(), k -> new ArrayList<>()).add(inspect);
                inspectMap.computeIfAbsent(inspect.getProductMainId(), key -> new ArrayList<>()).add(inspect);
            }
            List<Long> inspectIdList = inspectList.stream()
@@ -799,42 +805,44 @@
        }
        List<ProductionOrderWorkOrderDetailVo.WorkOrderDetail> workOrderDetailList = new ArrayList<>();
        for (ProductionOperationTask workOrder : workOrderList) {
        for (ProductionOperationTaskVo workOrder : workOrderList) {
            ProductionOrderWorkOrderDetailVo.WorkOrderDetail workOrderDetail = new ProductionOrderWorkOrderDetailVo.WorkOrderDetail();
            workOrderDetail.setWorkOrder(workOrder);
            List<ProductionProductMain> workOrderReportMainList = reportMainMap.get(workOrder.getId());
            if (workOrderReportMainList == null || workOrderReportMainList.isEmpty()) {
            List<ProductionProductMain> workOrderReportMainList = reportMainByWorkOrderMap.getOrDefault(workOrder.getId(), Collections.emptyList());
            if (workOrderReportMainList.isEmpty()) {
                workOrderDetail.setReportList(Collections.emptyList());
                workOrderDetail.setInspectList(Collections.emptyList());
                workOrderDetailList.add(workOrderDetail);
                continue;
            }
            List<ProductionOrderWorkOrderDetailVo.ReportDetail> reportDetailList = new ArrayList<>();
            List<ProductionOrderWorkOrderDetailVo.InspectDetail> inspectDetailList = new ArrayList<>();
            for (ProductionProductMain reportMain : workOrderReportMainList) {
                Long reportMainId = reportMain.getId();
                ProductionOrderWorkOrderDetailVo.ReportDetail reportDetail = new ProductionOrderWorkOrderDetailVo.ReportDetail();
                reportDetail.setReportMain(reportMain);
                reportDetail.setReportOutputList(reportOutputMap.getOrDefault(reportMainId, Collections.emptyList()));
                reportDetail.setReportParamList(reportParamMap.getOrDefault(reportMainId, Collections.emptyList()));
                List<QualityInspect> reportInspectList = inspectMap.get(reportMainId);
                if (reportInspectList == null || reportInspectList.isEmpty()) {
                    reportDetail.setInspectList(Collections.emptyList());
                } else {
                    List<ProductionOrderWorkOrderDetailVo.InspectDetail> inspectDetailList = new ArrayList<>();
                    for (QualityInspect inspect : reportInspectList) {
                        ProductionOrderWorkOrderDetailVo.InspectDetail inspectDetail = new ProductionOrderWorkOrderDetailVo.InspectDetail();
                        inspectDetail.setInspect(inspect);
                        inspectDetail.setInspectParamList(inspectParamMap.getOrDefault(inspect.getId(), Collections.emptyList()));
                        inspectDetail.setInspectFileList(inspectFileMap.getOrDefault(inspect.getId(), Collections.emptyList()));
                        inspectDetailList.add(inspectDetail);
                    }
                    reportDetail.setInspectList(inspectDetailList);
                }
                reportDetailList.add(reportDetail);
                List<QualityInspect> reportInspectList = inspectMap.getOrDefault(reportMainId, Collections.emptyList());
                for (QualityInspect inspect : reportInspectList) {
                    ProductionOrderWorkOrderDetailVo.InspectDetail inspectDetail = new ProductionOrderWorkOrderDetailVo.InspectDetail();
                    inspectDetail.setReportId(reportMainId);
                    inspectDetail.setReportNo(reportMain.getProductNo());
                    inspectDetail.setReportMain(reportMain);
                    inspectDetail.setInspect(inspect);
                    inspectDetail.setInspectParamList(inspectParamMap.getOrDefault(inspect.getId(), Collections.emptyList()));
                    inspectDetail.setInspectFileList(inspectFileMap.getOrDefault(inspect.getId(), Collections.emptyList()));
                    inspectDetailList.add(inspectDetail);
                }
            }
            workOrderDetail.setReportList(reportDetailList);
            workOrderDetail.setInspectList(inspectDetailList);
            workOrderDetailList.add(workOrderDetail);
        }
@@ -842,6 +850,26 @@
        return detailVo;
    }
    private Long resolveProductionOrderId(ProductionOrderDto dto) {
        if (dto == null) {
            throw new ServiceException("请传入生产订单ID或生产订单号");
        }
        if (dto.getId() != null) {
            return dto.getId();
        }
        if (dto.getNpsNo() == null || dto.getNpsNo().trim().isEmpty()) {
            throw new ServiceException("请传入生产订单ID或生产订单号");
        }
        ProductionOrder productionOrder = baseMapper.selectOne(
                Wrappers.<ProductionOrder>lambdaQuery()
                        .eq(ProductionOrder::getNpsNo, dto.getNpsNo().trim())
                        .last("limit 1"));
        if (productionOrder == null || productionOrder.getId() == null) {
            throw new ServiceException("生产订单不存在");
        }
        return productionOrder.getId();
    }
    @Override
    public List<ProductionOrderPickVo> pick(Long productionOrderId) {
        if (productionOrderId == null) {
src/main/java/com/ruoyi/project/system/service/impl/UnipushService.java
@@ -136,7 +136,7 @@
    /**
     * å‘送回访提醒
     */
    public void sendReturnVisitReminder(Long returnVisitId, String cid, String content, Integer customerId) {
    public void sendReturnVisitReminder(Long returnVisitId, String cid, String content, Long customerId) {
        String targetPath = "pages/cooperativeOffice/customerManage/detail?customerId=" + customerId;
        sendRoutingPush(returnVisitId, cid, "客户回访提醒", content, targetPath, false);
    }
src/main/java/com/ruoyi/purchase/dto/PurchaseLedgerDto.java
@@ -48,11 +48,11 @@
     */
    @Excel(name = "供应商名称")
    private String supplierName;
     /**
     * æ˜¯å¦ç™½åå•
     */
    @Excel(name = "是否白名单")
    private Integer isWhite;
//     /**
//     * æ˜¯å¦ç™½åå•
//     */
//    @Excel(name = "是否白名单")
//    private Integer isWhite;
    /**
     * å½•入人姓名id
src/main/java/com/ruoyi/purchase/service/impl/PurchaseLedgerServiceImpl.java
@@ -132,6 +132,8 @@
    @Transactional(rollbackFor = Exception.class)
    public int addOrEditPurchase(PurchaseLedgerDto purchaseLedgerDto) throws Exception {
        PurchaseLedger purchaseLedger = new PurchaseLedger();
        // DTO转Entity
        BeanUtils.copyProperties(purchaseLedgerDto, purchaseLedger);
        SalesLedger salesLedger = salesLedgerMapper.selectById(purchaseLedgerDto.getSalesLedgerId());
        //录入人
        SysUser sysUser = userMapper.selectUserById(purchaseLedgerDto.getRecorderId());
@@ -146,9 +148,6 @@
        SupplierManage supplierManage = supplierManageMapper.selectById(purchaseLedgerDto.getSupplierId());
        // DTO转Entity
        BeanUtils.copyProperties(purchaseLedgerDto, purchaseLedger);
        LoginUser loginUser = SecurityUtils.getLoginUser();
        if (ObjectUtils.isNotEmpty(loginUser) && null != loginUser.getTenantId()) {
            purchaseLedger.setTenantId(loginUser.getTenantId());
src/main/java/com/ruoyi/sales/controller/ShippingInfoController.java
@@ -2,8 +2,8 @@
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.approve.service.impl.ApproveProcessServiceImpl;
import com.ruoyi.approve.bean.vo.ApproveProcessVO;
import com.ruoyi.approve.service.impl.ApproveProcessServiceImpl;
import com.ruoyi.common.utils.OrderUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
@@ -12,12 +12,13 @@
import com.ruoyi.framework.security.LoginUser;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.sales.dto.ShippingInfoDto;
import com.ruoyi.sales.mapper.ShippingInfoMapper;
import com.ruoyi.sales.pojo.ShippingInfo;
import com.ruoyi.sales.service.ShippingInfoService;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import org.springframework.transaction.annotation.Transactional;
@@ -119,4 +120,9 @@
    public AjaxResult getByCustomerName(String customerName) {
        return AjaxResult.success(shippingInfoService.getShippingInfoByCustomerName(customerName));
    }
    @GetMapping("/getDateil/{id}")
    public R getDateil(@PathVariable("id") Long id) {
        return R.ok(shippingInfoService.getDetail(id));
    }
}
src/main/java/com/ruoyi/sales/dto/ShippingProductDetailDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,16 @@
package com.ruoyi.sales.dto;
import com.ruoyi.sales.pojo.ShippingProductDetail;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class ShippingProductDetailDto extends ShippingProductDetail {
    private String specificationModel;
    private String  productName;
    private BigDecimal deliveryQuantity;
}
src/main/java/com/ruoyi/sales/mapper/ShippingInfoMapper.java
@@ -3,11 +3,10 @@
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.procurementrecord.dto.ReturnSaleProductDto;
import com.ruoyi.sales.dto.SalesLedgerProductDto;
import com.ruoyi.sales.dto.ShippingInfoDto;
import com.ruoyi.sales.pojo.SalesLedgerProduct;
import com.ruoyi.sales.pojo.ShippingInfo;
import com.ruoyi.sales.pojo.ShippingProductDetail;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@@ -25,4 +24,5 @@
    List<ShippingInfo> getShippingInfoByCustomerName(String customerName);
    List<ShippingProductDetail> getDateil(Long id);
}
src/main/java/com/ruoyi/sales/mapper/ShippingProductDetailMapper.java
@@ -1,8 +1,11 @@
package com.ruoyi.sales.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.sales.dto.ShippingProductDetailDto;
import com.ruoyi.sales.pojo.ShippingProductDetail;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
 * <p>
@@ -15,4 +18,5 @@
@Mapper
public interface ShippingProductDetailMapper extends BaseMapper<ShippingProductDetail> {
    List<ShippingProductDetailDto> getDetail(Long id);
}
src/main/java/com/ruoyi/sales/pojo/SalesQuotation.java
@@ -3,7 +3,7 @@
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.framework.aspectj.lang.annotation.Excel;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
@@ -16,55 +16,58 @@
public class SalesQuotation {
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    @Schema(description = "报价单编号")
    @ApiModelProperty(value = "报价单编号")
    @Excel(name = "报价单编号")
    private String quotationNo;
    @Schema(description = "客户名称")
    @ApiModelProperty(value = "客户名称")
    @Excel(name = "客户名称")
    private String customer;
    @Schema(description = "业务员")
    @ApiModelProperty(value = "客户id")
    private Long customerId;
    @ApiModelProperty(value = "业务员")
    @Excel(name = "业务员")
    private String salesperson;
    @Schema(description = "报价日期")
    @ApiModelProperty(value = "报价日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    @Excel(name = "报价日期", width = 30, dateFormat = "yyyy-MM-dd")
    private LocalDate quotationDate;
    @Schema(description = "有效期至")
    @ApiModelProperty(value = "有效期至")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    @Excel(name = "有效期至", width = 30, dateFormat = "yyyy-MM-dd")
    private LocalDate validDate;
    @Schema(description = "付款方式")
    @ApiModelProperty(value = "付款方式")
    private String paymentMethod;
    @Schema(description = "交货周期天数")
    @ApiModelProperty(value = "交货周期天数")
    private String deliveryPeriod;
    @Schema(description = "状态")
    @ApiModelProperty(value = "状态")
    private String status;
    @Schema(description = "报价总金额")
    @ApiModelProperty(value = "报价总金额")
    @Excel(name = "报价金额")
    private BigDecimal totalAmount;
    @Schema(description = "备注")
    @ApiModelProperty(value = "备注")
    private String remark;
    @Schema(description = "创建时间")
    @ApiModelProperty(value = "创建时间")
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    @Schema(description = "修改时间")
    @ApiModelProperty(value = "修改时间")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
    @Schema(description = "创建用户")
    @ApiModelProperty(value = "创建用户")
    @TableField(fill = FieldFill.INSERT)
    private Integer createUser;
    @Schema(description = "修改用户")
    @ApiModelProperty(value = "修改用户")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Integer updateUser;
    @Schema(description = "租户ID")
    @ApiModelProperty(value = "租户ID")
    @TableField(fill = FieldFill.INSERT)
    private Long tenantId;
src/main/java/com/ruoyi/sales/service/ShippingInfoService.java
@@ -5,6 +5,7 @@
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.sales.dto.SalesLedgerProductDto;
import com.ruoyi.sales.dto.ShippingInfoDto;
import com.ruoyi.sales.dto.ShippingProductDetailDto;
import com.ruoyi.sales.pojo.ShippingInfo;
import java.util.List;
@@ -25,4 +26,6 @@
    List<ShippingInfo> getShippingInfoByCustomerName(String customerName);
    boolean add(ShippingInfoDto req);
    List<ShippingProductDetailDto> getDetail(Long id);
}
src/main/java/com/ruoyi/sales/service/impl/SalesLedgerServiceImpl.java
@@ -9,15 +9,10 @@
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.account.service.AccountIncomeService;
import com.ruoyi.basic.dto.CustomerPrivatePoolDto;
import com.ruoyi.basic.dto.StorageBlobVO;
import com.ruoyi.basic.enums.ApplicationTypeEnum;
import com.ruoyi.basic.enums.RecordTypeEnum;
import com.ruoyi.basic.mapper.CustomerMapper;
import com.ruoyi.basic.mapper.CustomerPrivatePoolMapper;
import com.ruoyi.basic.mapper.ProductMapper;
import com.ruoyi.basic.mapper.ProductModelMapper;
import com.ruoyi.basic.pojo.Customer;
import com.ruoyi.basic.utils.FileUtil;
import com.ruoyi.common.enums.FileNameType;
import com.ruoyi.common.enums.SaleEnum;
import com.ruoyi.common.exception.base.BaseException;
@@ -29,9 +24,8 @@
import com.ruoyi.framework.security.LoginUser;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.other.mapper.TempFileMapper;
import com.ruoyi.production.mapper.ProductionProductInputMapper;
import com.ruoyi.production.mapper.ProductionProductMainMapper;
import com.ruoyi.production.mapper.ProductionProductOutputMapper;
import com.ruoyi.other.pojo.TempFile;
import com.ruoyi.production.mapper.*;
import com.ruoyi.production.service.ProductionProductMainService;
import com.ruoyi.project.system.domain.SysDept;
import com.ruoyi.project.system.domain.SysUser;
@@ -47,8 +41,10 @@
import com.ruoyi.sales.vo.SalesLedgerVo;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FilenameUtils;
import org.jetbrains.annotations.Nullable;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
@@ -56,10 +52,15 @@
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.YearMonth;
@@ -82,11 +83,13 @@
    private static final String LOCK_PREFIX = "contract_no_lock:";
    private static final long LOCK_WAIT_TIMEOUT = 10; // é”ç­‰å¾…超时时间(秒)
    private static final long LOCK_EXPIRE_TIME = 30;  // é”è‡ªåŠ¨è¿‡æœŸæ—¶é—´ï¼ˆç§’ï¼‰
    private final AccountIncomeService accountIncomeService;
    private final SalesLedgerMapper salesLedgerMapper;
    private final CustomerMapper customerMapper;
    private final SalesLedgerProductMapper salesLedgerProductMapper;
    private final SalesLedgerProductServiceImpl salesLedgerProductServiceImpl;
    private final CommonFileMapper commonFileMapper;
    private final TempFileMapper tempFileMapper;
    private final ReceiptPaymentMapper receiptPaymentMapper;
    private final ShippingInfoServiceImpl shippingInfoServiceImpl;
    private final CommonFileServiceImpl commonFileService;
@@ -94,14 +97,27 @@
    private final InvoiceLedgerMapper invoiceLedgerMapper;
    private final InvoiceRegistrationProductMapper invoiceRegistrationProductMapper;
    private final InvoiceRegistrationMapper invoiceRegistrationMapper;
    private final ProductModelMapper productModelMapper;
    private final ProductionProductMainMapper productionProductMainMapper;
    private final ProductionProductOutputMapper productionProductOutputMapper;
    private final ProductionProductInputMapper productionProductInputMapper;
    private final QualityInspectMapper qualityInspectMapper;
    private final RedisTemplate<String, String> redisTemplate;
    private final SysDeptMapper sysDeptMapper;
    private final ProductionProductMainService productionProductMainService;
    private final PurchaseReturnOrderProductsMapper purchaseReturnOrderProductsMapper;
    private final SysUserMapper sysUserMapper;
    private final CustomerPrivatePoolMapper customerPrivatePoolMapper;
    private final FileUtil fileUtil;
    @Autowired
    private SysDeptMapper sysDeptMapper;
    @Value("${file.upload-dir}")
    private String uploadDir;
    @Autowired
    private ProductModelMapper productModelMapper;
    @Autowired
    private ProductMapper productMapper;
    @Autowired
    private ProductionProductMainService productionProductMainService;
    @Autowired
    private PurchaseReturnOrderProductsMapper purchaseReturnOrderProductsMapper;
    ;
    @Autowired
    private SysUserMapper sysUserMapper;
    @Override
    public List<SalesLedger> selectSalesLedgerList(SalesLedgerDto salesLedgerDto) {
@@ -185,9 +201,6 @@
            resultDto.setProductData(products);
            resultDto.setSalesLedgerFiles(salesLedgerFiles);
        }
        // 5. æŸ¥è¯¢é™„ä»¶
        List<StorageBlobVO> StorageBlobVOs = fileUtil.getStorageBlobVOsByRecordTypeAndRecordId(RecordTypeEnum.SALES_LEDGER, salesLedger.getId());
        resultDto.setStorageBlobVOs(StorageBlobVOs);
        return resultDto;
    }
@@ -514,7 +527,7 @@
        List<Long> productIds = products.stream()
                .map(SalesLedgerProduct::getId)
                .collect(Collectors.toList());
        //删除生产计划
        //删除生产数据
        salesLedgerProductServiceImpl.deleteProductionData(productIds);
        // æ‰¹é‡åˆ é™¤äº§å“å­è¡¨
@@ -569,43 +582,122 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int addOrUpdateSalesLedger(SalesLedgerDto salesLedgerDto) {
        // 1. æ ¡éªŒå®¢æˆ·ä¿¡æ¯
        CustomerPrivatePoolDto customer = customerPrivatePoolMapper.selectInfo(salesLedgerDto.getCustomerId());
        if (customer == null) {
            throw new BaseException("客户不存在");
        try {
            // 1. æ ¡éªŒå®¢æˆ·ä¿¡æ¯
            Customer customer = customerMapper.selectById(salesLedgerDto.getCustomerId());
            if (customer == null) {
                throw new BaseException("客户不存在");
            }
            // 2. DTO转Entity
            SalesLedger salesLedger = convertToEntity(salesLedgerDto);
            salesLedger.setCustomerName(customer.getCustomerName());
            salesLedger.setTenantId(customer.getTenantId());
            // 3. æ–°å¢žæˆ–更新主表
            if (salesLedger.getId() == null) {
                String contractNo = generateSalesContractNo();
                salesLedger.setSalesContractNo(contractNo);
                salesLedgerMapper.insert(salesLedger);
            } else {
                salesLedgerMapper.updateById(salesLedger);
            }
            // 4. å¤„理子表数据
            List<SalesLedgerProduct> productList = salesLedgerDto.getProductData();
            if (productList != null && !productList.isEmpty()) {
                handleSalesLedgerProducts(salesLedger.getId(), productList, EnumUtil.fromCode(SaleEnum.class, salesLedgerDto.getType()));
                updateMainContractAmount(
                        salesLedger.getId(),
                        productList,
                        SalesLedgerProduct::getTaxInclusiveTotalPrice,
                        salesLedgerMapper,
                        SalesLedger.class
                );
            }
            // 5. è¿ç§»ä¸´æ—¶æ–‡ä»¶åˆ°æ­£å¼ç›®å½•
            if (salesLedgerDto.getTempFileIds() != null && !salesLedgerDto.getTempFileIds().isEmpty()) {
                migrateTempFilesToFormal(salesLedger.getId(), salesLedgerDto.getTempFileIds());
            }
            return 1;
        } catch (IOException e) {
            throw new BaseException("文件迁移失败: " + e.getMessage());
        }
    }
    /**
     * å°†ä¸´æ—¶æ–‡ä»¶è¿ç§»åˆ°æ­£å¼ç›®å½•
     *
     * @param businessId  ä¸šåŠ¡ID(销售台账ID)
     * @param tempFileIds ä¸´æ—¶æ–‡ä»¶ID列表
     * @throws IOException æ–‡ä»¶æ“ä½œå¼‚常
     */
    private void migrateTempFilesToFormal(Long businessId, List<String> tempFileIds) throws IOException {
        if (CollectionUtils.isEmpty(tempFileIds)) {
            return;
        }
        // 2. DTO转Entity
        SalesLedger salesLedger = convertToEntity(salesLedgerDto);
        salesLedger.setCustomerName(customer.getCustomerName());
        salesLedger.setTenantId(customer.getTenantId());
        // 3. æ–°å¢žæˆ–更新主表
        if (salesLedger.getId() == null) {
            String contractNo = generateSalesContractNo();
            salesLedger.setSalesContractNo(contractNo);
            salesLedgerMapper.insert(salesLedger);
        } else {
            salesLedgerMapper.updateById(salesLedger);
        // æž„建正式目录路径(按业务类型和日期分组)
        String formalDir = uploadDir + LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE);
        Path formalDirPath = Paths.get(formalDir);
        // ç¡®ä¿æ­£å¼ç›®å½•存在(递归创建)
        if (!Files.exists(formalDirPath)) {
            Files.createDirectories(formalDirPath);
        }
        // 4. å¤„理子表数据
        List<SalesLedgerProduct> productList = salesLedgerDto.getProductData();
        if (productList != null && !productList.isEmpty()) {
            handleSalesLedgerProducts(salesLedger.getId(), productList, EnumUtil.fromCode(SaleEnum.class, salesLedgerDto.getType()));
            updateMainContractAmount(
                    salesLedger.getId(),
                    productList,
                    SalesLedgerProduct::getTaxInclusiveTotalPrice,
                    salesLedgerMapper,
                    SalesLedger.class
            );
        }
        for (String tempFileId : tempFileIds) {
            // æŸ¥è¯¢ä¸´æ—¶æ–‡ä»¶è®°å½•
            TempFile tempFile = tempFileMapper.selectById(tempFileId);
            if (tempFile == null) {
                log.warn("临时文件不存在,跳过处理: {}", tempFileId);
                continue;
            }
        // 5. ä¿å­˜æ–‡ä»¶
        if (salesLedgerDto.getStorageBlobDTOs() != null && !salesLedgerDto.getStorageBlobDTOs().isEmpty()) {
            fileUtil.saveStorageAttachment(ApplicationTypeEnum.FILE, RecordTypeEnum.SALES_LEDGER, salesLedger.getId(), salesLedgerDto.getStorageBlobDTOs());
            // æž„建正式文件名(包含业务ID和时间戳,避免冲突)
            String originalFilename = tempFile.getOriginalName();
            String fileExtension = FilenameUtils.getExtension(originalFilename);
            String formalFilename = businessId + "_" +
                    System.currentTimeMillis() + "_" +
                    UUID.randomUUID().toString().substring(0, 8) +
                    (StringUtils.hasText(fileExtension) ? "." + fileExtension : "");
            Path formalFilePath = formalDirPath.resolve(formalFilename);
            try {
                // æ‰§è¡Œæ–‡ä»¶è¿ç§»ï¼ˆä½¿ç”¨åŽŸå­æ“ä½œç¡®ä¿å®‰å…¨æ€§ï¼‰
//                Files.move(
//                        Paths.get(tempFile.getTempPath()),
//                        formalFilePath,
//                        StandardCopyOption.REPLACE_EXISTING,
//                        StandardCopyOption.ATOMIC_MOVE
//                );
                // åŽŸå­ç§»åŠ¨å¤±è´¥ï¼Œä½¿ç”¨å¤åˆ¶+删除
                Files.copy(Paths.get(tempFile.getTempPath()), formalFilePath, StandardCopyOption.REPLACE_EXISTING);
                Files.deleteIfExists(Paths.get(tempFile.getTempPath()));
                log.info("文件迁移成功: {} -> {}", tempFile.getTempPath(), formalFilePath);
                // æ›´æ–°æ–‡ä»¶è®°å½•(关联到业务ID)
                CommonFile fileRecord = new CommonFile();
                fileRecord.setCommonId(businessId);
                fileRecord.setName(originalFilename);
                fileRecord.setUrl(formalFilePath.toString());
                fileRecord.setCreateTime(LocalDateTime.now());
                //销售
                fileRecord.setType(FileNameType.SALE.getValue());
                commonFileMapper.insert(fileRecord);
                // åˆ é™¤ä¸´æ—¶æ–‡ä»¶è®°å½•
                tempFileMapper.deleteById(tempFile);
                log.info("文件迁移成功: {} -> {}", tempFile.getTempPath(), formalFilePath);
            } catch (IOException e) {
                log.error("文件迁移失败: {}", tempFile.getTempPath(), e);
                // å¯é€‰æ‹©å›žæ»šäº‹åŠ¡æˆ–è®°å½•å¤±è´¥æ–‡ä»¶
                throw new IOException("文件迁移异常", e);
            }
        }
        return 1;
    }
    // æ–‡ä»¶è¿ç§»æ–¹æ³•
src/main/java/com/ruoyi/sales/service/impl/SalesQuotationServiceImpl.java
@@ -11,8 +11,8 @@
import com.ruoyi.approve.service.impl.ApproveProcessServiceImpl;
import com.ruoyi.approve.bean.vo.ApproveGetAndUpdateVo;
import com.ruoyi.approve.bean.vo.ApproveProcessVO;
import com.ruoyi.basic.dto.CustomerPrivatePoolDto;
import com.ruoyi.basic.mapper.CustomerPrivatePoolMapper;
import com.ruoyi.basic.mapper.CustomerMapper;
import com.ruoyi.basic.pojo.Customer;
import com.ruoyi.common.utils.OrderUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.bean.BeanUtils;
@@ -42,7 +42,7 @@
    private final SalesQuotationProductService salesQuotationProductService;
    private final ApproveProcessServiceImpl approveProcessService;
    private final CustomerPrivatePoolMapper customerPrivatePoolMapper;
    private final CustomerMapper customerMapper;
    @Override
    public IPage<SalesQuotationDto> listPage(Page page, SalesQuotationDto salesQuotationDto) {
@@ -61,10 +61,9 @@
    public boolean add(SalesQuotationDto salesQuotationDto) {
        LoginUser loginUser = SecurityUtils.getLoginUser();
        SalesQuotation salesQuotation = new SalesQuotation();
        CustomerPrivatePoolDto customerPrivatePoolDto = customerPrivatePoolMapper.selectInfo(Long.valueOf(salesQuotationDto.getCustomer()));
        if (ObjectUtils.isNotEmpty(customerPrivatePoolDto))  {
            BeanUtils.copyProperties(salesQuotationDto, salesQuotation);
            salesQuotation.setCustomer(customerPrivatePoolDto.getCustomerName());
        Customer customer = customerMapper.selectById(Long.valueOf(salesQuotationDto.getCustomerId()));
        if (ObjectUtils.isNotEmpty(customer))  {
            salesQuotation.setCustomer(customer.getCustomerName());
        }
        String quotationNo = OrderUtils.countTodayByCreateTime(salesQuotationMapper, "QT","quotation_no");
        salesQuotation.setQuotationNo(quotationNo);
@@ -93,7 +92,7 @@
            approveProcessService.addApprove(approveProcessVO);
        }catch (Exception e){
            log.error("SalesQuotationServiceImpl error:{}", e);
            throw new RuntimeException("审批失败");
            throw                                new RuntimeException("审批失败");
        }
        return true;
    }
src/main/java/com/ruoyi/sales/service/impl/ShippingInfoServiceImpl.java
@@ -14,6 +14,7 @@
import com.ruoyi.procurementrecord.utils.StockUtils;
import com.ruoyi.sales.dto.SalesLedgerProductDto;
import com.ruoyi.sales.dto.ShippingInfoDto;
import com.ruoyi.sales.dto.ShippingProductDetailDto;
import com.ruoyi.sales.mapper.SalesLedgerProductMapper;
import com.ruoyi.sales.mapper.ShippingInfoMapper;
import com.ruoyi.sales.mapper.ShippingProductDetailMapper;
@@ -135,4 +136,9 @@
        shippingProductDetailMapper.insert(req.getBatchNoDetailList());
        return true;
    }
    @Override
    public List<ShippingProductDetailDto> getDetail(Long id) {
        return shippingProductDetailMapper.getDetail(id);
    }
}
src/main/java/com/ruoyi/staff/controller/PersonalAttendanceLocationConfigController.java
@@ -1,17 +1,12 @@
package com.ruoyi.staff.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.staff.dto.PersonalAttendanceRecordsDto;
import com.ruoyi.staff.pojo.PersonalAttendanceLocationConfig;
import com.ruoyi.staff.pojo.PersonalAttendanceRecords;
import com.ruoyi.staff.service.PersonalAttendanceLocationConfigService;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@@ -30,7 +25,7 @@
@RequiredArgsConstructor
public class PersonalAttendanceLocationConfigController {
    private PersonalAttendanceLocationConfigService personalAttendanceLocationConfigService;
    private final PersonalAttendanceLocationConfigService personalAttendanceLocationConfigService;
    @Operation(summary = "新增/修改人员打卡规则配置")
    @PostMapping("/add")
src/main/resources/application-demo.yml
@@ -13,11 +13,22 @@
  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: 9036
  port: 9003
  servlet:
    # åº”用的访问路径
    context-path: /
@@ -45,7 +56,7 @@
  accessKey: admin
  secretKey: 12345678
  preview-expiry: 24 # é¢„览地址默认24小时
  default-bucket: demo-product
  default-bucket: jxc
# ç”¨æˆ·é…ç½®
user:
  password:
@@ -62,9 +73,9 @@
    druid:
      # ä¸»åº“数据源
      master:
        url: jdbc:mysql://172.17.0.1:3306/product-inventory-management-demo?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
        url: jdbc:mysql://172.17.0.1:9002/product-inventory-management-sdjc?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
        username: root
        password: xd@123456..
        password: sdjc@123456..
      # ä»Žåº“数据源
      slave:
        # ä»Žæ•°æ®æºå¼€å…³/默认关闭
@@ -132,40 +143,69 @@
      # çƒ­éƒ¨ç½²å¼€å…³
      enabled: false
  # redis é…ç½®
  redis:
    # åœ°å€
#    host: 127.0.0.1
    host: 172.17.0.1
    # ç«¯å£ï¼Œé»˜è®¤ä¸º6379
    port: 6379
    # æ•°æ®åº“索引
    database: 0
    # å¯†ç 
#    password: root2022!
    password:
  data:
    mongodb:
      uri: mongodb://114.132.189.42:9028/chat_memory_db
    # 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
      # è¿žæŽ¥è¶…æ—¶æ—¶é—´
      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: abcdefghijklmnopqrstuvwxyz
  secret: xpAVjhCjQDaDB7mjPAzMDSbQWXNu2zYkTdDNUsPMS5Xx8QMmQVYN7n74eZrYJxDJ
  # ä»¤ç‰Œæœ‰æ•ˆæœŸï¼ˆé»˜è®¤30分钟)
  expireTime: 450
# MyBatis Plus配置
mybatis-plus:
  # æœç´¢æŒ‡å®šåŒ…别名   æ ¹æ®è‡ªå·±çš„项目来
@@ -178,7 +218,7 @@
    enable-sql-runner: true
    db-config:
      id-type: auto
# PageHelper分页插件
pagehelper:
  helperDialect: mysql
@@ -200,7 +240,7 @@
  excludes: /system/notice
  # åŒ¹é…é“¾æŽ¥
  urlPatterns: /system/*,/monitor/*,/tool/*
# ä»£ç ç”Ÿæˆ
gen:
  # ä½œè€…
@@ -214,6 +254,15 @@
  # æ˜¯å¦å…è®¸ç”Ÿæˆæ–‡ä»¶è¦†ç›–到本地(自定义路径),默认不允许
  allowOverwrite: false
# æ–‡ä»¶ä¸Šä¼ é…ç½®
file:
  temp-dir: /javaWork/product-inventory-management/file/temp/uploads
  upload-dir: /javaWork/product-inventory-management/file/prod/uploads
  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: /common # é“¾æŽ¥å‰ç¼€
  domain: http://36.138.236.153:9001 # åŸŸåå‰ç¼€
  expired: 120 # è¿‡æœŸæ—¶é—´(单位:分钟)
  useLimit: 10 # ä½¿ç”¨æ¬¡æ•°
  compress: true # æ˜¯å¦åŽ‹ç¼©
  needCompressSize: 10MB # åŽ‹ç¼©é˜ˆå€¼
  compressQuality: 0.5 # åŽ‹ç¼©è´¨é‡(0.0-1.0)
src/main/resources/application-new-pro.yml
@@ -256,11 +256,11 @@
# æ–‡ä»¶ä¸Šä¼ é…ç½®
file:
  temp-dir: D:/ruoyi/temp/uploads   # ä¸´æ—¶ç›®å½•
  upload-dir: D:/ruoyi/prod/uploads # æ­£å¼ç›®å½•
  path: C:/Users/12631/Desktop/download/uploads # ä¸Šä¼ ç›®å½•
  urlPrefix: /common # é“¾æŽ¥å‰ç¼€
  domain: http://127.0.0.1:7003 # åŸŸåå‰ç¼€
  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:9049 # åŸŸåå‰ç¼€
  expired: 120 # è¿‡æœŸæ—¶é—´(单位:分钟)
  useLimit: 10 # ä½¿ç”¨æ¬¡æ•°
  compress: true # æ˜¯å¦åŽ‹ç¼©
src/main/resources/mapper/basic/CustomerMapper.xml
@@ -6,17 +6,23 @@
    <resultMap id="BaseResultMap" type="com.ruoyi.basic.pojo.Customer">
        <id column="id" property="id" />
    </resultMap>
    <select id="listPage" resultType="com.ruoyi.basic.dto.CustomerDto">
    <select id="listPage" resultType="com.ruoyi.basic.vo.CustomerVo">
        select
        c.*,
        u.user_name usage_user_name,
        (
        select group_concat(u2.user_name separator ', ')
        from customer_private_pool cpp2
        left join sys_user u2 on cpp2.bound_id = u2.user_id
        where cpp2.customer_id = c.id and cpp2.delete_flag = 0
        and cpp2.bound_id != c.usage_user
        ) as together_user_names
        from customer_user cu
        left join sys_user u2 on cu.user_id = u2.user_id
        where cu.customer_id = c.id
        and cu.user_id != c.usage_user
        ) as together_user_names,
        (
        select group_concat(cu.user_id separator ',')
        from customer_user cu
        where cu.customer_id = c.id
        and cu.user_id != c.usage_user
        ) as user_ids_str
        from customer c
        left join sys_user u on c.usage_user = u.user_id
        <where>
@@ -26,8 +32,79 @@
            <if test="c.customerType != null and c.customerType != ''">
                and customer_type = #{c.customerType}
            </if>
            <!-- å…¬æµ·æŸ¥è¯¢ï¼štype = 1(公海客户)-->
            <if test="c.type != null and c.type == 1">
                and type = #{c.type}
            </if>
            <!-- ç§æµ·æŸ¥è¯¢ï¼štype = 0(私海客户)或者 type = 1(公海客户)且已被分配,并且是自己领用、自己创建或者共享给自己的客户 -->
            <if test="c.type != null and c.type == 0">
                and (
                    (type = #{c.type} or (type = 1 and is_assigned = 1))
                    and (
                        c.usage_user = #{loginUserId}
                        or c.create_user = #{loginUserId}
                        or exists (
                            select 1 from customer_user cu
                            where cu.customer_id = c.id
                            and cu.user_id = #{loginUserId}
                        )
                    )
                )
            </if>
        </where>
    </select>
</mapper>
    <select id="list" resultType="com.ruoyi.basic.vo.CustomerVo">
        select
        c.*,
        u.user_name usage_user_name,
        (
        select group_concat(u2.user_name separator ', ')
        from customer_user cu
        left join sys_user u2 on cu.user_id = u2.user_id
        where cu.customer_id = c.id
        and cu.user_id != c.usage_user
        ) as together_user_names,
        (
        select group_concat(cu.user_id separator ',')
        from customer_user cu
        where cu.customer_id = c.id
        and cu.user_id != c.usage_user
        ) as user_ids_str
        from customer c
        left join sys_user u on c.usage_user = u.user_id
        <where>
            <if test="c.ids != null and c.ids.length > 0">
                and c.id in
                <foreach collection="c.ids" item="id" open="(" separator="," close=")">
                    #{id}
                </foreach>
            </if>
            <if test="c.customerName != null and c.customerName != ''">
                and customer_name like concat('%', #{c.customerName}, '%')
            </if>
            <if test="c.customerType != null and c.customerType != ''">
                and customer_type = #{c.customerType}
            </if>
            <!-- å…¬æµ·æŸ¥è¯¢ï¼štype = 1(公海客户)-->
            <if test="c.type != null and c.type == 1">
                and type = #{c.type}
            </if>
            <!-- ç§æµ·æŸ¥è¯¢ï¼štype = 0(私海客户)或者 type = 1(公海客户)且已被分配,并且是自己领用、自己创建或者共享给自己的客户 -->
            <if test="c.type != null and c.type == 0">
                and (
                    (type = #{c.type} or (type = 1 and is_assigned = 1))
                    and (
                        c.usage_user = #{loginUserId}
                        or c.create_user = #{loginUserId}
                        or exists (
                            select 1 from customer_user cu
                            where cu.customer_id = c.id
                            and cu.user_id = #{loginUserId}
                        )
                    )
                )
            </if>
        </where>
    </select>
</mapper>
src/main/resources/mapper/basic/CustomerPrivateMapper.xml
ÎļþÒÑɾ³ý
src/main/resources/mapper/basic/CustomerPrivatePoolMapper.xml
ÎļþÒÑɾ³ý
src/main/resources/mapper/production/ProductionOperationTaskMapper.xml
@@ -29,7 +29,8 @@
               pm.model as model,
               pm.unit as unit,
               poro.operation_name as operationName,
        ROUND(pot.complete_quantity / pot.plan_quantity * 100, 2) AS completionStatus,
               IFNULL(scrapStat.scrapQty, 0) AS scrapQty,
        ROUND(IFNULL(pot.complete_quantity, 0) / NULLIF(pot.plan_quantity, 0) * 100, 2) AS completionStatus,
        CASE
            WHEN pot.work_order_no LIKE 'FG%' THEN '返工返修'
            ELSE '正常'
@@ -39,9 +40,19 @@
                 left join production_order_routing_operation poro on pot.production_order_routing_operation_id = poro.id
                 left join product_model pm on pm.id = ifnull(poro.product_model_id, po.product_model_id)
                 left join product p on pm.product_id = p.id
                 left join (
            select ppm.production_operation_task_id as taskId,
                   sum(ifnull(ppo.scrap_qty, 0)) as scrapQty
            from production_product_main ppm
                     left join production_product_output ppo on ppo.production_product_main_id = ppm.id
            group by ppm.production_operation_task_id
        ) scrapStat on scrapStat.taskId = pot.id
        <where>
            <if test="c != null and c.id != null">
                and pot.id = #{c.id}
            </if>
            <if test="c != null and c.npsNo != null">
                and po.nps_no like concat('%', #{c.npsNo}, '%')
            </if>
            <if test="c != null and c.productionOrderId != null">
                and pot.production_order_id = #{c.productionOrderId}
@@ -145,4 +156,58 @@
        WHERE pot.id = #{id}
    </select>
    <select id="getOperation" resultType="com.ruoyi.production.bean.vo.ProductionOperationTaskVo">
        select poro.operation_name as operationName,
               count(pot.id) as productionTaskCount,
               sum(ifnull(pot.plan_quantity, 0)) as planQuantity,
               sum(ifnull(pot.complete_quantity, 0)) as completeQuantity,
               sum(ifnull(pot.complete_quantity, 0)) as goodQuantity,
               sum(ifnull(outputStat.scrapQty, 0)) as scrapQty,
               round(
                   case
                       when sum(ifnull(pot.plan_quantity, 0)) = 0 then 0
                       else (sum(ifnull(pot.complete_quantity, 0)) + sum(ifnull(outputStat.scrapQty, 0)))
                           / sum(ifnull(pot.plan_quantity, 0)) * 100
                       end,
                   2
               ) as completionStatus
        from production_operation_task pot
                 left join production_order_routing_operation poro on pot.production_order_routing_operation_id = poro.id
                 left join (
            select ppm.production_operation_task_id as taskId,
                   sum(ifnull(ppo.scrap_qty, 0)) as scrapQty
            from production_product_main ppm
                     left join production_product_output ppo on ppo.production_product_main_id = ppm.id
            group by ppm.production_operation_task_id
        ) outputStat on outputStat.taskId = pot.id
        <where>
            <if test="c != null and c.startDate != null">
                and date(pot.create_time) &gt;= #{c.startDate}
            </if>
            <if test="c != null and c.endDate != null">
                and date(pot.create_time) &lt;= #{c.endDate}
            </if>
            <if test="c != null and c.planStartTime != null">
                and pot.plan_start_time &gt;= #{c.planStartTime}
            </if>
            <if test="c != null and c.planEndTime != null">
                and pot.plan_end_time &lt;= #{c.planEndTime}
            </if>
            <if test="c != null and c.productionOrderId != null">
                and pot.production_order_id = #{c.productionOrderId}
            </if>
            <if test="c != null and c.productionOrderRoutingOperationId != null">
                and pot.production_order_routing_operation_id = #{c.productionOrderRoutingOperationId}
            </if>
            <if test="c != null and c.status != null">
                and pot.status = #{c.status}
            </if>
            <if test="c != null and c.processName != null and c.processName != ''">
                and poro.operation_name like concat('%', #{c.processName}, '%')
            </if>
        </where>
        group by poro.operation_name
        order by min(poro.drag_sort), poro.operation_name
    </select>
</mapper>
src/main/resources/mapper/production/ProductionOrderMapper.xml
@@ -27,6 +27,7 @@
        <result column="productName" property="productName" />
        <result column="model" property="model" />
        <result column="processRouteCode" property="processRouteCode" />
        <result column="returned" property="returned" />
    </resultMap>
    <sql id="ProductionOrderVoColumns">
@@ -52,7 +53,8 @@
        po.is_end_order as endOrder,
        tr.process_route_code as processRouteCode,
        ROUND(po.complete_quantity / po.quantity * 100, 2) AS completionStatus,
        tb.bom_no as bomNo
        tb.bom_no as bomNo,
        pop_return.returned as returned
    </sql>
    <sql id="ProductionOrderVoFrom">
@@ -73,6 +75,12 @@
                 left join product p on pm.product_id = p.id
                 left join technology_routing tr on po.technology_routing_id = tr.id
                 left join technology_bom tb on tb.id = tr.bom_id
                 left join (
            select production_order_id as productionOrderId,
                   if(max(case when ifnull(is_returned, 0) = 1 then 1 else 0 end) = 1, true, false) as returned
            from production_order_pick
            group by production_order_id
        ) pop_return on pop_return.productionOrderId = po.id
    </sql>
    <sql id="ProductionOrderWhere">
src/main/resources/mapper/sales/ShippingProductDetailMapper.xml
@@ -10,5 +10,13 @@
        <result column="quantity" property="quantity" />
        <result column="shipping_info_id" property="shippingInfoId" />
    </resultMap>
    <select id="getDetail" resultType="com.ruoyi.sales.dto.ShippingProductDetailDto">
        select si.batch_no, pm.model as specification_model, p.product_name, spd.quantity as delivery_quantity
        from shipping_product_detail spd
                 left join stock_inventory si on si.id = spd.stock_inventory_id
                 left join product_model pm on pm.id = si.product_model_id
                 left join product p on p.id = pm.product_id
        where spd.shipping_info_id = #{id}
    </select>
</mapper>