From f93a8f3d091f1e9d1b9c2df246ad39df0e14cfdd Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期二, 12 五月 2026 15:22:36 +0800
Subject: [PATCH] feat(account): 实现总账科目树形结构及关联功能

---
 src/main/java/com/ruoyi/account/mapper/financial/FinVoucherMapper.java                    |   12 
 src/main/java/com/ruoyi/account/bean/dto/financial/FinLedgerQueryDto.java                 |   25 
 doc/20260512_财务管理模块前端联调文档.md                                                              |  288 +++++
 src/main/java/com/ruoyi/account/pojo/financial/FinFixedAsset.java                         |  101 +
 src/main/java/com/ruoyi/account/mapper/financial/FinFixedAssetMapper.java                 |   12 
 src/main/java/com/ruoyi/account/bean/dto/financial/FinVoucherStatusDto.java               |   15 
 src/main/java/com/ruoyi/account/service/impl/financial/FinLedgerServiceImpl.java          |  206 +++
 src/main/resources/mapper/account/AccountSubjectMapper.xml                                |   10 
 src/main/java/com/ruoyi/account/controller/financial/FinFixedAssetController.java         |   63 +
 doc/20260512_add_parent_id_to_account_subject.sql                                         |    5 
 src/main/java/com/ruoyi/account/service/impl/financial/FinVoucherServiceImpl.java         |  299 +++++
 src/main/java/com/ruoyi/account/service/AccountSubjectService.java                        |    8 
 src/main/java/com/ruoyi/account/mapper/financial/FinIntangibleAssetMapper.java            |   12 
 src/main/java/com/ruoyi/account/bean/dto/financial/FinFixedAssetDto.java                  |   13 
 doc/20260512_AccountSubject树形改造前端修改文档.md                                                  |  185 +++
 src/main/java/com/ruoyi/account/service/impl/financial/FinIntangibleAssetServiceImpl.java |  250 ++++
 src/main/java/com/ruoyi/account/service/financial/FinLedgerService.java                   |   17 
 src/main/resources/application-dev.yml                                                    |    2 
 src/main/java/com/ruoyi/account/controller/financial/FinLedgerController.java             |   39 
 src/main/java/com/ruoyi/account/bean/vo/AccountSubjectVo.java                             |   13 
 src/main/java/com/ruoyi/account/service/financial/FinVoucherService.java                  |   27 
 src/main/java/com/ruoyi/account/bean/vo/financial/FinLedgerEntryRecordVo.java             |   43 
 doc/20260512_create_financial_management_tables.sql                                       |  104 ++
 src/main/java/com/ruoyi/account/service/financial/FinFixedAssetService.java               |   26 
 src/main/java/com/ruoyi/account/service/impl/AccountSubjectServiceImpl.java               |  362 ++++++
 src/main/java/com/ruoyi/account/pojo/financial/FinVoucherEntry.java                       |   85 +
 src/main/java/com/ruoyi/account/service/financial/FinIntangibleAssetService.java          |   26 
 src/main/java/com/ruoyi/account/mapper/AccountSubjectMapper.java                          |    5 
 src/main/java/com/ruoyi/account/bean/dto/financial/FinVoucherEntryDto.java                |   13 
 src/main/java/com/ruoyi/account/pojo/AccountSubject.java                                  |    6 
 src/main/java/com/ruoyi/account/pojo/financial/FinIntangibleAsset.java                    |   98 +
 src/main/java/com/ruoyi/account/bean/dto/financial/FinVoucherPageDto.java                 |   37 
 src/main/java/com/ruoyi/account/mapper/financial/FinVoucherEntryMapper.java               |   28 
 src/main/java/com/ruoyi/account/bean/vo/financial/FinLedgerRowVo.java                     |   53 +
 src/main/java/com/ruoyi/account/bean/dto/financial/FinIdBatchDto.java                     |   17 
 src/main/java/com/ruoyi/account/bean/dto/financial/FinIntangibleAssetDto.java             |   13 
 src/main/java/com/ruoyi/account/bean/dto/financial/FinVoucherDto.java                     |   20 
 src/main/java/com/ruoyi/account/controller/financial/FinIntangibleAssetController.java    |   63 +
 src/main/java/com/ruoyi/account/service/impl/financial/FinFixedAssetServiceImpl.java      |  231 ++++
 src/main/java/com/ruoyi/account/bean/dto/financial/FinDetailLedgerQueryDto.java           |   22 
 src/main/java/com/ruoyi/account/pojo/financial/FinVoucher.java                            |   83 +
 src/main/java/com/ruoyi/account/controller/financial/FinVoucherController.java            |   69 +
 src/main/java/com/ruoyi/account/bean/vo/financial/FinVoucherDetailVo.java                 |   21 
 src/main/java/com/ruoyi/account/controller/AccountSubjectController.java                  |    8 
 src/main/resources/mapper/account/financial/FinVoucherEntryMapper.xml                     |   74 +
 45 files changed, 3,083 insertions(+), 26 deletions(-)

diff --git "a/doc/20260512_AccountSubject\346\240\221\345\275\242\346\224\271\351\200\240\345\211\215\347\253\257\344\277\256\346\224\271\346\226\207\346\241\243.md" "b/doc/20260512_AccountSubject\346\240\221\345\275\242\346\224\271\351\200\240\345\211\215\347\253\257\344\277\256\346\224\271\346\226\207\346\241\243.md"
new file mode 100644
index 0000000..c798c40
--- /dev/null
+++ "b/doc/20260512_AccountSubject\346\240\221\345\275\242\346\224\271\351\200\240\345\211\215\347\253\257\344\277\256\346\224\271\346\226\207\346\241\243.md"
@@ -0,0 +1,185 @@
+# AccountSubject 鏍戝舰鏀归�犲墠绔慨鏀规枃妗�
+
+鏇存柊鏃堕棿锛�2026-05-12
+
+## 1. 鍙樻洿鑳屾櫙
+
+`AccountSubjectController` 宸叉敼涓虹埗瀛愬眰绾ч�掑綊妯″紡锛宍/accountSubject/list` 鐜板湪杩斿洖鏍戝舰缁撴瀯锛坄children` 閫掑綊锛夛紝涓嶅啀鏄崟绾殑骞抽摵鍒楄〃銆�
+
+---
+
+## 2. 鍚庣鎺ュ彛鍙樺寲
+
+### 2.1 鏌ヨ鎺ュ彛锛堝凡鍙樻洿涓烘爲锛�
+
+- URL锛歚GET /accountSubject/list`
+- 鍏ュ弬锛氫繚鎸佷笉鍙橈紙`current,size,subjectCode,subjectName,subjectType,status`锛�
+- 鍑哄弬锛氫粛鏄垎椤靛3锛坄records,total`锛夛紝浣� `records` 鍙樹负鏍戣妭鐐规暟缁勶紙鏍硅妭鐐瑰垎椤碉紝瀛愯妭鐐归�掑綊鍐呭祵锛�
+
+绀轰緥锛�
+
+```json
+{
+  "code": 200,
+  "msg": "鎿嶄綔鎴愬姛",
+  "data": {
+    "records": [
+      {
+        "id": 1,
+        "parentId": null,
+        "subjectCode": "1002",
+        "subjectName": "閾惰瀛樻",
+        "subjectType": "璧勪骇绫�",
+        "balanceDirection": "鍊熸柟",
+        "status": 0,
+        "leaf": false,
+        "children": [
+          {
+            "id": 2,
+            "parentId": 1,
+            "subjectCode": "100201",
+            "subjectName": "宸ヨ瀛樻",
+            "subjectType": "璧勪骇绫�",
+            "balanceDirection": "鍊熸柟",
+            "status": 0,
+            "leaf": true,
+            "children": []
+          }
+        ]
+      }
+    ],
+    "total": 1
+  }
+}
+```
+
+### 2.2 鏂板/缂栬緫鎺ュ彛瀛楁鍙樺寲
+
+- `POST /accountSubject/add`
+- `PUT /accountSubject/edit`
+
+鏂板鏀寔瀛楁锛�
+
+- `parentId`锛氱埗鑺傜偣ID锛堜负绌鸿〃绀烘牴鑺傜偣锛�
+
+绀轰緥锛�
+
+```json
+{
+  "id": 2,
+  "parentId": 1,
+  "subjectCode": "100201",
+  "subjectName": "宸ヨ瀛樻",
+  "subjectType": "璧勪骇绫�",
+  "balanceDirection": "鍊熸柟",
+  "status": 0,
+  "remark": ""
+}
+```
+
+### 2.3 鍒犻櫎鎺ュ彛琛屼负鍙樺寲
+
+- `DELETE /accountSubject/remove/{ids}`
+
+琛屼负锛�
+
+1. 鍒犻櫎鐖惰妭鐐规椂浼氶�掑綊鍒犻櫎鎵�鏈夊瓙瀛欒妭鐐广��  
+2. 鑻ヤ换鎰忓緟鍒犻櫎鑺傜偣锛堝惈瀛愬瓩锛夊凡琚� `fin_voucher_entry.subject_code` 寮曠敤锛屽垯鏁翠綋鍒犻櫎澶辫触銆�
+
+---
+
+## 3. 鍓嶇鏀归�犳竻鍗�
+
+## 3.1 鎬昏处绉戠洰绠$悊椤�
+
+鏂囦欢锛歚src/views/financialManagement/generalLedger/index.vue`
+
+### 蹇呮敼椤�
+
+1. 鏂板鈥滅埗绉戠洰鈥濋�夋嫨鎺т欢锛坄el-cascader` 鎴� `el-tree-select`锛夛紝淇濆瓨鏃跺甫 `parentId`銆�  
+2. 鍒楄〃鏀逛负鏍戣〃灞曠ず锛堟帹鑽� `el-table` + `row-key` + `tree-props`锛夈��  
+3. 鎼滅储閫昏緫淇濇寔涓嶅彉锛屼絾瑕佸吋瀹� `records` 涓烘爲缁撴瀯銆�
+
+---
+
+## 3.2 鍑瘉椤电鐩笅鎷�
+
+鏂囦欢锛歚src/views/financialManagement/voucher/index.vue`
+
+褰撳墠鍑瘉鍒嗗綍浣跨敤 `el-select`锛堝钩閾猴級銆�  
+鍚庣宸茶繑鍥炴爲锛岄渶瑕佸墠绔墎骞冲寲鍚庡啀缁戝畾涓嬫媺銆�
+
+绀轰緥锛堝彲澶嶇敤锛夛細
+
+```js
+const flattenSubjectTree = (nodes, result = []) => {
+  (nodes || []).forEach(node => {
+    result.push({
+      code: node.subjectCode,
+      name: node.subjectName,
+      id: node.id,
+      parentId: node.parentId
+    });
+    if (node.children?.length) {
+      flattenSubjectTree(node.children, result);
+    }
+  });
+  return result;
+};
+
+// list 鎺ュ彛杩斿洖鍚庯細
+const treeRecords = response.data?.records || [];
+subjectList.value = flattenSubjectTree(treeRecords);
+```
+
+---
+
+## 3.3 绉戠洰鎬昏处/鏄庣粏璐﹂〉绾ц仈
+
+鏂囦欢锛�
+
+- `src/views/financialManagement/voucher/generalLedger.vue`
+- `src/views/financialManagement/voucher/detailLedger.vue`
+
+褰撳墠閫昏緫鏄妸 `records` 寮哄埗鏄犲皠鎴� `children: []`锛岄渶瑕佸垹闄よ繖娈碘�滃钩閾烘敼閫犫�濓紝鐩存帴浣跨敤鍚庣鏍戙��
+
+绀轰緥锛堝彲澶嶇敤锛夛細
+
+```js
+const toCascaderTree = (nodes = []) =>
+  nodes
+    .filter(item => item.subjectCode && item.subjectName)
+    .map(item => ({
+      code: item.subjectCode,
+      name: item.subjectName,
+      children: toCascaderTree(item.children || [])
+    }));
+
+subjectOptions.value = toCascaderTree(response.data?.records || []);
+```
+
+---
+
+## 4. 寤鸿鐨勫墠绔瓧娈电害瀹�
+
+寤鸿鍦ㄥ墠绔� `form` 澧炲姞锛�
+
+- `parentId: null`
+
+骞跺湪缂栬緫鍥炲~鏃朵繚鎸� `parentId`銆�
+
+---
+
+## 5. 鑱旇皟娉ㄦ剰浜嬮」
+
+1. `/accountSubject/list` 鐨� `total` 鏄牴鑺傜偣鏁伴噺锛屼笉鏄叏閲忚妭鐐规暟銆�  
+2. 鑻ラ〉闈粛鎸夊钩閾� `records.map(...)` 澶勭悊锛屼細涓㈠け瀛愯妭鐐广��  
+3. 鍒犻櫎绉戠洰澶辫触鏃讹紝浼樺厛妫�鏌ユ槸鍚﹁鍑瘉鍒嗗綍寮曠敤銆�  
+4. 淇濆瓨澶辫触鍑虹幇鈥滅埗绉戠洰涓嶈兘鏄綋鍓嶇鐩垨鍏跺瓙绉戠洰鈥濇椂锛岄渶瑕佸墠绔檺鍒剁埗鑺傜偣鍙�夎寖鍥淬��
+
+---
+
+## 6. 鏁版嵁搴撳瓧娈佃姹�
+
+`account_subject` 闇�瑕佸寘鍚� `parent_id` 瀛楁锛坄bigint`锛屽彲绌猴級銆�  
+鑻ョ嚎涓婂簱灏氭湭娣诲姞锛岃鍏堟墽琛� DDL 鍐嶈仈璋冦��
diff --git a/doc/20260512_add_parent_id_to_account_subject.sql b/doc/20260512_add_parent_id_to_account_subject.sql
new file mode 100644
index 0000000..cf33a33
--- /dev/null
+++ b/doc/20260512_add_parent_id_to_account_subject.sql
@@ -0,0 +1,5 @@
+-- account_subject 澧炲姞鐖剁骇绉戠洰瀛楁锛堟爲褰㈢粨鏋勶級
+ALTER TABLE `account_subject`
+ADD COLUMN `parent_id` bigint NULL COMMENT '鐖剁鐩甀D锛堜负绌鸿〃绀烘牴鑺傜偣锛�' AFTER `id`;
+
+CREATE INDEX `idx_account_subject_parent_id` ON `account_subject` (`parent_id`);
diff --git a/doc/20260512_create_financial_management_tables.sql b/doc/20260512_create_financial_management_tables.sql
new file mode 100644
index 0000000..db3caad
--- /dev/null
+++ b/doc/20260512_create_financial_management_tables.sql
@@ -0,0 +1,104 @@
+-- 璐㈠姟绠$悊妯″潡寤鸿〃鑴氭湰锛堝浐瀹氳祫浜�/鏃犲舰璧勪骇/鍑瘉/绉戠洰璐︼級
+-- 璇存槑锛�
+-- 1) 鎬昏处绉戠洰缁х画澶嶇敤宸叉湁琛� account_subject锛屼笉閲嶅鍒涘缓 fin_account_subject銆�
+-- 2) 閲戦瀛楁缁熶竴 decimal(18,2)銆�
+
+CREATE TABLE IF NOT EXISTS `fin_fixed_asset` (
+  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '涓婚敭ID',
+  `asset_code` varchar(64) NOT NULL COMMENT '璧勪骇缂栧彿',
+  `asset_name` varchar(128) NOT NULL COMMENT '璧勪骇鍚嶇О',
+  `category` varchar(64) NOT NULL COMMENT '璧勪骇绫诲埆',
+  `specification` varchar(255) DEFAULT NULL COMMENT '瑙勬牸鍨嬪彿',
+  `purchase_date` date NOT NULL COMMENT '璐疆鏃ユ湡',
+  `original_value` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '璧勪骇鍘熷��',
+  `useful_life` int NOT NULL DEFAULT '1' COMMENT '浣跨敤骞撮檺(骞�)',
+  `residual_rate` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '娈嬪�肩巼(%)',
+  `accumulated_depreciation` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '绱鎶樻棫',
+  `net_value` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '鍑�鍊�',
+  `location` varchar(255) DEFAULT NULL COMMENT '瀛樻斁鍦扮偣',
+  `department` varchar(128) DEFAULT NULL COMMENT '浣跨敤閮ㄩ棬',
+  `keeper` varchar(64) DEFAULT NULL COMMENT '淇濈浜�',
+  `status` varchar(32) NOT NULL DEFAULT 'in_use' COMMENT '鐘舵��: in_use/idle/repair/scrapped',
+  `remark` varchar(500) DEFAULT NULL COMMENT '澶囨敞',
+  `create_user` varchar(64) DEFAULT NULL COMMENT '鍒涘缓浜�',
+  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '鍒涘缓鏃堕棿',
+  `update_user` varchar(64) DEFAULT NULL COMMENT '淇敼浜�',
+  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '淇敼鏃堕棿',
+  `dept_id` bigint DEFAULT NULL COMMENT '閮ㄩ棬ID',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_fin_fixed_asset_code` (`asset_code`),
+  KEY `idx_fin_fixed_asset_status` (`status`),
+  KEY `idx_fin_fixed_asset_category` (`category`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='鍥哄畾璧勪骇';
+
+CREATE TABLE IF NOT EXISTS `fin_intangible_asset` (
+  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '涓婚敭ID',
+  `asset_code` varchar(64) NOT NULL COMMENT '璧勪骇缂栧彿',
+  `asset_name` varchar(128) NOT NULL COMMENT '璧勪骇鍚嶇О',
+  `category` varchar(64) NOT NULL COMMENT '璧勪骇绫诲埆',
+  `certificate_no` varchar(128) DEFAULT NULL COMMENT '璇佷功缂栧彿',
+  `acquisition_date` date NOT NULL COMMENT '鍙栧緱鏃ユ湡',
+  `original_value` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '璧勪骇鍘熷��',
+  `amortization_period` int NOT NULL DEFAULT '1' COMMENT '鎽婇攢骞撮檺(骞�)',
+  `residual_rate` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '娈嬪�肩巼(%)',
+  `accumulated_amortization` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '绱鎽婇攢',
+  `net_value` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '鍑�鍊�',
+  `validity_date` date DEFAULT NULL COMMENT '鏈夋晥鏈熻嚦',
+  `status` varchar(32) NOT NULL DEFAULT 'in_use' COMMENT '鐘舵��: in_use/expired/amortized',
+  `description` varchar(1000) DEFAULT NULL COMMENT '璧勪骇鎻忚堪',
+  `remark` varchar(500) DEFAULT NULL COMMENT '澶囨敞',
+  `create_user` varchar(64) DEFAULT NULL COMMENT '鍒涘缓浜�',
+  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '鍒涘缓鏃堕棿',
+  `update_user` varchar(64) DEFAULT NULL COMMENT '淇敼浜�',
+  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '淇敼鏃堕棿',
+  `dept_id` bigint DEFAULT NULL COMMENT '閮ㄩ棬ID',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_fin_intangible_asset_code` (`asset_code`),
+  KEY `idx_fin_intangible_asset_status` (`status`),
+  KEY `idx_fin_intangible_asset_category` (`category`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='鏃犲舰璧勪骇';
+
+CREATE TABLE IF NOT EXISTS `fin_voucher` (
+  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '涓婚敭ID',
+  `voucher_no` varchar(64) NOT NULL COMMENT '鍑瘉瀛楀彿',
+  `voucher_date` date NOT NULL COMMENT '鍑瘉鏃ユ湡',
+  `summary` varchar(500) DEFAULT NULL COMMENT '鎽樿',
+  `debit` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '鍊熸柟鍚堣',
+  `credit` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '璐锋柟鍚堣',
+  `creator` varchar(64) DEFAULT NULL COMMENT '鍒跺崟浜�',
+  `status` varchar(32) NOT NULL DEFAULT 'unposted' COMMENT '鐘舵��: unposted/posted/cancelled',
+  `attachment_count` int NOT NULL DEFAULT '0' COMMENT '闄勪欢寮犳暟',
+  `remark` varchar(500) DEFAULT NULL COMMENT '澶囨敞',
+  `create_user` varchar(64) DEFAULT NULL COMMENT '鍒涘缓浜�',
+  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '鍒涘缓鏃堕棿',
+  `update_user` varchar(64) DEFAULT NULL COMMENT '淇敼浜�',
+  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '淇敼鏃堕棿',
+  `dept_id` bigint DEFAULT NULL COMMENT '閮ㄩ棬ID',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_fin_voucher_no` (`voucher_no`),
+  KEY `idx_fin_voucher_date` (`voucher_date`),
+  KEY `idx_fin_voucher_status` (`status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='鍑瘉涓昏〃';
+
+CREATE TABLE IF NOT EXISTS `fin_voucher_entry` (
+  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '涓婚敭ID',
+  `voucher_id` bigint NOT NULL COMMENT '鍑瘉ID',
+  `row_no` int NOT NULL DEFAULT '1' COMMENT '琛屽彿',
+  `subject_code` varchar(64) NOT NULL COMMENT '绉戠洰缂栫爜',
+  `subject_name` varchar(128) DEFAULT NULL COMMENT '绉戠洰鍚嶇О',
+  `summary` varchar(500) DEFAULT NULL COMMENT '鎽樿',
+  `debit` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '鍊熸柟閲戦',
+  `credit` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '璐锋柟閲戦',
+  `auxiliary_type` varchar(32) DEFAULT NULL COMMENT '杈呭姪鏍哥畻绫诲瀷',
+  `auxiliary_id` varchar(64) DEFAULT NULL COMMENT '杈呭姪鏍哥畻瀵硅薄ID',
+  `auxiliary_name` varchar(128) DEFAULT NULL COMMENT '杈呭姪鏍哥畻瀵硅薄鍚嶇О',
+  `create_user` varchar(64) DEFAULT NULL COMMENT '鍒涘缓浜�',
+  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '鍒涘缓鏃堕棿',
+  `update_user` varchar(64) DEFAULT NULL COMMENT '淇敼浜�',
+  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '淇敼鏃堕棿',
+  `dept_id` bigint DEFAULT NULL COMMENT '閮ㄩ棬ID',
+  PRIMARY KEY (`id`),
+  KEY `idx_fin_voucher_entry_voucher` (`voucher_id`),
+  KEY `idx_fin_voucher_entry_subject` (`subject_code`),
+  KEY `idx_fin_voucher_entry_aux` (`auxiliary_type`, `auxiliary_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='鍑瘉鍒嗗綍';
diff --git "a/doc/20260512_\350\264\242\345\212\241\347\256\241\347\220\206\346\250\241\345\235\227\345\211\215\347\253\257\350\201\224\350\260\203\346\226\207\346\241\243.md" "b/doc/20260512_\350\264\242\345\212\241\347\256\241\347\220\206\346\250\241\345\235\227\345\211\215\347\253\257\350\201\224\350\260\203\346\226\207\346\241\243.md"
new file mode 100644
index 0000000..1804d82
--- /dev/null
+++ "b/doc/20260512_\350\264\242\345\212\241\347\256\241\347\220\206\346\250\241\345\235\227\345\211\215\347\253\257\350\201\224\350\260\203\346\226\207\346\241\243.md"
@@ -0,0 +1,288 @@
+# 璐㈠姟绠$悊妯″潡鍓嶇鑱旇皟鏂囨。锛坅ccount 妯″潡锛�
+
+鏇存柊鏃堕棿锛�2026-05-12
+
+## 1. 閫氱敤璇存槑
+
+### 1.1 鍝嶅簲缁撴瀯
+鎴愬姛鍝嶅簲锛�
+
+```json
+{
+  "code": 200,
+  "msg": "鎿嶄綔鎴愬姛",
+  "data": {}
+}
+```
+
+涓氬姟鏍¢獙澶辫触锛堜緥濡傚�熻捶涓嶅钩琛°�佸繀濉己澶憋級鐢卞叏灞�寮傚父杩斿洖锛�
+
+```json
+{
+  "code": 500,
+  "msg": "閿欒淇℃伅",
+  "data": null
+}
+```
+
+### 1.2 鍒嗛〉缁撴瀯
+鍒嗛〉鎺ュ彛缁熶竴浣跨敤 MyBatis-Plus `Page`锛�
+- 璇锋眰鍙傛暟锛歚current`銆乣size`
+- 杩斿洖锛歚data.records`銆乣data.total`
+
+---
+
+## 2. 鎬昏处绉戠洰锛堝凡鍦ㄥ師鎺ュ彛涓婂寮烘牎楠岋級
+
+鎺ュ彛淇濇寔涓嶅彉锛�
+- `GET /accountSubject/list`
+- `POST /accountSubject/add`
+- `PUT /accountSubject/edit`
+- `DELETE /accountSubject/remove/{ids}`
+- `POST /accountSubject/export`
+
+鏂板瑙勫垯锛�
+1. `subjectCode`銆乣subjectName`銆乣subjectType` 蹇呭~銆�
+2. `subjectCode` 鍞竴鏍¢獙銆�
+3. 鍒犻櫎鍓嶅仛寮曠敤鏍¢獙锛氳嫢琚嚟璇佸垎褰�(`fin_voucher_entry.subject_code`)寮曠敤锛岀姝㈠垹闄ゃ��
+
+---
+
+## 3. 鍥哄畾璧勪骇
+
+Base URL锛歚/financial/fixedAsset`
+
+### 3.1 鍒嗛〉鏌ヨ
+- `GET /page`
+- Query锛歚current,size,assetCode,assetName,category,status`
+
+### 3.2 鏂板
+- `POST /add`
+- Body锛圝SON锛夛細
+
+```json
+{
+  "assetCode": "GD20260512001",
+  "assetName": "鍔炲叕鐢佃剳",
+  "category": "electronic",
+  "specification": "ThinkPad X1",
+  "purchaseDate": "2026-05-01",
+  "originalValue": 8000.00,
+  "usefulLife": 5,
+  "residualRate": 5.00,
+  "location": "璐㈠姟閮�",
+  "department": "璐㈠姟閮�",
+  "keeper": "寮犱笁",
+  "status": "in_use",
+  "remark": "绀轰緥"
+}
+```
+
+### 3.3 淇敼
+- `PUT /update`
+- Body锛氬悓鏂板锛岄渶鍖呭惈 `id`
+
+### 3.4 鍒犻櫎
+- `DELETE /delete?ids=1&ids=2`
+
+### 3.5 鎶樻棫璁℃彁锛堟寜鏈堬級
+- `POST /depreciate`
+- Body 鍙�夛細
+  - 鍏ㄩ儴鍦ㄧ敤璧勪骇锛歚{}`
+  - 鎸囧畾璧勪骇锛歚{"ids":[1,2,3]}`
+
+鏍稿績鍏紡锛�
+- `monthlyDepreciation = originalValue * (1 - residualRate/100) / (usefulLife*12)`
+- `accumulatedDepreciation += monthlyDepreciation`
+- `netValue = originalValue - accumulatedDepreciation`
+
+鐘舵�佸缓璁�硷細
+- `in_use`銆乣idle`銆乣repair`銆乣scrapped`
+
+---
+
+## 4. 鏃犲舰璧勪骇
+
+Base URL锛歚/financial/intangibleAsset`
+
+### 4.1 鍒嗛〉鏌ヨ
+- `GET /page`
+- Query锛歚current,size,assetCode,assetName,category,status`
+
+### 4.2 鏂板
+- `POST /add`
+
+```json
+{
+  "assetCode": "WX20260512001",
+  "assetName": "ERP杞欢璁稿彲",
+  "category": "software",
+  "certificateNo": "SW-001",
+  "acquisitionDate": "2026-05-01",
+  "originalValue": 50000.00,
+  "amortizationPeriod": 10,
+  "residualRate": 0.00,
+  "validityDate": "2036-05-01",
+  "status": "in_use",
+  "description": "绀轰緥",
+  "remark": ""
+}
+```
+
+### 4.3 淇敼
+- `PUT /update`
+- Body锛氬悓鏂板锛岄渶鍖呭惈 `id`
+
+### 4.4 鍒犻櫎
+- `DELETE /delete?ids=1&ids=2`
+
+### 4.5 鎽婇攢璁℃彁锛堟寜鏈堬級
+- `POST /amortize`
+- Body 鍙�夛細
+  - 鍏ㄩ儴鍦ㄧ敤璧勪骇锛歚{}`
+  - 鎸囧畾璧勪骇锛歚{"ids":[1,2,3]}`
+
+鏍稿績鍏紡锛�
+- `monthlyAmortization = originalValue * (1 - residualRate/100) / (amortizationPeriod*12)`
+- `accumulatedAmortization += monthlyAmortization`
+- `netValue = originalValue - accumulatedAmortization`
+- 褰� `netValue <= 0` 鏃讹細`netValue=0` 涓� `status=amortized`
+
+鐘舵�佸缓璁�硷細
+- `in_use`銆乣expired`銆乣amortized`
+
+---
+
+## 5. 鍑瘉
+
+Base URL锛歚/financial/voucher`
+
+### 5.1 鍒嗛〉鏌ヨ
+- `GET /page`
+- Query锛歚current,size,voucherNo,creator,status,startDate,endDate`
+
+### 5.2 鏂板
+- `POST /add`
+
+```json
+{
+  "voucherNo": "璁�-0001",
+  "voucherDate": "2026-05-12",
+  "summary": "閿�鍞敹鍏�",
+  "creator": "寮犱笁",
+  "attachmentCount": 0,
+  "remark": "",
+  "entries": [
+    {
+      "subjectCode": "1002",
+      "subjectName": "閾惰瀛樻",
+      "summary": "鏀跺埌璐ф",
+      "debit": 1000.00,
+      "credit": 0
+    },
+    {
+      "subjectCode": "6001",
+      "subjectName": "涓昏惀涓氬姟鏀跺叆",
+      "summary": "纭鏀跺叆",
+      "debit": 0,
+      "credit": 1000.00
+    }
+  ]
+}
+```
+
+### 5.3 淇敼
+- `PUT /update`
+- Body锛氬悓鏂板锛岄渶鍖呭惈 `id`
+- 浠� `unposted` 鐘舵�佸厑璁镐慨鏀�
+
+### 5.4 杩囪处
+- `POST /post`
+
+```json
+{
+  "id": 1
+}
+```
+
+### 5.5 浣滃簾
+- `POST /cancel`
+
+```json
+{
+  "id": 1
+}
+```
+
+### 5.6 璇︽儏
+- `GET /detail/{id}`
+
+鍏抽敭鏍¢獙锛�
+1. 鍒嗗綍鑷冲皯涓�鏉℃湁鏁堣锛堢鐩笉绌猴紝涓斿�熸柟鎴栬捶鏂� > 0锛夈��
+2. 姣忔潯鏈夋晥鍒嗗綍涓嶈兘鍊熻捶鍚屾椂澶т簬 0銆�
+3. 鍊熻捶骞宠 锛歚sum(debit) == sum(credit)` 涓旈兘 > 0銆�
+4. `subjectCode` 蹇呴』瀛樺湪浜� `account_subject`銆�
+
+鐘舵�佹祦杞細
+- `unposted -> posted`
+- `unposted -> cancelled`
+
+---
+
+## 6. 绉戠洰鎬昏处
+
+### 6.1 鏌ヨ鎺ュ彛
+- `GET /financial/ledger/general`
+
+### 6.2 Query 鍙傛暟
+- `subjectCode`锛堝繀濉級
+- `startMonth`锛圷YYY-MM锛�
+- `endMonth`锛圷YYY-MM锛�
+
+### 6.3 杩斿洖瀛楁
+- `rowType`锛歚opening` / `entry` / `monthly_total` / `yearly_total`
+- `date`
+- `voucherNo`
+- `summary`
+- `debit`
+- `credit`
+- `direction`锛堝��/璐凤級
+- `balance`锛堝�熸璐疯礋锛�
+
+璇存槑锛�
+- 绉戠洰鏀寔鈥滄寚瀹氱鐩強鍏朵笅绾э紙鍓嶇紑鍖归厤锛夆�濇煡璇€��
+- 鑷姩杈撳嚭鈥滄湡鍒濅綑棰� / 鏈湀鍚堣 / 鏈勾绱鈥濄��
+
+---
+
+## 7. 绉戠洰鏄庣粏璐�
+
+### 7.1 鏌ヨ鎺ュ彛
+- `GET /financial/ledger/detail`
+
+### 7.2 Query 鍙傛暟
+- `subjectCode`锛堝繀濉級
+- `auxiliaryType`锛堝彲閫夛細`customer/supplier/department/employee/project`锛�
+- `auxiliaryId`锛堝彲閫夛級
+- `startMonth`锛圷YYY-MM锛�
+- `endMonth`锛圷YYY-MM锛�
+
+### 7.3 杩斿洖瀛楁
+鍚岀鐩�昏处锛�
+- `rowType,date,voucherNo,summary,debit,credit,direction,balance`
+
+---
+
+## 8. 鏁版嵁搴撹剼鏈�
+
+宸叉彁渚涜剼鏈細
+
+- `doc/20260512_create_financial_management_tables.sql`
+
+鍖呭惈锛�
+- `fin_fixed_asset`
+- `fin_intangible_asset`
+- `fin_voucher`
+- `fin_voucher_entry`
+
+鎬昏处绉戠洰澶嶇敤鐜版湁 `account_subject`銆�
diff --git a/src/main/java/com/ruoyi/account/bean/dto/financial/FinDetailLedgerQueryDto.java b/src/main/java/com/ruoyi/account/bean/dto/financial/FinDetailLedgerQueryDto.java
new file mode 100644
index 0000000..84a3676
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/bean/dto/financial/FinDetailLedgerQueryDto.java
@@ -0,0 +1,22 @@
+package com.ruoyi.account.bean.dto.financial;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 绉戠洰鏄庣粏璐︽煡璇㈠弬鏁帮紙鍚緟鍔╂牳绠楁潯浠讹級銆�
+ */
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class FinDetailLedgerQueryDto extends FinLedgerQueryDto {
+
+    /**
+     * 杈呭姪鏍哥畻绫诲瀷锛歝ustomer/supplier/department/employee/project銆�
+     */
+    private String auxiliaryType;
+
+    /**
+     * 杈呭姪鏍哥畻瀵硅薄ID銆�
+     */
+    private String auxiliaryId;
+}
diff --git a/src/main/java/com/ruoyi/account/bean/dto/financial/FinFixedAssetDto.java b/src/main/java/com/ruoyi/account/bean/dto/financial/FinFixedAssetDto.java
new file mode 100644
index 0000000..c6baeca
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/bean/dto/financial/FinFixedAssetDto.java
@@ -0,0 +1,13 @@
+package com.ruoyi.account.bean.dto.financial;
+
+import com.ruoyi.account.pojo.financial.FinFixedAsset;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 鍥哄畾璧勪骇鏌ヨ涓庝繚瀛� DTO銆�
+ */
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class FinFixedAssetDto extends FinFixedAsset {
+}
diff --git a/src/main/java/com/ruoyi/account/bean/dto/financial/FinIdBatchDto.java b/src/main/java/com/ruoyi/account/bean/dto/financial/FinIdBatchDto.java
new file mode 100644
index 0000000..e3023ed
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/bean/dto/financial/FinIdBatchDto.java
@@ -0,0 +1,17 @@
+package com.ruoyi.account.bean.dto.financial;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 鎵归噺ID璇锋眰鍙傛暟銆�
+ */
+@Data
+public class FinIdBatchDto {
+
+    /**
+     * ID闆嗗悎銆�
+     */
+    private List<Long> ids;
+}
diff --git a/src/main/java/com/ruoyi/account/bean/dto/financial/FinIntangibleAssetDto.java b/src/main/java/com/ruoyi/account/bean/dto/financial/FinIntangibleAssetDto.java
new file mode 100644
index 0000000..60dd50e
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/bean/dto/financial/FinIntangibleAssetDto.java
@@ -0,0 +1,13 @@
+package com.ruoyi.account.bean.dto.financial;
+
+import com.ruoyi.account.pojo.financial.FinIntangibleAsset;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 鏃犲舰璧勪骇鏌ヨ涓庝繚瀛� DTO銆�
+ */
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class FinIntangibleAssetDto extends FinIntangibleAsset {
+}
diff --git a/src/main/java/com/ruoyi/account/bean/dto/financial/FinLedgerQueryDto.java b/src/main/java/com/ruoyi/account/bean/dto/financial/FinLedgerQueryDto.java
new file mode 100644
index 0000000..cfc0553
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/bean/dto/financial/FinLedgerQueryDto.java
@@ -0,0 +1,25 @@
+package com.ruoyi.account.bean.dto.financial;
+
+import lombok.Data;
+
+/**
+ * 绉戠洰璐︽煡璇㈠弬鏁般��
+ */
+@Data
+public class FinLedgerQueryDto {
+
+    /**
+     * 绉戠洰缂栫爜锛堟敮鎸佹湯绾ф垨鎸囧畾绉戠洰锛夈��
+     */
+    private String subjectCode;
+
+    /**
+     * 寮�濮嬫湀浠斤紝鏍煎紡锛歒YYY-MM銆�
+     */
+    private String startMonth;
+
+    /**
+     * 缁撴潫鏈堜唤锛屾牸寮忥細YYYY-MM銆�
+     */
+    private String endMonth;
+}
diff --git a/src/main/java/com/ruoyi/account/bean/dto/financial/FinVoucherDto.java b/src/main/java/com/ruoyi/account/bean/dto/financial/FinVoucherDto.java
new file mode 100644
index 0000000..c7c6258
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/bean/dto/financial/FinVoucherDto.java
@@ -0,0 +1,20 @@
+package com.ruoyi.account.bean.dto.financial;
+
+import com.ruoyi.account.pojo.financial.FinVoucher;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.List;
+
+/**
+ * 鍑瘉淇濆瓨 DTO锛堜富琛� + 鍒嗗綍锛夈��
+ */
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class FinVoucherDto extends FinVoucher {
+
+    /**
+     * 鍑瘉鏄庣粏鍒嗗綍銆�
+     */
+    private List<FinVoucherEntryDto> entries;
+}
diff --git a/src/main/java/com/ruoyi/account/bean/dto/financial/FinVoucherEntryDto.java b/src/main/java/com/ruoyi/account/bean/dto/financial/FinVoucherEntryDto.java
new file mode 100644
index 0000000..f722d79
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/bean/dto/financial/FinVoucherEntryDto.java
@@ -0,0 +1,13 @@
+package com.ruoyi.account.bean.dto.financial;
+
+import com.ruoyi.account.pojo.financial.FinVoucherEntry;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 鍑瘉鍒嗗綍 DTO銆�
+ */
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class FinVoucherEntryDto extends FinVoucherEntry {
+}
diff --git a/src/main/java/com/ruoyi/account/bean/dto/financial/FinVoucherPageDto.java b/src/main/java/com/ruoyi/account/bean/dto/financial/FinVoucherPageDto.java
new file mode 100644
index 0000000..9955bcc
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/bean/dto/financial/FinVoucherPageDto.java
@@ -0,0 +1,37 @@
+package com.ruoyi.account.bean.dto.financial;
+
+import lombok.Data;
+
+import java.time.LocalDate;
+
+/**
+ * 鍑瘉鍒嗛〉鏌ヨ鍙傛暟銆�
+ */
+@Data
+public class FinVoucherPageDto {
+
+    /**
+     * 鍑瘉瀛楀彿锛堟ā绯婂尮閰嶏級銆�
+     */
+    private String voucherNo;
+
+    /**
+     * 鍒跺崟浜恒��
+     */
+    private String creator;
+
+    /**
+     * 鍑瘉鐘舵�併��
+     */
+    private String status;
+
+    /**
+     * 寮�濮嬫棩鏈燂紙鍚級銆�
+     */
+    private LocalDate startDate;
+
+    /**
+     * 缁撴潫鏃ユ湡锛堝惈锛夈��
+     */
+    private LocalDate endDate;
+}
diff --git a/src/main/java/com/ruoyi/account/bean/dto/financial/FinVoucherStatusDto.java b/src/main/java/com/ruoyi/account/bean/dto/financial/FinVoucherStatusDto.java
new file mode 100644
index 0000000..47c0900
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/bean/dto/financial/FinVoucherStatusDto.java
@@ -0,0 +1,15 @@
+package com.ruoyi.account.bean.dto.financial;
+
+import lombok.Data;
+
+/**
+ * 鍑瘉鐘舵�佸彉鏇村弬鏁般��
+ */
+@Data
+public class FinVoucherStatusDto {
+
+    /**
+     * 鍑瘉ID銆�
+     */
+    private Long id;
+}
diff --git a/src/main/java/com/ruoyi/account/bean/vo/AccountSubjectVo.java b/src/main/java/com/ruoyi/account/bean/vo/AccountSubjectVo.java
index 3154d1c..c6bb078 100644
--- a/src/main/java/com/ruoyi/account/bean/vo/AccountSubjectVo.java
+++ b/src/main/java/com/ruoyi/account/bean/vo/AccountSubjectVo.java
@@ -3,6 +3,19 @@
 import com.ruoyi.account.pojo.AccountSubject;
 import lombok.Data;
 
+import java.util.ArrayList;
+import java.util.List;
+
 @Data
 public class AccountSubjectVo extends AccountSubject {
+
+    /**
+     * 瀛愮鐩垪琛紙閫掑綊缁撴瀯锛夈��
+     */
+    private List<AccountSubjectVo> children = new ArrayList<>();
+
+    /**
+     * 鏄惁鍙跺瓙鑺傜偣銆�
+     */
+    private Boolean leaf;
 }
diff --git a/src/main/java/com/ruoyi/account/bean/vo/financial/FinLedgerEntryRecordVo.java b/src/main/java/com/ruoyi/account/bean/vo/financial/FinLedgerEntryRecordVo.java
new file mode 100644
index 0000000..846b350
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/bean/vo/financial/FinLedgerEntryRecordVo.java
@@ -0,0 +1,43 @@
+package com.ruoyi.account.bean.vo.financial;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+
+/**
+ * 绉戠洰璐﹀熀纭�鍒嗗綍鏌ヨ瀵硅薄锛圫QL鏄犲皠浣跨敤锛夈��
+ */
+@Data
+public class FinLedgerEntryRecordVo {
+
+    /**
+     * 鍑瘉鏃ユ湡銆�
+     */
+    private LocalDate voucherDate;
+
+    /**
+     * 鍑瘉瀛楀彿銆�
+     */
+    private String voucherNo;
+
+    /**
+     * 鎽樿銆�
+     */
+    private String summary;
+
+    /**
+     * 鍊熸柟閲戦銆�
+     */
+    private BigDecimal debit;
+
+    /**
+     * 璐锋柟閲戦銆�
+     */
+    private BigDecimal credit;
+
+    /**
+     * 琛屽彿锛堟帓搴忓瓧娈碉級銆�
+     */
+    private Integer rowNo;
+}
diff --git a/src/main/java/com/ruoyi/account/bean/vo/financial/FinLedgerRowVo.java b/src/main/java/com/ruoyi/account/bean/vo/financial/FinLedgerRowVo.java
new file mode 100644
index 0000000..d01baf5
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/bean/vo/financial/FinLedgerRowVo.java
@@ -0,0 +1,53 @@
+package com.ruoyi.account.bean.vo.financial;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+
+/**
+ * 绉戠洰璐﹁鏁版嵁杩斿洖瀵硅薄銆�
+ */
+@Data
+public class FinLedgerRowVo {
+
+    /**
+     * 琛岀被鍨嬶細opening/entry/monthly_total/yearly_total銆�
+     */
+    private String rowType;
+
+    /**
+     * 鏃ユ湡銆�
+     */
+    private LocalDate date;
+
+    /**
+     * 鍑瘉瀛楀彿銆�
+     */
+    private String voucherNo;
+
+    /**
+     * 鎽樿銆�
+     */
+    private String summary;
+
+    /**
+     * 鍊熸柟閲戦銆�
+     */
+    private BigDecimal debit;
+
+    /**
+     * 璐锋柟閲戦銆�
+     */
+    private BigDecimal credit;
+
+    /**
+     * 浣欓鏂瑰悜锛氬��/璐枫��
+     */
+    private String direction;
+
+    /**
+     * 浣欓锛堝�熸璐疯礋锛夈��
+     */
+    private BigDecimal balance;
+}
diff --git a/src/main/java/com/ruoyi/account/bean/vo/financial/FinVoucherDetailVo.java b/src/main/java/com/ruoyi/account/bean/vo/financial/FinVoucherDetailVo.java
new file mode 100644
index 0000000..d1eab48
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/bean/vo/financial/FinVoucherDetailVo.java
@@ -0,0 +1,21 @@
+package com.ruoyi.account.bean.vo.financial;
+
+import com.ruoyi.account.pojo.financial.FinVoucher;
+import com.ruoyi.account.pojo.financial.FinVoucherEntry;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.List;
+
+/**
+ * 鍑瘉璇︽儏杩斿洖瀵硅薄銆�
+ */
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class FinVoucherDetailVo extends FinVoucher {
+
+    /**
+     * 鍑瘉鍒嗗綍鍒楄〃銆�
+     */
+    private List<FinVoucherEntry> entries;
+}
diff --git a/src/main/java/com/ruoyi/account/controller/AccountSubjectController.java b/src/main/java/com/ruoyi/account/controller/AccountSubjectController.java
index 8282883..38dd0ce 100644
--- a/src/main/java/com/ruoyi/account/controller/AccountSubjectController.java
+++ b/src/main/java/com/ruoyi/account/controller/AccountSubjectController.java
@@ -33,7 +33,7 @@
 
     @GetMapping("/list")
     @Log(title = "鎬昏处绉戠洰鏁版嵁闆嗗悎", businessType = BusinessType.OTHER)
-    @Operation(summary = "鎬昏处绉戠洰鍒嗛〉鏌ヨ")
+    @Operation(summary = "鎬昏处绉戠洰鏍戝舰鏌ヨ锛堥�掑綊锛�")
     public R<IPage<AccountSubjectVo>> AccountSubjectDtoList(Page<AccountSubjectDto> page, AccountSubjectDto accountSubjectDto) {
         IPage<AccountSubjectVo> paramList = accountSubjectService.baseList(page, accountSubjectDto);
         return R.ok(paramList);
@@ -43,21 +43,21 @@
     @Log(title = "鏂板鎬昏处绉戠洰", businessType = BusinessType.INSERT)
     @Operation(summary = "鏂板鎬昏处绉戠洰")
     public R AccountSubjectDtoAdd(@RequestBody AccountSubjectDto accountSubjectDto) {
-        return R.ok(accountSubjectService.save(accountSubjectDto));
+        return R.ok(accountSubjectService.saveAccountSubject(accountSubjectDto));
     }
 
     @PutMapping("/edit")
     @Log(title = "淇敼鎬昏处绉戠洰", businessType = BusinessType.UPDATE)
     @Operation(summary = "淇敼鎬昏处绉戠洰")
     public R AccountSubjectDtoEdit(@RequestBody AccountSubjectDto accountSubjectDto) {
-        return R.ok(accountSubjectService.updateById(accountSubjectDto));
+        return R.ok(accountSubjectService.updateAccountSubject(accountSubjectDto));
     }
 
     @DeleteMapping("/remove/{ids}")
     @Log(title = "鍒犻櫎鎬昏处绉戠洰", businessType = BusinessType.DELETE)
     @Operation(summary = "鍒犻櫎鎬昏处绉戠洰")
     public R AccountSubjectDtooRemove(@PathVariable Long[] ids) {
-        return R.ok(accountSubjectService.removeBatchByIds(Arrays.asList(ids)));
+        return R.ok(accountSubjectService.removeAccountSubjectByIds(Arrays.asList(ids)));
     }
 
     @PostMapping("/export")
diff --git a/src/main/java/com/ruoyi/account/controller/financial/FinFixedAssetController.java b/src/main/java/com/ruoyi/account/controller/financial/FinFixedAssetController.java
new file mode 100644
index 0000000..a18c9da
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/controller/financial/FinFixedAssetController.java
@@ -0,0 +1,63 @@
+package com.ruoyi.account.controller.financial;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.ruoyi.account.bean.dto.financial.FinFixedAssetDto;
+import com.ruoyi.account.bean.dto.financial.FinIdBatchDto;
+import com.ruoyi.account.pojo.financial.FinFixedAsset;
+import com.ruoyi.account.service.financial.FinFixedAssetService;
+import com.ruoyi.framework.aspectj.lang.annotation.Log;
+import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
+import com.ruoyi.framework.web.domain.R;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Arrays;
+
+/**
+ * 鍥哄畾璧勪骇鎺у埗鍣ㄣ��
+ */
+@RestController
+@RequestMapping("/financial/fixedAsset")
+@RequiredArgsConstructor
+@Tag(name = "璐㈠姟绠$悊-鍥哄畾璧勪骇")
+public class FinFixedAssetController {
+
+    private final FinFixedAssetService finFixedAssetService;
+
+    @GetMapping("/page")
+    @Operation(summary = "鍥哄畾璧勪骇鍒嗛〉鏌ヨ")
+    public R<IPage<FinFixedAsset>> page(Page<FinFixedAsset> page, FinFixedAssetDto queryDto) {
+        return R.ok(finFixedAssetService.pageList(page, queryDto));
+    }
+
+    @PostMapping("/add")
+    @Log(title = "鍥哄畾璧勪骇", businessType = BusinessType.INSERT)
+    @Operation(summary = "鏂板鍥哄畾璧勪骇")
+    public R<Boolean> add(@RequestBody FinFixedAssetDto dto) {
+        return R.ok(finFixedAssetService.add(dto));
+    }
+
+    @PutMapping("/update")
+    @Log(title = "鍥哄畾璧勪骇", businessType = BusinessType.UPDATE)
+    @Operation(summary = "淇敼鍥哄畾璧勪骇")
+    public R<Boolean> update(@RequestBody FinFixedAssetDto dto) {
+        return R.ok(finFixedAssetService.update(dto));
+    }
+
+    @DeleteMapping("/delete")
+    @Log(title = "鍥哄畾璧勪骇", businessType = BusinessType.DELETE)
+    @Operation(summary = "鍒犻櫎鍥哄畾璧勪骇")
+    public R<Boolean> delete(@RequestParam("ids") Long[] ids) {
+        return R.ok(finFixedAssetService.deleteByIds(Arrays.asList(ids)));
+    }
+
+    @PostMapping("/depreciate")
+    @Log(title = "鍥哄畾璧勪骇鎶樻棫璁℃彁", businessType = BusinessType.UPDATE)
+    @Operation(summary = "鍥哄畾璧勪骇鎸夋湀璁℃彁鎶樻棫")
+    public R depreciate(@RequestBody(required = false) FinIdBatchDto dto) {
+        return R.ok(finFixedAssetService.depreciate(dto == null ? null : dto.getIds()));
+    }
+}
diff --git a/src/main/java/com/ruoyi/account/controller/financial/FinIntangibleAssetController.java b/src/main/java/com/ruoyi/account/controller/financial/FinIntangibleAssetController.java
new file mode 100644
index 0000000..cb12f8c
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/controller/financial/FinIntangibleAssetController.java
@@ -0,0 +1,63 @@
+package com.ruoyi.account.controller.financial;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.ruoyi.account.bean.dto.financial.FinIdBatchDto;
+import com.ruoyi.account.bean.dto.financial.FinIntangibleAssetDto;
+import com.ruoyi.account.pojo.financial.FinIntangibleAsset;
+import com.ruoyi.account.service.financial.FinIntangibleAssetService;
+import com.ruoyi.framework.aspectj.lang.annotation.Log;
+import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
+import com.ruoyi.framework.web.domain.R;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Arrays;
+
+/**
+ * 鏃犲舰璧勪骇鎺у埗鍣ㄣ��
+ */
+@RestController
+@RequestMapping("/financial/intangibleAsset")
+@RequiredArgsConstructor
+@Tag(name = "璐㈠姟绠$悊-鏃犲舰璧勪骇")
+public class FinIntangibleAssetController {
+
+    private final FinIntangibleAssetService finIntangibleAssetService;
+
+    @GetMapping("/page")
+    @Operation(summary = "鏃犲舰璧勪骇鍒嗛〉鏌ヨ")
+    public R<IPage<FinIntangibleAsset>> page(Page<FinIntangibleAsset> page, FinIntangibleAssetDto queryDto) {
+        return R.ok(finIntangibleAssetService.pageList(page, queryDto));
+    }
+
+    @PostMapping("/add")
+    @Log(title = "鏃犲舰璧勪骇", businessType = BusinessType.INSERT)
+    @Operation(summary = "鏂板鏃犲舰璧勪骇")
+    public R<Boolean> add(@RequestBody FinIntangibleAssetDto dto) {
+        return R.ok(finIntangibleAssetService.add(dto));
+    }
+
+    @PutMapping("/update")
+    @Log(title = "鏃犲舰璧勪骇", businessType = BusinessType.UPDATE)
+    @Operation(summary = "淇敼鏃犲舰璧勪骇")
+    public R<Boolean> update(@RequestBody FinIntangibleAssetDto dto) {
+        return R.ok(finIntangibleAssetService.update(dto));
+    }
+
+    @DeleteMapping("/delete")
+    @Log(title = "鏃犲舰璧勪骇", businessType = BusinessType.DELETE)
+    @Operation(summary = "鍒犻櫎鏃犲舰璧勪骇")
+    public R<Boolean> delete(@RequestParam("ids") Long[] ids) {
+        return R.ok(finIntangibleAssetService.deleteByIds(Arrays.asList(ids)));
+    }
+
+    @PostMapping("/amortize")
+    @Log(title = "鏃犲舰璧勪骇鎽婇攢璁℃彁", businessType = BusinessType.UPDATE)
+    @Operation(summary = "鏃犲舰璧勪骇鎸夋湀璁℃彁鎽婇攢")
+    public R amortize(@RequestBody(required = false) FinIdBatchDto dto) {
+        return R.ok(finIntangibleAssetService.amortize(dto == null ? null : dto.getIds()));
+    }
+}
diff --git a/src/main/java/com/ruoyi/account/controller/financial/FinLedgerController.java b/src/main/java/com/ruoyi/account/controller/financial/FinLedgerController.java
new file mode 100644
index 0000000..423030a
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/controller/financial/FinLedgerController.java
@@ -0,0 +1,39 @@
+package com.ruoyi.account.controller.financial;
+
+import com.ruoyi.account.bean.dto.financial.FinDetailLedgerQueryDto;
+import com.ruoyi.account.bean.dto.financial.FinLedgerQueryDto;
+import com.ruoyi.account.bean.vo.financial.FinLedgerRowVo;
+import com.ruoyi.account.service.financial.FinLedgerService;
+import com.ruoyi.framework.web.domain.R;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+/**
+ * 绉戠洰鎬昏处/鏄庣粏璐︽帶鍒跺櫒銆�
+ */
+@RestController
+@RequestMapping("/financial/ledger")
+@RequiredArgsConstructor
+@Tag(name = "璐㈠姟绠$悊-绉戠洰璐�")
+public class FinLedgerController {
+
+    private final FinLedgerService finLedgerService;
+
+    @GetMapping("/general")
+    @Operation(summary = "绉戠洰鎬昏处鏌ヨ")
+    public R<List<FinLedgerRowVo>> general(FinLedgerQueryDto queryDto) {
+        return R.ok(finLedgerService.queryGeneralLedger(queryDto));
+    }
+
+    @GetMapping("/detail")
+    @Operation(summary = "绉戠洰鏄庣粏璐︽煡璇�")
+    public R<List<FinLedgerRowVo>> detail(FinDetailLedgerQueryDto queryDto) {
+        return R.ok(finLedgerService.queryDetailLedger(queryDto));
+    }
+}
diff --git a/src/main/java/com/ruoyi/account/controller/financial/FinVoucherController.java b/src/main/java/com/ruoyi/account/controller/financial/FinVoucherController.java
new file mode 100644
index 0000000..beeaafa
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/controller/financial/FinVoucherController.java
@@ -0,0 +1,69 @@
+package com.ruoyi.account.controller.financial;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.ruoyi.account.bean.dto.financial.FinVoucherDto;
+import com.ruoyi.account.bean.dto.financial.FinVoucherPageDto;
+import com.ruoyi.account.bean.dto.financial.FinVoucherStatusDto;
+import com.ruoyi.account.bean.vo.financial.FinVoucherDetailVo;
+import com.ruoyi.account.pojo.financial.FinVoucher;
+import com.ruoyi.account.service.financial.FinVoucherService;
+import com.ruoyi.framework.aspectj.lang.annotation.Log;
+import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
+import com.ruoyi.framework.web.domain.R;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 鍑瘉鎺у埗鍣ㄣ��
+ */
+@RestController
+@RequestMapping("/financial/voucher")
+@RequiredArgsConstructor
+@Tag(name = "璐㈠姟绠$悊-鍑瘉")
+public class FinVoucherController {
+
+    private final FinVoucherService finVoucherService;
+
+    @GetMapping("/page")
+    @Operation(summary = "鍑瘉鍒嗛〉鏌ヨ")
+    public R<IPage<FinVoucher>> page(Page<FinVoucher> page, FinVoucherPageDto queryDto) {
+        return R.ok(finVoucherService.pageList(page, queryDto));
+    }
+
+    @PostMapping("/add")
+    @Log(title = "鍑瘉", businessType = BusinessType.INSERT)
+    @Operation(summary = "鏂板鍑瘉")
+    public R<Boolean> add(@RequestBody FinVoucherDto dto) {
+        return R.ok(finVoucherService.addVoucher(dto));
+    }
+
+    @PutMapping("/update")
+    @Log(title = "鍑瘉", businessType = BusinessType.UPDATE)
+    @Operation(summary = "淇敼鍑瘉")
+    public R<Boolean> update(@RequestBody FinVoucherDto dto) {
+        return R.ok(finVoucherService.updateVoucher(dto));
+    }
+
+    @PostMapping("/post")
+    @Log(title = "鍑瘉杩囪处", businessType = BusinessType.UPDATE)
+    @Operation(summary = "鍑瘉杩囪处")
+    public R<Boolean> post(@RequestBody FinVoucherStatusDto dto) {
+        return R.ok(finVoucherService.postVoucher(dto.getId()));
+    }
+
+    @PostMapping("/cancel")
+    @Log(title = "鍑瘉浣滃簾", businessType = BusinessType.UPDATE)
+    @Operation(summary = "鍑瘉浣滃簾")
+    public R<Boolean> cancel(@RequestBody FinVoucherStatusDto dto) {
+        return R.ok(finVoucherService.cancelVoucher(dto.getId()));
+    }
+
+    @GetMapping("/detail/{id}")
+    @Operation(summary = "鍑瘉璇︽儏")
+    public R<FinVoucherDetailVo> detail(@PathVariable("id") Long id) {
+        return R.ok(finVoucherService.detail(id));
+    }
+}
diff --git a/src/main/java/com/ruoyi/account/mapper/AccountSubjectMapper.java b/src/main/java/com/ruoyi/account/mapper/AccountSubjectMapper.java
index 1fface5..46a4968 100644
--- a/src/main/java/com/ruoyi/account/mapper/AccountSubjectMapper.java
+++ b/src/main/java/com/ruoyi/account/mapper/AccountSubjectMapper.java
@@ -3,6 +3,9 @@
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.ruoyi.account.pojo.AccountSubject;
 import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
 
 /**
  * <p>
@@ -15,4 +18,6 @@
 @Mapper
 public interface AccountSubjectMapper extends BaseMapper<AccountSubject> {
 
+    Long countReferencedBySubjectCodes(@Param("subjectCodes") List<String> subjectCodes);
+
 }
diff --git a/src/main/java/com/ruoyi/account/mapper/financial/FinFixedAssetMapper.java b/src/main/java/com/ruoyi/account/mapper/financial/FinFixedAssetMapper.java
new file mode 100644
index 0000000..da2ae49
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/mapper/financial/FinFixedAssetMapper.java
@@ -0,0 +1,12 @@
+package com.ruoyi.account.mapper.financial;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.ruoyi.account.pojo.financial.FinFixedAsset;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 鍥哄畾璧勪骇 Mapper銆�
+ */
+@Mapper
+public interface FinFixedAssetMapper extends BaseMapper<FinFixedAsset> {
+}
diff --git a/src/main/java/com/ruoyi/account/mapper/financial/FinIntangibleAssetMapper.java b/src/main/java/com/ruoyi/account/mapper/financial/FinIntangibleAssetMapper.java
new file mode 100644
index 0000000..8a7bbb2
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/mapper/financial/FinIntangibleAssetMapper.java
@@ -0,0 +1,12 @@
+package com.ruoyi.account.mapper.financial;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.ruoyi.account.pojo.financial.FinIntangibleAsset;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 鏃犲舰璧勪骇 Mapper銆�
+ */
+@Mapper
+public interface FinIntangibleAssetMapper extends BaseMapper<FinIntangibleAsset> {
+}
diff --git a/src/main/java/com/ruoyi/account/mapper/financial/FinVoucherEntryMapper.java b/src/main/java/com/ruoyi/account/mapper/financial/FinVoucherEntryMapper.java
new file mode 100644
index 0000000..2fa2b73
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/mapper/financial/FinVoucherEntryMapper.java
@@ -0,0 +1,28 @@
+package com.ruoyi.account.mapper.financial;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.ruoyi.account.bean.vo.financial.FinLedgerEntryRecordVo;
+import com.ruoyi.account.pojo.financial.FinVoucherEntry;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.time.LocalDate;
+import java.util.List;
+
+/**
+ * 鍑瘉鍒嗗綍 Mapper銆�
+ */
+@Mapper
+public interface FinVoucherEntryMapper extends BaseMapper<FinVoucherEntry> {
+
+    List<FinLedgerEntryRecordVo> listPostedEntries(@Param("subjectCode") String subjectCode,
+                                                   @Param("startDate") LocalDate startDate,
+                                                   @Param("endDate") LocalDate endDate,
+                                                   @Param("auxiliaryType") String auxiliaryType,
+                                                   @Param("auxiliaryId") String auxiliaryId);
+
+    List<FinLedgerEntryRecordVo> listPostedEntriesBefore(@Param("subjectCode") String subjectCode,
+                                                         @Param("beforeDate") LocalDate beforeDate,
+                                                         @Param("auxiliaryType") String auxiliaryType,
+                                                         @Param("auxiliaryId") String auxiliaryId);
+}
diff --git a/src/main/java/com/ruoyi/account/mapper/financial/FinVoucherMapper.java b/src/main/java/com/ruoyi/account/mapper/financial/FinVoucherMapper.java
new file mode 100644
index 0000000..b528b7c
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/mapper/financial/FinVoucherMapper.java
@@ -0,0 +1,12 @@
+package com.ruoyi.account.mapper.financial;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.ruoyi.account.pojo.financial.FinVoucher;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 鍑瘉涓昏〃 Mapper銆�
+ */
+@Mapper
+public interface FinVoucherMapper extends BaseMapper<FinVoucher> {
+}
diff --git a/src/main/java/com/ruoyi/account/pojo/AccountSubject.java b/src/main/java/com/ruoyi/account/pojo/AccountSubject.java
index 49ba3da..9616324 100644
--- a/src/main/java/com/ruoyi/account/pojo/AccountSubject.java
+++ b/src/main/java/com/ruoyi/account/pojo/AccountSubject.java
@@ -39,6 +39,12 @@
     private Long id;
 
     /**
+     * 鐖剁鐩甀D锛堜负绌鸿〃绀烘牴鑺傜偣锛�
+     */
+    @ApiModelProperty("鐖剁鐩甀D锛堜负绌鸿〃绀烘牴鑺傜偣锛�")
+    private Long parentId;
+
+    /**
      * 绉戠洰缂栫爜(鍞竴鏍囪瘑)
      */
     @ApiModelProperty("绉戠洰缂栫爜(鍞竴鏍囪瘑)")
diff --git a/src/main/java/com/ruoyi/account/pojo/financial/FinFixedAsset.java b/src/main/java/com/ruoyi/account/pojo/financial/FinFixedAsset.java
new file mode 100644
index 0000000..f11f700
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/pojo/financial/FinFixedAsset.java
@@ -0,0 +1,101 @@
+package com.ruoyi.account.pojo.financial;
+
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.ToString;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+/**
+ * 鍥哄畾璧勪骇瀹炰綋銆�
+ */
+@Getter
+@Setter
+@ToString
+@TableName("fin_fixed_asset")
+@ApiModel(value = "FinFixedAsset瀵硅薄", description = "鍥哄畾璧勪骇")
+public class FinFixedAsset implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty("涓婚敭ID")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    @ApiModelProperty("璧勪骇缂栧彿")
+    private String assetCode;
+
+    @ApiModelProperty("璧勪骇鍚嶇О")
+    private String assetName;
+
+    @ApiModelProperty("璧勪骇绫诲埆")
+    private String category;
+
+    @ApiModelProperty("瑙勬牸鍨嬪彿")
+    private String specification;
+
+    @ApiModelProperty("璐疆鏃ユ湡")
+    private LocalDate purchaseDate;
+
+    @ApiModelProperty("璧勪骇鍘熷��")
+    private BigDecimal originalValue;
+
+    @ApiModelProperty("浣跨敤骞撮檺(骞�)")
+    private Integer usefulLife;
+
+    @ApiModelProperty("娈嬪�肩巼(%)")
+    private BigDecimal residualRate;
+
+    @ApiModelProperty("绱鎶樻棫")
+    private BigDecimal accumulatedDepreciation;
+
+    @ApiModelProperty("鍑�鍊�")
+    private BigDecimal netValue;
+
+    @ApiModelProperty("瀛樻斁鍦扮偣")
+    private String location;
+
+    @ApiModelProperty("浣跨敤閮ㄩ棬")
+    private String department;
+
+    @ApiModelProperty("淇濈浜�")
+    private String keeper;
+
+    @ApiModelProperty("鐘舵��")
+    private String status;
+
+    @ApiModelProperty("澶囨敞")
+    private String remark;
+
+    @ApiModelProperty("鍒涘缓浜�")
+    @TableField(fill = FieldFill.INSERT)
+    private String createUser;
+
+    @ApiModelProperty("鍒涘缓鏃堕棿")
+    @TableField(fill = FieldFill.INSERT)
+    private LocalDateTime createTime;
+
+    @ApiModelProperty("淇敼浜�")
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private String updateUser;
+
+    @ApiModelProperty("淇敼鏃堕棿")
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private LocalDateTime updateTime;
+
+    @ApiModelProperty("閮ㄩ棬ID")
+    @TableField(fill = FieldFill.INSERT)
+    private Long deptId;
+}
diff --git a/src/main/java/com/ruoyi/account/pojo/financial/FinIntangibleAsset.java b/src/main/java/com/ruoyi/account/pojo/financial/FinIntangibleAsset.java
new file mode 100644
index 0000000..e8ab4d3
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/pojo/financial/FinIntangibleAsset.java
@@ -0,0 +1,98 @@
+package com.ruoyi.account.pojo.financial;
+
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.ToString;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+/**
+ * 鏃犲舰璧勪骇瀹炰綋銆�
+ */
+@Getter
+@Setter
+@ToString
+@TableName("fin_intangible_asset")
+@ApiModel(value = "FinIntangibleAsset瀵硅薄", description = "鏃犲舰璧勪骇")
+public class FinIntangibleAsset implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty("涓婚敭ID")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    @ApiModelProperty("璧勪骇缂栧彿")
+    private String assetCode;
+
+    @ApiModelProperty("璧勪骇鍚嶇О")
+    private String assetName;
+
+    @ApiModelProperty("璧勪骇绫诲埆")
+    private String category;
+
+    @ApiModelProperty("璇佷功缂栧彿")
+    private String certificateNo;
+
+    @ApiModelProperty("鍙栧緱鏃ユ湡")
+    private LocalDate acquisitionDate;
+
+    @ApiModelProperty("璧勪骇鍘熷��")
+    private BigDecimal originalValue;
+
+    @ApiModelProperty("鎽婇攢骞撮檺(骞�)")
+    private Integer amortizationPeriod;
+
+    @ApiModelProperty("娈嬪�肩巼(%)")
+    private BigDecimal residualRate;
+
+    @ApiModelProperty("绱鎽婇攢")
+    private BigDecimal accumulatedAmortization;
+
+    @ApiModelProperty("鍑�鍊�")
+    private BigDecimal netValue;
+
+    @ApiModelProperty("鏈夋晥鏈熻嚦")
+    private LocalDate validityDate;
+
+    @ApiModelProperty("鐘舵��")
+    private String status;
+
+    @ApiModelProperty("璧勪骇鎻忚堪")
+    private String description;
+
+    @ApiModelProperty("澶囨敞")
+    private String remark;
+
+    @ApiModelProperty("鍒涘缓浜�")
+    @TableField(fill = FieldFill.INSERT)
+    private String createUser;
+
+    @ApiModelProperty("鍒涘缓鏃堕棿")
+    @TableField(fill = FieldFill.INSERT)
+    private LocalDateTime createTime;
+
+    @ApiModelProperty("淇敼浜�")
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private String updateUser;
+
+    @ApiModelProperty("淇敼鏃堕棿")
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private LocalDateTime updateTime;
+
+    @ApiModelProperty("閮ㄩ棬ID")
+    @TableField(fill = FieldFill.INSERT)
+    private Long deptId;
+}
diff --git a/src/main/java/com/ruoyi/account/pojo/financial/FinVoucher.java b/src/main/java/com/ruoyi/account/pojo/financial/FinVoucher.java
new file mode 100644
index 0000000..0a2918c
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/pojo/financial/FinVoucher.java
@@ -0,0 +1,83 @@
+package com.ruoyi.account.pojo.financial;
+
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.ToString;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+/**
+ * 鍑瘉涓昏〃瀹炰綋銆�
+ */
+@Getter
+@Setter
+@ToString
+@TableName("fin_voucher")
+@ApiModel(value = "FinVoucher瀵硅薄", description = "鍑瘉涓昏〃")
+public class FinVoucher implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty("涓婚敭ID")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    @ApiModelProperty("鍑瘉瀛楀彿")
+    private String voucherNo;
+
+    @ApiModelProperty("鍑瘉鏃ユ湡")
+    private LocalDate voucherDate;
+
+    @ApiModelProperty("鎽樿")
+    private String summary;
+
+    @ApiModelProperty("鍊熸柟鍚堣")
+    private BigDecimal debit;
+
+    @ApiModelProperty("璐锋柟鍚堣")
+    private BigDecimal credit;
+
+    @ApiModelProperty("鍒跺崟浜�")
+    private String creator;
+
+    @ApiModelProperty("鐘舵��: unposted/posted/cancelled")
+    private String status;
+
+    @ApiModelProperty("闄勪欢鏁伴噺")
+    private Integer attachmentCount;
+
+    @ApiModelProperty("澶囨敞")
+    private String remark;
+
+    @ApiModelProperty("鍒涘缓浜�")
+    @TableField(fill = FieldFill.INSERT)
+    private String createUser;
+
+    @ApiModelProperty("鍒涘缓鏃堕棿")
+    @TableField(fill = FieldFill.INSERT)
+    private LocalDateTime createTime;
+
+    @ApiModelProperty("淇敼浜�")
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private String updateUser;
+
+    @ApiModelProperty("淇敼鏃堕棿")
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private LocalDateTime updateTime;
+
+    @ApiModelProperty("閮ㄩ棬ID")
+    @TableField(fill = FieldFill.INSERT)
+    private Long deptId;
+}
diff --git a/src/main/java/com/ruoyi/account/pojo/financial/FinVoucherEntry.java b/src/main/java/com/ruoyi/account/pojo/financial/FinVoucherEntry.java
new file mode 100644
index 0000000..44ac56e
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/pojo/financial/FinVoucherEntry.java
@@ -0,0 +1,85 @@
+package com.ruoyi.account.pojo.financial;
+
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.ToString;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 鍑瘉鍒嗗綍瀹炰綋銆�
+ */
+@Getter
+@Setter
+@ToString
+@TableName("fin_voucher_entry")
+@ApiModel(value = "FinVoucherEntry瀵硅薄", description = "鍑瘉鍒嗗綍")
+public class FinVoucherEntry implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty("涓婚敭ID")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    @ApiModelProperty("鍑瘉ID")
+    private Long voucherId;
+
+    @ApiModelProperty("琛屽彿")
+    private Integer rowNo;
+
+    @ApiModelProperty("绉戠洰缂栫爜")
+    private String subjectCode;
+
+    @ApiModelProperty("绉戠洰鍚嶇О")
+    private String subjectName;
+
+    @ApiModelProperty("鎽樿")
+    private String summary;
+
+    @ApiModelProperty("鍊熸柟閲戦")
+    private BigDecimal debit;
+
+    @ApiModelProperty("璐锋柟閲戦")
+    private BigDecimal credit;
+
+    @ApiModelProperty("杈呭姪鏍哥畻绫诲瀷")
+    private String auxiliaryType;
+
+    @ApiModelProperty("杈呭姪鏍哥畻瀵硅薄ID")
+    private String auxiliaryId;
+
+    @ApiModelProperty("杈呭姪鏍哥畻瀵硅薄鍚嶇О")
+    private String auxiliaryName;
+
+    @ApiModelProperty("鍒涘缓浜�")
+    @TableField(fill = FieldFill.INSERT)
+    private String createUser;
+
+    @ApiModelProperty("鍒涘缓鏃堕棿")
+    @TableField(fill = FieldFill.INSERT)
+    private LocalDateTime createTime;
+
+    @ApiModelProperty("淇敼浜�")
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private String updateUser;
+
+    @ApiModelProperty("淇敼鏃堕棿")
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private LocalDateTime updateTime;
+
+    @ApiModelProperty("閮ㄩ棬ID")
+    @TableField(fill = FieldFill.INSERT)
+    private Long deptId;
+}
diff --git a/src/main/java/com/ruoyi/account/service/AccountSubjectService.java b/src/main/java/com/ruoyi/account/service/AccountSubjectService.java
index a7dd670..bcbc57c 100644
--- a/src/main/java/com/ruoyi/account/service/AccountSubjectService.java
+++ b/src/main/java/com/ruoyi/account/service/AccountSubjectService.java
@@ -8,6 +8,8 @@
 import com.baomidou.mybatisplus.extension.service.IService;
 import jakarta.servlet.http.HttpServletResponse;
 
+import java.util.List;
+
 /**
  * <p>
  * 鎬昏处绉戠洰琛� 鏈嶅姟绫�
@@ -20,5 +22,11 @@
 
     IPage<AccountSubjectVo> baseList(Page<AccountSubjectDto> page, AccountSubjectDto accountSubjectDto);
 
+    Boolean saveAccountSubject(AccountSubjectDto accountSubjectDto);
+
+    Boolean updateAccountSubject(AccountSubjectDto accountSubjectDto);
+
+    Boolean removeAccountSubjectByIds(List<Long> ids);
+
     void exportAccountSubject(HttpServletResponse response);
 }
diff --git a/src/main/java/com/ruoyi/account/service/financial/FinFixedAssetService.java b/src/main/java/com/ruoyi/account/service/financial/FinFixedAssetService.java
new file mode 100644
index 0000000..0b2c1cc
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/service/financial/FinFixedAssetService.java
@@ -0,0 +1,26 @@
+package com.ruoyi.account.service.financial;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.ruoyi.account.bean.dto.financial.FinFixedAssetDto;
+import com.ruoyi.account.pojo.financial.FinFixedAsset;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 鍥哄畾璧勪骇鏈嶅姟銆�
+ */
+public interface FinFixedAssetService extends IService<FinFixedAsset> {
+
+    IPage<FinFixedAsset> pageList(Page<FinFixedAsset> page, FinFixedAssetDto queryDto);
+
+    Boolean add(FinFixedAssetDto dto);
+
+    Boolean update(FinFixedAssetDto dto);
+
+    Boolean deleteByIds(List<Long> ids);
+
+    Map<String, Object> depreciate(List<Long> ids);
+}
diff --git a/src/main/java/com/ruoyi/account/service/financial/FinIntangibleAssetService.java b/src/main/java/com/ruoyi/account/service/financial/FinIntangibleAssetService.java
new file mode 100644
index 0000000..46d946a
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/service/financial/FinIntangibleAssetService.java
@@ -0,0 +1,26 @@
+package com.ruoyi.account.service.financial;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.ruoyi.account.bean.dto.financial.FinIntangibleAssetDto;
+import com.ruoyi.account.pojo.financial.FinIntangibleAsset;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 鏃犲舰璧勪骇鏈嶅姟銆�
+ */
+public interface FinIntangibleAssetService extends IService<FinIntangibleAsset> {
+
+    IPage<FinIntangibleAsset> pageList(Page<FinIntangibleAsset> page, FinIntangibleAssetDto queryDto);
+
+    Boolean add(FinIntangibleAssetDto dto);
+
+    Boolean update(FinIntangibleAssetDto dto);
+
+    Boolean deleteByIds(List<Long> ids);
+
+    Map<String, Object> amortize(List<Long> ids);
+}
diff --git a/src/main/java/com/ruoyi/account/service/financial/FinLedgerService.java b/src/main/java/com/ruoyi/account/service/financial/FinLedgerService.java
new file mode 100644
index 0000000..c4e9fa4
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/service/financial/FinLedgerService.java
@@ -0,0 +1,17 @@
+package com.ruoyi.account.service.financial;
+
+import com.ruoyi.account.bean.dto.financial.FinDetailLedgerQueryDto;
+import com.ruoyi.account.bean.dto.financial.FinLedgerQueryDto;
+import com.ruoyi.account.bean.vo.financial.FinLedgerRowVo;
+
+import java.util.List;
+
+/**
+ * 绉戠洰璐︽湇鍔°��
+ */
+public interface FinLedgerService {
+
+    List<FinLedgerRowVo> queryGeneralLedger(FinLedgerQueryDto queryDto);
+
+    List<FinLedgerRowVo> queryDetailLedger(FinDetailLedgerQueryDto queryDto);
+}
diff --git a/src/main/java/com/ruoyi/account/service/financial/FinVoucherService.java b/src/main/java/com/ruoyi/account/service/financial/FinVoucherService.java
new file mode 100644
index 0000000..078bac4
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/service/financial/FinVoucherService.java
@@ -0,0 +1,27 @@
+package com.ruoyi.account.service.financial;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.ruoyi.account.bean.dto.financial.FinVoucherDto;
+import com.ruoyi.account.bean.dto.financial.FinVoucherPageDto;
+import com.ruoyi.account.bean.vo.financial.FinVoucherDetailVo;
+import com.ruoyi.account.pojo.financial.FinVoucher;
+
+/**
+ * 鍑瘉鏈嶅姟銆�
+ */
+public interface FinVoucherService extends IService<FinVoucher> {
+
+    IPage<FinVoucher> pageList(Page<FinVoucher> page, FinVoucherPageDto queryDto);
+
+    Boolean addVoucher(FinVoucherDto dto);
+
+    Boolean updateVoucher(FinVoucherDto dto);
+
+    Boolean postVoucher(Long id);
+
+    Boolean cancelVoucher(Long id);
+
+    FinVoucherDetailVo detail(Long id);
+}
diff --git a/src/main/java/com/ruoyi/account/service/impl/AccountSubjectServiceImpl.java b/src/main/java/com/ruoyi/account/service/impl/AccountSubjectServiceImpl.java
index 152966a..37bf64b 100644
--- a/src/main/java/com/ruoyi/account/service/impl/AccountSubjectServiceImpl.java
+++ b/src/main/java/com/ruoyi/account/service/impl/AccountSubjectServiceImpl.java
@@ -10,6 +10,7 @@
 import com.ruoyi.account.mapper.AccountSubjectMapper;
 import com.ruoyi.account.pojo.AccountSubject;
 import com.ruoyi.account.service.AccountSubjectService;
+import com.ruoyi.common.exception.ServiceException;
 import com.ruoyi.common.utils.StringUtils;
 import com.ruoyi.common.utils.poi.ExcelUtil;
 import jakarta.servlet.http.HttpServletResponse;
@@ -18,7 +19,16 @@
 import org.springframework.stereotype.Service;
 
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
 import java.util.stream.Collectors;
 
 /**
@@ -37,30 +47,78 @@
 
     @Override
     public IPage<AccountSubjectVo> baseList(Page<AccountSubjectDto> page, AccountSubjectDto accountSubjectDto) {
-        LambdaQueryWrapper<AccountSubject> queryWrapper = new LambdaQueryWrapper<>();
-        if (accountSubjectDto != null && StringUtils.isNotEmpty(accountSubjectDto.getSubjectCode())) {
-            queryWrapper.like(AccountSubject::getSubjectCode, accountSubjectDto.getSubjectCode());
-        }
-        if (accountSubjectDto != null && StringUtils.isNotEmpty(accountSubjectDto.getSubjectName())) {
-            queryWrapper.like(AccountSubject::getSubjectName, accountSubjectDto.getSubjectName());
-        }
-        if (accountSubjectDto != null && StringUtils.isNotEmpty(accountSubjectDto.getSubjectType())) {
-            queryWrapper.eq(AccountSubject::getSubjectType, accountSubjectDto.getSubjectType());
-        }
-        queryWrapper.orderByDesc(AccountSubject::getId);
+        Page<AccountSubjectDto> requestPage = page == null ? new Page<>(1, 10) : page;
+        List<AccountSubject> allSubjects = list(loadBaseQueryWrapper(accountSubjectDto));
+        List<AccountSubject> filteredSubjects = applyTreeFilter(allSubjects, accountSubjectDto);
+        List<AccountSubjectVo> fullTree = buildTree(filteredSubjects);
 
-        Page<AccountSubject> entityPage = new Page<>(page.getCurrent(), page.getSize(), page.getTotal());
-        Page<AccountSubject> paramPage = page(entityPage, queryWrapper);
+        long current = requestPage.getCurrent() <= 0 ? 1 : requestPage.getCurrent();
+        long size = requestPage.getSize() <= 0 ? 10 : requestPage.getSize();
+        int fromIndex = (int) Math.min((current - 1) * size, fullTree.size());
+        int toIndex = (int) Math.min(fromIndex + size, fullTree.size());
+        List<AccountSubjectVo> pagedRoots = fromIndex >= toIndex
+                ? Collections.emptyList()
+                : fullTree.subList(fromIndex, toIndex);
 
-        Page<AccountSubjectVo> resultPage = new Page<>(paramPage.getCurrent(), paramPage.getSize(), paramPage.getTotal());
-        List<AccountSubjectVo> records = new ArrayList<>(paramPage.getRecords().size());
-        for (AccountSubject item : paramPage.getRecords()) {
-            AccountSubjectVo vo = new AccountSubjectVo();
-            BeanUtils.copyProperties(item, vo);
-            records.add(vo);
-        }
-        resultPage.setRecords(records);
+        Page<AccountSubjectVo> resultPage = new Page<>(current, size, fullTree.size());
+        resultPage.setRecords(pagedRoots);
         return resultPage;
+    }
+
+    @Override
+    public Boolean saveAccountSubject(AccountSubjectDto accountSubjectDto) {
+        validateSubjectRequiredFields(accountSubjectDto);
+        validateSubjectCodeUnique(accountSubjectDto, false);
+        validateParent(accountSubjectDto.getParentId(), null);
+        if (accountSubjectDto.getStatus() == null) {
+            accountSubjectDto.setStatus(0);
+        }
+        return save(accountSubjectDto);
+    }
+
+    @Override
+    public Boolean updateAccountSubject(AccountSubjectDto accountSubjectDto) {
+        if (accountSubjectDto == null || accountSubjectDto.getId() == null) {
+            throw new ServiceException("淇敼澶辫触锛岀鐩甀D涓嶈兘涓虹┖");
+        }
+        if (getById(accountSubjectDto.getId()) == null) {
+            throw new ServiceException("淇敼澶辫触锛屾湭鎵惧埌瀵瑰簲绉戠洰");
+        }
+        validateParent(accountSubjectDto.getParentId(), accountSubjectDto.getId());
+        validateSubjectRequiredFields(accountSubjectDto);
+        validateSubjectCodeUnique(accountSubjectDto, true);
+        return updateById(accountSubjectDto);
+    }
+
+    @Override
+    public Boolean removeAccountSubjectByIds(List<Long> ids) {
+        if (ids == null || ids.isEmpty()) {
+            return true;
+        }
+        List<AccountSubject> allSubjects = list();
+        if (allSubjects == null || allSubjects.isEmpty()) {
+            return true;
+        }
+        Map<Long, List<Long>> childrenIdMap = buildChildrenIdMap(allSubjects);
+        Set<Long> removeIds = new LinkedHashSet<>();
+        for (Long id : ids) {
+            collectDescendantIds(id, childrenIdMap, removeIds);
+        }
+        if (removeIds.isEmpty()) {
+            return true;
+        }
+        List<String> subjectCodes = allSubjects.stream()
+                .filter(subject -> removeIds.contains(subject.getId()))
+                .map(AccountSubject::getSubjectCode)
+                .filter(StringUtils::isNotEmpty)
+                .collect(Collectors.toList());
+        if (!subjectCodes.isEmpty()) {
+            Long referencedCount = accountSubjectMapper.countReferencedBySubjectCodes(subjectCodes);
+            if (referencedCount != null && referencedCount > 0) {
+                throw new ServiceException("鍒犻櫎澶辫触锛岀鐩凡琚嚟璇佸垎褰曞紩鐢�");
+            }
+        }
+        return removeByIds(removeIds);
     }
 
     @Override
@@ -74,4 +132,266 @@
         ExcelUtil<AccountSubjectImportDto> util = new ExcelUtil<>(AccountSubjectImportDto.class);
         util.exportExcel(response, importDtos , "鎬昏处绉戠洰");
     }
+
+    /**
+     * 鏍¢獙绉戠洰蹇呭~瀛楁锛岄伩鍏嶈剰鏁版嵁鍐欏叆銆�
+     */
+    private void validateSubjectRequiredFields(AccountSubjectDto accountSubjectDto) {
+        if (accountSubjectDto == null) {
+            throw new ServiceException("鎬昏处绉戠洰鏁版嵁涓嶈兘涓虹┖");
+        }
+        if (StringUtils.isEmpty(accountSubjectDto.getSubjectCode())) {
+            throw new ServiceException("绉戠洰缂栫爜涓嶈兘涓虹┖");
+        }
+        if (StringUtils.isEmpty(accountSubjectDto.getSubjectName())) {
+            throw new ServiceException("绉戠洰鍚嶇О涓嶈兘涓虹┖");
+        }
+        if (StringUtils.isEmpty(accountSubjectDto.getSubjectType())) {
+            throw new ServiceException("绉戠洰绫诲瀷涓嶈兘涓虹┖");
+        }
+    }
+
+    /**
+     * 鏍¢獙绉戠洰缂栫爜鍞竴锛屾柊澧炲拰淇敼閮借鎵ц銆�
+     */
+    private void validateSubjectCodeUnique(AccountSubjectDto accountSubjectDto, boolean isUpdate) {
+        LambdaQueryWrapper<AccountSubject> codeQueryWrapper = new LambdaQueryWrapper<>();
+        codeQueryWrapper.eq(AccountSubject::getSubjectCode, accountSubjectDto.getSubjectCode());
+        if (isUpdate) {
+            codeQueryWrapper.ne(AccountSubject::getId, accountSubjectDto.getId());
+        }
+        AccountSubject exists = getOne(codeQueryWrapper, false);
+        if (Objects.nonNull(exists)) {
+            throw new ServiceException("绉戠洰缂栫爜宸插瓨鍦紝璇峰嬁閲嶅鎻愪氦");
+        }
+    }
+
+    /**
+     * 浠呮寜閫氱敤杩囨护鏉′欢鏌ヨ鍩虹鏁版嵁锛堟爲褰㈣繃婊ゅ悗缁啀鍋氾級銆�
+     */
+    private LambdaQueryWrapper<AccountSubject> loadBaseQueryWrapper(AccountSubjectDto accountSubjectDto) {
+        LambdaQueryWrapper<AccountSubject> queryWrapper = new LambdaQueryWrapper<>();
+        if (accountSubjectDto != null && accountSubjectDto.getStatus() != null) {
+            queryWrapper.eq(AccountSubject::getStatus, accountSubjectDto.getStatus());
+        }
+        queryWrapper.orderByAsc(AccountSubject::getSubjectCode).orderByAsc(AccountSubject::getId);
+        return queryWrapper;
+    }
+
+    /**
+     * 鏍戝舰杩囨护锛氬懡涓妭鐐瑰悗淇濈暀鍏剁埗閾句笌瀛愭爲锛屼繚璇侀�掑綊缁撴瀯瀹屾暣銆�
+     */
+    private List<AccountSubject> applyTreeFilter(List<AccountSubject> allSubjects, AccountSubjectDto queryDto) {
+        if (allSubjects == null || allSubjects.isEmpty()) {
+            return Collections.emptyList();
+        }
+        boolean hasFilter = queryDto != null && (
+                StringUtils.isNotEmpty(queryDto.getSubjectCode())
+                        || StringUtils.isNotEmpty(queryDto.getSubjectName())
+                        || StringUtils.isNotEmpty(queryDto.getSubjectType())
+        );
+        if (!hasFilter) {
+            return allSubjects;
+        }
+
+        Map<Long, AccountSubject> subjectMap = allSubjects.stream()
+                .filter(item -> item.getId() != null)
+                .collect(Collectors.toMap(AccountSubject::getId, item -> item, (a, b) -> a, LinkedHashMap::new));
+        Map<Long, List<AccountSubject>> childrenMap = buildChildrenMap(allSubjects);
+
+        Set<Long> matchedIds = new LinkedHashSet<>();
+        for (AccountSubject subject : allSubjects) {
+            if (subject.getId() == null) {
+                continue;
+            }
+            if (matchesFilter(subject, queryDto)) {
+                matchedIds.add(subject.getId());
+            }
+        }
+        if (matchedIds.isEmpty()) {
+            return Collections.emptyList();
+        }
+
+        Set<Long> resultIds = new LinkedHashSet<>(matchedIds);
+        for (Long matchedId : matchedIds) {
+            addAncestors(matchedId, subjectMap, resultIds);
+            addDescendants(matchedId, childrenMap, resultIds);
+        }
+
+        return allSubjects.stream()
+                .filter(item -> item.getId() != null && resultIds.contains(item.getId()))
+                .collect(Collectors.toList());
+    }
+
+    private boolean matchesFilter(AccountSubject subject, AccountSubjectDto queryDto) {
+        if (queryDto == null) {
+            return true;
+        }
+        if (StringUtils.isNotEmpty(queryDto.getSubjectCode())
+                && (subject.getSubjectCode() == null || !subject.getSubjectCode().contains(queryDto.getSubjectCode()))) {
+            return false;
+        }
+        if (StringUtils.isNotEmpty(queryDto.getSubjectName())
+                && (subject.getSubjectName() == null || !subject.getSubjectName().contains(queryDto.getSubjectName()))) {
+            return false;
+        }
+        if (StringUtils.isNotEmpty(queryDto.getSubjectType())
+                && !queryDto.getSubjectType().equals(subject.getSubjectType())) {
+            return false;
+        }
+        return true;
+    }
+
+    private void addAncestors(Long subjectId, Map<Long, AccountSubject> subjectMap, Set<Long> resultIds) {
+        AccountSubject current = subjectMap.get(subjectId);
+        if (current == null) {
+            return;
+        }
+        Long parentId = current.getParentId();
+        while (parentId != null && parentId > 0) {
+            AccountSubject parent = subjectMap.get(parentId);
+            if (parent == null) {
+                break;
+            }
+            if (!resultIds.add(parent.getId())) {
+                break;
+            }
+            parentId = parent.getParentId();
+        }
+    }
+
+    private void addDescendants(Long subjectId, Map<Long, List<AccountSubject>> childrenMap, Set<Long> resultIds) {
+        List<AccountSubject> children = childrenMap.getOrDefault(subjectId, Collections.emptyList());
+        for (AccountSubject child : children) {
+            if (child.getId() == null) {
+                continue;
+            }
+            if (resultIds.add(child.getId())) {
+                addDescendants(child.getId(), childrenMap, resultIds);
+            }
+        }
+    }
+
+    private Map<Long, List<AccountSubject>> buildChildrenMap(List<AccountSubject> subjects) {
+        Map<Long, List<AccountSubject>> childrenMap = new HashMap<>();
+        for (AccountSubject subject : subjects) {
+            if (subject.getId() == null) {
+                continue;
+            }
+            Long parentId = subject.getParentId();
+            if (parentId == null || parentId <= 0) {
+                continue;
+            }
+            childrenMap.computeIfAbsent(parentId, key -> new ArrayList<>()).add(subject);
+        }
+        return childrenMap;
+    }
+
+    /**
+     * 鍩轰簬 parentId 閫掑綊鏋勫缓绉戠洰鏍戙��
+     */
+    private List<AccountSubjectVo> buildTree(List<AccountSubject> subjects) {
+        if (subjects == null || subjects.isEmpty()) {
+            return Collections.emptyList();
+        }
+        List<AccountSubject> sortedSubjects = new ArrayList<>(subjects);
+        sortedSubjects.sort(Comparator
+                .comparing(AccountSubject::getSubjectCode, Comparator.nullsLast(String::compareTo))
+                .thenComparing(AccountSubject::getId, Comparator.nullsLast(Long::compareTo)));
+
+        Map<Long, AccountSubjectVo> subjectVoMap = new LinkedHashMap<>();
+        for (AccountSubject subject : sortedSubjects) {
+            if (subject.getId() == null) {
+                continue;
+            }
+            AccountSubjectVo vo = new AccountSubjectVo();
+            BeanUtils.copyProperties(subject, vo);
+            subjectVoMap.put(subject.getId(), vo);
+        }
+
+        List<AccountSubjectVo> roots = new ArrayList<>();
+        for (AccountSubject subject : sortedSubjects) {
+            if (subject.getId() == null) {
+                continue;
+            }
+            AccountSubjectVo current = subjectVoMap.get(subject.getId());
+            Long parentId = subject.getParentId();
+            if (parentId != null && parentId > 0 && subjectVoMap.containsKey(parentId)) {
+                subjectVoMap.get(parentId).getChildren().add(current);
+            } else {
+                roots.add(current);
+            }
+        }
+
+        markLeafRecursively(roots);
+        return roots;
+    }
+
+    private void markLeafRecursively(List<AccountSubjectVo> nodes) {
+        for (AccountSubjectVo node : nodes) {
+            List<AccountSubjectVo> children = node.getChildren();
+            node.setLeaf(children == null || children.isEmpty());
+            if (children != null && !children.isEmpty()) {
+                markLeafRecursively(children);
+            }
+        }
+    }
+
+    /**
+     * 鏍¢獙鐖跺瓙鍏崇郴锛氱埗鑺傜偣蹇呴』瀛樺湪锛屼笖涓嶈兘褰㈡垚寰幆寮曠敤銆�
+     */
+    private void validateParent(Long parentId, Long currentId) {
+        if (parentId == null || parentId <= 0) {
+            return;
+        }
+        if (currentId != null && parentId.equals(currentId)) {
+            throw new ServiceException("鐖剁鐩笉鑳介�夋嫨鑷韩");
+        }
+        AccountSubject parent = getById(parentId);
+        if (parent == null) {
+            throw new ServiceException("鐖剁鐩笉瀛樺湪锛岃閲嶆柊閫夋嫨");
+        }
+        // 闃叉褰㈡垚鐜細鏇存柊鏃讹紝鐖惰妭鐐逛笉鑳芥槸褰撳墠鑺傜偣鐨勪换鎰忓瓙瀛欒妭鐐广��
+        if (currentId != null) {
+            Set<Long> visited = new HashSet<>();
+            Long traceParentId = parentId;
+            while (traceParentId != null && traceParentId > 0) {
+                if (!visited.add(traceParentId)) {
+                    throw new ServiceException("绉戠洰灞傜骇瀛樺湪寰幆寮曠敤锛岃妫�鏌ョ埗绉戠洰璁剧疆");
+                }
+                if (traceParentId.equals(currentId)) {
+                    throw new ServiceException("鐖剁鐩笉鑳芥槸褰撳墠绉戠洰鎴栧叾瀛愮鐩�");
+                }
+                AccountSubject traceNode = getById(traceParentId);
+                if (traceNode == null) {
+                    break;
+                }
+                traceParentId = traceNode.getParentId();
+            }
+        }
+    }
+
+    private Map<Long, List<Long>> buildChildrenIdMap(List<AccountSubject> subjects) {
+        Map<Long, List<Long>> map = new HashMap<>();
+        for (AccountSubject subject : subjects) {
+            if (subject.getId() == null || subject.getParentId() == null || subject.getParentId() <= 0) {
+                continue;
+            }
+            map.computeIfAbsent(subject.getParentId(), key -> new ArrayList<>()).add(subject.getId());
+        }
+        return map;
+    }
+
+    /**
+     * 鏀堕泦寰呭垹闄よ妭鐐瑰強鍏舵墍鏈夊瓙瀛欒妭鐐广��
+     */
+    private void collectDescendantIds(Long id, Map<Long, List<Long>> childrenIdMap, Set<Long> result) {
+        if (id == null || !result.add(id)) {
+            return;
+        }
+        List<Long> children = childrenIdMap.getOrDefault(id, Collections.emptyList());
+        for (Long childId : children) {
+            collectDescendantIds(childId, childrenIdMap, result);
+        }
+    }
 }
diff --git a/src/main/java/com/ruoyi/account/service/impl/financial/FinFixedAssetServiceImpl.java b/src/main/java/com/ruoyi/account/service/impl/financial/FinFixedAssetServiceImpl.java
new file mode 100644
index 0000000..cb7a476
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/service/impl/financial/FinFixedAssetServiceImpl.java
@@ -0,0 +1,231 @@
+package com.ruoyi.account.service.impl.financial;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.ruoyi.account.bean.dto.financial.FinFixedAssetDto;
+import com.ruoyi.account.mapper.financial.FinFixedAssetMapper;
+import com.ruoyi.account.pojo.financial.FinFixedAsset;
+import com.ruoyi.account.service.financial.FinFixedAssetService;
+import com.ruoyi.common.exception.ServiceException;
+import com.ruoyi.common.utils.StringUtils;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+
+/**
+ * 鍥哄畾璧勪骇鏈嶅姟瀹炵幇銆�
+ */
+@Service
+@RequiredArgsConstructor
+public class FinFixedAssetServiceImpl extends ServiceImpl<FinFixedAssetMapper, FinFixedAsset> implements FinFixedAssetService {
+
+    private static final BigDecimal ONE_HUNDRED = new BigDecimal("100");
+    private static final BigDecimal ZERO = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
+    private static final DateTimeFormatter CODE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
+
+    @Override
+    public IPage<FinFixedAsset> pageList(Page<FinFixedAsset> page, FinFixedAssetDto queryDto) {
+        LambdaQueryWrapper<FinFixedAsset> wrapper = new LambdaQueryWrapper<>();
+        if (queryDto != null && StringUtils.isNotEmpty(queryDto.getAssetCode())) {
+            wrapper.like(FinFixedAsset::getAssetCode, queryDto.getAssetCode());
+        }
+        if (queryDto != null && StringUtils.isNotEmpty(queryDto.getAssetName())) {
+            wrapper.like(FinFixedAsset::getAssetName, queryDto.getAssetName());
+        }
+        if (queryDto != null && StringUtils.isNotEmpty(queryDto.getCategory())) {
+            wrapper.eq(FinFixedAsset::getCategory, queryDto.getCategory());
+        }
+        if (queryDto != null && StringUtils.isNotEmpty(queryDto.getStatus())) {
+            wrapper.eq(FinFixedAsset::getStatus, queryDto.getStatus());
+        }
+        wrapper.orderByDesc(FinFixedAsset::getId);
+        return page(page, wrapper);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean add(FinFixedAssetDto dto) {
+        validateForSave(dto, false);
+        if (StringUtils.isEmpty(dto.getAssetCode())) {
+            dto.setAssetCode(generateAssetCode());
+        }
+        BigDecimal residualRate = normalizeResidualRate(dto.getResidualRate());
+        dto.setResidualRate(residualRate);
+        BigDecimal accumulatedDepreciation = defaultMoney(dto.getAccumulatedDepreciation());
+        dto.setAccumulatedDepreciation(accumulatedDepreciation);
+        dto.setNetValue(calculateNetValue(dto.getOriginalValue(), accumulatedDepreciation));
+        if (StringUtils.isEmpty(dto.getStatus())) {
+            dto.setStatus("in_use");
+        }
+        return save(dto);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean update(FinFixedAssetDto dto) {
+        if (dto == null || dto.getId() == null) {
+            throw new ServiceException("淇敼澶辫触锛岃祫浜D涓嶈兘涓虹┖");
+        }
+        FinFixedAsset existed = getById(dto.getId());
+        if (existed == null) {
+            throw new ServiceException("淇敼澶辫触锛屽浐瀹氳祫浜т笉瀛樺湪");
+        }
+        if (StringUtils.isEmpty(dto.getAssetCode())) {
+            dto.setAssetCode(existed.getAssetCode());
+        }
+        if (StringUtils.isEmpty(dto.getStatus())) {
+            dto.setStatus(existed.getStatus());
+        }
+        validateForSave(dto, true);
+        BigDecimal residualRate = normalizeResidualRate(dto.getResidualRate());
+        dto.setResidualRate(residualRate);
+        if (dto.getAccumulatedDepreciation() == null) {
+            dto.setAccumulatedDepreciation(defaultMoney(existed.getAccumulatedDepreciation()));
+        }
+        dto.setNetValue(calculateNetValue(dto.getOriginalValue(), dto.getAccumulatedDepreciation()));
+        return updateById(dto);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean deleteByIds(List<Long> ids) {
+        if (ids == null || ids.isEmpty()) {
+            throw new ServiceException("鍒犻櫎澶辫触锛岃閫夋嫨瑕佸垹闄ょ殑鏁版嵁");
+        }
+        return removeByIds(ids);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Map<String, Object> depreciate(List<Long> ids) {
+        LambdaQueryWrapper<FinFixedAsset> wrapper = new LambdaQueryWrapper<>();
+        if (ids != null && !ids.isEmpty()) {
+            wrapper.in(FinFixedAsset::getId, ids);
+        } else {
+            wrapper.eq(FinFixedAsset::getStatus, "in_use");
+        }
+        List<FinFixedAsset> assets = list(wrapper);
+        BigDecimal totalMonthlyDepreciation = ZERO;
+        int processedCount = 0;
+        for (FinFixedAsset asset : assets) {
+            if (!"in_use".equals(asset.getStatus())) {
+                continue;
+            }
+            BigDecimal monthlyDepreciation = calculateMonthlyDepreciation(
+                    asset.getOriginalValue(),
+                    asset.getResidualRate(),
+                    asset.getUsefulLife()
+            );
+            BigDecimal accumulatedDepreciation = defaultMoney(asset.getAccumulatedDepreciation()).add(monthlyDepreciation);
+            if (accumulatedDepreciation.compareTo(defaultMoney(asset.getOriginalValue())) > 0) {
+                accumulatedDepreciation = defaultMoney(asset.getOriginalValue());
+            }
+            asset.setAccumulatedDepreciation(roundMoney(accumulatedDepreciation));
+            asset.setNetValue(calculateNetValue(asset.getOriginalValue(), asset.getAccumulatedDepreciation()));
+            updateById(asset);
+            processedCount++;
+            totalMonthlyDepreciation = totalMonthlyDepreciation.add(monthlyDepreciation);
+        }
+        Map<String, Object> result = new HashMap<>(4);
+        result.put("processedCount", processedCount);
+        result.put("totalMonthlyDepreciation", roundMoney(totalMonthlyDepreciation));
+        result.put("executionTime", LocalDateTime.now());
+        return result;
+    }
+
+    /**
+     * 鎸夋枃妗h鍒欐牎楠屽浐瀹氳祫浜ф暟鎹��
+     */
+    private void validateForSave(FinFixedAssetDto dto, boolean isUpdate) {
+        if (dto == null) {
+            throw new ServiceException("鍥哄畾璧勪骇鏁版嵁涓嶈兘涓虹┖");
+        }
+        if (isUpdate && dto.getId() == null) {
+            throw new ServiceException("淇敼澶辫触锛岃祫浜D涓嶈兘涓虹┖");
+        }
+        if (StringUtils.isEmpty(dto.getAssetName())) {
+            throw new ServiceException("璧勪骇鍚嶇О涓嶈兘涓虹┖");
+        }
+        if (StringUtils.isEmpty(dto.getCategory())) {
+            throw new ServiceException("璧勪骇绫诲埆涓嶈兘涓虹┖");
+        }
+        if (dto.getPurchaseDate() == null) {
+            throw new ServiceException("璐疆鏃ユ湡涓嶈兘涓虹┖");
+        }
+        if (dto.getOriginalValue() == null || dto.getOriginalValue().compareTo(BigDecimal.ZERO) < 0) {
+            throw new ServiceException("璧勪骇鍘熷�间笉鑳戒负绌轰笖涓嶈兘灏忎簬0");
+        }
+        if (dto.getUsefulLife() == null || dto.getUsefulLife() <= 0) {
+            throw new ServiceException("浣跨敤骞撮檺蹇呴』澶т簬0");
+        }
+        if (dto.getResidualRate() != null && dto.getResidualRate().compareTo(BigDecimal.ZERO) < 0) {
+            throw new ServiceException("娈嬪�肩巼涓嶈兘灏忎簬0");
+        }
+        if (dto.getResidualRate() != null && dto.getResidualRate().compareTo(ONE_HUNDRED) > 0) {
+            throw new ServiceException("娈嬪�肩巼涓嶈兘澶т簬100%");
+        }
+        if (StringUtils.isNotEmpty(dto.getAssetCode())) {
+            LambdaQueryWrapper<FinFixedAsset> wrapper = new LambdaQueryWrapper<>();
+            wrapper.eq(FinFixedAsset::getAssetCode, dto.getAssetCode());
+            if (isUpdate) {
+                wrapper.ne(FinFixedAsset::getId, dto.getId());
+            }
+            if (count(wrapper) > 0) {
+                throw new ServiceException("璧勪骇缂栧彿宸插瓨鍦紝璇峰嬁閲嶅鎻愪氦");
+            }
+        }
+    }
+
+    /**
+     * 鍥哄畾璧勪骇鎶樻棫鍏紡锛�
+     * monthlyDepreciation = originalValue * (1 - residualRate/100) / (usefulLife*12)
+     */
+    private BigDecimal calculateMonthlyDepreciation(BigDecimal originalValue, BigDecimal residualRate, Integer usefulLife) {
+        BigDecimal normalizedOriginalValue = defaultMoney(originalValue);
+        BigDecimal normalizedResidualRate = normalizeResidualRate(residualRate);
+        BigDecimal depreciableRatio = BigDecimal.ONE.subtract(normalizedResidualRate.divide(ONE_HUNDRED, 8, RoundingMode.HALF_UP));
+        BigDecimal months = BigDecimal.valueOf((long) usefulLife * 12L);
+        if (months.compareTo(BigDecimal.ZERO) <= 0) {
+            throw new ServiceException("浣跨敤骞撮檺鏃犳晥锛屾棤娉曡鎻愭姌鏃�");
+        }
+        return roundMoney(normalizedOriginalValue.multiply(depreciableRatio).divide(months, 8, RoundingMode.HALF_UP));
+    }
+
+    /**
+     * 鍑�鍊� = 鍘熷�� - 绱鎶樻棫銆�
+     */
+    private BigDecimal calculateNetValue(BigDecimal originalValue, BigDecimal accumulatedDepreciation) {
+        BigDecimal value = defaultMoney(originalValue).subtract(defaultMoney(accumulatedDepreciation));
+        if (value.compareTo(BigDecimal.ZERO) < 0) {
+            value = BigDecimal.ZERO;
+        }
+        return roundMoney(value);
+    }
+
+    private BigDecimal normalizeResidualRate(BigDecimal residualRate) {
+        return residualRate == null ? BigDecimal.ZERO : residualRate;
+    }
+
+    private BigDecimal defaultMoney(BigDecimal value) {
+        return value == null ? ZERO : roundMoney(value);
+    }
+
+    private BigDecimal roundMoney(BigDecimal value) {
+        if (value == null) {
+            return ZERO;
+        }
+        return value.setScale(2, RoundingMode.HALF_UP);
+    }
+
+    private String generateAssetCode() {
+        return "GD" + LocalDateTime.now().format(CODE_TIME_FORMATTER) + new Random().nextInt(10);
+    }
+}
diff --git a/src/main/java/com/ruoyi/account/service/impl/financial/FinIntangibleAssetServiceImpl.java b/src/main/java/com/ruoyi/account/service/impl/financial/FinIntangibleAssetServiceImpl.java
new file mode 100644
index 0000000..72e5f06
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/service/impl/financial/FinIntangibleAssetServiceImpl.java
@@ -0,0 +1,250 @@
+package com.ruoyi.account.service.impl.financial;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.ruoyi.account.bean.dto.financial.FinIntangibleAssetDto;
+import com.ruoyi.account.mapper.financial.FinIntangibleAssetMapper;
+import com.ruoyi.account.pojo.financial.FinIntangibleAsset;
+import com.ruoyi.account.service.financial.FinIntangibleAssetService;
+import com.ruoyi.common.exception.ServiceException;
+import com.ruoyi.common.utils.StringUtils;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+
+/**
+ * 鏃犲舰璧勪骇鏈嶅姟瀹炵幇銆�
+ */
+@Service
+@RequiredArgsConstructor
+public class FinIntangibleAssetServiceImpl extends ServiceImpl<FinIntangibleAssetMapper, FinIntangibleAsset> implements FinIntangibleAssetService {
+
+    private static final BigDecimal ONE_HUNDRED = new BigDecimal("100");
+    private static final BigDecimal ZERO = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
+    private static final DateTimeFormatter CODE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
+
+    @Override
+    public IPage<FinIntangibleAsset> pageList(Page<FinIntangibleAsset> page, FinIntangibleAssetDto queryDto) {
+        LambdaQueryWrapper<FinIntangibleAsset> wrapper = new LambdaQueryWrapper<>();
+        if (queryDto != null && StringUtils.isNotEmpty(queryDto.getAssetCode())) {
+            wrapper.like(FinIntangibleAsset::getAssetCode, queryDto.getAssetCode());
+        }
+        if (queryDto != null && StringUtils.isNotEmpty(queryDto.getAssetName())) {
+            wrapper.like(FinIntangibleAsset::getAssetName, queryDto.getAssetName());
+        }
+        if (queryDto != null && StringUtils.isNotEmpty(queryDto.getCategory())) {
+            wrapper.eq(FinIntangibleAsset::getCategory, queryDto.getCategory());
+        }
+        if (queryDto != null && StringUtils.isNotEmpty(queryDto.getStatus())) {
+            wrapper.eq(FinIntangibleAsset::getStatus, queryDto.getStatus());
+        }
+        wrapper.orderByDesc(FinIntangibleAsset::getId);
+        return page(page, wrapper);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean add(FinIntangibleAssetDto dto) {
+        validateForSave(dto, false);
+        if (StringUtils.isEmpty(dto.getAssetCode())) {
+            dto.setAssetCode(generateAssetCode());
+        }
+        BigDecimal residualRate = normalizeResidualRate(dto.getResidualRate());
+        dto.setResidualRate(residualRate);
+        BigDecimal accumulatedAmortization = defaultMoney(dto.getAccumulatedAmortization());
+        dto.setAccumulatedAmortization(accumulatedAmortization);
+        dto.setNetValue(calculateNetValue(dto.getOriginalValue(), accumulatedAmortization));
+        if (StringUtils.isEmpty(dto.getStatus())) {
+            dto.setStatus("in_use");
+        }
+        return save(dto);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean update(FinIntangibleAssetDto dto) {
+        if (dto == null || dto.getId() == null) {
+            throw new ServiceException("淇敼澶辫触锛岃祫浜D涓嶈兘涓虹┖");
+        }
+        FinIntangibleAsset existed = getById(dto.getId());
+        if (existed == null) {
+            throw new ServiceException("淇敼澶辫触锛屾棤褰㈣祫浜т笉瀛樺湪");
+        }
+        if (StringUtils.isEmpty(dto.getAssetCode())) {
+            dto.setAssetCode(existed.getAssetCode());
+        }
+        if (StringUtils.isEmpty(dto.getStatus())) {
+            dto.setStatus(existed.getStatus());
+        }
+        validateForSave(dto, true);
+        BigDecimal residualRate = normalizeResidualRate(dto.getResidualRate());
+        dto.setResidualRate(residualRate);
+        if (dto.getAccumulatedAmortization() == null) {
+            dto.setAccumulatedAmortization(defaultMoney(existed.getAccumulatedAmortization()));
+        }
+        dto.setNetValue(calculateNetValue(dto.getOriginalValue(), dto.getAccumulatedAmortization()));
+        if (dto.getNetValue().compareTo(BigDecimal.ZERO) <= 0) {
+            dto.setStatus("amortized");
+        } else if ("amortized".equals(dto.getStatus())) {
+            dto.setStatus("in_use");
+        }
+        if (dto.getValidityDate() != null
+                && dto.getValidityDate().isBefore(LocalDate.now())
+                && !"amortized".equals(dto.getStatus())) {
+            dto.setStatus("expired");
+        }
+        return updateById(dto);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean deleteByIds(List<Long> ids) {
+        if (ids == null || ids.isEmpty()) {
+            throw new ServiceException("鍒犻櫎澶辫触锛岃閫夋嫨瑕佸垹闄ょ殑鏁版嵁");
+        }
+        return removeByIds(ids);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Map<String, Object> amortize(List<Long> ids) {
+        LambdaQueryWrapper<FinIntangibleAsset> wrapper = new LambdaQueryWrapper<>();
+        if (ids != null && !ids.isEmpty()) {
+            wrapper.in(FinIntangibleAsset::getId, ids);
+        } else {
+            wrapper.eq(FinIntangibleAsset::getStatus, "in_use");
+        }
+        List<FinIntangibleAsset> assets = list(wrapper);
+        BigDecimal totalMonthlyAmortization = ZERO;
+        int processedCount = 0;
+        for (FinIntangibleAsset asset : assets) {
+            if (!"in_use".equals(asset.getStatus())) {
+                continue;
+            }
+            BigDecimal monthlyAmortization = calculateMonthlyAmortization(
+                    asset.getOriginalValue(),
+                    asset.getResidualRate(),
+                    asset.getAmortizationPeriod()
+            );
+            BigDecimal accumulatedAmortization = defaultMoney(asset.getAccumulatedAmortization()).add(monthlyAmortization);
+            if (accumulatedAmortization.compareTo(defaultMoney(asset.getOriginalValue())) > 0) {
+                accumulatedAmortization = defaultMoney(asset.getOriginalValue());
+            }
+            asset.setAccumulatedAmortization(roundMoney(accumulatedAmortization));
+            asset.setNetValue(calculateNetValue(asset.getOriginalValue(), asset.getAccumulatedAmortization()));
+
+            // 瑙勫垯锛氬綋鍑�鍊� <= 0 鏃讹紝鍑�鍊煎綊闆跺苟鏍囪涓哄凡鎽婇攢瀹屻��
+            if (asset.getNetValue().compareTo(BigDecimal.ZERO) <= 0) {
+                asset.setNetValue(BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP));
+                asset.setStatus("amortized");
+            } else if (asset.getValidityDate() != null && asset.getValidityDate().isBefore(LocalDate.now())) {
+                asset.setStatus("expired");
+            }
+            updateById(asset);
+            processedCount++;
+            totalMonthlyAmortization = totalMonthlyAmortization.add(monthlyAmortization);
+        }
+        Map<String, Object> result = new HashMap<>(4);
+        result.put("processedCount", processedCount);
+        result.put("totalMonthlyAmortization", roundMoney(totalMonthlyAmortization));
+        result.put("executionTime", LocalDateTime.now());
+        return result;
+    }
+
+    /**
+     * 鎸夋枃妗h鍒欐牎楠屾棤褰㈣祫浜ф暟鎹��
+     */
+    private void validateForSave(FinIntangibleAssetDto dto, boolean isUpdate) {
+        if (dto == null) {
+            throw new ServiceException("鏃犲舰璧勪骇鏁版嵁涓嶈兘涓虹┖");
+        }
+        if (isUpdate && dto.getId() == null) {
+            throw new ServiceException("淇敼澶辫触锛岃祫浜D涓嶈兘涓虹┖");
+        }
+        if (StringUtils.isEmpty(dto.getAssetName())) {
+            throw new ServiceException("璧勪骇鍚嶇О涓嶈兘涓虹┖");
+        }
+        if (StringUtils.isEmpty(dto.getCategory())) {
+            throw new ServiceException("璧勪骇绫诲埆涓嶈兘涓虹┖");
+        }
+        if (dto.getAcquisitionDate() == null) {
+            throw new ServiceException("鍙栧緱鏃ユ湡涓嶈兘涓虹┖");
+        }
+        if (dto.getOriginalValue() == null || dto.getOriginalValue().compareTo(BigDecimal.ZERO) < 0) {
+            throw new ServiceException("璧勪骇鍘熷�间笉鑳戒负绌轰笖涓嶈兘灏忎簬0");
+        }
+        if (dto.getAmortizationPeriod() == null || dto.getAmortizationPeriod() <= 0) {
+            throw new ServiceException("鎽婇攢骞撮檺蹇呴』澶т簬0");
+        }
+        if (dto.getResidualRate() != null && dto.getResidualRate().compareTo(BigDecimal.ZERO) < 0) {
+            throw new ServiceException("娈嬪�肩巼涓嶈兘灏忎簬0");
+        }
+        if (dto.getResidualRate() != null && dto.getResidualRate().compareTo(ONE_HUNDRED) > 0) {
+            throw new ServiceException("娈嬪�肩巼涓嶈兘澶т簬100%");
+        }
+        if (StringUtils.isNotEmpty(dto.getAssetCode())) {
+            LambdaQueryWrapper<FinIntangibleAsset> wrapper = new LambdaQueryWrapper<>();
+            wrapper.eq(FinIntangibleAsset::getAssetCode, dto.getAssetCode());
+            if (isUpdate) {
+                wrapper.ne(FinIntangibleAsset::getId, dto.getId());
+            }
+            if (count(wrapper) > 0) {
+                throw new ServiceException("璧勪骇缂栧彿宸插瓨鍦紝璇峰嬁閲嶅鎻愪氦");
+            }
+        }
+    }
+
+    /**
+     * 鏃犲舰璧勪骇鎽婇攢鍏紡锛�
+     * monthlyAmortization = originalValue * (1 - residualRate/100) / (amortizationPeriod*12)
+     */
+    private BigDecimal calculateMonthlyAmortization(BigDecimal originalValue, BigDecimal residualRate, Integer amortizationPeriod) {
+        BigDecimal normalizedOriginalValue = defaultMoney(originalValue);
+        BigDecimal normalizedResidualRate = normalizeResidualRate(residualRate);
+        BigDecimal amortizableRatio = BigDecimal.ONE.subtract(normalizedResidualRate.divide(ONE_HUNDRED, 8, RoundingMode.HALF_UP));
+        BigDecimal months = BigDecimal.valueOf((long) amortizationPeriod * 12L);
+        if (months.compareTo(BigDecimal.ZERO) <= 0) {
+            throw new ServiceException("鎽婇攢骞撮檺鏃犳晥锛屾棤娉曡鎻愭憡閿�");
+        }
+        return roundMoney(normalizedOriginalValue.multiply(amortizableRatio).divide(months, 8, RoundingMode.HALF_UP));
+    }
+
+    /**
+     * 鍑�鍊� = 鍘熷�� - 绱鎽婇攢銆�
+     */
+    private BigDecimal calculateNetValue(BigDecimal originalValue, BigDecimal accumulatedAmortization) {
+        BigDecimal value = defaultMoney(originalValue).subtract(defaultMoney(accumulatedAmortization));
+        if (value.compareTo(BigDecimal.ZERO) < 0) {
+            value = BigDecimal.ZERO;
+        }
+        return roundMoney(value);
+    }
+
+    private BigDecimal normalizeResidualRate(BigDecimal residualRate) {
+        return residualRate == null ? BigDecimal.ZERO : residualRate;
+    }
+
+    private BigDecimal defaultMoney(BigDecimal value) {
+        return value == null ? ZERO : roundMoney(value);
+    }
+
+    private BigDecimal roundMoney(BigDecimal value) {
+        if (value == null) {
+            return ZERO;
+        }
+        return value.setScale(2, RoundingMode.HALF_UP);
+    }
+
+    private String generateAssetCode() {
+        return "WX" + LocalDateTime.now().format(CODE_TIME_FORMATTER) + new Random().nextInt(10);
+    }
+}
diff --git a/src/main/java/com/ruoyi/account/service/impl/financial/FinLedgerServiceImpl.java b/src/main/java/com/ruoyi/account/service/impl/financial/FinLedgerServiceImpl.java
new file mode 100644
index 0000000..35360e3
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/service/impl/financial/FinLedgerServiceImpl.java
@@ -0,0 +1,206 @@
+package com.ruoyi.account.service.impl.financial;
+
+import com.ruoyi.account.bean.dto.financial.FinDetailLedgerQueryDto;
+import com.ruoyi.account.bean.dto.financial.FinLedgerQueryDto;
+import com.ruoyi.account.bean.vo.financial.FinLedgerEntryRecordVo;
+import com.ruoyi.account.bean.vo.financial.FinLedgerRowVo;
+import com.ruoyi.account.mapper.financial.FinVoucherEntryMapper;
+import com.ruoyi.account.service.financial.FinLedgerService;
+import com.ruoyi.common.exception.ServiceException;
+import com.ruoyi.common.utils.StringUtils;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDate;
+import java.time.YearMonth;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.util.*;
+
+/**
+ * 绉戠洰鎬昏处/鏄庣粏璐︽湇鍔″疄鐜般��
+ */
+@Service
+@RequiredArgsConstructor
+public class FinLedgerServiceImpl implements FinLedgerService {
+
+    private static final DateTimeFormatter MONTH_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM");
+    private static final BigDecimal ZERO = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
+
+    private final FinVoucherEntryMapper finVoucherEntryMapper;
+
+    @Override
+    public List<FinLedgerRowVo> queryGeneralLedger(FinLedgerQueryDto queryDto) {
+        if (queryDto == null || StringUtils.isEmpty(queryDto.getSubjectCode())) {
+            return Collections.emptyList();
+        }
+        YearMonth startMonth = parseMonth(queryDto.getStartMonth(), "寮�濮嬫湀浠�");
+        YearMonth endMonth = parseMonth(queryDto.getEndMonth(), "缁撴潫鏈堜唤");
+        if (startMonth.isAfter(endMonth)) {
+            throw new ServiceException("寮�濮嬫湀浠戒笉鑳藉ぇ浜庣粨鏉熸湀浠�");
+        }
+        return buildLedgerRows(queryDto.getSubjectCode(), startMonth, endMonth, null, null);
+    }
+
+    @Override
+    public List<FinLedgerRowVo> queryDetailLedger(FinDetailLedgerQueryDto queryDto) {
+        if (queryDto == null || StringUtils.isEmpty(queryDto.getSubjectCode())) {
+            return Collections.emptyList();
+        }
+        YearMonth startMonth = parseMonth(queryDto.getStartMonth(), "寮�濮嬫湀浠�");
+        YearMonth endMonth = parseMonth(queryDto.getEndMonth(), "缁撴潫鏈堜唤");
+        if (startMonth.isAfter(endMonth)) {
+            throw new ServiceException("寮�濮嬫湀浠戒笉鑳藉ぇ浜庣粨鏉熸湀浠�");
+        }
+        return buildLedgerRows(queryDto.getSubjectCode(), startMonth, endMonth, queryDto.getAuxiliaryType(), queryDto.getAuxiliaryId());
+    }
+
+    /**
+     * 鏋勫缓璐︾翱琛屾暟鎹紝杈撳嚭鏈熷垵銆佸垎褰曘�佹湰鏈堝悎璁°�佹湰骞寸疮璁°��
+     */
+    private List<FinLedgerRowVo> buildLedgerRows(String subjectCode,
+                                                 YearMonth startMonth,
+                                                 YearMonth endMonth,
+                                                 String auxiliaryType,
+                                                 String auxiliaryId) {
+        LocalDate startDate = startMonth.atDay(1);
+        LocalDate endDate = endMonth.atEndOfMonth();
+
+        List<FinLedgerEntryRecordVo> openingEntries = finVoucherEntryMapper.listPostedEntriesBefore(
+                subjectCode, startDate, auxiliaryType, auxiliaryId
+        );
+        BigDecimal openingBalance = calculateBalance(openingEntries);
+
+        List<FinLedgerEntryRecordVo> currentPeriodEntries = finVoucherEntryMapper.listPostedEntries(
+                subjectCode, startDate, endDate, auxiliaryType, auxiliaryId
+        );
+        Map<YearMonth, List<FinLedgerEntryRecordVo>> monthEntriesMap = groupEntriesByMonth(currentPeriodEntries);
+
+        List<FinLedgerRowVo> rows = new ArrayList<>();
+        BigDecimal runningBalance = openingBalance;
+        BigDecimal yearDebit = ZERO;
+        BigDecimal yearCredit = ZERO;
+
+        for (YearMonth month = startMonth; !month.isAfter(endMonth); month = month.plusMonths(1)) {
+            rows.add(buildOpeningRow(month.atDay(1), runningBalance));
+
+            List<FinLedgerEntryRecordVo> monthEntries = monthEntriesMap.getOrDefault(month, Collections.emptyList());
+            BigDecimal monthDebit = ZERO;
+            BigDecimal monthCredit = ZERO;
+            for (FinLedgerEntryRecordVo entry : monthEntries) {
+                BigDecimal debit = money(entry.getDebit());
+                BigDecimal credit = money(entry.getCredit());
+                runningBalance = runningBalance.add(debit).subtract(credit);
+                monthDebit = monthDebit.add(debit);
+                monthCredit = monthCredit.add(credit);
+
+                FinLedgerRowVo row = new FinLedgerRowVo();
+                row.setRowType("entry");
+                row.setDate(entry.getVoucherDate());
+                row.setVoucherNo(entry.getVoucherNo());
+                row.setSummary(StringUtils.isNotEmpty(entry.getSummary()) ? entry.getSummary() : "");
+                row.setDebit(debit);
+                row.setCredit(credit);
+                row.setBalance(money(runningBalance));
+                row.setDirection(resolveDirection(runningBalance));
+                rows.add(row);
+            }
+
+            rows.add(buildMonthlyTotalRow(month.atEndOfMonth(), monthDebit, monthCredit, runningBalance));
+            yearDebit = yearDebit.add(monthDebit);
+            yearCredit = yearCredit.add(monthCredit);
+        }
+
+        rows.add(buildYearlyTotalRow(endMonth.atEndOfMonth(), yearDebit, yearCredit, runningBalance));
+        return rows;
+    }
+
+    private Map<YearMonth, List<FinLedgerEntryRecordVo>> groupEntriesByMonth(List<FinLedgerEntryRecordVo> entries) {
+        Map<YearMonth, List<FinLedgerEntryRecordVo>> map = new LinkedHashMap<>();
+        for (FinLedgerEntryRecordVo entry : entries) {
+            if (entry.getVoucherDate() == null) {
+                continue;
+            }
+            YearMonth month = YearMonth.from(entry.getVoucherDate());
+            map.computeIfAbsent(month, key -> new ArrayList<>()).add(entry);
+        }
+        return map;
+    }
+
+    private FinLedgerRowVo buildOpeningRow(LocalDate date, BigDecimal openingBalance) {
+        FinLedgerRowVo row = new FinLedgerRowVo();
+        row.setRowType("opening");
+        row.setDate(date);
+        row.setVoucherNo("-");
+        row.setSummary("鏈熷垵浣欓");
+        row.setDebit(ZERO);
+        row.setCredit(ZERO);
+        row.setBalance(money(openingBalance));
+        row.setDirection(resolveDirection(openingBalance));
+        return row;
+    }
+
+    private FinLedgerRowVo buildMonthlyTotalRow(LocalDate date,
+                                                BigDecimal monthDebit,
+                                                BigDecimal monthCredit,
+                                                BigDecimal monthBalance) {
+        FinLedgerRowVo row = new FinLedgerRowVo();
+        row.setRowType("monthly_total");
+        row.setDate(date);
+        row.setVoucherNo("-");
+        row.setSummary("鏈湀鍚堣");
+        row.setDebit(money(monthDebit));
+        row.setCredit(money(monthCredit));
+        row.setBalance(money(monthBalance));
+        row.setDirection(resolveDirection(monthBalance));
+        return row;
+    }
+
+    private FinLedgerRowVo buildYearlyTotalRow(LocalDate date,
+                                               BigDecimal yearDebit,
+                                               BigDecimal yearCredit,
+                                               BigDecimal yearBalance) {
+        FinLedgerRowVo row = new FinLedgerRowVo();
+        row.setRowType("yearly_total");
+        row.setDate(date);
+        row.setVoucherNo("-");
+        row.setSummary("鏈勾绱");
+        row.setDebit(money(yearDebit));
+        row.setCredit(money(yearCredit));
+        row.setBalance(money(yearBalance));
+        row.setDirection(resolveDirection(yearBalance));
+        return row;
+    }
+
+    private BigDecimal calculateBalance(List<FinLedgerEntryRecordVo> entries) {
+        BigDecimal balance = ZERO;
+        for (FinLedgerEntryRecordVo entry : entries) {
+            balance = balance.add(money(entry.getDebit())).subtract(money(entry.getCredit()));
+        }
+        return money(balance);
+    }
+
+    private String resolveDirection(BigDecimal balance) {
+        return money(balance).compareTo(BigDecimal.ZERO) >= 0 ? "鍊�" : "璐�";
+    }
+
+    private YearMonth parseMonth(String value, String fieldLabel) {
+        if (StringUtils.isEmpty(value)) {
+            throw new ServiceException(fieldLabel + "涓嶈兘涓虹┖锛屾牸寮忓簲涓篩YYY-MM");
+        }
+        try {
+            return YearMonth.parse(value, MONTH_FORMATTER);
+        } catch (DateTimeParseException ex) {
+            throw new ServiceException(fieldLabel + "鏍煎紡閿欒锛屾牸寮忓簲涓篩YYY-MM");
+        }
+    }
+
+    private BigDecimal money(BigDecimal value) {
+        if (value == null) {
+            return ZERO;
+        }
+        return value.setScale(2, RoundingMode.HALF_UP);
+    }
+}
diff --git a/src/main/java/com/ruoyi/account/service/impl/financial/FinVoucherServiceImpl.java b/src/main/java/com/ruoyi/account/service/impl/financial/FinVoucherServiceImpl.java
new file mode 100644
index 0000000..9e09020
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/service/impl/financial/FinVoucherServiceImpl.java
@@ -0,0 +1,299 @@
+package com.ruoyi.account.service.impl.financial;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.ruoyi.account.bean.dto.financial.FinVoucherDto;
+import com.ruoyi.account.bean.dto.financial.FinVoucherEntryDto;
+import com.ruoyi.account.bean.dto.financial.FinVoucherPageDto;
+import com.ruoyi.account.bean.vo.financial.FinVoucherDetailVo;
+import com.ruoyi.account.mapper.AccountSubjectMapper;
+import com.ruoyi.account.mapper.financial.FinVoucherEntryMapper;
+import com.ruoyi.account.mapper.financial.FinVoucherMapper;
+import com.ruoyi.account.pojo.AccountSubject;
+import com.ruoyi.account.pojo.financial.FinVoucher;
+import com.ruoyi.account.pojo.financial.FinVoucherEntry;
+import com.ruoyi.account.service.financial.FinVoucherService;
+import com.ruoyi.common.exception.ServiceException;
+import com.ruoyi.common.utils.StringUtils;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.BeanUtils;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 鍑瘉鏈嶅姟瀹炵幇銆�
+ */
+@Service
+@RequiredArgsConstructor
+public class FinVoucherServiceImpl extends ServiceImpl<FinVoucherMapper, FinVoucher> implements FinVoucherService {
+
+    private static final BigDecimal ZERO = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
+
+    private final FinVoucherEntryMapper finVoucherEntryMapper;
+    private final AccountSubjectMapper accountSubjectMapper;
+
+    @Override
+    public IPage<FinVoucher> pageList(Page<FinVoucher> page, FinVoucherPageDto queryDto) {
+        LambdaQueryWrapper<FinVoucher> wrapper = new LambdaQueryWrapper<>();
+        if (queryDto != null && StringUtils.isNotEmpty(queryDto.getVoucherNo())) {
+            wrapper.like(FinVoucher::getVoucherNo, queryDto.getVoucherNo());
+        }
+        if (queryDto != null && StringUtils.isNotEmpty(queryDto.getCreator())) {
+            wrapper.eq(FinVoucher::getCreator, queryDto.getCreator());
+        }
+        if (queryDto != null && StringUtils.isNotEmpty(queryDto.getStatus())) {
+            wrapper.eq(FinVoucher::getStatus, queryDto.getStatus());
+        }
+        if (queryDto != null && queryDto.getStartDate() != null) {
+            wrapper.ge(FinVoucher::getVoucherDate, queryDto.getStartDate());
+        }
+        if (queryDto != null && queryDto.getEndDate() != null) {
+            wrapper.le(FinVoucher::getVoucherDate, queryDto.getEndDate());
+        }
+        wrapper.orderByDesc(FinVoucher::getVoucherDate).orderByDesc(FinVoucher::getId);
+        return page(page, wrapper);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean addVoucher(FinVoucherDto dto) {
+        validateVoucherBasicInfo(dto, false);
+        List<FinVoucherEntry> validEntries = buildAndValidateEntries(dto);
+
+        FinVoucher voucher = new FinVoucher();
+        BeanUtils.copyProperties(dto, voucher);
+        voucher.setStatus("unposted");
+        voucher.setAttachmentCount(voucher.getAttachmentCount() == null ? 0 : voucher.getAttachmentCount());
+        BigDecimal totalDebit = calculateTotalDebit(validEntries);
+        BigDecimal totalCredit = calculateTotalCredit(validEntries);
+        voucher.setDebit(totalDebit);
+        voucher.setCredit(totalCredit);
+        if (StringUtils.isEmpty(voucher.getSummary())) {
+            voucher.setSummary(findDefaultSummary(validEntries));
+        }
+        save(voucher);
+        saveEntries(voucher.getId(), validEntries);
+        return true;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean updateVoucher(FinVoucherDto dto) {
+        validateVoucherBasicInfo(dto, true);
+        FinVoucher existed = getById(dto.getId());
+        if (existed == null) {
+            throw new ServiceException("淇敼澶辫触锛屽嚟璇佷笉瀛樺湪");
+        }
+        if (!"unposted".equals(existed.getStatus())) {
+            throw new ServiceException("浠呮湭杩囪处鍑瘉鍏佽淇敼");
+        }
+        List<FinVoucherEntry> validEntries = buildAndValidateEntries(dto);
+
+        FinVoucher voucher = new FinVoucher();
+        BeanUtils.copyProperties(dto, voucher);
+        voucher.setStatus(existed.getStatus());
+        voucher.setAttachmentCount(voucher.getAttachmentCount() == null ? 0 : voucher.getAttachmentCount());
+        BigDecimal totalDebit = calculateTotalDebit(validEntries);
+        BigDecimal totalCredit = calculateTotalCredit(validEntries);
+        voucher.setDebit(totalDebit);
+        voucher.setCredit(totalCredit);
+        if (StringUtils.isEmpty(voucher.getSummary())) {
+            voucher.setSummary(findDefaultSummary(validEntries));
+        }
+        updateById(voucher);
+
+        LambdaQueryWrapper<FinVoucherEntry> deleteWrapper = new LambdaQueryWrapper<>();
+        deleteWrapper.eq(FinVoucherEntry::getVoucherId, voucher.getId());
+        finVoucherEntryMapper.delete(deleteWrapper);
+        saveEntries(voucher.getId(), validEntries);
+        return true;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean postVoucher(Long id) {
+        FinVoucher voucher = getById(id);
+        if (voucher == null) {
+            throw new ServiceException("杩囪处澶辫触锛屽嚟璇佷笉瀛樺湪");
+        }
+        if (!"unposted".equals(voucher.getStatus())) {
+            throw new ServiceException("浠呮湭杩囪处鍑瘉鍏佽杩囪处");
+        }
+        voucher.setStatus("posted");
+        return updateById(voucher);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean cancelVoucher(Long id) {
+        FinVoucher voucher = getById(id);
+        if (voucher == null) {
+            throw new ServiceException("浣滃簾澶辫触锛屽嚟璇佷笉瀛樺湪");
+        }
+        if (!"unposted".equals(voucher.getStatus())) {
+            throw new ServiceException("浠呮湭杩囪处鍑瘉鍏佽浣滃簾");
+        }
+        voucher.setStatus("cancelled");
+        return updateById(voucher);
+    }
+
+    @Override
+    public FinVoucherDetailVo detail(Long id) {
+        FinVoucher voucher = getById(id);
+        if (voucher == null) {
+            throw new ServiceException("鏌ヨ澶辫触锛屽嚟璇佷笉瀛樺湪");
+        }
+        LambdaQueryWrapper<FinVoucherEntry> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(FinVoucherEntry::getVoucherId, id)
+                .orderByAsc(FinVoucherEntry::getRowNo)
+                .orderByAsc(FinVoucherEntry::getId);
+        List<FinVoucherEntry> entries = finVoucherEntryMapper.selectList(wrapper);
+
+        FinVoucherDetailVo vo = new FinVoucherDetailVo();
+        BeanUtils.copyProperties(voucher, vo);
+        vo.setEntries(entries);
+        return vo;
+    }
+
+    /**
+     * 鏍¢獙鍑瘉涓昏〃瀛楁銆佺姸鎬佸瓧娈典笌鍞竴鎬с��
+     */
+    private void validateVoucherBasicInfo(FinVoucherDto dto, boolean isUpdate) {
+        if (dto == null) {
+            throw new ServiceException("鍑瘉鏁版嵁涓嶈兘涓虹┖");
+        }
+        if (isUpdate && dto.getId() == null) {
+            throw new ServiceException("淇敼澶辫触锛屽嚟璇両D涓嶈兘涓虹┖");
+        }
+        if (StringUtils.isEmpty(dto.getVoucherNo())) {
+            throw new ServiceException("鍑瘉瀛楀彿涓嶈兘涓虹┖");
+        }
+        if (dto.getVoucherDate() == null) {
+            throw new ServiceException("鍑瘉鏃ユ湡涓嶈兘涓虹┖");
+        }
+        LambdaQueryWrapper<FinVoucher> uniqueWrapper = new LambdaQueryWrapper<>();
+        uniqueWrapper.eq(FinVoucher::getVoucherNo, dto.getVoucherNo());
+        if (isUpdate) {
+            uniqueWrapper.ne(FinVoucher::getId, dto.getId());
+        }
+        if (count(uniqueWrapper) > 0) {
+            throw new ServiceException("鍑瘉瀛楀彿宸插瓨鍦紝璇峰嬁閲嶅鎻愪氦");
+        }
+    }
+
+    /**
+     * 杩囨护鏈夋晥鍒嗗綍骞舵墽琛屽�熻捶骞宠 鏍¢獙銆�
+     */
+    private List<FinVoucherEntry> buildAndValidateEntries(FinVoucherDto dto) {
+        List<FinVoucherEntryDto> rawEntries = dto.getEntries();
+        if (rawEntries == null || rawEntries.isEmpty()) {
+            throw new ServiceException("鍒嗗綍涓嶈兘涓虹┖锛岃嚦灏戦渶瑕佷竴鏉℃湁鏁堝垎褰�");
+        }
+        List<FinVoucherEntry> validEntries = new ArrayList<>();
+        int rowNo = 1;
+        for (FinVoucherEntryDto entryDto : rawEntries) {
+            if (entryDto == null || StringUtils.isEmpty(entryDto.getSubjectCode())) {
+                continue;
+            }
+            BigDecimal debit = defaultMoney(entryDto.getDebit());
+            BigDecimal credit = defaultMoney(entryDto.getCredit());
+            if (debit.compareTo(BigDecimal.ZERO) <= 0 && credit.compareTo(BigDecimal.ZERO) <= 0) {
+                continue;
+            }
+            if (debit.compareTo(BigDecimal.ZERO) > 0 && credit.compareTo(BigDecimal.ZERO) > 0) {
+                throw new ServiceException("鍒嗗綍鍊熸柟鍜岃捶鏂逛笉鑳藉悓鏃跺ぇ浜�0");
+            }
+            FinVoucherEntry entry = new FinVoucherEntry();
+            BeanUtils.copyProperties(entryDto, entry);
+            entry.setDebit(debit);
+            entry.setCredit(credit);
+            entry.setRowNo(rowNo++);
+            validEntries.add(entry);
+        }
+        if (validEntries.isEmpty()) {
+            throw new ServiceException("鍒嗗綍鑷冲皯闇�瑕佷竴鏉℃湁鏁堣锛堢鐩笉绌猴紝涓斿�熸柟鎴栬捶鏂瑰ぇ浜�0锛�");
+        }
+
+        // 鍒嗗綍绉戠洰蹇呴』瀛樺湪锛岄伩鍏嶈剰绉戠洰缂栫爜鍏ヨ处銆�
+        Set<String> subjectCodes = validEntries.stream()
+                .map(FinVoucherEntry::getSubjectCode)
+                .filter(StringUtils::isNotEmpty)
+                .collect(Collectors.toSet());
+        if (subjectCodes.isEmpty()) {
+            throw new ServiceException("鍒嗗綍绉戠洰涓嶈兘涓虹┖");
+        }
+        LambdaQueryWrapper<AccountSubject> subjectWrapper = new LambdaQueryWrapper<>();
+        subjectWrapper.in(AccountSubject::getSubjectCode, subjectCodes);
+        List<AccountSubject> subjects = accountSubjectMapper.selectList(subjectWrapper);
+        Map<String, AccountSubject> subjectMap = subjects.stream()
+                .collect(Collectors.toMap(AccountSubject::getSubjectCode, it -> it, (a, b) -> a));
+        for (FinVoucherEntry entry : validEntries) {
+            AccountSubject accountSubject = subjectMap.get(entry.getSubjectCode());
+            if (accountSubject == null) {
+                throw new ServiceException("绉戠洰缂栫爜涓嶅瓨鍦細" + entry.getSubjectCode());
+            }
+            if (StringUtils.isEmpty(entry.getSubjectName())) {
+                entry.setSubjectName(accountSubject.getSubjectName());
+            }
+        }
+
+        BigDecimal totalDebit = calculateTotalDebit(validEntries);
+        BigDecimal totalCredit = calculateTotalCredit(validEntries);
+        if (totalDebit.compareTo(BigDecimal.ZERO) <= 0 || totalCredit.compareTo(BigDecimal.ZERO) <= 0) {
+            throw new ServiceException("鍊熻捶閲戦蹇呴』澶т簬0");
+        }
+        if (totalDebit.compareTo(totalCredit) != 0) {
+            throw new ServiceException("鍊熻捶涓嶅钩琛★紝绂佹淇濆瓨");
+        }
+        return validEntries;
+    }
+
+    private void saveEntries(Long voucherId, List<FinVoucherEntry> entries) {
+        if (voucherId == null) {
+            throw new ServiceException("鍑瘉ID涓嶈兘涓虹┖");
+        }
+        for (FinVoucherEntry entry : entries) {
+            entry.setVoucherId(voucherId);
+            finVoucherEntryMapper.insert(entry);
+        }
+    }
+
+    private String findDefaultSummary(List<FinVoucherEntry> entries) {
+        for (FinVoucherEntry entry : entries) {
+            if (StringUtils.isNotEmpty(entry.getSummary())) {
+                return entry.getSummary();
+            }
+        }
+        return "";
+    }
+
+    private BigDecimal calculateTotalDebit(List<FinVoucherEntry> entries) {
+        BigDecimal total = BigDecimal.ZERO;
+        for (FinVoucherEntry entry : entries) {
+            total = total.add(defaultMoney(entry.getDebit()));
+        }
+        return total.setScale(2, RoundingMode.HALF_UP);
+    }
+
+    private BigDecimal calculateTotalCredit(List<FinVoucherEntry> entries) {
+        BigDecimal total = BigDecimal.ZERO;
+        for (FinVoucherEntry entry : entries) {
+            total = total.add(defaultMoney(entry.getCredit()));
+        }
+        return total.setScale(2, RoundingMode.HALF_UP);
+    }
+
+    private BigDecimal defaultMoney(BigDecimal value) {
+        if (value == null) {
+            return ZERO;
+        }
+        return value.setScale(2, RoundingMode.HALF_UP);
+    }
+}
diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml
index 334e5d7..bda6bcf 100644
--- a/src/main/resources/application-dev.yml
+++ b/src/main/resources/application-dev.yml
@@ -28,7 +28,7 @@
 # 寮�鍙戠幆澧冮厤缃�
 server:
   # 鏈嶅姟鍣ㄧ殑HTTP绔彛锛岄粯璁や负8080
-  port: 7005
+  port: 7006
   servlet:
     # 搴旂敤鐨勮闂矾寰�
     context-path: /
diff --git a/src/main/resources/mapper/account/AccountSubjectMapper.xml b/src/main/resources/mapper/account/AccountSubjectMapper.xml
index 179d858..95f450f 100644
--- a/src/main/resources/mapper/account/AccountSubjectMapper.xml
+++ b/src/main/resources/mapper/account/AccountSubjectMapper.xml
@@ -5,6 +5,7 @@
     <!-- 閫氱敤鏌ヨ鏄犲皠缁撴灉 -->
     <resultMap id="BaseResultMap" type="com.ruoyi.account.pojo.AccountSubject">
         <id column="id" property="id" />
+        <result column="parent_id" property="parentId" />
         <result column="subject_code" property="subjectCode" />
         <result column="subject_name" property="subjectName" />
         <result column="subject_type" property="subjectType" />
@@ -18,4 +19,13 @@
         <result column="dept_id" property="deptId" />
     </resultMap>
 
+    <select id="countReferencedBySubjectCodes" resultType="java.lang.Long">
+        SELECT COUNT(1)
+        FROM fin_voucher_entry
+        WHERE subject_code IN
+        <foreach collection="subjectCodes" item="item" open="(" separator="," close=")">
+            #{item}
+        </foreach>
+    </select>
+
 </mapper>
diff --git a/src/main/resources/mapper/account/financial/FinVoucherEntryMapper.xml b/src/main/resources/mapper/account/financial/FinVoucherEntryMapper.xml
new file mode 100644
index 0000000..56633ba
--- /dev/null
+++ b/src/main/resources/mapper/account/financial/FinVoucherEntryMapper.xml
@@ -0,0 +1,74 @@
+<?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.account.mapper.financial.FinVoucherEntryMapper">
+
+    <resultMap id="BaseResultMap" type="com.ruoyi.account.pojo.financial.FinVoucherEntry">
+        <id column="id" property="id"/>
+        <result column="voucher_id" property="voucherId"/>
+        <result column="row_no" property="rowNo"/>
+        <result column="subject_code" property="subjectCode"/>
+        <result column="subject_name" property="subjectName"/>
+        <result column="summary" property="summary"/>
+        <result column="debit" property="debit"/>
+        <result column="credit" property="credit"/>
+        <result column="auxiliary_type" property="auxiliaryType"/>
+        <result column="auxiliary_id" property="auxiliaryId"/>
+        <result column="auxiliary_name" property="auxiliaryName"/>
+        <result column="create_user" property="createUser"/>
+        <result column="create_time" property="createTime"/>
+        <result column="update_user" property="updateUser"/>
+        <result column="update_time" property="updateTime"/>
+        <result column="dept_id" property="deptId"/>
+    </resultMap>
+
+    <select id="listPostedEntries" resultType="com.ruoyi.account.bean.vo.financial.FinLedgerEntryRecordVo">
+        SELECT
+            v.voucher_date AS voucherDate,
+            v.voucher_no AS voucherNo,
+            CASE
+                WHEN e.summary IS NOT NULL AND e.summary != '' THEN e.summary
+                ELSE v.summary
+            END AS summary,
+            e.debit AS debit,
+            e.credit AS credit,
+            e.row_no AS rowNo
+        FROM fin_voucher_entry e
+        INNER JOIN fin_voucher v ON e.voucher_id = v.id
+        WHERE v.status = 'posted'
+          AND (e.subject_code = #{subjectCode} OR e.subject_code LIKE CONCAT(#{subjectCode}, '%'))
+          AND v.voucher_date <![CDATA[>=]]> #{startDate}
+          AND v.voucher_date <![CDATA[<=]]> #{endDate}
+        <if test="auxiliaryType != null and auxiliaryType != ''">
+          AND e.auxiliary_type = #{auxiliaryType}
+        </if>
+        <if test="auxiliaryId != null and auxiliaryId != ''">
+          AND e.auxiliary_id = #{auxiliaryId}
+        </if>
+        ORDER BY v.voucher_date ASC, v.id ASC, e.row_no ASC, e.id ASC
+    </select>
+
+    <select id="listPostedEntriesBefore" resultType="com.ruoyi.account.bean.vo.financial.FinLedgerEntryRecordVo">
+        SELECT
+            v.voucher_date AS voucherDate,
+            v.voucher_no AS voucherNo,
+            CASE
+                WHEN e.summary IS NOT NULL AND e.summary != '' THEN e.summary
+                ELSE v.summary
+            END AS summary,
+            e.debit AS debit,
+            e.credit AS credit,
+            e.row_no AS rowNo
+        FROM fin_voucher_entry e
+        INNER JOIN fin_voucher v ON e.voucher_id = v.id
+        WHERE v.status = 'posted'
+          AND (e.subject_code = #{subjectCode} OR e.subject_code LIKE CONCAT(#{subjectCode}, '%'))
+          AND v.voucher_date <![CDATA[<]]> #{beforeDate}
+        <if test="auxiliaryType != null and auxiliaryType != ''">
+          AND e.auxiliary_type = #{auxiliaryType}
+        </if>
+        <if test="auxiliaryId != null and auxiliaryId != ''">
+          AND e.auxiliary_id = #{auxiliaryId}
+        </if>
+    </select>
+
+</mapper>

--
Gitblit v1.9.3