From 11214e3074266a23fe61e8eebbce647fdb7305ef Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期五, 12 六月 2026 18:02:03 +0800
Subject: [PATCH] 报价单修改-优化,增加导入记录,降价历史

---
 src/main/java/com/ruoyi/sales/pojo/SalesQuotationImportLog.java             |   74 +++
 src/main/java/com/ruoyi/sales/dto/SalesQuotationImportDto.java              |   44 +
 src/main/java/com/ruoyi/sales/controller/SalesQuotationController.java      |   49 +
 src/main/java/com/ruoyi/sales/pojo/SalesQuotationPriceHistory.java          |   69 +++
 docs/sales_quotation_price_history.sql                                      |   56 ++
 src/main/java/com/ruoyi/sales/dto/SalesQuotationProductImportDto.java       |   38 +
 src/main/java/com/ruoyi/framework/web/exception/GlobalExceptionHandler.java |   12 
 docs/sales_quotation_import_api.md                                          |  223 +++++++++
 src/main/resources/mapper/sales/SalesQuotationImportLogMapper.xml           |    5 
 src/main/java/com/ruoyi/sales/mapper/SalesQuotationImportLogMapper.java     |    9 
 src/main/java/com/ruoyi/sales/service/impl/SalesQuotationServiceImpl.java   |  328 ++++++++++++++
 src/main/java/com/ruoyi/sales/mapper/SalesQuotationPriceHistoryMapper.java  |    9 
 src/main/java/com/ruoyi/sales/dto/SalesQuotationMainImportDto.java          |   50 ++
 src/main/resources/mapper/sales/SalesQuotationPriceHistoryMapper.xml        |    5 
 src/main/java/com/ruoyi/common/utils/poi/ExcelUtil.java                     |  340 ++++++++++++--
 src/main/java/com/ruoyi/sales/service/SalesQuotationService.java            |   30 +
 16 files changed, 1,285 insertions(+), 56 deletions(-)

diff --git a/docs/sales_quotation_import_api.md b/docs/sales_quotation_import_api.md
new file mode 100644
index 0000000..8613ff2
--- /dev/null
+++ b/docs/sales_quotation_import_api.md
@@ -0,0 +1,223 @@
+# 閿�鍞姤浠峰鍏ュ姛鑳� - 鍓嶇鑱旇皟鏂囨。
+
+## 涓�銆佸姛鑳芥杩�
+
+閿�鍞姤浠锋ā鍧楁敮鎸佸Sheet妯℃澘瀵煎叆锛屾ā鏉跨粨鏋勫弬鑰冮攢鍞彴璐︼細
+1. **鎶ヤ环鍗曟暟鎹甋heet**锛氬寘鍚姤浠峰崟鍩烘湰淇℃伅
+2. **鎶ヤ环浜у搧鏁版嵁Sheet**锛氬寘鍚骇鍝佹槑缁嗕俊鎭�
+
+---
+
+## 浜屻�佹ā鏉跨粨鏋�
+
+### 2.1 鎶ヤ环鍗曟暟鎹甋heet
+
+| 瀛楁 | 璇存槑 | 鏄惁蹇呭~ |
+|------|------|----------|
+| 鎶ヤ环鍗曞彿 | 鎶ヤ环鍗曠紪鍙凤紙涓嶅~鑷姩鐢熸垚锛� | 鍚� |
+| 瀹㈡埛鍚嶇О | 瀹㈡埛鍚嶇О | 鏄� |
+| 涓氬姟鍛� | 閿�鍞汉鍛樺鍚� | 鍚� |
+| 鎶ヤ环鏃ユ湡 | 鏍煎紡yyyy-MM-dd | 鍚� |
+| 鏈夋晥鏈熻嚦 | 鏍煎紡yyyy-MM-dd | 鍚� |
+| 浠樻鏂瑰紡 | 濡�"鏈堢粨30澶�" | 鍚� |
+| 浜よ揣鍛ㄦ湡 | 濡�"7澶�" | 鍚� |
+| 澶囨敞 | 鍏朵粬璇存槑 | 鍚� |
+
+### 2.2 鎶ヤ环浜у搧鏁版嵁Sheet
+
+| 瀛楁 | 璇存槑 | 鏄惁蹇呭~ |
+|------|------|----------|
+| 鎶ヤ环鍗曞彿 | 鍏宠仈鎶ヤ环鍗曟暟鎹甋heet鐨勬姤浠峰崟鍙� | 鏄� |
+| 浜у搧澶х被 | 浜у搧鍒嗙被鍚嶇О | 鏄� |
+| 瑙勬牸鍨嬪彿 | 浜у搧瑙勬牸鍨嬪彿 | 鏄� |
+| 鍗曚綅 | 璁¢噺鍗曚綅 | 鍚� |
+| 鏁伴噺 | 鎶ヤ环鏁伴噺 | 鏄� |
+| 鍚◣鍗曚环 | 鍗曚环锛堝厓锛� | 鏄� |
+| 鍚◣鎬讳环 | 鎬讳环锛堝厓锛� | 鍚︼紙涓嶅~鑷姩璁$畻锛� |
+| 澶囨敞 | 浜у搧澶囨敞 | 鍚� |
+
+---
+
+## 涓夈�佹帴鍙h鎯�
+
+### 3.1 涓嬭浇鎶ヤ环瀵煎叆妯℃澘
+
+**璇锋眰**
+```
+GET /sales/quotation/downloadTemplate
+```
+
+**鍝嶅簲**
+- Excel鏂囦欢涓嬭浇锛屽寘鍚袱涓猄heet锛�
+  - 鎶ヤ环鍗曟暟鎹紙鍚ず渚嬫暟鎹級
+  - 鎶ヤ环浜у搧鏁版嵁锛堝惈绀轰緥鏁版嵁锛�
+
+### 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鏂囦欢鏍煎紡閿欒 |
+| 鎶ヤ环鍗曟暟鎹负绌猴紝璇锋鏌ユā鏉垮唴瀹� | 鎶ヤ环鍗昐heet鏃犳暟鎹� |
+
+### 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": "寮犱笁"
+        }
+    ]
+}
+```
+
+---
+
+## 鍥涖�佷笟鍔℃祦绋�
+
+```
+涓嬭浇妯℃澘
+    鈹�
+    鈻�
+濉啓鎶ヤ环鍗曟暟鎹甋heet锛堟瘡琛屼竴涓姤浠峰崟锛�
+濉啓鎶ヤ环浜у搧鏁版嵁Sheet锛堥�氳繃鎶ヤ环鍗曞彿鍏宠仈锛�
+    鈹�
+    鈻�
+涓婁紶鏂囦欢
+    鈹�
+    鈻�
+妫�鏌ュ鎵规ā鏉挎槸鍚﹀瓨鍦�
+    鈹�
+    鈹溾攢鈹� 涓嶅瓨鍦� 鈹�鈹�鈻� 閿欒锛�"璇峰厛閰嶇疆鎶ヤ环瀹℃壒妯℃澘锛屾棤娉曞鍏�"
+    鈹�
+    鈹斺攢鈹� 瀛樺湪 鈹�鈹�鈻� 瑙f瀽涓や釜Sheet鏁版嵁
+    鈹�
+    鈻�
+閬嶅巻鎶ヤ环鍗曟暟鎹�
+    鈹�
+    鈹溾攢鈹� 妫�鏌ユ姤浠峰崟鍙锋槸鍚﹀凡瀛樺湪 鈹�鈹�鈻� 宸插瓨鍦ㄥ垯璺宠繃
+    鈹�
+    鈹溾攢鈹� 鍒涘缓鎶ヤ环鍗曡褰�
+    鈹溾攢鈹� 鍏宠仈浜у搧鏁版嵁锛堥�氳繃鎶ヤ环鍗曞彿鍖归厤锛�
+    鈹溾攢鈹� 璁板綍闄嶄环鍘嗗彶锛堝姣斿巻鍙蹭环鏍硷級
+    鈹斺攢鈹� 鍒涘缓瀹℃壒娴佺▼
+    鈹�
+    鈻�
+杩斿洖瀵煎叆缁撴灉
+```
+
+---
+
+## 浜斻�佺ず渚嬫暟鎹�
+
+**鎶ヤ环鍗曟暟鎹甋heet**
+| 鎶ヤ环鍗曞彿 | 瀹㈡埛鍚嶇О | 涓氬姟鍛� | 浠樻鏂瑰紡 | 浜よ揣鍛ㄦ湡 |
+|----------|----------|--------|----------|----------|
+| 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`
\ No newline at end of file
diff --git a/docs/sales_quotation_price_history.sql b/docs/sales_quotation_price_history.sql
new file mode 100644
index 0000000..e69de81
--- /dev/null
+++ b/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 '鎶ヤ环鍗旾D',
+    `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 '鎿嶄綔浜篒D',
+    `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 '鎿嶄綔浜篒D',
+    `create_user_name` VARCHAR(100) DEFAULT NULL COMMENT '鎿嶄綔浜哄鍚�',
+    `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '瀵煎叆鏃堕棿',
+    `audit_user` BIGINT DEFAULT NULL COMMENT '瀹℃牳浜篒D',
+    `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='閿�鍞姤浠峰鍏ヨ褰曡〃';
diff --git a/src/main/java/com/ruoyi/common/utils/poi/ExcelUtil.java b/src/main/java/com/ruoyi/common/utils/poi/ExcelUtil.java
index d103265..167f4b4 100644
--- a/src/main/java/com/ruoyi/common/utils/poi/ExcelUtil.java
+++ b/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;
     }
+
+    /**
+     * 瀵煎嚭澶歋heet Excel妯℃澘锛堥潤鎬佹柟娉曪級
+     * 鏀寔涓嶅悓绫诲瀷鐨凞TO瀵煎嚭鍒颁笉鍚岀殑Sheet
+     *
+     * @param response HTTP鍝嶅簲
+     * @param sheetDataMap Map<Sheet鍚嶇О, SheetData>锛孲heetData鍖呭惈鏁版嵁鍒楄〃鍜屽搴旂殑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("瀵煎嚭澶歋heet 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 });
+                }
+            }
+        }
+
+        // 鎸塻ort鎺掑簭
+        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);
+        }
+    }
 }
diff --git a/src/main/java/com/ruoyi/framework/web/exception/GlobalExceptionHandler.java b/src/main/java/com/ruoyi/framework/web/exception/GlobalExceptionHandler.java
index f0467b3..1789bb3 100644
--- a/src/main/java/com/ruoyi/framework/web/exception/GlobalExceptionHandler.java
+++ b/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)
diff --git a/src/main/java/com/ruoyi/sales/controller/SalesQuotationController.java b/src/main/java/com/ruoyi/sales/controller/SalesQuotationController.java
index f9251f8..9e174ef 100644
--- a/src/main/java/com/ruoyi/sales/controller/SalesQuotationController.java
+++ b/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));
+    }
 }
diff --git a/src/main/java/com/ruoyi/sales/dto/SalesQuotationImportDto.java b/src/main/java/com/ruoyi/sales/dto/SalesQuotationImportDto.java
new file mode 100644
index 0000000..21e3238
--- /dev/null
+++ b/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;
+
+/**
+ * 閿�鍞姤浠峰鍏TO锛堝吋瀹瑰崟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;
+}
\ No newline at end of file
diff --git a/src/main/java/com/ruoyi/sales/dto/SalesQuotationMainImportDto.java b/src/main/java/com/ruoyi/sales/dto/SalesQuotationMainImportDto.java
new file mode 100644
index 0000000..93d07ee
--- /dev/null
+++ b/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;
+
+/**
+ * 閿�鍞姤浠蜂富琛ㄥ鍏TO锛堟姤浠峰崟鏁版嵁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;
+}
\ No newline at end of file
diff --git a/src/main/java/com/ruoyi/sales/dto/SalesQuotationProductImportDto.java b/src/main/java/com/ruoyi/sales/dto/SalesQuotationProductImportDto.java
new file mode 100644
index 0000000..9a86a9b
--- /dev/null
+++ b/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;
+
+/**
+ * 閿�鍞姤浠蜂骇鍝佸鍏TO锛堟姤浠蜂骇鍝佹暟鎹甋heet锛�
+ */
+@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;
+}
\ No newline at end of file
diff --git a/src/main/java/com/ruoyi/sales/mapper/SalesQuotationImportLogMapper.java b/src/main/java/com/ruoyi/sales/mapper/SalesQuotationImportLogMapper.java
new file mode 100644
index 0000000..64899cf
--- /dev/null
+++ b/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> {
+}
\ No newline at end of file
diff --git a/src/main/java/com/ruoyi/sales/mapper/SalesQuotationPriceHistoryMapper.java b/src/main/java/com/ruoyi/sales/mapper/SalesQuotationPriceHistoryMapper.java
new file mode 100644
index 0000000..3385450
--- /dev/null
+++ b/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> {
+}
\ No newline at end of file
diff --git a/src/main/java/com/ruoyi/sales/pojo/SalesQuotationImportLog.java b/src/main/java/com/ruoyi/sales/pojo/SalesQuotationImportLog.java
new file mode 100644
index 0000000..e6a6acf
--- /dev/null
+++ b/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 = "鎿嶄綔浜篒D")
+    private Long createUser;
+
+    @Schema(description = "鎿嶄綔浜哄鍚�")
+    private String createUserName;
+
+    @Schema(description = "瀵煎叆鏃堕棿")
+    @TableField(fill = FieldFill.INSERT)
+    private LocalDateTime createTime;
+
+    @Schema(description = "瀹℃牳浜篒D")
+    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;
+}
\ No newline at end of file
diff --git a/src/main/java/com/ruoyi/sales/pojo/SalesQuotationPriceHistory.java b/src/main/java/com/ruoyi/sales/pojo/SalesQuotationPriceHistory.java
new file mode 100644
index 0000000..e0c45fc
--- /dev/null
+++ b/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 = "鎶ヤ环鍗旾D")
+    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 = "鎿嶄綔浜篒D")
+    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;
+}
\ No newline at end of file
diff --git a/src/main/java/com/ruoyi/sales/service/SalesQuotationService.java b/src/main/java/com/ruoyi/sales/service/SalesQuotationService.java
index 7f29a7f..805ab1e 100644
--- a/src/main/java/com/ruoyi/sales/service/SalesQuotationService.java
+++ b/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);
 }
diff --git a/src/main/java/com/ruoyi/sales/service/impl/SalesQuotationServiceImpl.java b/src/main/java/com/ruoyi/sales/service/impl/SalesQuotationServiceImpl.java
index c2748f0..c1084b3 100644
--- a/src/main/java/com/ruoyi/sales/service/impl/SalesQuotationServiceImpl.java
+++ b/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("璇峰厛閰嶇疆鎶ヤ环瀹℃壒妯℃澘锛屾棤娉曞鍏�");
+        }
+
+        // 瑙f瀽澶歋heet 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));
+    }
 }
diff --git a/src/main/resources/mapper/sales/SalesQuotationImportLogMapper.xml b/src/main/resources/mapper/sales/SalesQuotationImportLogMapper.xml
new file mode 100644
index 0000000..19a487c
--- /dev/null
+++ b/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>
\ No newline at end of file
diff --git a/src/main/resources/mapper/sales/SalesQuotationPriceHistoryMapper.xml b/src/main/resources/mapper/sales/SalesQuotationPriceHistoryMapper.xml
new file mode 100644
index 0000000..69ece09
--- /dev/null
+++ b/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>
\ No newline at end of file

--
Gitblit v1.9.3