docs/sales_quotation_import_api.md
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,223 @@ # é宿¥ä»·å¯¼å ¥åè½ - å端èè°ææ¡£ ## ä¸ãåè½æ¦è¿° é宿¥ä»·æ¨¡åæ¯æå¤Sheet模æ¿å¯¼å ¥ï¼æ¨¡æ¿ç»æåèéå®å°è´¦ï¼ 1. **æ¥ä»·åæ°æ®Sheet**ï¼å 嫿¥ä»·ååºæ¬ä¿¡æ¯ 2. **æ¥ä»·äº§åæ°æ®Sheet**ï¼å å«äº§åæç»ä¿¡æ¯ --- ## äºã模æ¿ç»æ ### 2.1 æ¥ä»·åæ°æ®Sheet | åæ®µ | 说æ | æ¯å¦å¿ å¡« | |------|------|----------| | æ¥ä»·åå· | æ¥ä»·åç¼å·ï¼ä¸å¡«èªå¨çæï¼ | å¦ | | 客æ·åç§° | 客æ·åç§° | æ¯ | | ä¸å¡å | éå®äººåå§å | å¦ | | æ¥ä»·æ¥æ | æ ¼å¼yyyy-MM-dd | å¦ | | æææè³ | æ ¼å¼yyyy-MM-dd | å¦ | | 仿¬¾æ¹å¼ | å¦"æç»30天" | å¦ | | äº¤è´§å¨æ | å¦"7天" | å¦ | | 夿³¨ | å ¶ä»è¯´æ | å¦ | ### 2.2 æ¥ä»·äº§åæ°æ®Sheet | åæ®µ | 说æ | æ¯å¦å¿ å¡« | |------|------|----------| | æ¥ä»·åå· | å ³èæ¥ä»·åæ°æ®Sheetçæ¥ä»·åå· | æ¯ | | 产å大类 | 产ååç±»åç§° | æ¯ | | è§æ ¼åå· | 产åè§æ ¼åå· | æ¯ | | åä½ | 计éåä½ | å¦ | | æ°é | æ¥ä»·æ°é | æ¯ | | å«ç¨åä»· | åä»·ï¼å ï¼ | æ¯ | | å«ç¨æ»ä»· | æ»ä»·ï¼å ï¼ | å¦ï¼ä¸å¡«èªå¨è®¡ç®ï¼ | | 夿³¨ | 产å夿³¨ | å¦ | --- ## ä¸ãæ¥å£è¯¦æ ### 3.1 ä¸è½½æ¥ä»·å¯¼å ¥æ¨¡æ¿ **请æ±** ``` GET /sales/quotation/downloadTemplate ``` **ååº** - Excelæä»¶ä¸è½½ï¼å å«ä¸¤ä¸ªSheetï¼ - æ¥ä»·åæ°æ®ï¼å«ç¤ºä¾æ°æ®ï¼ - æ¥ä»·äº§åæ°æ®ï¼å«ç¤ºä¾æ°æ®ï¼ ### 3.2 å¯¼å ¥æ¥ä»·å **请æ±** ``` POST /sales/quotation/import Content-Type: multipart/form-data file: [Excelæä»¶] ``` **ååº** ```json { "code": 200, "msg": "æä½æå", "data": { "id": 1, "batchNo": "QT_IMP_20260612143000", "fileName": "æ¥ä»·å.xlsx", "totalCount": 5, "successCount": 5, "newCount": 5, "failCount": 0, "status": "completed", "createTime": "2026-06-12 14:30:00", "createUserName": "å¼ ä¸" } } ``` **ä¸å¡é»è¾** 1. **æ¨¡æ¿æ£æ¥**ï¼å¯¼å ¥åæ£æ¥æ¥ä»·å®¡æ¹æ¨¡æ¿æ¯å¦åå¨ - ä¸åå¨ï¼è¿åé误"请å é ç½®æ¥ä»·å®¡æ¹æ¨¡æ¿ï¼æ æ³å¯¼å ¥" - åå¨ï¼ç»§ç»å¯¼å ¥ 2. **æ¥ä»·åå·å¤ç**ï¼ - 妿填åäºæ¥ä»·åå·ï¼ä½¿ç¨å¡«åçåå· - 妿æªå¡«åï¼èªå¨çæåå·ï¼æ ¼å¼ï¼QT + æ¥æ + åºå·ï¼ - æ¥ä»·åå·å·²åå¨åè·³è¿ 3. **产åå ³è**ï¼éè¿æ¥ä»·åå·å°äº§åæ°æ®å ³èå°å¯¹åºçæ¥ä»·å 4. **å®¡æ¹æµç¨**ï¼å¯¼å ¥æååèªå¨åå»ºå®¡æ¹æµç¨ 5. **éä»·è®°å½**ï¼ç¸åè§æ ¼åå·ç产å对æ¯åå²ä»·æ ¼ï¼èªå¨è®°å½éä»· **é误ç ** | éè¯¯ä¿¡æ¯ | 说æ | |----------|------| | 请å é ç½®æ¥ä»·å®¡æ¹æ¨¡æ¿ï¼æ æ³å¯¼å ¥ | ç³»ç»æªé ç½®æ¥ä»·å®¡æ¹æ¨¡æ¿ | | 读åæä»¶å¤±è´¥ | Excelæä»¶æ ¼å¼é误 | | æ¥ä»·åæ°æ®ä¸ºç©ºï¼è¯·æ£æ¥æ¨¡æ¿å 容 | æ¥ä»·åSheetæ æ°æ® | ### 3.3 æ¥è¯¢å¯¼å ¥è®°å½ **请æ±** ``` GET /sales/quotation/importLog/list?pageNum=1&pageSize=10 ``` **ååº** ```json { "code": 200, "data": { "total": 20, "records": [ { "id": 1, "batchNo": "QT_IMP_20260612143000", "fileName": "æ¥ä»·å.xlsx", "totalCount": 5, "successCount": 5, "newCount": 5, "failCount": 0, "status": "completed", "createUserName": "å¼ ä¸", "createTime": "2026-06-12 14:30:00" } ] } } ``` ### 3.4 æ¥è¯¢éä»·åå² **请æ±** ``` GET /sales/quotation/priceHistory/list?quotationProductId=123 ``` **ååº** ```json { "code": 200, "data": [ { "id": 1, "productName": "çµæ± ç»ä»¶A", "specification": "MODEL-A-100W", "oldPrice": 150.00, "newPrice": 120.00, "priceChange": -30.00, "changeReason": "éä»·", "importBatch": "QT_IMP_20260612143000", "importTime": "2026-06-12 14:30:00", "createUserName": "å¼ ä¸" } ] } ``` --- ## åãä¸å¡æµç¨ ``` ä¸è½½æ¨¡æ¿ â â¼ å¡«åæ¥ä»·åæ°æ®Sheetï¼æ¯è¡ä¸ä¸ªæ¥ä»·åï¼ å¡«åæ¥ä»·äº§åæ°æ®Sheetï¼éè¿æ¥ä»·åå·å ³èï¼ â â¼ ä¸ä¼ æä»¶ â â¼ æ£æ¥å®¡æ¹æ¨¡æ¿æ¯å¦åå¨ â âââ ä¸åå¨ ââ⺠é误ï¼"请å é ç½®æ¥ä»·å®¡æ¹æ¨¡æ¿ï¼æ æ³å¯¼å ¥" â âââ åå¨ ââ⺠解æä¸¤ä¸ªSheetæ°æ® â â¼ éåæ¥ä»·åæ°æ® â âââ æ£æ¥æ¥ä»·åå·æ¯å¦å·²åå¨ ââ⺠已åå¨åè·³è¿ â âââ å建æ¥ä»·åè®°å½ âââ å ³èäº§åæ°æ®ï¼éè¿æ¥ä»·åå·å¹é ï¼ âââ è®°å½éä»·åå²ï¼å¯¹æ¯åå²ä»·æ ¼ï¼ âââ åå»ºå®¡æ¹æµç¨ â â¼ è¿åå¯¼å ¥ç»æ ``` --- ## äºãç¤ºä¾æ°æ® **æ¥ä»·åæ°æ®Sheet** | æ¥ä»·åå· | 客æ·åç§° | ä¸å¡å | 仿¬¾æ¹å¼ | äº¤è´§å¨æ | |----------|----------|--------|----------|----------| | QT202606120001 | 示ä¾å®¢æ· | å¼ ä¸ | æç»30天 | 7天 | **æ¥ä»·äº§åæ°æ®Sheet** | æ¥ä»·åå· | 产å大类 | è§æ ¼åå· | åä½ | æ°é | å«ç¨åä»· | å«ç¨æ»ä»· | |----------|----------|----------|------|------|----------|----------| | QT202606120001 | çµæ± ç»ä»¶ | MODEL-A-100W | ç | 100 | 150.00 | 15000.00 | | QT202606120001 | çµæ± ç»ä»¶ | MODEL-B-200W | ç | 50 | 200.00 | 10000.00 | --- ## å ãæ°æ®åºåæ´ éæ§è¡ï¼`docs/sales_quotation_price_history.sql` docs/sales_quotation_price_history.sql
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,56 @@ -- ============================================================ -- é宿¥ä»·éä»·è®°å½è¡¨ -- éç¨åºæ¯ï¼è®°å½æ¥ä»·å项ç®çåå²ä»·æ ¼åå -- çææ¥æï¼2026-06-12 -- ============================================================ -- é宿¥ä»·éä»·è®°å½è¡¨ CREATE TABLE `sales_quotation_price_history` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主é®ID', `quotation_id` BIGINT NOT NULL COMMENT 'æ¥ä»·åID', `quotation_product_id` BIGINT NOT NULL COMMENT 'æ¥ä»·ååID', `product_name` VARCHAR(200) DEFAULT NULL COMMENT 'åååç§°ï¼åä½åå¨ï¼', `specification` VARCHAR(200) DEFAULT NULL COMMENT 'ååè§æ ¼ï¼åä½åå¨ï¼', `old_price` DECIMAL(24, 4) DEFAULT NULL COMMENT 'ååä»·', `new_price` DECIMAL(24, 4) DEFAULT NULL COMMENT 'æ°åä»·', `price_change` DECIMAL(24, 4) DEFAULT NULL COMMENT 'ä»·æ ¼åå¨ï¼æ°-æ§ï¼è´æ°è¡¨ç¤ºéä»·ï¼', `change_reason` VARCHAR(500) DEFAULT NULL COMMENT 'åå¨åå ', `import_batch` VARCHAR(50) DEFAULT NULL COMMENT 'å¯¼å ¥æ¹æ¬¡å·', `import_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'å¯¼å ¥æ¶é´', `create_user` BIGINT DEFAULT NULL COMMENT 'æä½äººID', `create_user_name` VARCHAR(100) DEFAULT NULL COMMENT 'æä½äººå§å', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'å建æ¶é´', `tenant_id` BIGINT DEFAULT NULL COMMENT 'ç§æ·ID', `dept_id` BIGINT DEFAULT NULL COMMENT 'é¨é¨ID', PRIMARY KEY (`id`), KEY `idx_quotation_id` (`quotation_id`), KEY `idx_quotation_product_id` (`quotation_product_id`), KEY `idx_import_batch` (`import_batch`), KEY `idx_import_time` (`import_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='é宿¥ä»·éä»·è®°å½è¡¨'; -- é宿¥ä»·å¯¼å ¥è®°å½è¡¨ CREATE TABLE `sales_quotation_import_log` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主é®ID', `batch_no` VARCHAR(50) NOT NULL COMMENT 'å¯¼å ¥æ¹æ¬¡å·', `file_name` VARCHAR(200) DEFAULT NULL COMMENT 'å¯¼å ¥æä»¶å', `total_count` INT DEFAULT 0 COMMENT 'æ»è®°å½æ°', `success_count` INT DEFAULT 0 COMMENT 'æåè®°å½æ°', `update_count` INT DEFAULT 0 COMMENT 'æ´æ°è®°å½æ°', `new_count` INT DEFAULT 0 COMMENT 'æ°å¢è®°å½æ°', `fail_count` INT DEFAULT 0 COMMENT 'å¤±è´¥è®°å½æ°', `status` VARCHAR(20) DEFAULT 'pending' COMMENT 'ç¶æï¼pending-å¾ å®¡æ ¸, approved-å·²éè¿, rejected-å·²æç»', `remark` VARCHAR(500) DEFAULT NULL COMMENT '夿³¨', `create_user` BIGINT DEFAULT NULL COMMENT 'æä½äººID', `create_user_name` VARCHAR(100) DEFAULT NULL COMMENT 'æä½äººå§å', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'å¯¼å ¥æ¶é´', `audit_user` BIGINT DEFAULT NULL COMMENT 'å®¡æ ¸äººID', `audit_user_name` VARCHAR(100) DEFAULT NULL COMMENT 'å®¡æ ¸äººå§å', `audit_time` DATETIME DEFAULT NULL COMMENT 'å®¡æ ¸æ¶é´', `tenant_id` BIGINT DEFAULT NULL COMMENT 'ç§æ·ID', `dept_id` BIGINT DEFAULT NULL COMMENT 'é¨é¨ID', PRIMARY KEY (`id`), UNIQUE KEY `uk_batch_no` (`batch_no`), KEY `idx_status` (`status`), KEY `idx_create_time` (`create_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='é宿¥ä»·å¯¼å ¥è®°å½è¡¨'; src/main/java/com/ruoyi/basic/service/impl/CustomerServiceImpl.java
@@ -16,6 +16,7 @@ import com.ruoyi.basic.pojo.CustomerUser; import com.ruoyi.basic.service.*; import com.ruoyi.basic.vo.CustomerVo; import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.poi.ExcelUtil; @@ -199,6 +200,8 @@ LoginUser loginUser = SecurityUtils.getLoginUser(); Long tenantId = loginUser.getTenantId(); customer.setTenantId(tenantId); // æ ¡éªå®¢æ·åç§°å¯ä¸æ§ checkCustomerNameUnique(customer.getCustomerName(), tenantId, null); return customerMapper.insert(customer); } @@ -213,10 +216,34 @@ LoginUser loginUser = SecurityUtils.getLoginUser(); Long tenantId = loginUser.getTenantId(); customer.setTenantId(tenantId); // æ ¡éªå®¢æ·åç§°å¯ä¸æ§ï¼æé¤èªèº«ï¼ checkCustomerNameUnique(customer.getCustomerName(), tenantId, customer.getId()); return customerMapper.updateById(customer); } /** * æ ¡éªå®¢æ·åç§°å¯ä¸æ§ * * @param customerName 客æ·åç§° * @param tenantId ç§æ·ID * @param excludeId æé¤ç客æ·IDï¼ä¿®æ¹æ¶æé¤èªèº«ï¼ */ private void checkCustomerNameUnique(String customerName, Long tenantId, Long excludeId) { if (StringUtils.isNotEmpty(customerName)) { LambdaQueryWrapper<Customer> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(Customer::getTenantId, tenantId) .eq(Customer::getCustomerName, customerName); if (excludeId != null) { queryWrapper.ne(Customer::getId, excludeId); } Long count = customerMapper.selectCount(queryWrapper); if (count > 0) { throw new ServiceException("客æ·åç§°'" + customerName + "'å·²åå¨ï¼è¯·ä¿®æ¹"); } } } /** * æ¹éå é¤å®¢æ·æ¡£æ¡ * * @param ids éè¦å é¤çå®¢æ·æ¡£æ¡ä¸»é® src/main/java/com/ruoyi/common/utils/poi/ExcelUtil.java
@@ -10,6 +10,7 @@ import java.lang.reflect.ParameterizedType; import java.math.BigDecimal; import java.text.DecimalFormat; import java.text.SimpleDateFormat; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.ArrayList; @@ -465,37 +466,37 @@ { val = Convert.toBigDecimal(val); } else if (Date.class == fieldType) { if (val instanceof String) { val = DateUtils.parseDate(val); else if (Date.class == fieldType) { if (val instanceof String) { val = DateUtils.parseDate(val); } else if (val instanceof Double) { val = DateUtil.getJavaDate((Double) val); } } else if (LocalDate.class == fieldType) { if (val instanceof String) { Date date = DateUtils.parseDate(val); val = StringUtils.isNull(date) ? null : DateUtils.toLocalDate(date); } else if (val instanceof Date) { val = DateUtils.toLocalDate((Date) val); } else if (val instanceof Double) { val = DateUtils.toLocalDate(DateUtil.getJavaDate((Double) val)); } } else if (Boolean.TYPE == fieldType || Boolean.class == fieldType) { val = Convert.toBool(val, false); } { val = DateUtil.getJavaDate((Double) val); } } else if (LocalDate.class == fieldType) { if (val instanceof String) { Date date = DateUtils.parseDate(val); val = StringUtils.isNull(date) ? null : DateUtils.toLocalDate(date); } else if (val instanceof Date) { val = DateUtils.toLocalDate((Date) val); } else if (val instanceof Double) { val = DateUtils.toLocalDate(DateUtil.getJavaDate((Double) val)); } } else if (Boolean.TYPE == fieldType || Boolean.class == fieldType) { val = Convert.toBool(val, false); } if (StringUtils.isNotNull(fieldType)) { String propertyName = field.getName(); @@ -667,24 +668,24 @@ val = Convert.toFloat(val); } else if (BigDecimal.class == fieldType) { val = Convert.toBigDecimal(val); } else if (Date.class == fieldType) { if (val instanceof String) { val = DateUtils.parseDate(val); } else if (val instanceof Double) { val = DateUtil.getJavaDate((Double) val); } } else if (LocalDate.class == fieldType) { if (val instanceof String) { Date date = DateUtils.parseDate(val); val = StringUtils.isNull(date) ? null : DateUtils.toLocalDate(date); } else if (val instanceof Date) { val = DateUtils.toLocalDate((Date) val); } else if (val instanceof Double) { val = DateUtils.toLocalDate(DateUtil.getJavaDate((Double) val)); } } else if (Boolean.TYPE == fieldType || Boolean.class == fieldType) { val = Convert.toBool(val, false); } } else if (Date.class == fieldType) { if (val instanceof String) { val = DateUtils.parseDate(val); } else if (val instanceof Double) { val = DateUtil.getJavaDate((Double) val); } } else if (LocalDate.class == fieldType) { if (val instanceof String) { Date date = DateUtils.parseDate(val); val = StringUtils.isNull(date) ? null : DateUtils.toLocalDate(date); } else if (val instanceof Date) { val = DateUtils.toLocalDate((Date) val); } else if (val instanceof Double) { val = DateUtils.toLocalDate(DateUtil.getJavaDate((Double) val)); } } else if (Boolean.TYPE == fieldType || Boolean.class == fieldType) { val = Convert.toBool(val, false); } if (StringUtils.isNotNull(fieldType)) { String propertyName = field.getName(); @@ -2068,7 +2069,7 @@ /** * è·å对象çååè¡¨æ¹æ³ * * * @param name åç§° * @param pojoClass 类对象 * @return ååè¡¨æ¹æ³ @@ -2089,4 +2090,247 @@ } return method; } /** * 导åºå¤Sheet Excel模æ¿ï¼éææ¹æ³ï¼ * æ¯æä¸åç±»åçDTO导åºå°ä¸åçSheet * * @param response HTTPååº * @param sheetDataMap Map<Sheetåç§°, SheetData>ï¼SheetDataå 嫿°æ®å表å对åºçClassç±»å * @param fileName æä»¶å */ @SuppressWarnings("unchecked") public static void exportExcelMultiSheet(HttpServletResponse response, Map<String, SheetData<?>> sheetDataMap, String fileName) { try (SXSSFWorkbook workbook = new SXSSFWorkbook()) { // åå»ºæ ·å¼ CellStyle headerStyle = createHeaderStyle(workbook); CellStyle dataStyle = createDataStyle(workbook); // éåæ¯ä¸ªSheet for (Map.Entry<String, SheetData<?>> entry : sheetDataMap.entrySet()) { String sheetName = entry.getKey(); SheetData<?> sheetData = entry.getValue(); List<?> dataList = sheetData.getDataList(); Class<?> clazz = sheetData.getClazz(); // å建Sheet Sheet sheet = workbook.createSheet(sheetName); // è·ååæ®µä¿¡æ¯ List<Object[]> fields = getFieldsByClass(clazz, Type.IMPORT); if (fields.isEmpty()) { continue; } // å建表头 Row headerRow = sheet.createRow(0); int colIndex = 0; for (Object[] fieldObj : fields) { Field field = (Field) fieldObj[0]; Excel excel = (Excel) fieldObj[1]; Cell cell = headerRow.createCell(colIndex); cell.setCellValue(excel.name()); cell.setCellStyle(headerStyle); // 设置å宽 sheet.setColumnWidth(colIndex, (int) ((excel.width() + 0.72) * 256)); colIndex++; } // åå ¥æ°æ® if (dataList != null && !dataList.isEmpty()) { int rowIndex = 1; for (Object data : dataList) { Row dataRow = sheet.createRow(rowIndex); colIndex = 0; for (Object[] fieldObj : fields) { Field field = (Field) fieldObj[0]; Excel excel = (Excel) fieldObj[1]; field.setAccessible(true); Object value = field.get(data); Cell cell = dataRow.createCell(colIndex); setCellValueByType(cell, value, excel, dataStyle, workbook); colIndex++; } rowIndex++; } } } // è¾åºå°ååº response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setCharacterEncoding("utf-8"); String encodedFileName = new String(fileName.getBytes("GBK"), "ISO8859_1") + ".xlsx"; response.addHeader("Content-Disposition", "attachment;filename=" + encodedFileName); workbook.write(response.getOutputStream()); } catch (Exception e) { log.error("导åºå¤Sheet Excelå¼å¸¸: {}", e.getMessage()); throw new UtilException("导åºExcel失败: " + e.getMessage()); } } /** * Sheetæ°æ®å°è£ ç±» */ public static class SheetData<T> { private List<T> dataList; private Class<T> clazz; public SheetData(List<T> dataList, Class<T> clazz) { this.dataList = dataList; this.clazz = clazz; } public List<T> getDataList() { return dataList; } public Class<T> getClazz() { return clazz; } } /** * æ ¹æ®ç±»è·ååæ®µå表ï¼éææ¹æ³ï¼ */ private static List<Object[]> getFieldsByClass(Class<?> clazz, Type type) { List<Object[]> fields = new ArrayList<>(); List<Field> tempFields = new ArrayList<>(); tempFields.addAll(Arrays.asList(clazz.getSuperclass().getDeclaredFields())); tempFields.addAll(Arrays.asList(clazz.getDeclaredFields())); for (Field field : tempFields) { if (field.isAnnotationPresent(Excel.class)) { Excel excel = field.getAnnotation(Excel.class); if (excel != null && (excel.type() == Type.ALL || excel.type() == type)) { fields.add(new Object[] { field, excel }); } } } // æsortæåº fields.sort(Comparator.comparing(objects -> ((Excel) objects[1]).sort())); return fields; } /** * åå»ºè¡¨å¤´æ ·å¼ */ private static CellStyle createHeaderStyle(Workbook workbook) { CellStyle style = workbook.createCellStyle(); style.setAlignment(HorizontalAlignment.CENTER); style.setVerticalAlignment(VerticalAlignment.CENTER); style.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex()); style.setFillPattern(FillPatternType.SOLID_FOREGROUND); style.setBorderRight(BorderStyle.THIN); style.setRightBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); style.setBorderLeft(BorderStyle.THIN); style.setLeftBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); style.setBorderTop(BorderStyle.THIN); style.setTopBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); style.setBorderBottom(BorderStyle.THIN); style.setBottomBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); Font font = workbook.createFont(); font.setFontName("Arial"); font.setFontHeightInPoints((short) 10); font.setBold(true); style.setFont(font); DataFormat dataFormat = workbook.createDataFormat(); style.setDataFormat(dataFormat.getFormat("@")); return style; } /** * åå»ºæ°æ®æ ·å¼ */ private static CellStyle createDataStyle(Workbook workbook) { CellStyle style = workbook.createCellStyle(); style.setAlignment(HorizontalAlignment.CENTER); style.setVerticalAlignment(VerticalAlignment.CENTER); style.setBorderRight(BorderStyle.THIN); style.setRightBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); style.setBorderLeft(BorderStyle.THIN); style.setLeftBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); style.setBorderTop(BorderStyle.THIN); style.setTopBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); style.setBorderBottom(BorderStyle.THIN); style.setBottomBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); Font font = workbook.createFont(); font.setFontName("Arial"); font.setFontHeightInPoints((short) 10); style.setFont(font); return style; } /** * 设置åå æ ¼å¼ */ private static void setCellValueByType(Cell cell, Object value, Excel excel, CellStyle dataStyle, Workbook workbook) { cell.setCellStyle(dataStyle); if (value == null) { cell.setCellValue(""); return; } String dateFormat = excel.dateFormat(); if (StringUtils.isNotEmpty(dateFormat) && value instanceof Date) { cell.setCellValue(new SimpleDateFormat(dateFormat).format((Date) value)); } else if (value instanceof Date) { cell.setCellValue(new SimpleDateFormat("yyyy-MM-dd").format((Date) value)); } else if (value instanceof BigDecimal) { cell.setCellValue(((BigDecimal) value).doubleValue()); } else if (value instanceof Number) { cell.setCellValue(((Number) value).doubleValue()); } else if (value instanceof Boolean) { cell.setCellValue((Boolean) value); } else { String strValue = Convert.toStr(value); // 鲿¢CSVæ³¨å ¥ if (StringUtils.startsWithAny(strValue, FORMULA_STR)) { strValue = RegExUtils.replaceFirst(strValue, FORMULA_REGEX_STR, "\t$0"); } cell.setCellValue(strValue); } } } src/main/java/com/ruoyi/framework/web/exception/GlobalExceptionHandler.java
@@ -8,6 +8,7 @@ import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.MissingPathVariableException; import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; @@ -75,6 +76,17 @@ } /** * 请æ±åæ°ç¼ºå¤± */ @ExceptionHandler(MissingServletRequestParameterException.class) public AjaxResult handleMissingServletRequestParameterException(MissingServletRequestParameterException e, HttpServletRequest request) { String requestURI = request.getRequestURI(); log.error("请æ±å°å'{}',缺å°å¿ éç请æ±åæ°'{}'", requestURI, e.getParameterName()); return AjaxResult.error(String.format("缺å°å¿ éç请æ±åæ°[%s]", e.getParameterName())); } /** * 请æ±åæ°ç±»åä¸å¹é */ @ExceptionHandler(MethodArgumentTypeMismatchException.class) src/main/java/com/ruoyi/sales/controller/SalesQuotationController.java
@@ -1,46 +1,85 @@ package com.ruoyi.sales.controller; import com.ruoyi.common.utils.poi.ExcelUtil; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ruoyi.common.utils.poi.ExcelUtil; import com.ruoyi.framework.web.domain.AjaxResult; import com.ruoyi.framework.web.domain.R; import com.ruoyi.sales.dto.SalesQuotationDto; import com.ruoyi.sales.pojo.SalesQuotationImportLog; import com.ruoyi.sales.pojo.SalesQuotationPriceHistory; import com.ruoyi.sales.service.SalesQuotationService; 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.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.util.List; @RestController @RequestMapping("/sales/quotation") @AllArgsConstructor @Tag(name = "é宿¥ä»·ç®¡ç") public class SalesQuotationController { private final SalesQuotationService salesQuotationService; @GetMapping("/list") @Operation(summary = "å页æ¥è¯¢æ¥ä»·åå表") public AjaxResult getList(Page page, SalesQuotationDto salesQuotationDto) { return AjaxResult.success(salesQuotationService.listPage(page, salesQuotationDto)); } @PostMapping("/export") @Operation(summary = "å¯¼åºæ¥ä»·å") public void export(HttpServletResponse response) { Page page = new Page(-1,-1); SalesQuotationDto afterSalesService = new SalesQuotationDto(); IPage<SalesQuotationDto> listPage = salesQuotationService.listPage(page, afterSalesService); ExcelUtil<SalesQuotationDto> util = new ExcelUtil<SalesQuotationDto>(SalesQuotationDto.class); util.exportExcel(response, listPage.getRecords() , "é宿¥ä»·"); ExcelUtil<SalesQuotationDto> util = new ExcelUtil<>(SalesQuotationDto.class); util.exportExcel(response, listPage.getRecords(), "é宿¥ä»·"); } @PostMapping("/add") @Operation(summary = "æ°å¢æ¥ä»·å") public AjaxResult add(@RequestBody SalesQuotationDto salesQuotationDto) { return AjaxResult.success(salesQuotationService.add(salesQuotationDto)); } @PostMapping("/update") @Operation(summary = "ä¿®æ¹æ¥ä»·å") public AjaxResult update(@RequestBody SalesQuotationDto salesQuotationDto) { return AjaxResult.success(salesQuotationService.edit(salesQuotationDto)); } @DeleteMapping("/delete") @Operation(summary = "å 餿¥ä»·å") public AjaxResult delete(@RequestBody Long id) { return AjaxResult.success(salesQuotationService.delete(id)); } @GetMapping("/downloadTemplate") @Operation(summary = "ä¸è½½æ¥ä»·å¯¼å ¥æ¨¡æ¿") public void downloadTemplate(HttpServletResponse response) { salesQuotationService.downloadTemplate(response); } @PostMapping("/import") @Operation(summary = "å¯¼å ¥æ¥ä»·å") public R<SalesQuotationImportLog> importQuotation(@RequestParam("file") MultipartFile file) { return R.ok(salesQuotationService.importQuotation(file)); } @GetMapping("/importLog/list") @Operation(summary = "æ¥è¯¢å¯¼å ¥è®°å½å表") public R<IPage<SalesQuotationImportLog>> listImportLog(Page page) { return R.ok(salesQuotationService.listImportLog(page)); } @GetMapping("/priceHistory/list") @Operation(summary = "æ¥è¯¢éä»·åå²è®°å½") public R<List<SalesQuotationPriceHistory>> listPriceHistory(@RequestParam("quotationProductId") Long quotationProductId) { return R.ok(salesQuotationService.listPriceHistory(quotationProductId)); } } src/main/java/com/ruoyi/sales/dto/SalesQuotationImportDto.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,44 @@ package com.ruoyi.sales.dto; import com.fasterxml.jackson.annotation.JsonFormat; import com.ruoyi.framework.aspectj.lang.annotation.Excel; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import java.math.BigDecimal; import java.util.Date; /** * é宿¥ä»·å¯¼å ¥DTOï¼å ¼å®¹åSheetå¯¼å ¥ï¼ */ @Data @EqualsAndHashCode(callSuper = true) public class SalesQuotationImportDto extends SalesQuotationProductImportDto { @Excel(name = "客æ·åç§°") @Schema(description = "客æ·åç§°") private String customerName; @Excel(name = "ä¸å¡å") @Schema(description = "ä¸å¡å") private String salesperson; @JsonFormat(pattern = "yyyy-MM-dd") @Excel(name = "æ¥ä»·æ¥æ", width = 30, dateFormat = "yyyy-MM-dd") @Schema(description = "æ¥ä»·æ¥æ") private Date quotationDate; @JsonFormat(pattern = "yyyy-MM-dd") @Excel(name = "æææè³", width = 30, dateFormat = "yyyy-MM-dd") @Schema(description = "æææè³") private Date validDate; @Excel(name = "仿¬¾æ¹å¼") @Schema(description = "仿¬¾æ¹å¼") private String paymentMethod; @Excel(name = "äº¤è´§å¨æ") @Schema(description = "äº¤è´§å¨æ") private String deliveryPeriod; } src/main/java/com/ruoyi/sales/dto/SalesQuotationMainImportDto.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,50 @@ package com.ruoyi.sales.dto; import com.fasterxml.jackson.annotation.JsonFormat; import com.ruoyi.framework.aspectj.lang.annotation.Excel; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.math.BigDecimal; import java.util.Date; /** * é宿¥ä»·ä¸»è¡¨å¯¼å ¥DTOï¼æ¥ä»·åæ°æ®Sheetï¼ */ @Data public class SalesQuotationMainImportDto { @Excel(name = "æ¥ä»·åå·") @Schema(description = "æ¥ä»·åå·ï¼å¯éï¼ä¸å¡«èªå¨çæï¼") private String quotationNo; @Excel(name = "客æ·åç§°") @Schema(description = "客æ·åç§°") private String customerName; @Excel(name = "ä¸å¡å") @Schema(description = "ä¸å¡å") private String salesperson; @JsonFormat(pattern = "yyyy-MM-dd") @Excel(name = "æ¥ä»·æ¥æ", width = 30, dateFormat = "yyyy-MM-dd") @Schema(description = "æ¥ä»·æ¥æ") private Date quotationDate; @JsonFormat(pattern = "yyyy-MM-dd") @Excel(name = "æææè³", width = 30, dateFormat = "yyyy-MM-dd") @Schema(description = "æææè³") private Date validDate; @Excel(name = "仿¬¾æ¹å¼") @Schema(description = "仿¬¾æ¹å¼") private String paymentMethod; @Excel(name = "äº¤è´§å¨æ") @Schema(description = "äº¤è´§å¨æ") private String deliveryPeriod; @Excel(name = "夿³¨") @Schema(description = "夿³¨") private String remark; } src/main/java/com/ruoyi/sales/dto/SalesQuotationProductImportDto.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,38 @@ package com.ruoyi.sales.dto; import com.ruoyi.framework.aspectj.lang.annotation.Excel; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.math.BigDecimal; /** * é宿¥ä»·äº§åå¯¼å ¥DTOï¼æ¥ä»·äº§åæ°æ®Sheetï¼ */ @Data public class SalesQuotationProductImportDto { @Excel(name = "æ¥ä»·åå·") @Schema(description = "æ¥ä»·åå·ï¼å ³è主表ï¼") private String quotationNo; @Excel(name = "产å大类") @Schema(description = "产å大类") private String productCategory; @Excel(name = "è§æ ¼åå·") @Schema(description = "è§æ ¼åå·") private String specificationModel; @Excel(name = "åä½") @Schema(description = "åä½") private String unit; @Excel(name = "åä»·") @Schema(description = "åä»·") private BigDecimal unitPrice; @Excel(name = "夿³¨") @Schema(description = "夿³¨") private String remark; } src/main/java/com/ruoyi/sales/mapper/SalesQuotationImportLogMapper.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,9 @@ package com.ruoyi.sales.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.ruoyi.sales.pojo.SalesQuotationImportLog; import org.apache.ibatis.annotations.Mapper; @Mapper public interface SalesQuotationImportLogMapper extends BaseMapper<SalesQuotationImportLog> { } src/main/java/com/ruoyi/sales/mapper/SalesQuotationPriceHistoryMapper.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,9 @@ package com.ruoyi.sales.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.ruoyi.sales.pojo.SalesQuotationPriceHistory; import org.apache.ibatis.annotations.Mapper; @Mapper public interface SalesQuotationPriceHistoryMapper extends BaseMapper<SalesQuotationPriceHistory> { } src/main/java/com/ruoyi/sales/pojo/SalesQuotationImportLog.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,74 @@ package com.ruoyi.sales.pojo; import com.baomidou.mybatisplus.annotation.*; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; /** * é宿¥ä»·å¯¼å ¥è®°å½ */ @Data @TableName("sales_quotation_import_log") @Schema(description = "é宿¥ä»·å¯¼å ¥è®°å½") public class SalesQuotationImportLog { @TableId(value = "id", type = IdType.AUTO) @Schema(description = "主é®ID") private Long id; @Schema(description = "å¯¼å ¥æ¹æ¬¡å·") private String batchNo; @Schema(description = "å¯¼å ¥æä»¶å") private String fileName; @Schema(description = "æ»è®°å½æ°") private Integer totalCount; @Schema(description = "æåè®°å½æ°") private Integer successCount; @Schema(description = "æ´æ°è®°å½æ°") private Integer updateCount; @Schema(description = "æ°å¢è®°å½æ°") private Integer newCount; @Schema(description = "å¤±è´¥è®°å½æ°") private Integer failCount; @Schema(description = "ç¶æï¼pending-å¾ å®¡æ ¸, approved-å·²éè¿, rejected-å·²æç»") private String status; @Schema(description = "夿³¨") private String remark; @Schema(description = "æä½äººID") private Long createUser; @Schema(description = "æä½äººå§å") private String createUserName; @Schema(description = "å¯¼å ¥æ¶é´") @TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; @Schema(description = "å®¡æ ¸äººID") private Long auditUser; @Schema(description = "å®¡æ ¸äººå§å") private String auditUserName; @Schema(description = "å®¡æ ¸æ¶é´") private LocalDateTime auditTime; @Schema(description = "ç§æ·ID") @TableField(fill = FieldFill.INSERT) private Long tenantId; @Schema(description = "é¨é¨ID") @TableField(fill = FieldFill.INSERT) private Long deptId; } src/main/java/com/ruoyi/sales/pojo/SalesQuotationPriceHistory.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,69 @@ package com.ruoyi.sales.pojo; import com.baomidou.mybatisplus.annotation.*; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.math.BigDecimal; import java.time.LocalDateTime; /** * é宿¥ä»·éä»·è®°å½ */ @Data @TableName("sales_quotation_price_history") @Schema(description = "é宿¥ä»·éä»·è®°å½") public class SalesQuotationPriceHistory { @TableId(value = "id", type = IdType.AUTO) @Schema(description = "主é®ID") private Long id; @Schema(description = "æ¥ä»·åID") private Long quotationId; @Schema(description = "æ¥ä»·ååID") private Long quotationProductId; @Schema(description = "åååç§°") private String productName; @Schema(description = "ååè§æ ¼") private String specification; @Schema(description = "ååä»·") private BigDecimal oldPrice; @Schema(description = "æ°åä»·") private BigDecimal newPrice; @Schema(description = "ä»·æ ¼åå¨ï¼æ°-æ§ï¼è´æ°è¡¨ç¤ºéä»·ï¼") private BigDecimal priceChange; @Schema(description = "åå¨åå ") private String changeReason; @Schema(description = "å¯¼å ¥æ¹æ¬¡å·") private String importBatch; @Schema(description = "å¯¼å ¥æ¶é´") private LocalDateTime importTime; @Schema(description = "æä½äººID") private Long createUser; @Schema(description = "æä½äººå§å") private String createUserName; @Schema(description = "å建æ¶é´") @TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; @Schema(description = "ç§æ·ID") @TableField(fill = FieldFill.INSERT) private Long tenantId; @Schema(description = "é¨é¨ID") @TableField(fill = FieldFill.INSERT) private Long deptId; } src/main/java/com/ruoyi/sales/service/SalesQuotationService.java
@@ -3,8 +3,15 @@ import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.service.IService; import com.ruoyi.sales.dto.SalesQuotationDto; import com.ruoyi.sales.dto.SalesQuotationImportDto; import com.ruoyi.sales.pojo.SalesQuotation; import com.ruoyi.sales.pojo.SalesQuotationPriceHistory; import com.ruoyi.sales.pojo.SalesQuotationImportLog; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import org.springframework.web.multipart.MultipartFile; import jakarta.servlet.http.HttpServletResponse; import java.util.List; public interface SalesQuotationService extends IService<SalesQuotation> { IPage listPage(Page page, SalesQuotationDto salesQuotationDto); @@ -14,4 +21,27 @@ boolean delete(Long id); boolean edit(SalesQuotationDto salesQuotationDto); /** * ä¸è½½æ¥ä»·æ¨¡æ¿ */ void downloadTemplate(HttpServletResponse response); /** * å¯¼å ¥æ¥ä»·åï¼å¯¼å ¥åæ£æ¥å®¡æ¹æ¨¡æ¿æ¯å¦åå¨ï¼ * @param file å¯¼å ¥æä»¶ * @return å¯¼å ¥ç»æ */ SalesQuotationImportLog importQuotation(MultipartFile file); /** * æ¥è¯¢å¯¼å ¥è®°å½å表 */ IPage<SalesQuotationImportLog> listImportLog(Page page); /** * æ¥è¯¢éä»·åå²è®°å½ * @param quotationId æ¥ä»·ååID */ List<SalesQuotationPriceHistory> listPriceHistory(Long quotationId); } src/main/java/com/ruoyi/sales/service/impl/SalesQuotationServiceImpl.java
@@ -18,25 +18,39 @@ import com.ruoyi.basic.mapper.CustomerMapper; import com.ruoyi.basic.pojo.Customer; import com.ruoyi.common.enums.IsDeleteEnum; import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.utils.OrderUtils; import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.common.utils.bean.BeanUtils; import com.ruoyi.common.utils.poi.ExcelUtil; import com.ruoyi.framework.security.LoginUser; import com.ruoyi.sales.dto.SalesQuotationDto; import com.ruoyi.sales.dto.SalesQuotationImportDto; import com.ruoyi.sales.dto.SalesQuotationMainImportDto; import com.ruoyi.sales.dto.SalesQuotationProductImportDto; import com.ruoyi.sales.mapper.SalesQuotationImportLogMapper; import com.ruoyi.sales.mapper.SalesQuotationMapper; import com.ruoyi.sales.mapper.SalesQuotationPriceHistoryMapper; import com.ruoyi.sales.mapper.SalesQuotationProductMapper; import com.ruoyi.sales.pojo.SalesQuotation; import com.ruoyi.sales.pojo.SalesQuotationProduct; import com.ruoyi.sales.pojo.*; import com.ruoyi.sales.service.SalesQuotationProductService; import com.ruoyi.sales.service.SalesQuotationService; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.*; import java.util.stream.Collectors; @Slf4j @Service @Transactional(rollbackFor = Exception.class) @RequiredArgsConstructor @@ -44,6 +58,8 @@ private final SalesQuotationProductMapper salesQuotationProductMapper; private final SalesQuotationMapper salesQuotationMapper; private final SalesQuotationProductService salesQuotationProductService; private final SalesQuotationPriceHistoryMapper priceHistoryMapper; private final SalesQuotationImportLogMapper importLogMapper; private final ApproveProcessServiceImpl approveProcessService; private final CustomerMapper customerMapper; @@ -85,7 +101,7 @@ SalesQuotation salesQuotation = new SalesQuotation(); BeanUtils.copyProperties(salesQuotationDto, salesQuotation); salesQuotation.setId(null); Customer customer = customerMapper.selectById(Long.valueOf(salesQuotationDto.getCustomerId())); Customer customer = customerMapper.selectById(salesQuotationDto.getCustomerId()); if (ObjectUtils.isNotEmpty(customer)) { salesQuotation.setCustomer(customer.getCustomerName()); } @@ -132,6 +148,7 @@ } return true; } @Override public boolean edit(SalesQuotationDto salesQuotationDto) { SalesQuotation salesQuotation = new SalesQuotation(); @@ -188,6 +205,7 @@ } return true; } @Override public boolean delete(Long id) { SalesQuotation salesQuotation = salesQuotationMapper.selectById(id); @@ -205,5 +223,309 @@ return true; } @Override public void downloadTemplate(HttpServletResponse response) { // æ¥ä»·åæ°æ®ç¤ºä¾ List<SalesQuotationMainImportDto> mainList = new ArrayList<>(); SalesQuotationMainImportDto mainExample = new SalesQuotationMainImportDto(); mainExample.setQuotationNo("QT202606120001"); mainExample.setCustomerName("示ä¾å®¢æ·"); mainExample.setSalesperson("å¼ ä¸"); mainExample.setPaymentMethod("æç»30天"); mainExample.setDeliveryPeriod("7天"); mainExample.setRemark("ç¤ºä¾æ¥ä»·å"); mainList.add(mainExample); // æ¥ä»·äº§åæ°æ®ç¤ºä¾ List<SalesQuotationProductImportDto> productList = new ArrayList<>(); SalesQuotationProductImportDto productExample1 = new SalesQuotationProductImportDto(); productExample1.setQuotationNo("QT202606120001"); productExample1.setProductCategory("çµæ± ç»ä»¶"); productExample1.setSpecificationModel("MODEL-A-100W"); productExample1.setUnit("ç"); productExample1.setUnitPrice(new BigDecimal("150.00")); productList.add(productExample1); SalesQuotationProductImportDto productExample2 = new SalesQuotationProductImportDto(); productExample2.setQuotationNo("QT202606120001"); productExample2.setProductCategory("çµæ± ç»ä»¶"); productExample2.setSpecificationModel("MODEL-B-200W"); productExample2.setUnit("ç"); productExample2.setUnitPrice(new BigDecimal("200.00")); productList.add(productExample2); // 使ç¨éææ¹æ³å¯¼åºå¤Sheetæ¨¡æ¿ Map<String, ExcelUtil.SheetData<?>> sheetDataMap = new LinkedHashMap<>(); sheetDataMap.put("æ¥ä»·åæ°æ®", new ExcelUtil.SheetData<>(mainList, SalesQuotationMainImportDto.class)); sheetDataMap.put("æ¥ä»·äº§åæ°æ®", new ExcelUtil.SheetData<>(productList, SalesQuotationProductImportDto.class)); ExcelUtil.exportExcelMultiSheet(response, sheetDataMap, "é宿¥ä»·å¯¼å ¥æ¨¡æ¿"); } @Override @Transactional(rollbackFor = Exception.class) public SalesQuotationImportLog importQuotation(MultipartFile file) { LoginUser loginUser = SecurityUtils.getLoginUser(); // æ£æ¥å®¡æ¹æ¨¡æ¿æ¯å¦åå¨ ApprovalTemplate approvalTemplate = approvalTemplateMapper.selectOne( new LambdaQueryWrapper<ApprovalTemplate>() .eq(ApprovalTemplate::getBusinessType, 6L) .eq(ApprovalTemplate::getDeleted, 0) .orderByDesc(ApprovalTemplate::getId) .last("LIMIT 1") ); if (approvalTemplate == null) { throw new ServiceException("请å é ç½®æ¥ä»·å®¡æ¹æ¨¡æ¿ï¼æ æ³å¯¼å ¥"); } // è§£æå¤Sheet Excelæä»¶ ExcelUtil<SalesQuotationMainImportDto> mainUtil = new ExcelUtil<>(SalesQuotationMainImportDto.class); Map<String, List<SalesQuotationMainImportDto>> sheetMap; try { sheetMap = mainUtil.importExcelMultiSheet(Arrays.asList("æ¥ä»·åæ°æ®", "æ¥ä»·äº§åæ°æ®"), file.getInputStream(), 0); } catch (IOException e) { throw new ServiceException("读åæä»¶å¤±è´¥: " + e.getMessage()); } List<SalesQuotationMainImportDto> mainList = sheetMap.get("æ¥ä»·åæ°æ®"); List<SalesQuotationMainImportDto> productListRaw = sheetMap.get("æ¥ä»·äº§åæ°æ®"); if (CollectionUtils.isEmpty(mainList)) { throw new ServiceException("æ¥ä»·åæ°æ®ä¸ºç©ºï¼è¯·æ£æ¥æ¨¡æ¿å 容"); } // å°äº§åæ°æ®è½¬ä¸ºæ£ç¡®çDTOç±»å ExcelUtil<SalesQuotationProductImportDto> productUtil = new ExcelUtil<>(SalesQuotationProductImportDto.class); Map<String, List<SalesQuotationProductImportDto>> productSheetMap; try { productSheetMap = productUtil.importExcelMultiSheet(Arrays.asList("æ¥ä»·äº§åæ°æ®"), file.getInputStream(), 0); } catch (IOException e) { throw new ServiceException("读åäº§åæ°æ®å¤±è´¥: " + e.getMessage()); } List<SalesQuotationProductImportDto> productList = productSheetMap.get("æ¥ä»·äº§åæ°æ®"); // çææ¹æ¬¡å· String batchNo = "QT_IMP_" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")); // åå»ºå¯¼å ¥è®°å½ SalesQuotationImportLog importLog = new SalesQuotationImportLog(); importLog.setBatchNo(batchNo); importLog.setFileName(file.getOriginalFilename()); importLog.setTotalCount(mainList.size()); importLog.setSuccessCount(0); importLog.setUpdateCount(0); importLog.setNewCount(0); importLog.setFailCount(0); importLog.setStatus("completed"); importLog.setCreateUser(loginUser.getUserId()); importLog.setCreateUserName(loginUser.getNickName()); importLogMapper.insert(importLog); // æ¥è¯¢ç¸å ³æ°æ® List<Customer> customers = customerMapper.selectList( new LambdaQueryWrapper<Customer>() .in(Customer::getCustomerName, mainList.stream() .map(SalesQuotationMainImportDto::getCustomerName) .filter(Objects::nonNull) .collect(Collectors.toList())) ); Map<String, Customer> customerMap = customers.stream() .collect(Collectors.toMap(Customer::getCustomerName, c -> c, (a, b) -> a)); // ææ¥ä»·åå·åç»äº§å Map<String, List<SalesQuotationProductImportDto>> productGroupMap = new HashMap<>(); if (!CollectionUtils.isEmpty(productList)) { productGroupMap = productList.stream() .filter(p -> p.getQuotationNo() != null && !p.getQuotationNo().isEmpty()) .collect(Collectors.groupingBy(SalesQuotationProductImportDto::getQuotationNo)); } int successCount = 0; int newCount = 0; int updateCount = 0; int failCount = 0; for (SalesQuotationMainImportDto mainDto : mainList) { try { String customerName = mainDto.getCustomerName(); if (customerName == null || customerName.isEmpty()) { failCount++; continue; } // æ¥æ¾å®¢æ· Customer customer = customerMap.get(customerName); // å建æ¥ä»·å SalesQuotation quotation = new SalesQuotation(); quotation.setCustomer(customerName); quotation.setCustomerId(customer != null ? customer.getId() : null); // çææ¥ä»·åå· String quotationNo; if (mainDto.getQuotationNo() != null && !mainDto.getQuotationNo().isEmpty()) { quotationNo = mainDto.getQuotationNo(); } else { quotationNo = OrderUtils.countTodayByCreateTime(salesQuotationMapper, "QT", "quotation_no", LocalDateTime.now()); } quotation.setQuotationNo(quotationNo); // æ£æ¥æ¥ä»·åå·æ¯å¦å·²åå¨ SalesQuotation existing = salesQuotationMapper.selectOne( new LambdaQueryWrapper<SalesQuotation>() .eq(SalesQuotation::getQuotationNo, quotationNo) .last("LIMIT 1") ); boolean isUpdate = existing != null; if (isUpdate) { quotation.setId(existing.getId()); quotation.setQuotationDate(existing.getQuotationDate()); quotation.setStatus(existing.getStatus()); } else { quotation.setQuotationDate(LocalDate.now()); quotation.setStatus("å¾ å®¡æ¹"); } quotation.setSalesperson(mainDto.getSalesperson()); quotation.setPaymentMethod(mainDto.getPaymentMethod()); quotation.setDeliveryPeriod(mainDto.getDeliveryPeriod()); quotation.setRemark("å¯¼å ¥æ¹æ¬¡å·: " + batchNo + (mainDto.getRemark() != null ? "ï¼" + mainDto.getRemark() : "")); // è®¡ç®æ»éé¢å¹¶ä¿å产å BigDecimal totalAmount = BigDecimal.ZERO; List<SalesQuotationProduct> quotationProducts = new ArrayList<>(); List<SalesQuotationProductImportDto> products = productGroupMap.get(quotationNo); if (!CollectionUtils.isEmpty(products)) { for (SalesQuotationProductImportDto productDto : products) { SalesQuotationProduct product = new SalesQuotationProduct(); product.setProduct(productDto.getProductCategory()); product.setSpecification(productDto.getSpecificationModel()); product.setUnit(productDto.getUnit()); product.setQuantity(0); product.setUnitPrice(productDto.getUnitPrice() != null ? productDto.getUnitPrice().doubleValue() : 0.0); BigDecimal amount = productDto.getUnitPrice() != null ? productDto.getUnitPrice() : BigDecimal.ZERO; product.setAmount(amount.doubleValue()); totalAmount = totalAmount.add(amount); quotationProducts.add(product); } } quotation.setTotalAmount(totalAmount); if (isUpdate) { salesQuotationMapper.updateById(quotation); // å 餿§äº§åæ°æ® salesQuotationProductMapper.delete(new LambdaQueryWrapper<SalesQuotationProduct>() .eq(SalesQuotationProduct::getSalesQuotationId, existing.getId())); } else { salesQuotationMapper.insert(quotation); } // ä¿å产åå¹¶è®°å½éä»·åå² for (SalesQuotationProduct product : quotationProducts) { product.setSalesQuotationId(quotation.getId()); salesQuotationProductMapper.insert(product); recordPriceHistory(quotation.getId(), product, batchNo); } if (!isUpdate) { // å建审æ¹å®ä¾ ApprovalInstanceDto approvalInstance = new ApprovalInstanceDto(); approvalInstance.setTemplateId(approvalTemplate.getId()); approvalInstance.setTemplateName(approvalTemplate.getTemplateName()); approvalInstance.setBusinessId(quotation.getId()); approvalInstance.setBusinessType(6L); approvalInstance.setCurrentLevel(1); approvalInstance.setTitle(quotationNo + "审æ¹"); approvalInstance.setApplicantId(loginUser.getUserId()); approvalInstance.setApplicantName(loginUser.getNickName()); approvalInstance.setApplyTime(LocalDateTime.now()); approvalInstanceService.add(approvalInstance); } successCount++; if (isUpdate) { updateCount++; } else { newCount++; } } catch (Exception e) { log.error("å¯¼å ¥æ¥ä»·å失败: {}", e.getMessage()); failCount++; } } // æ´æ°å¯¼å ¥è®°å½ importLog.setSuccessCount(successCount); importLog.setNewCount(newCount); importLog.setUpdateCount(updateCount); importLog.setFailCount(failCount); importLogMapper.updateById(importLog); return importLog; } /** * è®°å½éä»·åå² */ private void recordPriceHistory(Long quotationId, SalesQuotationProduct product, String batchNo) { LoginUser loginUser = SecurityUtils.getLoginUser(); // æ¥æ¾ç¸å项ç®åç§°çå岿¥ä»·äº§å List<SalesQuotationProduct> historyProducts = salesQuotationProductMapper.selectList( new LambdaQueryWrapper<SalesQuotationProduct>() .eq(SalesQuotationProduct::getProduct, product.getProduct()) .ne(SalesQuotationProduct::getId, product.getId()) .orderByDesc(SalesQuotationProduct::getCreateTime) .last("LIMIT 1") ); if (!historyProducts.isEmpty()) { SalesQuotationProduct historyProduct = historyProducts.get(0); BigDecimal oldPrice = historyProduct.getUnitPrice() != null ? new BigDecimal(historyProduct.getUnitPrice().toString()) : BigDecimal.ZERO; BigDecimal newPrice = product.getUnitPrice() != null ? new BigDecimal(product.getUnitPrice().toString()) : BigDecimal.ZERO; // å¦æä»·æ ¼æååï¼è®°å½éä»·åå² if (oldPrice.compareTo(newPrice) != 0) { SalesQuotationPriceHistory priceHistory = new SalesQuotationPriceHistory(); priceHistory.setQuotationId(quotationId); priceHistory.setQuotationProductId(product.getId()); priceHistory.setProductName(product.getProduct()); priceHistory.setSpecification(product.getSpecification()); priceHistory.setOldPrice(oldPrice); priceHistory.setNewPrice(newPrice); priceHistory.setPriceChange(newPrice.subtract(oldPrice)); priceHistory.setImportBatch(batchNo); priceHistory.setImportTime(LocalDateTime.now()); priceHistory.setCreateUser(loginUser.getUserId()); priceHistory.setCreateUserName(loginUser.getNickName()); priceHistory.setChangeReason(newPrice.compareTo(oldPrice) < 0 ? "éä»·" : "涨价"); priceHistoryMapper.insert(priceHistory); } } } @Override public IPage<SalesQuotationImportLog> listImportLog(Page page) { return importLogMapper.selectPage(page, new LambdaQueryWrapper<SalesQuotationImportLog>() .orderByDesc(SalesQuotationImportLog::getCreateTime)); } @Override public List<SalesQuotationPriceHistory> listPriceHistory(Long quotationId) { return priceHistoryMapper.selectList( new LambdaQueryWrapper<SalesQuotationPriceHistory>() .eq(SalesQuotationPriceHistory::getQuotationId, quotationId) .orderByDesc(SalesQuotationPriceHistory::getImportTime)); } } src/main/resources/application-dev.yml
@@ -74,7 +74,7 @@ druid: # ä¸»åºæ°æ®æº master: url: jdbc:mysql://localhost:3306/product-inventory-management-new-pro?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 url: jdbc:mysql://localhost:3306/product-inventory-management-jghg-pro?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 username: root password: 123456 # ä»åºæ°æ®æº src/main/resources/application-jghg.yml
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,268 @@ # 项ç®ç¸å ³é ç½® ruoyi: # åç§° name: RuoYi # çæ¬ version: 3.8.9 # çæå¹´ä»½ copyrightYear: 2025 # æä»¶è·¯å¾ 示ä¾ï¼ Windowsé ç½®D:/ruoyi/uploadPathï¼Linuxé ç½® /home/ruoyi/uploadPathï¼ profile: /javaWork/product-inventory-management/file # è·åipå°åå¼å ³ addressEnabled: false # éªè¯ç ç±»å math æ°åè®¡ç® char å符éªè¯ captchaType: math # åå审æ¹ç¼å·åç¼(é ç½®æä»¶åç¼å½å) approvalNumberPrefix: NEW # ä¸ªæ¨ Unipush é ç½® getui: appId: PfjyAAE0FK64FaO1w2CMb1 appKey: zTMb831OEL6J4GK1uE3Ob4 masterSecret: K1GFtsv42v61tXGnF7SGE5 domain: https://restapi.getui.cn/v2/ # 离线æ¨é使ç¨çå å/ç»ä»¶å intentComponent: uni.app.UNI099A590/io.dcloud.PandoraEntry # å¼åç¯å¢é ç½® server: # æå¡å¨çHTTP端å£ï¼é»è®¤ä¸º8080 port: 9003 servlet: # åºç¨ç访é®è·¯å¾ context-path: / tomcat: # tomcatçURIç¼ç uri-encoding: UTF-8 # è¿æ¥æ°æ»¡åçæéæ°ï¼é»è®¤ä¸º100 accept-count: 1000 threads: # tomcatæå¤§çº¿ç¨æ°ï¼é»è®¤ä¸º200 max: 800 # Tomcatå¯å¨åå§åççº¿ç¨æ°ï¼é»è®¤å¼10 min-spare: 100 # æ¥å¿é ç½® logging: level: com.ruoyi: warn org.springframework: warn minio: endpoint: http://114.132.189.42/ port: 7019 secure: false accessKey: admin secretKey: 12345678 preview-expiry: 24 # é¢è§å°åé»è®¤24å°æ¶ default-bucket: jxc # ç¨æ·é ç½® user: password: # å¯ç æå¤§éè¯¯æ¬¡æ° maxRetryCount: 5 # å¯ç é宿¶é´ï¼é»è®¤10åéï¼ lockTime: 10 # Springé ç½® spring: datasource: type: com.alibaba.druid.pool.DruidDataSource driverClassName: com.mysql.cj.jdbc.Driver druid: # ä¸»åºæ°æ®æº master: url: jdbc:mysql://172.17.0.1:3306/product-inventory-management-jghg-pro?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 username: root password: xd@123456.. # ä»åºæ°æ®æº slave: # 仿°æ®æºå¼å ³/é»è®¤å ³é enabled: false url: username: password: # åå§è¿æ¥æ° initialSize: 5 # æå°è¿æ¥æ± æ°é minIdle: 10 # æå¤§è¿æ¥æ± æ°é maxActive: 20 # é ç½®è·åè¿æ¥çå¾ è¶ æ¶çæ¶é´ maxWait: 60000 # é ç½®è¿æ¥è¶ æ¶æ¶é´ connectTimeout: 30000 # é ç½®ç½ç»è¶ æ¶æ¶é´ socketTimeout: 60000 # é ç½®é´éå¤ä¹ æè¿è¡ä¸æ¬¡æ£æµï¼æ£æµéè¦å ³éç空é²è¿æ¥ï¼å使¯æ¯«ç§ timeBetweenEvictionRunsMillis: 60000 # é ç½®ä¸ä¸ªè¿æ¥å¨æ± 䏿å°çåçæ¶é´ï¼å使¯æ¯«ç§ minEvictableIdleTimeMillis: 300000 # é ç½®ä¸ä¸ªè¿æ¥å¨æ± 䏿大çåçæ¶é´ï¼å使¯æ¯«ç§ maxEvictableIdleTimeMillis: 900000 # é ç½®æ£æµè¿æ¥æ¯å¦ææ validationQuery: SELECT 1 FROM DUAL testWhileIdle: true testOnBorrow: false testOnReturn: false webStatFilter: enabled: true statViewServlet: enabled: true # 设置ç½ååï¼ä¸å¡«åå 许ææè®¿é® allow: url-pattern: /druid/* # æ§å¶å°ç®¡çç¨æ·ååå¯ç login-username: ruoyi login-password: 123456 filter: stat: enabled: true # æ ¢SQLè®°å½ log-slow-sql: true slow-sql-millis: 1000 merge-sql: true wall: config: multi-statement-allow: true # èµæºä¿¡æ¯ messages: # å½é åèµæºæä»¶è·¯å¾ basename: i18n/messages # æä»¶ä¸ä¼ servlet: multipart: # å个æä»¶å¤§å° max-file-size: 1GB # 设置æ»ä¸ä¼ çæä»¶å¤§å° max-request-size: 2GB # æå¡æ¨¡å devtools: restart: # çé¨ç½²å¼å ³ enabled: false # redis é ç½® data: mongodb: uri: mongodb://114.132.189.42:9028/chat_memory_db_jghg # redis é ç½® redis: # å°å # host: 127.0.0.1 host: 172.17.0.1 # 端å£ï¼é»è®¤ä¸º6379 port: 6379 # æ°æ®åºç´¢å¼ database: 0 # å¯ç # password: root2022! password: # è¿æ¥è¶ æ¶æ¶é´ timeout: 10s lettuce: pool: # è¿æ¥æ± ä¸çæå°ç©ºé²è¿æ¥ min-idle: 0 # è¿æ¥æ± ä¸çæå¤§ç©ºé²è¿æ¥ max-idle: 8 # è¿æ¥æ± çæå¤§æ°æ®åºè¿æ¥æ° max-active: 8 # #è¿æ¥æ± æå¤§é»å¡çå¾ æ¶é´ï¼ä½¿ç¨è´å¼è¡¨ç¤ºæ²¡æéå¶ï¼ max-wait: -1ms # Quartz宿¶ä»»å¡é ç½®ï¼æ°å¢é¨åï¼ quartz: job-store-type: jdbc # ä½¿ç¨æ°æ®åºåå¨ jdbc: initialize-schema: never # 馿¬¡è¿è¡æ¶èªå¨åå»ºè¡¨ç»æï¼æååæ¹ä¸ºnever schema: classpath:org/quartz/impl/jdbcjobstore/tables_mysql_innodb.sql # MySQLè¡¨ç»æèæ¬ properties: org: quartz: scheduler: instanceName: RuoYiScheduler instanceId: AUTO jobStore: class: org.quartz.impl.jdbcjobstore.JobStoreTX driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate # MySQLéé tablePrefix: qrtz_ # 表ååç¼ï¼ä¸èæ¬ä¸è´ isClustered: false # åèç¹æ¨¡å¼ï¼éç¾¤éæ¹ä¸ºtrueï¼ clusterCheckinInterval: 10000 txIsolationLevelSerializable: true threadPool: class: org.quartz.simpl.SimpleThreadPool threadCount: 10 # çº¿ç¨æ± å¤§å° threadPriority: 5 makeThreadsDaemons: true updateCheck: false # å ³éçæ¬æ£æ¥ # tokené ç½® token: # 令çèªå®ä¹æ è¯ header: Authorization # 令çå¯é¥ secret: xpAVjhCjQDaDB7mjPAzMDSbQWXNu2zYkTdDNUsPMS5Xx8QMmQVYN7n74eZrYJxDJ # ä»¤çæææï¼é»è®¤30åéï¼ expireTime: 450 # MyBatis Plusé ç½® mybatis-plus: # æç´¢æå®å å«å æ ¹æ®èªå·±çé¡¹ç®æ¥ typeAliasesPackage: com.ruoyi.**.pojo # é ç½®mapperçæ«æï¼æ¾å°ææçmapper.xmlæ å°æä»¶ mapperLocations: classpath*:mapper/**/*Mapper.xml # å è½½å ¨å±çé ç½®æä»¶ configLocation: classpath:mybatis/mybatis-config.xml global-config: enable-sql-runner: true db-config: id-type: auto # PageHelperå页æä»¶ pagehelper: helperDialect: mysql supportMethodsArguments: true params: count=countSql # Swaggeré ç½® swagger: # æ¯å¦å¼å¯swagger enabled: true # 请æ±åç¼ pathMapping: /dev-api # 鲿¢XSSæ»å» xss: # è¿æ»¤å¼å ³ enabled: true # æé¤é¾æ¥ï¼å¤ä¸ªç¨éå·åéï¼ excludes: /system/notice # å¹é 龿¥ urlPatterns: /system/*,/monitor/*,/tool/* # 代ç çæ gen: # ä½è author: ruoyi # é»è®¤çæå è·¯å¾ system éæ¹æèªå·±ç模ååç§° å¦ system monitor tool packageName: com.ruoyi.project.system # èªå¨å»é¤è¡¨åç¼ï¼é»è®¤æ¯true autoRemovePre: false # 表åç¼ï¼çæç±»åä¸ä¼å å«è¡¨åç¼ï¼å¤ä¸ªç¨éå·åéï¼ tablePrefix: sys_ # æ¯å¦å è®¸çææä»¶è¦çå°æ¬å°ï¼èªå®ä¹è·¯å¾ï¼ï¼é»è®¤ä¸å 许 allowOverwrite: false # æä»¶ä¸ä¼ é ç½® file: temp-dir: /javaWork/product-inventory-management/file/temp/uploads # 临æ¶ç®å½ upload-dir: /javaWork/product-inventory-management/file/prod/uploads # æ£å¼ç®å½ path: /javaWork/product-inventory-management/file # ä¸ä¼ ç®å½ urlPrefix: /prod-api/common # 龿¥åç¼ domain: http://1.15.17.182:9097 # åååç¼ expired: 120 # è¿ææ¶é´(åä½:åé) useLimit: 10 # ä½¿ç¨æ¬¡æ° compress: true # æ¯å¦å缩 needCompressSize: 10MB # å缩éå¼ compressQuality: 0.5 # å缩质é(0.0-1.0) src/main/resources/mapper/sales/SalesQuotationImportLogMapper.xml
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,5 @@ <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.ruoyi.sales.mapper.SalesQuotationImportLogMapper"> </mapper> src/main/resources/mapper/sales/SalesQuotationPriceHistoryMapper.xml
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,5 @@ <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.ruoyi.sales.mapper.SalesQuotationPriceHistoryMapper"> </mapper>