feat(erp/mes): 新增采购订单/销售订单/计量器具 Excel 导入,含导入菜单权限 SQL

- 采购订单导入:模板/导入接口、明细分组、供应商按名称匹配、状态可置已审核
- 销售订单导入:模板/导入接口、客户按名称匹配(扩展 CrmCustomerApi.getCustomerSimpleList)
- 计量器具导入:模板/导入接口、部门/责任人按名称匹配、字典转换、编码已删可恢复
- 菜单权限:新增 erp:purchase-order:import、erp:sale-order:import、mes:dv-metering:import、hrm:employee-contract:import
已添加10个文件
已修改14个文件
1164 ■■■■■ 文件已修改
sql/mysql/erp_purchase_order_import_menu.sql 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/erp_sale_order_import_menu.sql 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/hrm_employee_contract_import_menu.sql 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/import_menu_patch.sql 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/mes_dv_metering_import_menu.sql 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/api/customer/CrmCustomerApi.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/api/customer/CrmCustomerApi.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/api/customer/CrmCustomerApiImpl.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchaseOrderController.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/order/ErpPurchaseOrderImportExcelVO.java 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/order/ErpPurchaseOrderImportRespVO.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/ErpSaleOrderController.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderImportExcelVO.java 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderImportRespVO.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseOrderService.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseOrderServiceImpl.java 179 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOrderService.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOrderServiceImpl.java 182 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/metering/MesDvMeteringController.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/metering/vo/MesDvMeteringImportExcelVO.java 81 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/metering/vo/MesDvMeteringImportRespVO.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/dv/metering/MesDvMeteringMapper.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/metering/MesDvMeteringService.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/metering/MesDvMeteringServiceImpl.java 173 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/erp_purchase_order_import_menu.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,23 @@
-- é‡‡è´­è®¢å• - å¯¼å…¥æŒ‰é’®æƒé™ï¼ˆçˆ¶èœå•:采购订单 id=2666)
-- ä¿®æ­£è¯´æ˜Žï¼šåŽŸè„šæœ¬å†™æ­» id=602511,与“生产计划-新增(mes:pro-mps:create)”撞车导致 INSERT è¢«è·³è¿‡ã€‚
--          çŽ°æ”¹ä¸ºåŠ¨æ€å– system_menu æœ€å¤§ id + 1,并用变量预判,可重复执行(幂等)。
SET @perm_exists := (SELECT COUNT(*) FROM system_menu WHERE permission = 'erp:purchase-order:import');
SET @import_menu_id := IFNULL((SELECT MAX(id) FROM system_menu), 0) + 1;
-- 1) æ–°å¢žâ€œé‡‡è´­è®¢å•导入”按钮权限(已存在则不插)
INSERT INTO system_menu (id, name, permission, type, sort, parent_id, path, icon, component,
                         component_name, status, visible, keep_alive, always_show,
                         creator, create_time, updater, update_time, deleted)
SELECT @import_menu_id, '采购订单导入', 'erp:purchase-order:import', 3, 8, 2666, '', '#', '', NULL,
       0, 1, 0, 1, '1', NOW(), '1', NOW(), 0
FROM DUAL
WHERE @perm_exists = 0;
-- 2) æŽˆäºˆè¶…级管理员角色(role_id=1)该按钮权限(已授权则不插)
SET @menu_id := (SELECT id FROM system_menu WHERE permission = 'erp:purchase-order:import');
SET @grant_exists := (SELECT COUNT(*) FROM system_role_menu WHERE role_id = 1 AND menu_id = @menu_id);
INSERT INTO system_role_menu (role_id, menu_id, creator, create_time, updater, update_time, deleted)
SELECT 1, @menu_id, '1', NOW(), '1', NOW(), 0
FROM DUAL
WHERE @menu_id IS NOT NULL AND @grant_exists = 0;
sql/mysql/erp_sale_order_import_menu.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
-- é”€å”®è®¢å• - å¯¼å…¥æŒ‰é’®æƒé™ï¼ˆçˆ¶èœå•:销售订单 id=2638)
-- ç”¨åŠ¨æ€ id(MAX(id)+1),避免与既有菜单撞车;可重复执行(幂等)。
SET @perm_exists := (SELECT COUNT(*) FROM system_menu WHERE permission = 'erp:sale-order:import');
SET @import_menu_id := IFNULL((SELECT MAX(id) FROM system_menu), 0) + 1;
INSERT INTO system_menu (id, name, permission, type, sort, parent_id, path, icon, component,
                         component_name, status, visible, keep_alive, always_show,
                         creator, create_time, updater, update_time, deleted)
SELECT @import_menu_id, '销售订单导入', 'erp:sale-order:import', 3, 8, 2638, '', '#', '', NULL,
       0, 1, 0, 1, '1', NOW(), '1', NOW(), 0
FROM DUAL
WHERE @perm_exists = 0;
SET @menu_id := (SELECT id FROM system_menu WHERE permission = 'erp:sale-order:import');
SET @grant_exists := (SELECT COUNT(*) FROM system_role_menu WHERE role_id = 1 AND menu_id = @menu_id);
INSERT INTO system_role_menu (role_id, menu_id, creator, create_time, updater, update_time, deleted)
SELECT 1, @menu_id, '1', NOW(), '1', NOW(), 0
FROM DUAL
WHERE @menu_id IS NOT NULL AND @grant_exists = 0;
sql/mysql/hrm_employee_contract_import_menu.sql
@@ -1,13 +1,20 @@
-- å‘˜å·¥åˆåŒ - å¯¼å…¥/导入模板 æŒ‰é’®æƒé™
-- çˆ¶èœå•:员工合同(parent_id = 1063105)
-- å‚照:合同新增按钮 id=1063107(type=3, permission=hrm:employee-contract:create)
-- å‘˜å·¥åˆåŒ - å¯¼å…¥æŒ‰é’®æƒé™ï¼ˆçˆ¶èœå•:员工合同 id=1063105)
-- åŠ¨æ€ id(MAX(id)+1),避免与既有菜单撞车;可重复执行(幂等)。
SET @perm_exists := (SELECT COUNT(*) FROM system_menu WHERE permission = 'hrm:employee-contract:import');
SET @import_menu_id := IFNULL((SELECT MAX(id) FROM system_menu), 0) + 1;
INSERT INTO system_menu (id, name, permission, type, sort, parent_id, path, icon, component,
                         component_name, status, visible, keep_alive, always_show,
                         creator, create_time, updater, update_time, deleted)
VALUES (1063300, '合同导入', 'hrm:employee-contract:import', 3, 5, 1063105, '', '#', '', NULL,
        0, 1, 0, 1, '1', NOW(), '1', NOW(), 0);
SELECT @import_menu_id, '合同导入', 'hrm:employee-contract:import', 3, 5, 1063105, '', '#', '', NULL,
       0, 1, 0, 1, '1', NOW(), '1', NOW(), 0
FROM DUAL
WHERE @perm_exists = 0;
-- æŽˆäºˆè¶…级管理员角色(role_id=1)该按钮权限(幂等)
SET @menu_id := (SELECT id FROM system_menu WHERE permission = 'hrm:employee-contract:import');
SET @grant_exists := (SELECT COUNT(*) FROM system_role_menu WHERE role_id = 1 AND menu_id = @menu_id);
INSERT INTO system_role_menu (role_id, menu_id, creator, create_time, updater, update_time, deleted)
SELECT 1, 1063300, '1', NOW(), '1', NOW(), 0 FROM DUAL
WHERE NOT EXISTS (SELECT 1 FROM system_role_menu WHERE role_id = 1 AND menu_id = 1063300);
SELECT 1, @menu_id, '1', NOW(), '1', NOW(), 0
FROM DUAL
WHERE @menu_id IS NOT NULL AND @grant_exists = 0;
sql/mysql/import_menu_patch.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,77 @@
-- =====================================================================
-- å››æ¨¡å—“导入”按钮权限 åˆå¹¶è¡¥ä¸
--   1) é‡‡è´­è®¢å•导入  erp:purchase-order:import
--   2) é”€å”®è®¢å•导入  erp:sale-order:import
--   3) è®¡é‡å™¨å…·å¯¼å…¥  mes:dv-metering:import
--   4) å‘˜å·¥åˆåŒå¯¼å…¥  hrm:employee-contract:import
--
-- ç”¨æ³•:在【目标数据库】上整体执行一次即可(本地开发库 / è¿œç¨‹æµ‹è¯•库 / ç”Ÿäº§åº“)。
-- ç‰¹ç‚¹ï¼š
--   * åŠ¨æ€å– system_menu æœ€å¤§ id + 1,不会与既有菜单 id æ’žè½¦ï¼›
--   * æŒ‰ permission åˆ¤æ–­ï¼Œå¹‚等,可重复执行;
--   * ä»…授权“超级管理员”角色 role_id=1;如要让其它角色也能导入,
--     è¯·åœ¨ã€ç³»ç»Ÿç®¡ç† â†’ è§’色管理】里勾选对应“XX导入”按钮。
-- æ³¨æ„ï¼š
--   * parent_id ä¸ºå„模块菜单 id(采购订单=2666、销售订单=2638、计量器具=5380、员工合同=1063105)。
--     è‹¥ç›®æ ‡åº“菜单结构被改过,请先核对这几个父菜单 id å†æ‰§è¡Œã€‚
-- =====================================================================
-- ------------- 1) é‡‡è´­è®¢å•导入 -------------
SET @perm_exists := (SELECT COUNT(*) FROM system_menu WHERE permission = 'erp:purchase-order:import');
SET @new_id := IFNULL((SELECT MAX(id) FROM system_menu), 0) + 1;
INSERT INTO system_menu (id, name, permission, type, sort, parent_id, path, icon, component,
                         component_name, status, visible, keep_alive, always_show,
                         creator, create_time, updater, update_time, deleted)
SELECT @new_id, '采购订单导入', 'erp:purchase-order:import', 3, 8, 2666, '', '#', '', NULL,
       0, 1, 0, 1, '1', NOW(), '1', NOW(), 0
FROM DUAL WHERE @perm_exists = 0;
SET @menu_id := (SELECT id FROM system_menu WHERE permission = 'erp:purchase-order:import');
SET @grant_exists := (SELECT COUNT(*) FROM system_role_menu WHERE role_id = 1 AND menu_id = @menu_id);
INSERT INTO system_role_menu (role_id, menu_id, creator, create_time, updater, update_time, deleted)
SELECT 1, @menu_id, '1', NOW(), '1', NOW(), 0 FROM DUAL
WHERE @menu_id IS NOT NULL AND @grant_exists = 0;
-- ------------- 2) é”€å”®è®¢å•导入 -------------
SET @perm_exists := (SELECT COUNT(*) FROM system_menu WHERE permission = 'erp:sale-order:import');
SET @new_id := IFNULL((SELECT MAX(id) FROM system_menu), 0) + 1;
INSERT INTO system_menu (id, name, permission, type, sort, parent_id, path, icon, component,
                         component_name, status, visible, keep_alive, always_show,
                         creator, create_time, updater, update_time, deleted)
SELECT @new_id, '销售订单导入', 'erp:sale-order:import', 3, 8, 2638, '', '#', '', NULL,
       0, 1, 0, 1, '1', NOW(), '1', NOW(), 0
FROM DUAL WHERE @perm_exists = 0;
SET @menu_id := (SELECT id FROM system_menu WHERE permission = 'erp:sale-order:import');
SET @grant_exists := (SELECT COUNT(*) FROM system_role_menu WHERE role_id = 1 AND menu_id = @menu_id);
INSERT INTO system_role_menu (role_id, menu_id, creator, create_time, updater, update_time, deleted)
SELECT 1, @menu_id, '1', NOW(), '1', NOW(), 0 FROM DUAL
WHERE @menu_id IS NOT NULL AND @grant_exists = 0;
-- ------------- 3) è®¡é‡å™¨å…·å¯¼å…¥ -------------
SET @perm_exists := (SELECT COUNT(*) FROM system_menu WHERE permission = 'mes:dv-metering:import');
SET @new_id := IFNULL((SELECT MAX(id) FROM system_menu), 0) + 1;
INSERT INTO system_menu (id, name, permission, type, sort, parent_id, path, icon, component,
                         component_name, status, visible, keep_alive, always_show,
                         creator, create_time, updater, update_time, deleted)
SELECT @new_id, '计量器具导入', 'mes:dv-metering:import', 3, 8, 5380, '', '#', '', NULL,
       0, 1, 0, 1, '1', NOW(), '1', NOW(), 0
FROM DUAL WHERE @perm_exists = 0;
SET @menu_id := (SELECT id FROM system_menu WHERE permission = 'mes:dv-metering:import');
SET @grant_exists := (SELECT COUNT(*) FROM system_role_menu WHERE role_id = 1 AND menu_id = @menu_id);
INSERT INTO system_role_menu (role_id, menu_id, creator, create_time, updater, update_time, deleted)
SELECT 1, @menu_id, '1', NOW(), '1', NOW(), 0 FROM DUAL
WHERE @menu_id IS NOT NULL AND @grant_exists = 0;
-- ------------- 4) å‘˜å·¥åˆåŒå¯¼å…¥ -------------
SET @perm_exists := (SELECT COUNT(*) FROM system_menu WHERE permission = 'hrm:employee-contract:import');
SET @new_id := IFNULL((SELECT MAX(id) FROM system_menu), 0) + 1;
INSERT INTO system_menu (id, name, permission, type, sort, parent_id, path, icon, component,
                         component_name, status, visible, keep_alive, always_show,
                         creator, create_time, updater, update_time, deleted)
SELECT @new_id, '合同导入', 'hrm:employee-contract:import', 3, 5, 1063105, '', '#', '', NULL,
       0, 1, 0, 1, '1', NOW(), '1', NOW(), 0
FROM DUAL WHERE @perm_exists = 0;
SET @menu_id := (SELECT id FROM system_menu WHERE permission = 'hrm:employee-contract:import');
SET @grant_exists := (SELECT COUNT(*) FROM system_role_menu WHERE role_id = 1 AND menu_id = @menu_id);
INSERT INTO system_role_menu (role_id, menu_id, creator, create_time, updater, update_time, deleted)
SELECT 1, @menu_id, '1', NOW(), '1', NOW(), 0 FROM DUAL
WHERE @menu_id IS NOT NULL AND @grant_exists = 0;
sql/mysql/mes_dv_metering_import_menu.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
-- è®¡é‡å™¨å…· - å¯¼å…¥æŒ‰é’®æƒé™ï¼ˆçˆ¶èœå•:计量器具 id=5380)
-- åŠ¨æ€ id(MAX(id)+1),避免与既有菜单撞车;可重复执行(幂等)。
SET @perm_exists := (SELECT COUNT(*) FROM system_menu WHERE permission = 'mes:dv-metering:import');
SET @import_menu_id := IFNULL((SELECT MAX(id) FROM system_menu), 0) + 1;
INSERT INTO system_menu (id, name, permission, type, sort, parent_id, path, icon, component,
                         component_name, status, visible, keep_alive, always_show,
                         creator, create_time, updater, update_time, deleted)
SELECT @import_menu_id, '计量器具导入', 'mes:dv-metering:import', 3, 8, 5380, '', '#', '', NULL,
       0, 1, 0, 1, '1', NOW(), '1', NOW(), 0
FROM DUAL
WHERE @perm_exists = 0;
SET @menu_id := (SELECT id FROM system_menu WHERE permission = 'mes:dv-metering:import');
SET @grant_exists := (SELECT COUNT(*) FROM system_role_menu WHERE role_id = 1 AND menu_id = @menu_id);
INSERT INTO system_role_menu (role_id, menu_id, creator, create_time, updater, update_time, deleted)
SELECT 1, @menu_id, '1', NOW(), '1', NOW(), 0
FROM DUAL
WHERE @menu_id IS NOT NULL AND @grant_exists = 0;
yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/api/customer/CrmCustomerApi.java
@@ -56,4 +56,13 @@
     */
    List<Long> getPermittedCustomerIds();
    /**
     * èŽ·å¾—å®¢æˆ·ç®€å•åˆ—è¡¨ï¼ˆç¼–å· + åç§°ï¼‰
     *
     * ç”¨äºŽå…¶å®ƒæ¨¡å—(如 ERP é”€å”®è®¢å•导入)按客户名称匹配客户编号
     *
     * @return å®¢æˆ·ç®€å•列表
     */
    List<CrmCustomerRespDTO> getCustomerSimpleList();
}
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/api/customer/CrmCustomerApi.java
@@ -56,4 +56,13 @@
     */
    List<Long> getPermittedCustomerIds();
    /**
     * èŽ·å¾—å®¢æˆ·ç®€å•åˆ—è¡¨ï¼ˆç¼–å· + åç§°ï¼‰
     *
     * ç”¨äºŽå…¶å®ƒæ¨¡å—(如 ERP é”€å”®è®¢å•导入)按客户名称匹配客户编号
     *
     * @return å®¢æˆ·ç®€å•列表
     */
    List<CrmCustomerRespDTO> getCustomerSimpleList();
}
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/api/customer/CrmCustomerApiImpl.java
@@ -121,6 +121,12 @@
        return new ArrayList<>(permittedIds);
    }
    @Override
    public List<CrmCustomerRespDTO> getCustomerSimpleList() {
        List<CrmCustomerDO> customers = customerService.getCustomerList();
        return BeanUtils.toBean(customers, CrmCustomerRespDTO.class);
    }
    /**
     * è¿‡æ»¤æŽ‰ç”¨æˆ·æ²¡æœ‰æƒé™çš„客户
     * <p>
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchaseOrderController.java
@@ -11,6 +11,8 @@
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderPageReqVO;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderRespVO;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderSaveReqVO;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderImportExcelVO;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderImportRespVO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseOrderDO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseOrderItemDO;
import cn.iocoder.yudao.module.erp.enums.ErpAuditStatus;
@@ -35,10 +37,12 @@
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.List;
import java.util.Arrays;
import java.util.Map;
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
@@ -168,6 +172,29 @@
        ExcelUtils.write(response, "采购订单.xls", "数据", ErpPurchaseOrderRespVO.class, list);
    }
    @GetMapping("/get-import-template")
    @Operation(summary = "获得导入采购订单模板")
    @PreAuthorize("@ss.hasPermission('erp:purchase-order:import')")
    public void importTemplate(HttpServletResponse response) throws IOException {
        List<ErpPurchaseOrderImportExcelVO> list = Arrays.asList(
                ErpPurchaseOrderImportExcelVO.builder()
                        .no("CGDD20260101000001").supplierName("示例供应商")
                        .orderTime("2026-01-01 10:00:00").productCode("ITEM001").productName("示例产品A")
                        .count(new BigDecimal("100")).productPrice(new BigDecimal("10.00"))
                        .taxPercent(new BigDecimal("13")).qcCheckFlag("否").remark("示例").build());
        ExcelUtils.write(response, "采购订单导入模板.xls", "采购订单", ErpPurchaseOrderImportExcelVO.class, list);
    }
    @PostMapping("/import")
    @Operation(summary = "导入采购订单")
    @PreAuthorize("@ss.hasPermission('erp:purchase-order:import')")
    public CommonResult<ErpPurchaseOrderImportRespVO> importPurchaseOrderExcel(
            @RequestParam("file") MultipartFile file,
            @RequestParam(value = "directApprove", required = false, defaultValue = "false") Boolean directApprove) throws Exception {
        List<ErpPurchaseOrderImportExcelVO> list = ExcelUtils.read(file, ErpPurchaseOrderImportExcelVO.class);
        return success(purchaseOrderService.importPurchaseOrderList(list, directApprove));
    }
    @PostMapping("/confirm-receipt")
    @Operation(summary = "确认收货")
    @Parameter(name = "id", description = "采购订单编号", required = true)
@@ -219,7 +246,9 @@
                .getPurchaseInvoicePriceSumMapByOrderIds(convertSet(pageResult.getList(), ErpPurchaseOrderDO::getId));
        // 2. å¼€å§‹æ‹¼æŽ¥
        return BeanUtils.toBean(pageResult, ErpPurchaseOrderRespVO.class, purchaseOrder -> {
            purchaseOrder.setItems(BeanUtils.toBean(purchaseOrderItemMap.get(purchaseOrder.getId()), ErpPurchaseOrderRespVO.Item.class,
            purchaseOrder.setItems(BeanUtils.toBean(
                    purchaseOrderItemMap.getOrDefault(purchaseOrder.getId(), java.util.Collections.<ErpPurchaseOrderItemDO>emptyList()),
                    ErpPurchaseOrderRespVO.Item.class,
                    item -> {
                        MapUtils.findAndThen(itemMap, item.getProductId(), mdmItem -> item.setProductName(mdmItem.getName())
                                .setProductBarCode(mdmItem.getBarCode()));
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/order/ErpPurchaseOrderImportExcelVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,67 @@
package cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order;
import cn.idev.excel.annotation.ExcelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
/**
 * ç®¡ç†åŽå° - é‡‡è´­è®¢å•导入 Excel VO
 *
 * è¯´æ˜Žï¼šä¸€è¡Œ = ä¸€æ¡äº§å“æ˜Žç»†ï¼›ç›¸åŒâ€œé‡‡è´­å•编号”的多行会合并为一张订单(主表字段取该组首行)。
 * â€œé‡‡è´­å•编号”留空时,每一行会各自生成一张新订单(单条明细)。
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - é‡‡è´­è®¢å•导入 Excel VO")
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ErpPurchaseOrderImportExcelVO {
    @ExcelProperty("采购单编号")
    @Schema(description = "采购单编号,留空则自动生成;相同编号的多行合并为同一张订单")
    private String no;
    @ExcelProperty("供应商名称")
    @Schema(description = "供应商名称,需与系统内供应商名称(或编码)一致", requiredMode = Schema.RequiredMode.REQUIRED)
    private String supplierName;
    @ExcelProperty("采购时间")
    @Schema(description = "采购时间,格式如 2026-01-01 10:00:00;留空取当前时间")
    private String orderTime;
    @ExcelProperty("产品编码")
    @Schema(description = "产品(物料)编码", requiredMode = Schema.RequiredMode.REQUIRED)
    private String productCode;
    @ExcelProperty("产品名称")
    @Schema(description = "产品名称(仅供对照,匹配仍以产品编码为准)")
    private String productName;
    @ExcelProperty("数量")
    @Schema(description = "产品数量", requiredMode = Schema.RequiredMode.REQUIRED)
    private BigDecimal count;
    @ExcelProperty("单价")
    @Schema(description = "产品单价")
    private BigDecimal productPrice;
    @ExcelProperty("税率(%)")
    @Schema(description = "税率,百分比,例如 13 è¡¨ç¤º 13%")
    private BigDecimal taxPercent;
    @ExcelProperty("是否来料检验")
    @Schema(description = "是否需要来料检验:填“是”为需要,其它为不需要")
    private String qcCheckFlag;
    @ExcelProperty("备注")
    @Schema(description = "备注")
    private String remark;
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/order/ErpPurchaseOrderImportRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
package cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
import java.util.List;
import java.util.Map;
/**
 * ç®¡ç†åŽå° - é‡‡è´­è®¢å•导入 Response VO
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - é‡‡è´­è®¢å•导入 Response VO")
@Data
@Builder
public class ErpPurchaseOrderImportRespVO {
    @Schema(description = "创建成功的采购单编号数组", example = "CGDD20260101000001")
    private List<String> createList;
    @Schema(description = "更新成功的采购单编号数组", example = "CGDD20260101000002")
    private List<String> updateList;
    @Schema(description = "导入失败集合,key ä¸ºé‡‡è´­å•编号或行号,value ä¸ºå¤±è´¥åŽŸå› ")
    private Map<String, String> failureList;
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/ErpSaleOrderController.java
@@ -14,6 +14,8 @@
import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.order.ErpSaleOrderPageReqVO;
import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.order.ErpSaleOrderRespVO;
import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.order.ErpSaleOrderSaveReqVO;
import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.order.ErpSaleOrderImportExcelVO;
import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.order.ErpSaleOrderImportRespVO;
import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpSaleOrderDO;
import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpSaleOrderItemDO;
import cn.iocoder.yudao.module.erp.service.sale.ErpSaleOrderService;
@@ -37,10 +39,12 @@
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.List;
import java.util.Arrays;
import java.util.Map;
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
@@ -206,6 +210,28 @@
        ExcelUtils.write(response, "销售订单.xls", "数据", ErpSaleOrderRespVO.class, list);
    }
    @GetMapping("/get-import-template")
    @Operation(summary = "获得导入销售订单模板")
    @PreAuthorize("@ss.hasPermission('erp:sale-order:import')")
    public void importTemplate(HttpServletResponse response) throws IOException {
        List<ErpSaleOrderImportExcelVO> list = Arrays.asList(
                ErpSaleOrderImportExcelVO.builder()
                        .no("XSDD20260101000001").customerName("示例客户").contractNo("HT20260101")
                        .orderTime("2026-01-01 10:00:00").productCode("ITEM001").productName("示例产品A")
                        .count(new BigDecimal("100")).productPrice(new BigDecimal("20.00"))
                        .taxPercent(new BigDecimal("13")).needProduction("是").remark("示例").build());
        ExcelUtils.write(response, "销售订单导入模板.xls", "销售订单", ErpSaleOrderImportExcelVO.class, list);
    }
    @PostMapping("/import")
    @Operation(summary = "导入销售订单")
    @PreAuthorize("@ss.hasPermission('erp:sale-order:import')")
    public CommonResult<ErpSaleOrderImportRespVO> importSaleOrderExcel(
            @RequestParam("file") MultipartFile file) throws Exception {
        List<ErpSaleOrderImportExcelVO> list = ExcelUtils.read(file, ErpSaleOrderImportExcelVO.class);
        return success(saleOrderService.importSaleOrderList(list));
    }
    private PageResult<ErpSaleOrderRespVO> buildSaleOrderVOPageResult(PageResult<ErpSaleOrderDO> pageResult) {
        if (CollUtil.isEmpty(pageResult.getList())) {
            return PageResult.empty(pageResult.getTotal());
@@ -268,7 +294,9 @@
        }
        // 2. å¼€å§‹æ‹¼æŽ¥
        return BeanUtils.toBean(pageResult, ErpSaleOrderRespVO.class, saleOrder -> {
            saleOrder.setItems(BeanUtils.toBean(saleOrderItemMap.get(saleOrder.getId()), ErpSaleOrderRespVO.Item.class,
            saleOrder.setItems(BeanUtils.toBean(
                    saleOrderItemMap.getOrDefault(saleOrder.getId(), java.util.Collections.<ErpSaleOrderItemDO>emptyList()),
                    ErpSaleOrderRespVO.Item.class,
                    item -> {
                        fillItemPrice(item);
                        item.setBatchCode(batchCodeMap.get(item.getId()));
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderImportExcelVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,71 @@
package cn.iocoder.yudao.module.erp.controller.admin.sale.vo.order;
import cn.idev.excel.annotation.ExcelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
/**
 * ç®¡ç†åŽå° - é”€å”®è®¢å•导入 Excel VO
 *
 * è¯´æ˜Žï¼šä¸€è¡Œ = ä¸€æ¡äº§å“æ˜Žç»†ï¼›ç›¸åŒâ€œé”€å”®å•编号”的多行会合并为一张订单(主表字段取该组首行)。
 * â€œé”€å”®å•编号”留空时,每一行会各自生成一张新订单(单条明细)。
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - é”€å”®è®¢å•导入 Excel VO")
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ErpSaleOrderImportExcelVO {
    @ExcelProperty("销售单编号")
    @Schema(description = "销售单编号,留空则自动生成;相同编号的多行合并为同一张订单")
    private String no;
    @ExcelProperty("客户名称")
    @Schema(description = "客户名称,需与 CRM ä¸­çš„客户名称一致", requiredMode = Schema.RequiredMode.REQUIRED)
    private String customerName;
    @ExcelProperty("合同单号")
    @Schema(description = "关联合同单号,可空")
    private String contractNo;
    @ExcelProperty("下单时间")
    @Schema(description = "下单时间,格式如 2026-01-01 10:00:00;留空取当前时间")
    private String orderTime;
    @ExcelProperty("产品编码")
    @Schema(description = "产品(物料)编码", requiredMode = Schema.RequiredMode.REQUIRED)
    private String productCode;
    @ExcelProperty("产品名称")
    @Schema(description = "产品名称(仅供对照,匹配仍以产品编码为准)")
    private String productName;
    @ExcelProperty("数量")
    @Schema(description = "产品数量", requiredMode = Schema.RequiredMode.REQUIRED)
    private BigDecimal count;
    @ExcelProperty("单价")
    @Schema(description = "产品单价;留空则取该物料的销售价")
    private BigDecimal productPrice;
    @ExcelProperty("税率(%)")
    @Schema(description = "税率,百分比,例如 13 è¡¨ç¤º 13%")
    private BigDecimal taxPercent;
    @ExcelProperty("是否需要生产")
    @Schema(description = "是否需要生产:填“是/否”,留空默认“是”")
    private String needProduction;
    @ExcelProperty("备注")
    @Schema(description = "备注;同一张订单取首行备注作为整单备注")
    private String remark;
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderImportRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
package cn.iocoder.yudao.module.erp.controller.admin.sale.vo.order;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
import java.util.List;
import java.util.Map;
/**
 * ç®¡ç†åŽå° - é”€å”®è®¢å•导入 Response VO
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - é”€å”®è®¢å•导入 Response VO")
@Data
@Builder
public class ErpSaleOrderImportRespVO {
    @Schema(description = "创建成功的销售单编号数组", example = "XSDD20260101000001")
    private List<String> createList;
    @Schema(description = "更新成功的销售单编号数组", example = "XSDD20260101000002")
    private List<String> updateList;
    @Schema(description = "导入失败集合,key ä¸ºé”€å”®å•编号或行号,value ä¸ºå¤±è´¥åŽŸå› ")
    private Map<String, String> failureList;
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseOrderService.java
@@ -3,6 +3,8 @@
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderPageReqVO;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderSaveReqVO;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderImportExcelVO;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderImportRespVO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseOrderDO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseOrderItemDO;
import jakarta.validation.Valid;
@@ -28,6 +30,15 @@
    Long createPurchaseOrder(@Valid ErpPurchaseOrderSaveReqVO createReqVO);
    /**
     * å¯¼å…¥é‡‡è´­è®¢å•
     *
     * @param importList    å¯¼å…¥æ•°æ®ï¼ˆæ¯ä¸€è¡Œ = ä¸€æ¡æ˜Žç»†ï¼Œç›¸åŒé‡‡è´­å•编号合并为一张订单)
     * @param directApprove æ˜¯å¦å°†å¯¼å…¥çš„订单直接置为“已审核”:true=已审核(20),false=未审核(10)
     * @return å¯¼å…¥ç»“æžœ
     */
    ErpPurchaseOrderImportRespVO importPurchaseOrderList(List<ErpPurchaseOrderImportExcelVO> importList, Boolean directApprove);
    /**
     * æ›´æ–°é‡‡è´­è®¢å•
     *
     * @param updateReqVO æ›´æ–°ä¿¡æ¯
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseOrderServiceImpl.java
@@ -2,6 +2,8 @@
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.number.MoneyUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
@@ -14,6 +16,8 @@
import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderPageReqVO;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderSaveReqVO;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderImportExcelVO;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderImportRespVO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseOrderDO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseOrderItemDO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseRequestDO;
@@ -26,6 +30,10 @@
import cn.iocoder.yudao.module.erp.enums.ErpAuditStatus;
import cn.iocoder.yudao.module.erp.enums.ErpPurchaseOrderInStatusEnum;
import cn.iocoder.yudao.module.erp.service.finance.ErpAccountService;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
import cn.iocoder.yudao.module.srm.api.supplier.SrmSupplierApi;
import cn.iocoder.yudao.module.srm.api.supplier.dto.SrmSupplierRespDTO;
import cn.iocoder.yudao.module.mes.api.arrivalnotice.MesArrivalNoticeApi;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
import jakarta.annotation.Resource;
@@ -37,6 +45,9 @@
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -94,6 +105,11 @@
    @Resource
    private StorageAttachmentApi storageAttachmentApi;
    @Resource
    private SrmSupplierApi srmSupplierApi;
    @Resource
    private MdmItemApi mdmItemApi;
    @Override
    @Transactional(rollbackFor = Exception.class)
@@ -159,6 +175,169 @@
        storageAttachmentApi.updateAttachments("file", "erp_purchase_order", updateReqVO.getId(), updateReqVO.getBlobIds());
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public ErpPurchaseOrderImportRespVO importPurchaseOrderList(List<ErpPurchaseOrderImportExcelVO> importList,
                                                               Boolean directApprove) {
        ErpPurchaseOrderImportRespVO respVO = ErpPurchaseOrderImportRespVO.builder()
                .createList(new ArrayList<>())
                .updateList(new ArrayList<>())
                .failureList(new LinkedHashMap<>())
                .build();
        if (CollUtil.isEmpty(importList)) {
            return respVO;
        }
        // 1. é¢„热“供应商名称/编码 -> ä¾›åº”商编号”映射(ERP ä¾›åº”商主数据在 SRM æ¨¡å—)
        Map<String, Long> supplierIdMap = new HashMap<>();
        CommonResult<List<SrmSupplierRespDTO>> supplierResult = srmSupplierApi.getSupplierSimpleList();
        if (supplierResult != null && CollUtil.isNotEmpty(supplierResult.getData())) {
            supplierResult.getData().forEach(supplier -> {
                if (StrUtil.isNotBlank(supplier.getName())) {
                    supplierIdMap.put(supplier.getName().trim(), supplier.getId());
                }
                if (StrUtil.isNotBlank(supplier.getCode())) {
                    supplierIdMap.putIfAbsent(supplier.getCode().trim(), supplier.getId());
                }
            });
        }
        // 2. æŒ‰â€œé‡‡è´­å•编号”分组:同编号多行 = ä¸€å¼ è®¢å•的多条明细;编号为空则每行独立成单
        Map<String, List<ErpPurchaseOrderImportExcelVO>> orderGroups = new LinkedHashMap<>();
        for (int i = 0; i < importList.size(); i++) {
            ErpPurchaseOrderImportExcelVO row = importList.get(i);
            if (row == null || (StrUtil.isBlank(row.getSupplierName()) && StrUtil.isBlank(row.getProductCode())
                    && StrUtil.isBlank(row.getNo()))) {
                continue; // è·³è¿‡ç©ºè¡Œ
            }
            String key = StrUtil.isNotBlank(row.getNo()) ? row.getNo().trim() : "__row_" + (i + 2);
            orderGroups.computeIfAbsent(key, k -> new ArrayList<>()).add(row);
        }
        // 3. é€ä¸ªè®¢å•导入(单张订单失败不影响其它)
        for (Map.Entry<String, List<ErpPurchaseOrderImportExcelVO>> entry : orderGroups.entrySet()) {
            List<ErpPurchaseOrderImportExcelVO> rows = entry.getValue();
            String label = StrUtil.isNotBlank(rows.get(0).getNo()) ? rows.get(0).getNo().trim()
                    : "第 " + entry.getKey().substring("__row_".length()) + " è¡Œ";
            try {
                ErpPurchaseOrderDO purchaseOrder = createPurchaseOrderByImport(rows, supplierIdMap, directApprove);
                respVO.getCreateList().add(purchaseOrder.getNo());
            } catch (Exception ex) {
                respVO.getFailureList().put(label, resolveImportErrorMessage(ex));
            }
        }
        return respVO;
    }
    /**
     * ä¾æ®å¯¼å…¥æ•°æ®åˆ›å»ºä¸€å¼ é‡‡è´­è®¢å•(含明细)。
     *
     * æ³¨æ„ï¼šæ­¤å¤„不调用 submitPurchaseOrder,因此不会创建 BPM å·¥ä½œæµï¼›è®¢å•状态由 directApprove ç›´æŽ¥å†³å®šï¼š
     * å‹¾é€‰â€œæ˜¯å¦å®¡æ ¸â€ => å·²å®¡æ ¸(20),未勾选 => æœªå®¡æ ¸(10)。
     */
    private ErpPurchaseOrderDO createPurchaseOrderByImport(List<ErpPurchaseOrderImportExcelVO> rows,
                                                           Map<String, Long> supplierIdMap, Boolean directApprove) {
        // 1. ä¾›åº”商(取该组首行)
        ErpPurchaseOrderImportExcelVO head = rows.get(0);
        if (StrUtil.isBlank(head.getSupplierName())) {
            throw new IllegalArgumentException("供应商名称不能为空");
        }
        Long supplierId = supplierIdMap.get(head.getSupplierName().trim());
        if (supplierId == null) {
            throw new IllegalArgumentException("供应商不存在:" + head.getSupplierName());
        }
        supplierService.validateSupplier(supplierId);
        // 2. æ˜Žç»†
        List<ErpPurchaseOrderSaveReqVO.Item> items = new ArrayList<>();
        for (ErpPurchaseOrderImportExcelVO row : rows) {
            if (StrUtil.isBlank(row.getProductCode())) {
                throw new IllegalArgumentException("产品编码不能为空");
            }
            CommonResult<MdmItemRespDTO> itemResult = mdmItemApi.getItemByCode(row.getProductCode().trim());
            MdmItemRespDTO item = itemResult == null ? null : itemResult.getData();
            if (item == null) {
                throw new IllegalArgumentException("产品不存在:" + row.getProductCode());
            }
            if (row.getCount() == null) {
                throw new IllegalArgumentException("产品[" + row.getProductCode() + "]的数量不能为空");
            }
            if (item.getUnitMeasureId() == null) {
                throw new IllegalArgumentException("产品[" + row.getProductCode() + "]未配置计量单位,请先在物料中维护");
            }
            ErpPurchaseOrderSaveReqVO.Item itemVO = new ErpPurchaseOrderSaveReqVO.Item();
            itemVO.setProductId(item.getId());
            itemVO.setProductUnitId(item.getUnitMeasureId());
            itemVO.setProductPrice(row.getProductPrice() == null ? BigDecimal.ZERO : row.getProductPrice());
            itemVO.setCount(row.getCount());
            itemVO.setTaxPercent(row.getTaxPercent());
            itemVO.setRemark(row.getRemark());
            itemVO.setQcCheckFlag("是".equals(StrUtil.trim(row.getQcCheckFlag())));
            items.add(itemVO);
        }
        // 3. å•号:填了就用(校验唯一),没填自动生成
        String no = StrUtil.isNotBlank(head.getNo()) ? head.getNo().trim()
                : noRedisDAO.generate(ErpNoRedisDAO.PURCHASE_ORDER_NO_PREFIX);
        if (purchaseOrderMapper.selectByNo(no) != null) {
            throw new IllegalArgumentException("采购单编号已存在:" + no);
        }
        // 4. ç»„装主表:勾选“是否审核” => å·²å®¡æ ¸(20),否则未审核(10)
        ErpPurchaseOrderDO purchaseOrder = new ErpPurchaseOrderDO();
        purchaseOrder.setNo(no);
        purchaseOrder.setSupplierId(supplierId);
        purchaseOrder.setOrderTime(parseImportOrderTime(head.getOrderTime()));
        purchaseOrder.setRemark(head.getRemark());
        purchaseOrder.setStatus(Boolean.TRUE.equals(directApprove)
                ? ErpAuditStatus.APPROVE.getStatus() : ErpAuditStatus.PROCESS.getStatus());
        purchaseOrder.setDepositPrice(BigDecimal.ZERO); // deposit_price ä¸º NOT NULL ä¸”无默认值,必须显式赋值
        purchaseOrder.setInCount(BigDecimal.ZERO);
        purchaseOrder.setInStatus(ErpPurchaseOrderInStatusEnum.NOT_IN.getStatus());
        List<ErpPurchaseOrderItemDO> itemDOs = validatePurchaseOrderItems(items);
        calculateTotalPrice(purchaseOrder, itemDOs);
        purchaseOrderMapper.insert(purchaseOrder);
        itemDOs.forEach(item -> item.setOrderId(purchaseOrder.getId()));
        purchaseOrderItemMapper.insertBatch(itemDOs);
        return purchaseOrder;
    }
    /**
     * è§£æžå¯¼å…¥çš„采购时间,兼容多种常见格式;为空取当前时间
     */
    private LocalDateTime parseImportOrderTime(String text) {
        if (StrUtil.isBlank(text)) {
            return LocalDateTime.now();
        }
        String value = text.trim();
        for (String pattern : new String[]{"yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy/MM/dd HH:mm:ss"}) {
            try {
                return LocalDateTime.parse(value, DateTimeFormatter.ofPattern(pattern));
            } catch (Exception ignored) {
                // ç»§ç»­å°è¯•下一个格式
            }
        }
        for (String pattern : new String[]{"yyyy-MM-dd", "yyyy/M/d"}) {
            try {
                return LocalDate.parse(value, DateTimeFormatter.ofPattern(pattern)).atStartOfDay();
            } catch (Exception ignored) {
                // ç»§ç»­å°è¯•下一个格式
            }
        }
        throw new IllegalArgumentException("采购时间格式不正确:" + text + ",示例 2026-01-01 10:00:00");
    }
    /**
     * æå–导入失败原因(ServiceException å–业务提示)
     */
    private String resolveImportErrorMessage(Exception ex) {
        if (ex instanceof cn.iocoder.yudao.framework.common.exception.ServiceException serviceException) {
            return serviceException.getMessage();
        }
        return StrUtil.blankToDefault(ex.getMessage(), "导入失败");
    }
    private void calculateTotalPrice(ErpPurchaseOrderDO purchaseOrder, List<ErpPurchaseOrderItemDO> purchaseOrderItems) {
        purchaseOrder.setTotalCount(getSumValue(purchaseOrderItems, ErpPurchaseOrderItemDO::getCount, BigDecimal::add));
        purchaseOrder.setTotalProductPrice(getSumValue(purchaseOrderItems, ErpPurchaseOrderItemDO::getTotalPrice, BigDecimal::add, BigDecimal.ZERO));
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOrderService.java
@@ -3,6 +3,8 @@
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.order.ErpSaleOrderPageReqVO;
import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.order.ErpSaleOrderSaveReqVO;
import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.order.ErpSaleOrderImportExcelVO;
import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.order.ErpSaleOrderImportRespVO;
import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpSaleOrderDO;
import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpSaleOrderItemDO;
import jakarta.validation.Valid;
@@ -28,6 +30,14 @@
    Long createSaleOrder(@Valid ErpSaleOrderSaveReqVO createReqVO);
    /**
     * å¯¼å…¥é”€å”®è®¢å•(导入后为“未提交”状态,可继续编辑或提交审批)
     *
     * @param importList å¯¼å…¥æ•°æ®ï¼ˆæ¯ä¸€è¡Œ = ä¸€æ¡æ˜Žç»†ï¼Œç›¸åŒé”€å”®å•编号合并为一张订单)
     * @return å¯¼å…¥ç»“æžœ
     */
    ErpSaleOrderImportRespVO importSaleOrderList(List<ErpSaleOrderImportExcelVO> importList);
    /**
     * æ›´æ–°é”€å”®è®¢å•
     *
     * @param updateReqVO æ›´æ–°ä¿¡æ¯
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOrderServiceImpl.java
@@ -2,6 +2,8 @@
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.number.MoneyUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
@@ -13,9 +15,12 @@
import cn.iocoder.yudao.module.bpm.service.definition.BpmCategoryService;
import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
import cn.iocoder.yudao.module.crm.api.customer.CrmCustomerApi;
import cn.iocoder.yudao.module.crm.api.customer.dto.CrmCustomerRespDTO;
import cn.iocoder.yudao.module.crm.api.contract.CrmContractApi;
import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.order.ErpSaleOrderPageReqVO;
import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.order.ErpSaleOrderSaveReqVO;
import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.order.ErpSaleOrderImportExcelVO;
import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.order.ErpSaleOrderImportRespVO;
import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpSaleOrderDO;
import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpSaleOrderItemDO;
import cn.iocoder.yudao.module.erp.dal.mysql.sale.ErpSaleOrderItemMapper;
@@ -39,12 +44,15 @@
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -180,6 +188,180 @@
        storageAttachmentApi.updateAttachments("file", "erp_sale_order", updateReqVO.getId(), updateReqVO.getBlobIds());
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public ErpSaleOrderImportRespVO importSaleOrderList(List<ErpSaleOrderImportExcelVO> importList) {
        ErpSaleOrderImportRespVO respVO = ErpSaleOrderImportRespVO.builder()
                .createList(new ArrayList<>())
                .updateList(new ArrayList<>())
                .failureList(new LinkedHashMap<>())
                .build();
        if (CollUtil.isEmpty(importList)) {
            return respVO;
        }
        // 1. é¢„热“客户名称 -> å®¢æˆ·ç¼–号”映射(客户主数据在 CRM æ¨¡å—)
        Map<String, Long> customerIdMap = new HashMap<>();
        List<CrmCustomerRespDTO> customers = customerApi.getCustomerSimpleList();
        if (CollUtil.isNotEmpty(customers)) {
            customers.forEach(customer -> {
                if (StrUtil.isNotBlank(customer.getName())) {
                    customerIdMap.putIfAbsent(customer.getName().trim(), customer.getId());
                }
            });
        }
        // 2. æŒ‰â€œé”€å”®å•编号”分组:同编号多行 = ä¸€å¼ è®¢å•的多条明细;编号为空则每行独立成单
        Map<String, List<ErpSaleOrderImportExcelVO>> orderGroups = new LinkedHashMap<>();
        for (int i = 0; i < importList.size(); i++) {
            ErpSaleOrderImportExcelVO row = importList.get(i);
            if (row == null || (StrUtil.isBlank(row.getCustomerName()) && StrUtil.isBlank(row.getProductCode())
                    && StrUtil.isBlank(row.getNo()))) {
                continue; // è·³è¿‡ç©ºè¡Œ
            }
            String key = StrUtil.isNotBlank(row.getNo()) ? row.getNo().trim() : "__row_" + (i + 2);
            orderGroups.computeIfAbsent(key, k -> new ArrayList<>()).add(row);
        }
        // 3. é€ä¸ªè®¢å•导入(单张订单失败不影响其它)
        for (Map.Entry<String, List<ErpSaleOrderImportExcelVO>> entry : orderGroups.entrySet()) {
            List<ErpSaleOrderImportExcelVO> rows = entry.getValue();
            String label = StrUtil.isNotBlank(rows.get(0).getNo()) ? rows.get(0).getNo().trim()
                    : "第 " + entry.getKey().substring("__row_".length()) + " è¡Œ";
            try {
                ErpSaleOrderDO saleOrder = createSaleOrderByImport(rows, customerIdMap);
                respVO.getCreateList().add(saleOrder.getNo());
            } catch (Exception ex) {
                respVO.getFailureList().put(label, resolveImportErrorMessage(ex));
            }
        }
        return respVO;
    }
    /**
     * ä¾æ®å¯¼å…¥æ•°æ®åˆ›å»ºä¸€å¼ é”€å”®è®¢å•(含明细)。
     *
     * æ³¨æ„ï¼šæ­¤å¤„不调用 submitSaleOrder,因此不会创建 BPM å·¥ä½œæµï¼›è®¢å•状态固定为“未提交(0)”,
     * å¯åœ¨åˆ—表中继续编辑或提交审批。
     */
    private ErpSaleOrderDO createSaleOrderByImport(List<ErpSaleOrderImportExcelVO> rows,
                                                   Map<String, Long> customerIdMap) {
        // 1. å®¢æˆ·ï¼ˆå–该组首行)
        ErpSaleOrderImportExcelVO head = rows.get(0);
        if (StrUtil.isBlank(head.getCustomerName())) {
            throw new IllegalArgumentException("客户名称不能为空");
        }
        Long customerId = customerIdMap.get(head.getCustomerName().trim());
        if (customerId == null) {
            throw new IllegalArgumentException("客户不存在:" + head.getCustomerName());
        }
        customerApi.validateCustomer(customerId);
        // 2. æ˜Žç»†
        List<ErpSaleOrderSaveReqVO.Item> items = new ArrayList<>();
        for (ErpSaleOrderImportExcelVO row : rows) {
            if (StrUtil.isBlank(row.getProductCode())) {
                throw new IllegalArgumentException("产品编码不能为空");
            }
            CommonResult<MdmItemRespDTO> itemResult = mdmItemApi.getItemByCode(row.getProductCode().trim());
            MdmItemRespDTO item = itemResult == null ? null : itemResult.getData();
            if (item == null) {
                throw new IllegalArgumentException("产品不存在:" + row.getProductCode());
            }
            if (row.getCount() == null) {
                throw new IllegalArgumentException("产品[" + row.getProductCode() + "]的数量不能为空");
            }
            if (item.getUnitMeasureId() == null) {
                throw new IllegalArgumentException("产品[" + row.getProductCode() + "]未配置计量单位,请先在物料中维护");
            }
            ErpSaleOrderSaveReqVO.Item itemVO = new ErpSaleOrderSaveReqVO.Item();
            itemVO.setProductId(item.getId());
            itemVO.setProductUnitId(item.getUnitMeasureId());
            // å•价:未填写时取物料的销售价(与页面选产品后的默认值一致)
            itemVO.setProductPrice(row.getProductPrice() != null ? row.getProductPrice()
                    : ObjectUtil.defaultIfNull(item.getSalesPrice(), BigDecimal.ZERO));
            itemVO.setCount(row.getCount());
            itemVO.setTaxPercent(row.getTaxPercent());
            itemVO.setNeedProduction(parseNeedProduction(row.getNeedProduction()));
            itemVO.setRemark(row.getRemark());
            items.add(itemVO);
        }
        // 3. å•号:填了就用(校验唯一),没填自动生成
        String no = StrUtil.isNotBlank(head.getNo()) ? head.getNo().trim()
                : noRedisDAO.generate(ErpNoRedisDAO.SALE_ORDER_NO_PREFIX);
        if (saleOrderMapper.selectByNo(no) != null) {
            throw new IllegalArgumentException("销售单编号已存在:" + no);
        }
        // 4. ç»„装主表:导入后固定为“未提交(0)”状态
        ErpSaleOrderDO saleOrder = new ErpSaleOrderDO();
        saleOrder.setNo(no);
        saleOrder.setCustomerId(customerId);
        saleOrder.setContractNo(head.getContractNo());
        saleOrder.setOrderTime(parseImportOrderTime(head.getOrderTime()));
        saleOrder.setRemark(head.getRemark());
        saleOrder.setStatus(ErpAuditStatus.DRAFT.getStatus());
        saleOrder.setDepositPrice(BigDecimal.ZERO);
        saleOrder.setOutCount(BigDecimal.ZERO);
        saleOrder.setOutStatus(ErpSaleOrderOutStatusEnum.NOT_OUT.getStatus());
        List<ErpSaleOrderItemDO> itemDOs = validateSaleOrderItems(items);
        calculateTotalPrice(saleOrder, itemDOs);
        saleOrderMapper.insert(saleOrder);
        itemDOs.forEach(item -> item.setOrderId(saleOrder.getId()));
        saleOrderItemMapper.insertBatch(itemDOs);
        return saleOrder;
    }
    /**
     * è§£æžâ€œæ˜¯å¦éœ€è¦ç”Ÿäº§â€ï¼šç•™ç©ºé»˜è®¤â€œæ˜¯(1)”,填“否/0/不需要”则为“否(0)”
     */
    private Integer parseNeedProduction(String text) {
        if (StrUtil.isBlank(text)) {
            return 1; // ä¸Žé¡µé¢æ–°å¢žæ˜Žç»†çš„默认值一致
        }
        String value = text.trim();
        boolean no = "否".equals(value) || "0".equals(value) || "不需要".equals(value)
                || "false".equalsIgnoreCase(value) || "no".equalsIgnoreCase(value);
        return no ? 0 : 1;
    }
    /**
     * è§£æžå¯¼å…¥çš„下单时间,兼容多种常见格式;为空取当前时间
     */
    private LocalDateTime parseImportOrderTime(String text) {
        if (StrUtil.isBlank(text)) {
            return LocalDateTime.now();
        }
        String value = text.trim();
        for (String pattern : new String[]{"yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy/MM/dd HH:mm:ss"}) {
            try {
                return LocalDateTime.parse(value, DateTimeFormatter.ofPattern(pattern));
            } catch (Exception ignored) {
                // ç»§ç»­å°è¯•下一个格式
            }
        }
        for (String pattern : new String[]{"yyyy-MM-dd", "yyyy/M/d"}) {
            try {
                return LocalDate.parse(value, DateTimeFormatter.ofPattern(pattern)).atStartOfDay();
            } catch (Exception ignored) {
                // ç»§ç»­å°è¯•下一个格式
            }
        }
        throw new IllegalArgumentException("下单时间格式不正确:" + text + ",示例 2026-01-01 10:00:00");
    }
    /**
     * æå–导入失败原因(ServiceException å–业务提示)
     */
    private String resolveImportErrorMessage(Exception ex) {
        if (ex instanceof cn.iocoder.yudao.framework.common.exception.ServiceException serviceException) {
            return serviceException.getMessage();
        }
        return StrUtil.blankToDefault(ex.getMessage(), "导入失败");
    }
    private void calculateTotalPrice(ErpSaleOrderDO saleOrder, List<ErpSaleOrderItemDO> saleOrderItems) {
        saleOrder.setTotalCount(getSumValue(saleOrderItems, ErpSaleOrderItemDO::getCount, BigDecimal::add));
        saleOrder.setTotalProductPrice(getSumValue(saleOrderItems, ErpSaleOrderItemDO::getTotalPrice, BigDecimal::add, BigDecimal.ZERO));
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/metering/MesDvMeteringController.java
@@ -11,6 +11,8 @@
import cn.iocoder.yudao.module.mes.controller.admin.dv.metering.vo.MesDvMeteringPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.dv.metering.vo.MesDvMeteringRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.dv.metering.vo.MesDvMeteringSaveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.dv.metering.vo.MesDvMeteringImportExcelVO;
import cn.iocoder.yudao.module.mes.controller.admin.dv.metering.vo.MesDvMeteringImportRespVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.dv.metering.MesDvMeteringDO;
import cn.iocoder.yudao.module.mes.enums.dv.MesDvMeteringExpireStatusEnum;
import cn.iocoder.yudao.module.mes.service.dv.metering.MesDvMeteringService;
@@ -27,10 +29,12 @@
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -108,6 +112,29 @@
                buildMeteringRespVOList(list));
    }
    @GetMapping("/get-import-template")
    @Operation(summary = "获得计量器具导入模板")
    @PreAuthorize("@ss.hasPermission('mes:dv-metering:import')")
    public void importTemplate(HttpServletResponse response) throws IOException {
        List<MesDvMeteringImportExcelVO> list = Collections.singletonList(
                MesDvMeteringImportExcelVO.builder()
                        .code("JL-001").name("示例游标卡尺").category(1).specification("0-150mm")
                        .brand("示例品牌").precision("0.02mm").range("0-150mm")
                        .deptName("研发部门").chargeUserName("超级管理员")
                        .checkCycleCount(12).useDate("2026-01-01").status(1).remark("示例数据,导入前请替换")
                        .build());
        ExcelUtils.write(response, "计量器具导入模板.xls", "计量器具", MesDvMeteringImportExcelVO.class, list);
    }
    @PostMapping("/import")
    @Operation(summary = "导入计量器具")
    @PreAuthorize("@ss.hasPermission('mes:dv-metering:import')")
    public CommonResult<MesDvMeteringImportRespVO> importMeteringExcel(
            @RequestParam("file") MultipartFile file) throws Exception {
        List<MesDvMeteringImportExcelVO> list = ExcelUtils.read(file, MesDvMeteringImportExcelVO.class);
        return success(meteringService.importMeteringList(list));
    }
    @GetMapping("/simple-list")
    @Operation(summary = "获得计量器具精简列表", description = "主要用于前端的下拉选项")
    @PreAuthorize("@ss.hasPermission('mes:dv-metering:query')")
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/metering/vo/MesDvMeteringImportExcelVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,81 @@
package cn.iocoder.yudao.module.mes.controller.admin.dv.metering.vo;
import cn.idev.excel.annotation.ExcelProperty;
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
import cn.iocoder.yudao.module.mes.enums.DictTypeConstants;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
 * ç®¡ç†åŽå° - è®¡é‡å™¨å…·å¯¼å…¥ Excel VO
 *
 * è¯´æ˜Žï¼šä¸Žå¯¼å‡ºåˆ—保持一致,剔除系统列(编号、上次/下次检测日期、到期状态、剩余天数、创建时间)。
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - è®¡é‡å™¨å…·å¯¼å…¥ Excel VO")
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class MesDvMeteringImportExcelVO {
    @ExcelProperty("计量器具编码")
    @Schema(description = "计量器具编码,唯一", requiredMode = Schema.RequiredMode.REQUIRED)
    private String code;
    @ExcelProperty("计量器具名称")
    @Schema(description = "计量器具名称", requiredMode = Schema.RequiredMode.REQUIRED)
    private String name;
    @ExcelProperty(value = "器具类别", converter = DictConvert.class)
    @DictFormat(DictTypeConstants.MES_DV_METERING_CATEGORY)
    @Schema(description = "器具类别(填字典中文,如 å¡å°º/其他)", requiredMode = Schema.RequiredMode.REQUIRED)
    private Integer category;
    @ExcelProperty("规格型号")
    @Schema(description = "规格型号")
    private String specification;
    @ExcelProperty("品牌/生产厂家")
    @Schema(description = "品牌/生产厂家")
    private String brand;
    @ExcelProperty("精度")
    @Schema(description = "精度")
    private String precision;
    @ExcelProperty("量程")
    @Schema(description = "量程")
    private String range;
    @ExcelProperty("使用部门")
    @Schema(description = "使用部门名称(需与系统部门名称一致)")
    private String deptName;
    @ExcelProperty("责任人")
    @Schema(description = "责任人昵称(需与系统用户昵称一致)")
    private String chargeUserName;
    @ExcelProperty("校准周期(月)")
    @Schema(description = "校准周期(月);留空默认 12", requiredMode = Schema.RequiredMode.REQUIRED)
    private Integer checkCycleCount;
    @ExcelProperty("投入使用日期")
    @Schema(description = "投入使用日期,格式 yyyy-MM-dd")
    private String useDate;
    @ExcelProperty(value = "状态", converter = DictConvert.class)
    @DictFormat(DictTypeConstants.MES_DV_METERING_STATUS)
    @Schema(description = "状态(填字典中文,如 åœ¨ç”¨/停用/报废)", requiredMode = Schema.RequiredMode.REQUIRED)
    private Integer status;
    @ExcelProperty("备注")
    @Schema(description = "备注")
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/metering/vo/MesDvMeteringImportRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
package cn.iocoder.yudao.module.mes.controller.admin.dv.metering.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
import java.util.List;
import java.util.Map;
/**
 * ç®¡ç†åŽå° - è®¡é‡å™¨å…·å¯¼å…¥ Response VO
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - è®¡é‡å™¨å…·å¯¼å…¥ Response VO")
@Data
@Builder
public class MesDvMeteringImportRespVO {
    @Schema(description = "创建成功的计量器具编码数组", example = "JL-001")
    private List<String> createList;
    @Schema(description = "更新成功的计量器具编码数组")
    private List<String> updateList;
    @Schema(description = "导入失败集合,key ä¸ºè®¡é‡å™¨å…·ç¼–码或行号,value ä¸ºå¤±è´¥åŽŸå› ")
    private Map<String, String> failureList;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/dv/metering/MesDvMeteringMapper.java
@@ -1,11 +1,15 @@
package cn.iocoder.yudao.module.mes.dal.mysql.dv.metering;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.mes.controller.admin.dv.metering.vo.MesDvMeteringPageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.dv.metering.MesDvMeteringDO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import java.time.LocalDate;
import java.util.List;
@@ -36,6 +40,20 @@
        return selectOne(MesDvMeteringDO::getCode, code);
    }
    /**
     * æŒ‰ç¼–码查询(含“已被逻辑删除”的记录)——用于导入时判重
     */
    @InterceptorIgnore(tenantLine = "true", dataPermission = "true")
    @Select("SELECT * FROM mes_dv_metering WHERE code = #{code} ORDER BY id DESC LIMIT 1")
    MesDvMeteringDO selectByCodeIncludeDeleted(@Param("code") String code);
    /**
     * æ¢å¤è¢«é€»è¾‘删除的记录(把 deleted ç½®å›ž 0)
     */
    @InterceptorIgnore(tenantLine = "true", dataPermission = "true")
    @Update("UPDATE mes_dv_metering SET deleted = 0 WHERE id = #{id}")
    int restoreDeletedById(@Param("id") Long id);
    default List<MesDvMeteringDO> selectListByNextCheckDateBetween(LocalDate begin, LocalDate end) {
        return selectList(new LambdaQueryWrapperX<MesDvMeteringDO>()
                .ge(MesDvMeteringDO::getNextCheckDate, begin)
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/metering/MesDvMeteringService.java
@@ -3,6 +3,8 @@
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mes.controller.admin.dv.metering.vo.MesDvMeteringPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.dv.metering.vo.MesDvMeteringSaveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.dv.metering.vo.MesDvMeteringImportExcelVO;
import cn.iocoder.yudao.module.mes.controller.admin.dv.metering.vo.MesDvMeteringImportRespVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.dv.metering.MesDvMeteringDO;
import jakarta.validation.Valid;
@@ -29,6 +31,14 @@
    Long createMetering(@Valid MesDvMeteringSaveReqVO createReqVO);
    /**
     * å¯¼å…¥è®¡é‡å™¨å…·ï¼ˆå¯¼å…¥åŽä¸ºæ­£å¸¸æ•°æ®ï¼Œç¼–码已存在则记入失败列表)
     *
     * @param importList å¯¼å…¥æ•°æ®
     * @return å¯¼å…¥ç»“æžœ
     */
    MesDvMeteringImportRespVO importMeteringList(List<MesDvMeteringImportExcelVO> importList);
    /**
     * æ›´æ–°è®¡é‡å™¨å…·
     *
     * @param updateReqVO æ›´æ–°ä¿¡æ¯
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/metering/MesDvMeteringServiceImpl.java
@@ -1,6 +1,12 @@
package cn.iocoder.yudao.module.mes.service.dv.metering;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.module.mes.controller.admin.dv.metering.vo.MesDvMeteringImportExcelVO;
import cn.iocoder.yudao.module.mes.controller.admin.dv.metering.vo.MesDvMeteringImportRespVO;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
@@ -14,9 +20,14 @@
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@@ -54,6 +65,11 @@
    @Resource
    private ConfigApi configApi;
    @Resource
    private DeptApi deptApi;
    @Resource
    private AdminUserApi adminUserApi;
    @Override
    public Long createMetering(MesDvMeteringSaveReqVO createReqVO) {
        // æ ¡éªŒç¼–码唯一
@@ -66,6 +82,163 @@
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public MesDvMeteringImportRespVO importMeteringList(List<MesDvMeteringImportExcelVO> importList) {
        MesDvMeteringImportRespVO respVO = MesDvMeteringImportRespVO.builder()
                .createList(new ArrayList<>())
                .updateList(new ArrayList<>())
                .failureList(new LinkedHashMap<>())
                .build();
        if (CollUtil.isEmpty(importList)) {
            return respVO;
        }
        // 1. é¢„热“部门名称 -> éƒ¨é—¨ç¼–号”映射(部门/责任人为非必填,匹配不到该行失败)
        Map<String, Long> deptIdMap = new HashMap<>();
        List<DeptRespDTO> depts = deptApi.getDeptList();
        if (CollUtil.isNotEmpty(depts)) {
            depts.forEach(dept -> {
                if (StrUtil.isNotBlank(dept.getName())) {
                    deptIdMap.putIfAbsent(dept.getName().trim(), dept.getId());
                }
            });
        }
        // 2. é€è¡Œå¯¼å…¥ï¼ˆå•行失败不影响其它行)
        for (int i = 0; i < importList.size(); i++) {
            MesDvMeteringImportExcelVO row = importList.get(i);
            if (row == null || (StrUtil.isBlank(row.getCode()) && StrUtil.isBlank(row.getName()))) {
                continue; // è·³è¿‡ç©ºè¡Œ
            }
            String label = StrUtil.isNotBlank(row.getCode()) ? row.getCode().trim() : "第 " + (i + 2) + " è¡Œ";
            try {
                boolean restored = createMeteringByImport(row, deptIdMap);
                if (restored) {
                    respVO.getUpdateList().add(row.getCode().trim()); // æ¢å¤/覆盖更新
                } else {
                    respVO.getCreateList().add(row.getCode().trim());
                }
            } catch (Exception ex) {
                respVO.getFailureList().put(label, resolveImportErrorMessage(ex));
            }
        }
        return respVO;
    }
    /**
     * ä¾æ®å¯¼å…¥è¡Œåˆ›å»ºä¸€æ¡è®¡é‡å™¨å…·è®°å½•
     */
    private boolean createMeteringByImport(MesDvMeteringImportExcelVO row, Map<String, Long> deptIdMap) {
        // 1. å¿…填校验
        if (StrUtil.isBlank(row.getCode())) {
            throw new IllegalArgumentException("计量器具编码不能为空");
        }
        if (StrUtil.isBlank(row.getName())) {
            throw new IllegalArgumentException("计量器具名称不能为空");
        }
        if (row.getCategory() == null) {
            throw new IllegalArgumentException("器具类别不能为空(请填字典中文,如 å¡å°º/其他)");
        }
        if (row.getStatus() == null) {
            throw new IllegalArgumentException("状态不能为空(请填字典中文,如 åœ¨ç”¨/停用/报废)");
        }
        // 2. ç¼–码判重(含“已被逻辑删除”的记录):未删除 => æŠ¥é”™ï¼›å·²åˆ é™¤ => ç¨åŽæ¢å¤å¹¶æ›´æ–°
        MesDvMeteringDO exist = meteringMapper.selectByCodeIncludeDeleted(row.getCode().trim());
        if (exist != null && !Boolean.TRUE.equals(exist.getDeleted())) {
            throw new IllegalArgumentException("计量器具编码已存在:" + row.getCode());
        }
        // 3. ç»„装
        MesDvMeteringSaveReqVO reqVO = new MesDvMeteringSaveReqVO();
        reqVO.setCode(row.getCode().trim());
        reqVO.setName(row.getName().trim());
        reqVO.setCategory(row.getCategory());
        reqVO.setSpecification(row.getSpecification());
        reqVO.setBrand(row.getBrand());
        reqVO.setPrecision(row.getPrecision());
        reqVO.setRange(row.getRange());
        reqVO.setCheckCycleCount(row.getCheckCycleCount() == null ? 12 : row.getCheckCycleCount());
        reqVO.setUseDate(parseImportUseDate(row.getUseDate()));
        reqVO.setStatus(row.getStatus());
        reqVO.setRemark(row.getRemark());
        // ä½¿ç”¨éƒ¨é—¨ï¼šæŒ‰åç§°åŒ¹é…
        if (StrUtil.isNotBlank(row.getDeptName())) {
            Long deptId = deptIdMap.get(row.getDeptName().trim());
            if (deptId == null) {
                throw new IllegalArgumentException("使用部门不存在:" + row.getDeptName());
            }
            reqVO.setDeptId(deptId);
        }
        // è´£ä»»äººï¼šæŒ‰æ˜µç§°åŒ¹é…
        if (StrUtil.isNotBlank(row.getChargeUserName())) {
            Long userId = matchUserIdByNickname(row.getChargeUserName().trim());
            if (userId == null) {
                throw new IllegalArgumentException("责任人不存在:" + row.getChargeUserName());
            }
            reqVO.setChargeUserId(userId);
        }
        // 4. è½åº“:命中“已删除”的同编码 => å…ˆæ¢å¤å†æ›´æ–°ï¼›å¦åˆ™æ–°å¢ž
        if (exist != null) {
            meteringMapper.restoreDeletedById(exist.getId());
            reqVO.setId(exist.getId());
            updateMetering(reqVO);
            return true; // è¡¨ç¤ºâ€œæ¢å¤/更新”
        }
        createMetering(reqVO);
        return false;
    }
    /**
     * æŒ‰æ˜µç§°åŒ¹é…ç”¨æˆ·ç¼–号(精确匹配优先,取不到则用模糊结果第一条)
     */
    private Long matchUserIdByNickname(String nickname) {
        List<AdminUserRespDTO> users = adminUserApi.getUserListByNickname(nickname);
        if (CollUtil.isEmpty(users)) {
            return null;
        }
        return users.stream()
                .filter(user -> nickname.equals(user.getNickname()))
                .map(AdminUserRespDTO::getId)
                .findFirst()
                .orElseGet(() -> users.get(0).getId());
    }
    /**
     * è§£æžæŠ•入使用日期(yyyy-MM-dd / yyyy/M/d);为空返回 null
     */
    private LocalDate parseImportUseDate(String text) {
        if (StrUtil.isBlank(text)) {
            return null;
        }
        String value = text.trim();
        for (String pattern : new String[]{"yyyy-MM-dd", "yyyy/M/d"}) {
            try {
                return LocalDate.parse(value, java.time.format.DateTimeFormatter.ofPattern(pattern));
            } catch (Exception ignored) {
                // ç»§ç»­å°è¯•下一个格式
            }
        }
        try {
            return java.time.LocalDateTime.parse(value,
                    java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")).toLocalDate();
        } catch (Exception ignored) {
            // å¿½ç•¥
        }
        throw new IllegalArgumentException("投入使用日期格式不正确:" + text + ",示例 2026-01-01");
    }
    /**
     * æå–导入失败原因(ServiceException å–业务提示)
     */
    private String resolveImportErrorMessage(Exception ex) {
        if (ex instanceof cn.iocoder.yudao.framework.common.exception.ServiceException serviceException) {
            return serviceException.getMessage();
        }
        return StrUtil.blankToDefault(ex.getMessage(), "导入失败");
    }
    @Override
    public void updateMetering(MesDvMeteringSaveReqVO updateReqVO) {
        // æ ¡éªŒå­˜åœ¨
        validateMeteringExists(updateReqVO.getId());