chore: 添加输入输出文件 + .claude记忆和计划

输入文件:
- 租赁任务考核_2026年{1,2,3}月.xlsx (考核源数据)
- {1,2}月.xlsx (客户盈亏表)
- 车辆里程考核与奖金发放规则(V.1.2).docx

输出文件:
- 里程任务考核_{1,2,3}月核算.xlsx (月度核算结果)
- 里程任务考核_Q1汇总.xlsx (含车辆台账)
- 3月客户盈亏表(待填写).xlsx (模版)

.claude_memory: 项目记忆(规则/偏好/架构/测试车辆)
.claude_plans: 历次计划文件

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
kkfluous
2026-04-07 14:09:24 +08:00
parent da487c41d4
commit 573f8397a6
32 changed files with 3491 additions and 0 deletions

View File

@@ -0,0 +1,72 @@
# 资产状态多选改造
## Context
当前"总体任务-实时里程"页面的"资产状态"筛选器是单选下拉框,用户需要改为支持多选,以便同时筛选多个资产状态(如同时查看"在库"和"租赁"的车辆)。
## 后端接口修改建议
后端 API 端点:`GET /mileage/vehicle/list`
当前 `storageStatus` 参数为 `string` 类型(单值),需要改为支持多值。**建议两种方案(推荐方案一)**
### 方案一:逗号分隔字符串(推荐,改动最小)
- 参数名不变:`storageStatus`
- 类型不变:`String`
- 前端传值:`storageStatus=在库,租赁,自营`
- 后端解析:用 `split(",")` 拆分为数组SQL 查询改为 `WHERE storage_status IN (...)`
- **优点**前后端改动最小URL 参数简洁,向后兼容(单值时无逗号,行为不变)
### 方案二:数组参数
- 参数名改为:`storageStatuses``storageStatus[]`
- 前端传值:`storageStatuses=在库&storageStatuses=租赁`
- 后端用 `@RequestParam List<String> storageStatuses` 接收
- **优点**:更 RESTful**缺点**:前后端改动较大
### 后端改动清单(方案一)
1. Controller 层:参数类型保持 `String storageStatus`
2. Service 层:`storageStatus.split(",")` 得到数组
3. MyBatis/SQL`WHERE storage_status = #{storageStatus}` 改为:
```xml
<if test="storageStatusList != null and storageStatusList.size() > 0">
AND storage_status IN
<foreach collection="storageStatusList" item="item" open="(" separator="," close=")">
#{item}
</foreach>
</if>
```
4. 导出接口 `/mileage/vehicle/export` 同理修改
## 前端修改计划
### 涉及文件
1. `src/pages/VehicleManagement/index.tsx` — 主页面
2. `src/services/vehicleService.ts` — API 服务
3. `src/types/mileage.ts` — 类型定义
4. `src/pages/H5Mobile/index.tsx` — 移动端页面(同步修改)
### 修改步骤
#### Step 1: 修改类型定义 (`src/types/mileage.ts`)
- `storageStatus?: string` → `storageStatus?: string | string[]`(兼容多选)
#### Step 2: 修改 API 服务 (`src/services/vehicleService.ts`)
- `getVehicleList` 的 `storageStatus` 类型改为 `string | string[]`
- `exportVehicleList` 的 `storageStatus` 类型同样修改
- 在传参时,如果是数组则 `join(',')` 转为逗号分隔字符串发送给后端
#### Step 3: 修改主页面 (`src/pages/VehicleManagement/index.tsx`)
- **搜索参数初始化** (L94): `storageStatus: undefined` → 类型改为 `string[] | undefined`
- **重置** (L346): 保持 `storageStatus: undefined`
- **Select 组件** (L1607-1619): 添加 `mode="multiple"` 属性,启用多选
- **导出参数** (L1800): 数组 join(',') 传给导出函数
- **loadData** (L192): 在传参前将数组 join(',')
#### Step 4: 修改移动端页面 (`src/pages/H5Mobile/index.tsx`)
- 同步修改 Select 为多选模式
## 验证方式
1. 打开"总体任务-实时里程"页面,确认资产状态下拉框可多选
2. 选择多个状态后点击搜索,检查网络请求参数格式正确(逗号分隔)
3. 点击重置,确认多选状态被清空
4. 导出 Excel 时确认多选参数正确传递
5. 移动端页面同步验证

View File

@@ -0,0 +1,47 @@
# 修复区域图表"其他"重复问题
## Context
区域资产分布概览的"按城市"柱状图出现两个"其他"。原因28 辆运营车辆的 `city``province` 均为 `null`GPS 未上报),`resolveCity` 返回"其他",排进 Top 8。然后 Top 8 之外的城市又合并出另一个"其他"。
## 方案
不改 `resolveCity`(城市为空归"其他"是合理的)。修改 `/region-chart` 端点的 Top N 合并逻辑:**先把名为"其他"的条目从排序列表中移除,取 Top N 后,将剩余部分(包括原始的"其他")一起合并为最终的"其他"**。
**文件**: `src/server/routes/vehicles.ts``/region-chart` 端点
### 修改合并逻辑
```typescript
app.get('/region-chart', async (c) => {
const vehicles = await getVehicles();
const operating = vehicles.filter((v) => v.status === 'Operating');
const groupBy = c.req.query('groupBy') || 'region';
const top = Number(c.req.query('top')) || 8;
const counts = new Map<string, number>();
for (const v of operating) {
const key = groupBy === 'city' ? resolveCity(v.city, v.province) : mapMacroRegion(v.province, v.city);
counts.set(key, (counts.get(key) || 0) + 1);
}
// 分离"其他",对非"其他"排序取 Top N剩余全部合入"其他"
const otherCount = counts.get('其他') || 0;
counts.delete('其他');
const sorted = Array.from(counts.entries())
.map(([name, value]) => ({ name, value }))
.sort((a, b) => b.value - a.value);
const result = sorted.slice(0, top);
const restTotal = sorted.slice(top).reduce((s, item) => s + item.value, 0) + otherCount;
if (restTotal > 0) result.push({ name: '其他', value: restTotal });
return c.json(result);
});
```
## 验证
1. `npx tsc --noEmit` 通过
2. `curl http://localhost:3000/api/vehicles/region-chart?groupBy=city&top=8` — 只有一个"其他"
3. 页面柱状图每个名称唯一

View File

@@ -0,0 +1,483 @@
# 还车应结款模块 — 完整实施计划
## Context
OneOS 需要实现完整的「还车应结款」多角色费用核算模块。当前状态:
- **原型**: 3个JSX页面列表/查看/费用明细),需求文档完整
- **老系统**: lingniu_asset_server 有完整实现4表+多部门审批+能源校验),可参考角色设计和业务流程
- **OneOS后端**: ReturnPaymentController/Service 已有骨架但本质是纯CRUD**return_payment 系列表尚未建到数据库**
- **OneOS前端**: 页面UI完成度高但100%使用mock数据
- **数据库现状**: return_vehicle_task(81条)、delivery_vehicle(32条)、traffic_violation(0条)、accident_info(1条)、energy_account(有数据)、vehicle_check_item(轮胎检查项齐全)
### 核心目标
实现多角色4个部门独立填报→提交→审批→生成账单的完整业务流程各部门只能看到和操作自己的费用区块。
---
## 第一阶段:数据库建表 + 角色定义
### 1.1 角色映射(参照老系统 SettlementDepartment
老系统4个结算部门 → OneOS 已有系统角色映射:
| 结算部门 | depCode | OneOS 角色 | role_key | 职责 |
|---------|---------|-----------|----------|------|
| 业务服务组 | BUSINESS | 业务服务组(id=3) / 业务服务主管 / 业务经理 | 业务服务组 | 填写违章违约金、保险上浮、ETC费用、租金计算 |
| 能源采购组 | ENERGY | **需新建角色: 能源采购组** | energy_group | 填写氢量差、能源费补缴、预付款退费 |
| 运维部 | OPERATION | 运维专员 / 运维主管 / 运维助理 | 运维专员 | 填写清洗费、保养维修、车损、证件丢失、轮胎磨损 |
| 安全组 | SAFETY | 安全(id=2026890857448787969) | 安全 | 确认违章清单、事故清单 |
审批角色:
| 角色 | 职责 |
|------|------|
| 业务服务主管 / 业务负责人 | 汇总审核,提交总审批 |
| 财务 / 财务主管 | 生成账单、付款确认 |
| 总经理 | 终审(可选) |
### 1.2 建表SQL重新设计参照老系统+原型需求)
```sql
-- 主表:还车应结款单(一车一单)
CREATE TABLE return_settlement (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
return_task_id BIGINT UNSIGNED NOT NULL COMMENT '关联 return_vehicle_task.id',
delivery_vehicle_id BIGINT UNSIGNED NOT NULL COMMENT '关联 delivery_vehicle.id',
contract_id BIGINT NOT NULL COMMENT '关联 vehicle_lease_contract_info.id',
order_detail_id BIGINT COMMENT '关联 vehicle_lease_order_detail.id',
contract_code VARCHAR(100) COMMENT '合同编号',
project_name VARCHAR(200) COMMENT '项目名称',
customer_id BIGINT COMMENT '客户ID',
customer_name VARCHAR(200) COMMENT '客户名称',
plate_number VARCHAR(50) COMMENT '车牌号',
vehicle_id BIGINT UNSIGNED COMMENT '车辆ID',
business_dept_name VARCHAR(100) COMMENT '业务部门',
business_owner_name VARCHAR(100) COMMENT '业务负责人',
delivery_time DATETIME COMMENT '交车时间',
return_time DATETIME COMMENT '还车时间',
returner_name VARCHAR(100) COMMENT '还车人',
-- 保险标识(从合同/服务项推导)
has_fragile_insurance TINYINT(1) DEFAULT 0 COMMENT '易损保 0否1是',
has_tire_insurance TINYINT(1) DEFAULT 0 COMMENT '轮胎保 0否1是',
has_maintenance_insurance TINYINT(1) DEFAULT 0 COMMENT '养护保 0否1是',
-- 金额汇总
deposit_amount DECIMAL(18,2) DEFAULT 0 COMMENT '保证金总额',
pending_settle_amount DECIMAL(18,2) DEFAULT 0 COMMENT '待结算总额',
should_refund_amount DECIMAL(18,2) DEFAULT 0 COMMENT '应退还总额',
should_pay_amount DECIMAL(18,2) DEFAULT 0 COMMENT '应补缴总额',
-- 审批状态(参照老系统)
approval_status TINYINT DEFAULT 0 COMMENT '0待提交 1撤回 10待审批 20审批中 30审批完成 40审批驳回',
approval_submit_time DATETIME COMMENT '提交审批时间',
is_last_vehicle TINYINT(1) DEFAULT 0 COMMENT '是否合同最后一辆车',
generated_at DATETIME COMMENT '还车应结款生成时间(15天倒计时起点)',
-- 审计字段
del_flag CHAR(1) DEFAULT '0',
create_by BIGINT,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_by BIGINT,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_return_task (return_task_id),
INDEX idx_contract (contract_id),
INDEX idx_plate (plate_number),
INDEX idx_status (approval_status)
) COMMENT '还车应结款主表';
-- 部门提交状态表4个部门各一行
CREATE TABLE return_settlement_dept_status (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
settlement_id BIGINT UNSIGNED NOT NULL COMMENT '关联 return_settlement.id',
dep_code VARCHAR(20) NOT NULL COMMENT 'BUSINESS/ENERGY/OPERATION/SAFETY',
dep_name VARCHAR(50) COMMENT '部门名称',
status TINYINT DEFAULT 0 COMMENT '0待提交 1已提交 2已撤回',
submit_by BIGINT COMMENT '提交人ID',
submit_by_name VARCHAR(100) COMMENT '提交人姓名',
submit_time DATETIME COMMENT '提交时间',
total_amount DECIMAL(18,2) DEFAULT 0 COMMENT '部门费用合计',
remark VARCHAR(500),
del_flag CHAR(1) DEFAULT '0',
create_by BIGINT,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_by BIGINT,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_settlement (settlement_id),
UNIQUE KEY uk_settlement_dep (settlement_id, dep_code)
) COMMENT '部门提交状态';
-- 业务服务组费用明细
CREATE TABLE return_settlement_business_fee (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
settlement_id BIGINT UNSIGNED NOT NULL,
seq INT COMMENT '序号',
fee_item VARCHAR(200) NOT NULL COMMENT '费用项名称',
amount DECIMAL(18,2) DEFAULT 0 COMMENT '金额',
remark VARCHAR(500),
photos TEXT COMMENT '照片URL JSON数组',
attachments TEXT COMMENT '附件URL JSON数组',
is_fixed TINYINT(1) DEFAULT 0 COMMENT '是否固定费用项(不可删)',
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
del_flag CHAR(1) DEFAULT '0',
create_by BIGINT,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_settlement (settlement_id)
) COMMENT '业务服务组费用';
-- 业务服务组-车辆租金
CREATE TABLE return_settlement_rent (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
settlement_id BIGINT UNSIGNED NOT NULL,
bill_start_date DATE COMMENT '账单开始日期',
vehicle_month_rent DECIMAL(18,2) COMMENT '车辆月租金',
received_rent DECIMAL(18,2) DEFAULT 0 COMMENT '本期已收租金',
actual_rent DECIMAL(18,2) DEFAULT 0 COMMENT '车辆实际租金',
should_refund_rent DECIMAL(18,2) DEFAULT 0 COMMENT '车辆应退租金',
del_flag CHAR(1) DEFAULT '0',
create_by BIGINT,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_by BIGINT,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_settlement (settlement_id)
) COMMENT '车辆租金';
-- 能源采购组
CREATE TABLE return_settlement_energy (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
settlement_id BIGINT UNSIGNED NOT NULL,
delivery_hydrogen DECIMAL(10,2) COMMENT '交车氢量(MPa)',
return_hydrogen DECIMAL(10,2) COMMENT '还车氢量(MPa)',
hydrogen_unit_price DECIMAL(10,2) COMMENT '退还车氢气单价',
hydrogen_supplement DECIMAL(18,2) DEFAULT 0 COMMENT '氢量差补缴金额',
hydrogen_fee DECIMAL(18,2) DEFAULT 0 COMMENT '氢费补缴金额',
electric_fee DECIMAL(18,2) DEFAULT 0 COMMENT '电费补缴金额',
prepay_refund DECIMAL(18,2) DEFAULT 0 COMMENT '预付款退费金额',
user_balance DECIMAL(18,2) DEFAULT 0 COMMENT '项目预充值余额(快照)',
del_flag CHAR(1) DEFAULT '0',
create_by BIGINT,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_by BIGINT,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_settlement (settlement_id)
) COMMENT '能源采购组费用';
-- 运维部费用明细
CREATE TABLE return_settlement_operation_fee (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
settlement_id BIGINT UNSIGNED NOT NULL,
seq INT COMMENT '序号',
fee_item VARCHAR(200) NOT NULL COMMENT '费用项',
amount DECIMAL(18,2) DEFAULT 0 COMMENT '金额',
worry_free_discount DECIMAL(18,2) DEFAULT 0 COMMENT '无忧包减免',
remark VARCHAR(500),
photos TEXT COMMENT '照片URL JSON数组',
attachments TEXT COMMENT '附件URL JSON数组',
is_fixed TINYINT(1) DEFAULT 0 COMMENT '是否固定费用项',
is_readonly TINYINT(1) DEFAULT 0 COMMENT '是否只读(送车/接车服务费)',
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
del_flag CHAR(1) DEFAULT '0',
create_by BIGINT,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_settlement (settlement_id)
) COMMENT '运维部费用';
-- 运维部-轮胎胎纹明细
CREATE TABLE return_settlement_tire_tread (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
settlement_id BIGINT UNSIGNED NOT NULL,
tire_name VARCHAR(100) COMMENT '轮胎名称(左前轮等)',
delivery_depth DECIMAL(6,2) COMMENT '交车胎纹深度(mm)',
return_depth DECIMAL(6,2) COMMENT '还车胎纹深度(mm)',
depth_diff DECIMAL(6,2) COMMENT '胎纹差(mm)',
unit_price DECIMAL(10,2) COMMENT '单价(元/mm)',
total_amount DECIMAL(18,2) DEFAULT 0 COMMENT '总金额',
del_flag CHAR(1) DEFAULT '0',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_settlement (settlement_id)
) COMMENT '轮胎胎纹明细';
-- 安全组-违章快照
CREATE TABLE return_settlement_violation (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
settlement_id BIGINT UNSIGNED NOT NULL,
source_violation_id BIGINT COMMENT '来源 traffic_violation.id',
violation_code VARCHAR(100) COMMENT '违章编码',
plate_number VARCHAR(50),
violation_behavior VARCHAR(500),
violation_time DATETIME,
penalty_amount DECIMAL(10,2) DEFAULT 0,
payment_status VARCHAR(20) COMMENT '缴费状态',
score INT DEFAULT 0 COMMENT '计分值',
handle_status VARCHAR(20) COMMENT '处理状态',
violation_customer VARCHAR(200),
violation_photo VARCHAR(500),
remark VARCHAR(500),
del_flag CHAR(1) DEFAULT '0',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_settlement (settlement_id)
) COMMENT '安全组违章快照';
-- 安全组-事故快照
CREATE TABLE return_settlement_accident (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
settlement_id BIGINT UNSIGNED NOT NULL,
source_accident_id BIGINT COMMENT '来源 accident_info.id',
accident_code VARCHAR(100),
plate_number VARCHAR(50),
accident_time DATETIME,
accident_place VARCHAR(200),
accident_type VARCHAR(50),
customer_name VARCHAR(200),
our_claim_amount DECIMAL(10,2),
their_claim_amount DECIMAL(10,2),
responsibility VARCHAR(50),
accident_status VARCHAR(50),
close_time DATE,
other_fee DECIMAL(10,2),
remark VARCHAR(500),
del_flag CHAR(1) DEFAULT '0',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_settlement (settlement_id)
) COMMENT '安全组事故快照';
```
### 1.3 新建角色
```sql
-- 能源采购组角色(系统中尚未有)
INSERT INTO `ry-cloud`.sys_role (role_id, role_name, role_key, role_sort, status, del_flag, create_by, create_time)
VALUES (2040000000000000001, '能源采购组', 'energy_group', 15, '0', '0', 1, NOW());
```
---
## 第二阶段:后端核心重构
### 2.1 文件结构(在 ln-asset-management 内)
```
modules/contract/
├── controller/
│ └── ReturnSettlementController.java ← 新建,替代 ReturnPaymentController
├── service/
│ ├── ReturnSettlementService.java ← 接口
│ └── impl/ReturnSettlementServiceImpl.java ← 核心实现
├── entity/settlement/
│ ├── po/
│ │ ├── ReturnSettlement.java
│ │ ├── ReturnSettlementDeptStatus.java
│ │ ├── ReturnSettlementBusinessFee.java
│ │ ├── ReturnSettlementRent.java
│ │ ├── ReturnSettlementEnergy.java
│ │ ├── ReturnSettlementOperationFee.java
│ │ ├── ReturnSettlementTireTread.java
│ │ ├── ReturnSettlementViolation.java
│ │ └── ReturnSettlementAccident.java
│ ├── req/ ← 各部门提交请求DTO
│ ├── vo/ ← 列表/详情/查看响应DTO
│ └── enums/
│ ├── SettlementDeptCode.java ← BUSINESS/ENERGY/OPERATION/SAFETY
│ ├── DeptSubmitStatus.java ← 待提交/已提交/已撤回
│ └── SettlementApprovalStatus.java ← 0/1/10/20/30/40
├── mapper/
│ └── (9个mapper对应9张表)
```
### 2.2 核心API设计
```
前缀: /asset/return-settlement
列表与查询:
POST /page ← 分页列表(筛选:合同/客户/项目/车牌/还车时间/审批状态)
GET /detail/{id} ← 详情(根据角色过滤可见区块)
GET /view/{id} ← 只读查看(同详情但全部只读)
创建(还车完成时自动触发):
POST /create-from-return/{returnTaskId} ← 从还车任务创建结算单,自动拉取关联数据
各部门提交/撤回(需角色鉴权):
POST /business/save ← 业务服务组保存
POST /business/submit ← 业务服务组提交
POST /business/revoke/{id} ← 业务服务组撤回
POST /energy/save ← 能源采购组保存
POST /energy/submit ← 能源采购组提交
POST /energy/revoke/{id} ← 能源采购组撤回
POST /operation/save ← 运维部保存
POST /operation/submit ← 运维部提交
POST /operation/revoke/{id} ← 运维部撤回
POST /safety/save ← 安全组保存
POST /safety/submit ← 安全组提交
POST /safety/revoke/{id} ← 安全组撤回
审批:
POST /submit-review/{id} ← 提交总审批校验4组已提交 + 15天倒计时
POST /revoke-review/{id} ← 撤回审批
POST /generate-bill/{id} ← 生成账单
```
### 2.3 核心业务逻辑
**create-from-return 自动拉取数据(关键):**
```
1. 从 return_vehicle_task 取:还车氢量、还车里程、接车服务费、还车时间、还车人
2. 从 delivery_vehicle 取:交车氢量、交车里程、送车服务费、月租金、保证金、交车时间
3. 从 vehicle_lease_contract_info 取:合同编号、项目名称、客户、业务部门/负责人
4. 从 vehicle_lease_order_detail 取:月租金(vehicle_rent)、保证金(deposit)、账单开始日
5. 从 vehicle_lease_order_service_item 取:推导保险标识
- 有"设备损坏金(包含易损件)"→ has_fragile_insurance=1
- 有"轮胎磨损费"→ has_tire_insurance=1
- (养护保需确认service_item名称)
6. 从 vehicle_check_item + delivery_vehicle_check_item 取:交车轮胎胎纹深度
7. 从 return_vehicle_check_item 取:还车轮胎胎纹深度
8. 从 traffic_violation 按 vehicle_id + 交车~还车时间范围 查:违章记录快照
9. 从 accident_info 按 vehicle_id + 交车~还车时间范围 查:事故记录快照
10. 从 ln_energy.energy_account 通过 Dubbo 查:客户能源账户余额
11. 判断 is_last_vehicle查同合同下其他 delivery_vehicle 是否都已有还车记录
12. 自动初始化4个 dept_status 记录BUSINESS/ENERGY/OPERATION/SAFETY状态=待提交)
13. 自动初始化固定费用行业务5项 + 运维10项
14. 自动计算轮胎磨损(胎纹差 * 单价)
15. 自动计算证件丢失费(对比交车/还车检查项中的行驶证、营运证等)
16. generated_at = NOW()15天倒计时起点
```
**角色鉴权逻辑:**
```java
// 根据当前用户角色判断可见/可操作的部门区块
public SettlementDeptCode resolveUserDept(Long userId) {
List<String> roleKeys = getUserRoleKeys(userId);
if (roleKeys.contains("业务服务组") || roleKeys.contains("业务服务主管")
|| roleKeys.contains("业务经理") || roleKeys.contains("业务负责人")
|| roleKeys.contains("业务总负责人"))
return BUSINESS;
if (roleKeys.contains("energy_group"))
return ENERGY;
if (roleKeys.contains("运维专员") || roleKeys.contains("运维主管")
|| roleKeys.contains("运维助理") || roleKeys.contains("运维总负责人"))
return OPERATION;
if (roleKeys.contains("安全"))
return SAFETY;
if (roleKeys.contains("财务") || roleKeys.contains("财务主管") || roleKeys.contains("财务总监"))
return null; // 财务可见全部,但不可编辑
if (roleKeys.contains("superadmin") || roleKeys.contains("总经理"))
return null; // 全部可见
return null;
}
```
**费用汇总计算(服务端计算,不依赖前端):**
```
businessTotal = SUM(business_fee.amount) + rent.should_refund_rent
energyTotal = hydrogen_supplement + hydrogen_fee + electric_fee - prepay_refund
operationTotal = SUM(operation_fee.amount)
pendingSettle = businessTotal + energyTotal + operationTotal
shouldRefund = max(0, deposit - pendingSettle)
shouldPay = max(0, pendingSettle - deposit)
```
---
## 第三阶段:前端对接
### 3.1 替换mock为真实API
修改文件:
- `apps/web-antd/src/api/financial/return-payment/index.ts` — 所有API函数指向新后端端点
- `apps/web-antd/src/views/financial/returnPayment/index.vue` — 列表页调真实API
- `apps/web-antd/src/views/financial/returnPayment/detail.vue` — 详情页调真实API
### 3.2 角色可见性控制
```typescript
// 根据当前用户角色控制UI区块
const userDeptCode = computed(() => {
// 从用户信息中获取角色,映射到部门
const roles = userStore.getRoles;
if (roles.includes('业务服务组') || roles.includes('业务服务主管') ...) return 'BUSINESS';
if (roles.includes('energy_group')) return 'ENERGY';
if (roles.includes('运维专员') || roles.includes('运维主管') ...) return 'OPERATION';
if (roles.includes('安全')) return 'SAFETY';
return 'ALL'; // 财务/管理员
});
// 业务服务组区块:仅业务角色可编辑,其他角色不可见或只读
const canEditBusiness = computed(() => userDeptCode.value === 'BUSINESS' || userDeptCode.value === 'ALL');
const canEditEnergy = computed(() => userDeptCode.value === 'ENERGY' || userDeptCode.value === 'ALL');
// ...
```
### 3.3 关键交互保持原型一致
- 保留15天倒计时后端校验 + 前端展示)
- 保留4组折叠/展开
- 保留Popover费用明细弹出
- 保留轮胎磨损hover弹窗
- 保留无忧包减免联动(根据保险标识启用/禁用)
---
## 第四阶段Dubbo接口跨服务数据
### 4.1 ln-energy 暴露接口
```java
// 在 ln-energy 中新建
public interface RemoteEnergySettlementService {
/** 查询客户/项目的能源账户余额 */
BigDecimal getAccountBalance(Long customerId);
/** 查询客户ETC未缴费用合计 */
BigDecimal getUnpaidEtcFee(Long customerId, String plateNumber,
LocalDate startDate, LocalDate endDate);
}
```
### 4.2 ln-asset-management 调用
在 ReturnSettlementServiceImpl 中通过 `@DubboReference` 注入调用。
---
## 实施顺序
| 步骤 | 内容 | 依赖 |
|------|------|------|
| S1 | 执行建表SQL + 新建能源采购组角色 | 无 |
| S2 | 后端Entity/Mapper/Service骨架 | S1 |
| S3 | create-from-return 自动数据拉取 | S2 |
| S4 | 4组 save/submit/revoke API | S2 |
| S5 | 列表/详情/查看 API + 角色鉴权 | S4 |
| S6 | 前端 mock→API 切换 + 角色可见性 | S5 |
| S7 | submit-review + 15天校验 | S4 |
| S8 | Dubbo 接口(能源余额/ETC费用 | S5 |
| S9 | 生成账单/导出 | S7 |
| S10 | 端到端测试 | S6-S9 |
---
## 验证计划
1. **建表验证**: 连接数据库确认9张表已创建
2. **数据拉取验证**: 用已有还车记录调 create-from-return确认自动填充的数据完整性
3. **角色隔离验证**: 分别用不同角色登录,确认只能看到/操作自己的费用区块
4. **费用计算验证**: 修改各部门费用后,确认汇总金额正确
5. **提交流程验证**: 4组依次提交→倒计时结束→提交审核→审批
6. **前端联调**: 列表筛选、详情展示、Popover、轮胎弹窗、15天倒计时
### 关键文件清单
**后端(修改/新建):**
- `ln-asset-management/src/main/java/com/ln/asset/modules/contract/controller/ReturnSettlementController.java`
- `ln-asset-management/src/main/java/com/ln/asset/modules/contract/service/ReturnSettlementService.java`
- `ln-asset-management/src/main/java/com/ln/asset/modules/contract/service/impl/ReturnSettlementServiceImpl.java`
- `ln-asset-management/src/main/java/com/ln/asset/modules/contract/entity/settlement/` (9个PO + req/vo/enums)
- `ln-asset-management/src/main/java/com/ln/asset/modules/contract/mapper/` (9个mapper)
- `ln-asset-management/src/main/resources/db/migration/` (建表SQL)
**前端(修改):**
- `ln-one-os-web/apps/web-antd/src/api/financial/return-payment/index.ts`
- `ln-one-os-web/apps/web-antd/src/views/financial/returnPayment/index.vue`
- `ln-one-os-web/apps/web-antd/src/views/financial/returnPayment/detail.vue`
- `ln-one-os-web/apps/web-antd/src/types/return-payment.ts`
**跨服务(新建):**
- `ln-energy/src/main/java/.../api/RemoteEnergySettlementService.java`

View File

@@ -0,0 +1,123 @@
# 羚牛 BI 报表服务实现计划
## Context
基于 AI Studio 生成的前端原型(`-V1.0`),构建连接真实 MySQL 数据库的 BI 报表服务。前端复用 React+Vite+Tailwind 代码,后端使用 Hono + TypeScript替换 mock 数据为真实数据库查询结果。
**数据库**: `192.168.130.111:3306/lingniu_prod3` (root/lingniu#2024)
---
## 项目结构
```
ln-bi/
├── package.json
├── tsconfig.json
├── vite.config.ts
├── .env # 数据库连接配置
├── src/
│ ├── server/
│ │ ├── index.ts # Hono 服务入口
│ │ ├── db.ts # MySQL 连接池mysql2
│ │ ├── routes/
│ │ │ └── vehicles.ts # 车辆数据 API 路由
│ │ └── types.ts # 后端类型定义
│ ├── App.tsx # 前端主组件(改造自 -V1.0
│ ├── api.ts # 前端 API 调用层
│ ├── types.ts # 前端共享类型
│ ├── main.tsx
│ └── index.css
└── index.html
```
---
## 实现步骤
### Step 1: 项目初始化
1.`ln-bi/` 初始化项目,安装依赖:
- **前端**: react, react-dom, lucide-react, motion, tailwindcss, vite, @vitejs/plugin-react, @tailwindcss/vite
- **后端**: hono, @hono/node-server, mysql2, dotenv
- **开发**: typescript, tsx, @types/node, @types/react, @types/react-dom, concurrently
2. 配置 `tsconfig.json``vite.config.ts`(含 API 代理到后端)
3. 创建 `.env` 文件(数据库连接信息)
4. 配置 `package.json` scripts
- `dev:server` — tsx 启动后端
- `dev:client` — vite 启动前端
- `dev` — concurrently 同时启动前后端
### Step 2: 后端 — 数据库连接与查询
**关键文件**: `src/server/db.ts`, `src/server/routes/vehicles.ts`
1. 创建 MySQL 连接池mysql2/promise
2. 实现主查询 API `GET /api/vehicles`,执行用户提供的 SQL返回所有营运车辆数据
3. 后端对原始数据做聚合计算,返回前端需要的结构:
- `GET /api/vehicles/summary` — 总览统计(总资产、运营数、库存数等)
- `GET /api/vehicles/list` — 车辆列表(支持分页/过滤)
- `GET /api/vehicles/by-type` — 按车型分组汇总
- `GET /api/vehicles/by-batch` — 按批次分组汇总(如果数据库有批次字段)
- `GET /api/vehicles/inventory-analysis` — 库存分析(按区域分布)
**数据映射**SQL 字段 → 前端字段):
| SQL 字段 | 前端字段 | 说明 |
|---------|---------|------|
| 车牌号 | plateNumber | 车牌 |
| 车辆型号Label | type | 车型分类4.5T/18T/49T等 |
| 车辆品牌Label + 车辆型号Label | model | 品牌车型组合 |
| 省/市 | location | 区域(映射为嘉兴/广东/北京/新疆/其他) |
| 车辆租赁状态Label | status | Operating/Inventory/Abnormal |
| 车辆归属状态Label | ownership | Self/Leased/Public/Hanging |
| 合同编码 | batch | 批次信息(或从其他字段推断) |
**注意**: 省/市需映射为前端的 5 大区域(嘉兴、广东、北京、新疆、其他)。归属状态、租赁状态需根据字典值映射为英文枚举。
### Step 3: 后端 — Hono 服务
**关键文件**: `src/server/index.ts`
1. 创建 Hono app挂载车辆路由
2. 添加 CORS 中间件(开发时前端在不同端口)
3. 监听端口 3001
4. 错误处理中间件
### Step 4: 前端改造
**关键文件**: `src/App.tsx`, `src/api.ts`
1.`-V1.0/src/App.tsx` 复制前端代码
2. 创建 `src/api.ts` — 封装 fetch 调用后端 API
3. 改造 `App.tsx`
- 删除 `MOCK_VEHICLES``SUMMARY` 常量
- 删除 `getProcessedData``getProcessedDataByBatch``getInventoryAnalysisData` 纯前端聚合函数(改为后端聚合)
- 添加 `useEffect` + `useState` 从 API 获取数据
- 添加 loading 状态和错误处理
- 保留所有 UI 组件和交互逻辑不变
4. 保留主题切换、展开/折叠、模态框等交互功能
5. 车牌号弹窗改为调用 API 按条件查询
### Step 5: Vite 代理配置
`vite.config.ts` 中配置 `/api` 代理到后端 `http://localhost:3001`,开发时无需 CORS。
---
## 暂不实现(后续补充)
以下统计指标当前 SQL 未覆盖,先用 0 或占位:
- 本周新增 / 本周移除
- 待交车数量
- 本周交付 / 本周归还 / 本周替换
- 批次信息(需确认数据库中对应字段)
---
## 验证方案
1. `npm run dev` 同时启动前后端
2. 访问 `http://localhost:3000` 查看页面
3. 检查浏览器 Network 面板确认 API 调用正常
4. 对比前端显示数据与数据库查询结果是否一致
5. 测试交互功能:主题切换、展开折叠、车牌号弹窗、按型号/批次视图切换

View File

@@ -0,0 +1,275 @@
# 还车费用核算模块 — 完整实施计划
## Context
还车费用核算ReturnSettlement模块框架已搭建但与需求文档`docs/还车费用核算-业务流程与时序.md`)对比存在多个 Gap审批流转未对接 Warm-Flow、驳回重置未实现、无生成账单接口、无权限/状态校验、全量 `Map<String,Object>` 传参(已有强类型 VO/Req 未使用)。本计划按"先重构后补功能"策略分阶段实施。
---
## Phase 0: 代码质量重构(强类型替换 Map
**目标:** 消除运行时字段错误风险,为后续所有功能奠定基础。
### 0.1 Controller 签名替换
| 当前 | 替换为 |
|------|--------|
| `page(@RequestParam Map<String, Object>)``Map` | `page(ReturnSettlementQuery)``IPage<SettlementListVO>` |
| `getDetail(Long id)``Map` | → `SettlementDetailVO` |
| `saveBusiness(@RequestBody Map)` | `@RequestBody @Valid SaveBusinessReq` |
| `saveEnergy(@RequestBody Map)` | `@RequestBody @Valid SaveEnergyReq` |
| `saveOperation(@RequestBody Map)` | `@RequestBody @Valid SaveOperationReq` |
| `saveSafety(@RequestBody Map)` | `@RequestBody @Valid SaveSafetyReq` |
submit 方法与 save 使用相同 Req 类。
### 0.2 Service 接口签名同步
`ReturnSettlementService` 中所有方法签名从 `Map` 改为强类型。
### 0.3 ServiceImpl 内部重构
- `convertToListVO` → 返回 `SettlementListVO`
- `getDetail` → 构建 `SettlementDetailVO` 而非手动 put Map
- `saveBusinessData` / `saveEnergyData` / `saveOperationData` → 接收强类型 Req
- 删除 `getStr()`, `getBigDecimal()`, `getInt()` 工具方法
### 0.4 VO 字段补齐
`SettlementDetailVO` 内嵌类需补充字段:
- `DeptStatusVO` 增加 `status` (Integer, 0/1/2 三态) + `totalAmount` (BigDecimal)
- `BusinessFeeVO` 增加 `id`, `seq`, `isFixed`
- `OperationFeeVO` 增加 `id`, `seq`, `isFixed`, `isReadonly`
- `TireTreadVO` 增加 `id`
### 0.5 N+1 查询优化
`page()` 方法中批量查所有 settlementId 对应的 deptStatus构建 `Map<Long, List<DeptStatus>>` 缓存,避免列表每条记录单独查询。
### 验证
- 编译通过
- Postman 调用 page、detail、save、submit 接口JSON 结构与前端兼容
- 前端页面功能无回归
### 关键文件
- `ReturnSettlementController.java`
- `ReturnSettlementService.java`
- `ReturnSettlementServiceImpl.java`
- `SettlementDetailVO.java` / `SettlementListVO.java`
- `SaveBusinessReq.java` / `SaveEnergyReq.java` / `SaveOperationReq.java` / `SaveSafetyReq.java`
- `ReturnSettlementQuery.java`
---
## Phase 1: 权限与状态校验
**目标:** 堵住安全漏洞。
### 1.1 权限校验
新增公共方法:
```java
private void assertDeptPermission(SettlementDeptCode requiredDept) {
String userDept = resolveUserDept();
if (userDept != null && !requiredDept.getCode().equals(userDept)) {
throw new ServiceException("您没有权限操作" + requiredDept.getName() + "的数据");
}
}
```
在所有 save/submit/revoke 方法开头调用。
### 1.2 审批状态前置校验
```java
private void assertEditableStatus(Long settlementId) {
ReturnSettlement s = assertSettlementExists(settlementId);
if (s.getApprovalStatus() >= SettlementApprovalStatus.PENDING_APPROVAL.getCode()) {
throw new ServiceException("结算单已进入审批流程,无法编辑");
}
}
```
### 1.3 部门提交状态校验
- submit 校验当前部门状态不为 SUBMITTED防重复提交
- revoke 校验状态为 SUBMITTED
### 验证
- 不同角色账号跨部门操作 → 403
- 审批中状态下 save → 被拦截
### 关键文件
- `ReturnSettlementServiceImpl.java`
---
## Phase 2: Warm-Flow 审批对接
**目标:** submitReview 启动工作流,审批完成/驳回通过 Listener 回调。
### 2.1 submitReview 对接
注入 `RemoteWorkflowService``@DubboReference`submitReview 中调用 `startCompleteTask` 启动流程。
- businessId 格式:`"ReturnSettlement_" + id`
- flowCode`returnSettlement`(需在 Warm-Flow 后台配置)
- 状态直接写 `APPROVING(20)`startCompleteTask 完成第一个任务后已在审批中)
### 2.2 revokeReview 对接
调用 `remoteWorkflowService.cancelProcessApply` 撤销流程实例。
### 2.3 新增 Dubbo 远程接口
`ln-asset-api` 新增 `RemoteReturnSettlementService`
```java
public interface RemoteReturnSettlementService {
boolean updateApprovalStatus(Long settlementId, Integer approvalStatus);
boolean resetDeptStatuses(Long settlementId);
}
```
`ln-asset-management` 实现 `RemoteReturnSettlementServiceImpl`
### 2.4 新增 Warm-Flow Listener
参照 `ContractStatusChangeListener.java` 模式:
- `SettlementApprovalListener.java` — 审批通过 → `updateApprovalStatus(id, 30)`
- `SettlementRejectListener.java` — 审批驳回 → `updateApprovalStatus(id, 40)` + `resetDeptStatuses(id)`
### 2.5 审批驳回重置逻辑
`resetDeptStatuses`4个部门状态全部重置为 `PENDING(0)`,清空 submitBy/submitByName/submitTime主表 approvalStatus 置为 `PENDING_SUBMIT(0)`
### 审批状态机
```
0(待提交) --submitReview--> 20(审批中) --approve--> 30(审批完成)
--reject--> 40(驳回) --reset--> 0(待提交)
<--revokeReview-- 20(审批中) --> 1(撤回)
```
### 验证
- 启动微服务集群 → 创建结算单 → 4部门填报 → submitReview → 工作流待办出现
- 审批通过 → approvalStatus = 30
- 审批驳回 → approvalStatus = 40 + 4个 deptStatus 回到 0
- revokeReview → 工作流实例撤销
### 关键文件
- `ReturnSettlementServiceImpl.java`
- 新增 `RemoteReturnSettlementService.java` (ln-asset-api)
- 新增 `RemoteReturnSettlementServiceImpl.java` (ln-asset-management)
- 新增 `SettlementApprovalListener.java` (ln-cloud/ruoyi-workflow)
- 新增 `SettlementRejectListener.java` (ln-cloud/ruoyi-workflow)
- 参照 `ContractStatusChangeListener.java`
- 参照 `RemoteContractService.java`
---
## Phase 3: 生成账单
**目标:** 审批完成后财务可生成账单。
### 3.1 Controller 新增端点
```java
@PostMapping("/generate-bill/{id}")
public R<Long> generateBill(@PathVariable Long id)
```
### 3.2 Service 实现
1. 校验 approvalStatus == 30
2. 校验权限(仅 superadmin/财务)
3. 幂等检查ReturnSettlement 主表增加 `bill_id` 字段)
4. 从结算单构建 `Bills` 对象(参照 `BillGenerateService.createBills`
5. 保存 Bills回写 bill_id
### 3.3 前端补充
`return-payment/index.ts` 新增 `generateBill(id)` API。列表页增加"生成账单"按钮(仅 approvalStatus=30 + 财务角色可见)。
### 验证
- 审批完成的结算单调用 generate-bill → Bills 表有记录
- 非完成状态调用 → 错误
- 重复调用 → 幂等
### 关键文件
- `ReturnSettlementController.java`
- `ReturnSettlementService.java` / `ReturnSettlementServiceImpl.java`
- `ReturnSettlement.java` (新增 bill_id 字段)
- 前端 `return-payment/index.ts`
- 参照 `BillsServiceImpl.java`
---
## Phase 4: 能源余额拉取 + 租金自动计算
**目标:** 补充业务计算逻辑,独立于 Phase 1-3可并行。
### 4.1 能源余额
注入 `RemoteEnergyAccountService``@DubboReference`),在 `createInitialEnergy` 中拉取 userBalance。调用失败降级为 ZERO + warn 日志。
### 4.2 租金自动计算
`createInitialRent` 中:`actualRent = (月租金 / 30) * (交车日到还车日天数)`,业务组 save 时可手动覆盖。
### 验证
- 创建结算单 → energy.userBalance 正确
- rent.actualRent 正确计算
- ln-energy 不可用时降级不阻断
### 关键文件
- `ReturnSettlementServiceImpl.java`
---
## Phase 5: 低优先级
### 5.1 倒计时字段
VO 增加 `deadline` (Date) + `remainingDays` (Integer)。
### 5.2 导出 Excel
新增 `GET /return-settlement/export`
### 5.3 无忧包减免后端校验
saveOperationData 中校验 worryFreeDiscount > 0 时对应保险标识为 1。
---
## 依赖关系
```
Phase 0 (重构) ─────┐
├──► Phase 1 (权限+状态) ──► Phase 2 (审批) ──► Phase 3 (账单)
Phase 4 (能源+租金) ─┘ (可与 Phase 1/2 并行)
Phase 5 (低优先级) — 独立
```
## 新增文件清单
| 文件 | 位置 | 用途 |
|------|------|------|
| `RemoteReturnSettlementService.java` | ln-asset-api | Dubbo 远程接口 |
| `RemoteReturnSettlementServiceImpl.java` | ln-asset-management | Dubbo 实现 |
| `SettlementApprovalListener.java` | ln-cloud/ruoyi-workflow | 审批通过监听 |
| `SettlementRejectListener.java` | ln-cloud/ruoyi-workflow | 审批驳回监听 |

View File

@@ -0,0 +1,599 @@
# 车辆租赁合同模块实施计划
## Context背景
### 为什么需要这个变更
车辆租赁业务是 OneOS 系统的核心业务模块,需要管理与客户签订的车辆租赁合同,包括合同信息、车辆订单、服务项目、被授权人、审批流程等。该模块需要与 BPM 工作流引擎深度集成,实现合同的审批流转。
### 问题或需求
1. **业务需求**:管理车辆租赁合同的全生命周期(创建→审批→执行→到期→续签/终止)
2. **审批流程**4级审批业务部主管→事业部主管→财务部→法务部
3. **复杂关联**:合同关联客户、车辆、服务项目、被授权人、附件等多个实体
4. **特殊业务**:续签合同、转正式合同、变更为三方合同、新增车辆等
5. **状态管理**:审批状态 × 合同状态的复杂状态机
### 预期结果
构建一个完整的车辆租赁合同管理模块,支持合同的 CRUD、审批流程、特殊业务操作并与现有的客户管理、车辆管理、BPM 工作流模块无缝集成。
---
## 架构设计
### 整体架构
```
Controller (REST API)
Service (业务逻辑)
├── Mapper (数据访问)
├── BpmProcessInstanceApi (流程启动)
└── FileApi (附件上传)
BpmProcessInstanceStatusEventListener (流程状态监听)
Service (更新合同状态)
```
### 模块结构
```
yudao-module-asset/yudao-module-asset-server/
├── controller/admin/contract/
│ └── ContractController.java
├── service/contract/
│ ├── ContractService.java
│ ├── ContractServiceImpl.java
│ └── listener/
│ └── ContractBpmListener.java
├── dal/
│ ├── dataobject/contract/
│ │ ├── ContractDO.java
│ │ ├── ContractVehicleDO.java
│ │ ├── ContractVehicleServiceDO.java
│ │ ├── ContractAuthorizedDO.java
│ │ ├── ContractAttachmentDO.java
│ │ └── ContractChangeHistoryDO.java
│ └── mysql/contract/
│ ├── ContractMapper.java
│ ├── ContractVehicleMapper.java
│ ├── ContractVehicleServiceMapper.java
│ ├── ContractAuthorizedMapper.java
│ ├── ContractAttachmentMapper.java
│ └── ContractChangeHistoryMapper.java
├── controller/admin/contract/vo/
│ ├── ContractBaseVO.java
│ ├── ContractSaveReqVO.java
│ ├── ContractRespVO.java
│ ├── ContractPageReqVO.java
│ ├── ContractDetailRespVO.java
│ ├── ContractVehicleVO.java
│ ├── ContractVehicleServiceVO.java
│ └── ContractAuthorizedVO.java
├── convert/contract/
│ └── ContractConvert.java
└── enums/
├── ContractTypeEnum.java
├── ContractApprovalStatusEnum.java
└── ContractStatusEnum.java
```
---
## 数据库表设计
### 1. asset_contract合同主表
```sql
CREATE TABLE `asset_contract` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
-- 合同基本信息
`contract_code` VARCHAR(50) NOT NULL COMMENT '合同编码',
`contract_type` TINYINT NOT NULL DEFAULT 1 COMMENT '合同类型1=试用 2=正式)',
`project_name` VARCHAR(200) NOT NULL COMMENT '项目名称',
`start_date` DATE NOT NULL COMMENT '生效日期',
`end_date` DATE NOT NULL COMMENT '结束日期',
`payment_method` VARCHAR(50) COMMENT '付款方式',
`payment_cycle` VARCHAR(50) COMMENT '付款周期',
`signing_company` VARCHAR(200) COMMENT '签约公司(乙方)',
`delivery_province` VARCHAR(50) COMMENT '交车省份',
`delivery_city` VARCHAR(50) COMMENT '交车城市',
`delivery_location` VARCHAR(255) COMMENT '交车地点',
`remark` VARCHAR(1000) COMMENT '备注',
-- 甲方客户信息(关联 asset_customer
`customer_id` BIGINT NOT NULL COMMENT '客户ID',
`customer_name` VARCHAR(200) NOT NULL COMMENT '客户名称(冗余)',
-- 丙方客户信息(三方合同,可选)
`third_party_enabled` BIT(1) NOT NULL DEFAULT b'0' COMMENT '是否三方合同',
`third_party_customer_id` BIGINT COMMENT '丙方客户ID',
`third_party_name` VARCHAR(200) COMMENT '丙方名称',
-- 业务信息
`business_dept_id` BIGINT NOT NULL COMMENT '业务部门ID',
`business_manager_id` BIGINT NOT NULL COMMENT '业务负责人ID',
-- 审批状态
`approval_status` TINYINT NOT NULL DEFAULT 0 COMMENT '审批状态0=草稿 1=审批中 2=审批通过 3=审批拒绝 4=已撤回)',
`bpm_instance_id` VARCHAR(64) COMMENT 'BPM流程实例ID',
-- 合同状态
`contract_status` TINYINT NOT NULL DEFAULT 0 COMMENT '合同状态0=草稿 1=待生效 2=进行中 3=已到期 4=已终止 5=已续签)',
`effective_time` DATETIME COMMENT '实际生效时间',
`terminate_time` DATETIME COMMENT '终止时间',
`terminate_reason` VARCHAR(500) COMMENT '终止原因',
-- 续签信息
`renewed_contract_id` BIGINT COMMENT '续签后的新合同ID',
`original_contract_id` BIGINT COMMENT '原合同ID如果是续签合同',
-- 系统字段
`creator` VARCHAR(64) DEFAULT '' COMMENT '创建者',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` VARCHAR(64) DEFAULT '' COMMENT '更新者',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
`tenant_id` BIGINT NOT NULL DEFAULT 0 COMMENT '租户编号',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_contract_code` (`contract_code`, `deleted`),
INDEX `idx_customer_id` (`customer_id`),
INDEX `idx_approval_status` (`approval_status`),
INDEX `idx_contract_status` (`contract_status`),
INDEX `idx_business_dept` (`business_dept_id`),
INDEX `idx_start_date` (`start_date`),
INDEX `idx_end_date` (`end_date`),
INDEX `idx_create_time` (`create_time`),
INDEX `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='车辆租赁合同表';
```
### 2. asset_contract_vehicle车辆租赁订单
```sql
CREATE TABLE `asset_contract_vehicle` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`contract_id` BIGINT NOT NULL COMMENT '合同ID',
`vehicle_id` BIGINT COMMENT '车辆ID关联 asset_vehicle_base',
`brand` VARCHAR(100) NOT NULL COMMENT '品牌',
`model` VARCHAR(100) NOT NULL COMMENT '型号',
`plate_no` VARCHAR(20) COMMENT '车牌号',
`vin` VARCHAR(50) COMMENT 'VIN码',
`month_rent` DECIMAL(10,2) NOT NULL COMMENT '月租金(元)',
`deposit` DECIMAL(10,2) NOT NULL COMMENT '保证金(元)',
`vehicle_status` TINYINT DEFAULT 0 COMMENT '车辆状态0=待交车 1=已交车 2=已退车)',
`actual_delivery_time` DATETIME COMMENT '实际交车时间',
`delivery_person` VARCHAR(50) COMMENT '交车人',
`remark` VARCHAR(500) COMMENT '备注',
`creator` VARCHAR(64) DEFAULT '' COMMENT '创建者',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` VARCHAR(64) DEFAULT '' COMMENT '更新者',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
`tenant_id` BIGINT NOT NULL DEFAULT 0 COMMENT '租户编号',
PRIMARY KEY (`id`),
INDEX `idx_contract_id` (`contract_id`),
INDEX `idx_vehicle_id` (`vehicle_id`),
INDEX `idx_plate_no` (`plate_no`),
INDEX `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='合同车辆租赁订单表';
```
### 3. asset_contract_vehicle_service服务项目
```sql
CREATE TABLE `asset_contract_vehicle_service` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`contract_vehicle_id` BIGINT NOT NULL COMMENT '合同车辆ID',
`service_name` VARCHAR(100) NOT NULL COMMENT '服务项目名称',
`service_fee` DECIMAL(10,2) NOT NULL COMMENT '服务费用(元)',
`effective_date` DATE COMMENT '生效日期',
`creator` VARCHAR(64) DEFAULT '' COMMENT '创建者',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` VARCHAR(64) DEFAULT '' COMMENT '更新者',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
`tenant_id` BIGINT NOT NULL DEFAULT 0 COMMENT '租户编号',
PRIMARY KEY (`id`),
INDEX `idx_contract_vehicle_id` (`contract_vehicle_id`),
INDEX `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='合同车辆服务项目表';
```
### 4. asset_contract_authorized被授权人
```sql
CREATE TABLE `asset_contract_authorized` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`contract_id` BIGINT NOT NULL COMMENT '合同ID',
`name` VARCHAR(50) NOT NULL COMMENT '姓名',
`phone` VARCHAR(20) NOT NULL COMMENT '电话',
`id_card` VARCHAR(18) NOT NULL COMMENT '身份证号',
`creator` VARCHAR(64) DEFAULT '' COMMENT '创建者',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` VARCHAR(64) DEFAULT '' COMMENT '更新者',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
`tenant_id` BIGINT NOT NULL DEFAULT 0 COMMENT '租户编号',
PRIMARY KEY (`id`),
INDEX `idx_contract_id` (`contract_id`),
INDEX `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='合同被授权人表';
```
### 5. asset_contract_attachment合同附件
```sql
CREATE TABLE `asset_contract_attachment` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`contract_id` BIGINT NOT NULL COMMENT '合同ID',
`attachment_type` TINYINT NOT NULL COMMENT '附件类型1=合同原件 2=盖章合同)',
`file_id` BIGINT NOT NULL COMMENT '文件ID关联 infra_file',
`file_name` VARCHAR(255) NOT NULL COMMENT '文件名称',
`file_url` VARCHAR(500) NOT NULL COMMENT '文件URL',
`file_size` BIGINT COMMENT '文件大小(字节)',
`upload_time` DATETIME NOT NULL COMMENT '上传时间',
`uploader` VARCHAR(64) COMMENT '上传人',
`creator` VARCHAR(64) DEFAULT '' COMMENT '创建者',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` VARCHAR(64) DEFAULT '' COMMENT '更新者',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
`tenant_id` BIGINT NOT NULL DEFAULT 0 COMMENT '租户编号',
PRIMARY KEY (`id`),
INDEX `idx_contract_id` (`contract_id`),
INDEX `idx_file_id` (`file_id`),
INDEX `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='合同附件表';
```
### 6. asset_contract_change_history变更历史
```sql
CREATE TABLE `asset_contract_change_history` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`contract_id` BIGINT NOT NULL COMMENT '合同ID',
`change_type` VARCHAR(50) NOT NULL COMMENT '变更类型(保存/提交审批/审批通过/审批驳回/撤回/终止/续签/转正式/变更三方/新增车辆等)',
`change_content` VARCHAR(1000) COMMENT '变更内容',
`operator` VARCHAR(64) NOT NULL COMMENT '操作人',
`operate_time` DATETIME NOT NULL COMMENT '操作时间',
`creator` VARCHAR(64) DEFAULT '' COMMENT '创建者',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
`tenant_id` BIGINT NOT NULL DEFAULT 0 COMMENT '租户编号',
PRIMARY KEY (`id`),
INDEX `idx_contract_id` (`contract_id`),
INDEX `idx_operate_time` (`operate_time`),
INDEX `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='合同变更历史表';
```
---
## BPM 集成方案
### 流程定义
- **流程定义 Key**: `rental_contract_approval`
- **流程名称**: 车辆租赁合同审批
- **审批节点**:
1. 业务部主管审批
2. 事业部主管审批
3. 财务部审批
4. 法务部审批(上传盖章合同)
### 流程变量
```java
Map<String, Object> variables = new HashMap<>();
variables.put("contractId", contractId);
variables.put("contractCode", contractCode);
variables.put("contractType", contractType);
variables.put("customerName", customerName);
variables.put("projectName", projectName);
variables.put("totalAmount", totalAmount);
variables.put("businessDeptId", businessDeptId);
variables.put("businessManagerId", businessManagerId);
```
### 候选人策略
- **业务部主管**: 根据 `businessDeptId` 动态分配(部门主管角色)
- **事业部主管**: 固定角色 `BUSINESS_DIRECTOR`
- **财务部**: 财务部角色 `FINANCE_DEPT`
- **法务部**: 法务部角色 `LEGAL_DEPT`
### 事件监听器
```java
@Component
public class ContractBpmListener {
@Resource
private ContractService contractService;
@EventListener
public void onProcessInstanceStatusChange(BpmProcessInstanceStatusEvent event) {
// 只处理租赁合同审批流程
if (!"rental_contract_approval".equals(event.getProcessDefinitionKey())) {
return;
}
Long contractId = Long.parseLong(event.getBusinessKey());
switch (event.getStatus()) {
case APPROVE: // 审批通过
contractService.handleApprovalApproved(contractId, event.getResult());
break;
case REJECT: // 审批驳回
contractService.handleApprovalRejected(contractId, event.getResult());
break;
case CANCEL: // 取消/撤回
contractService.handleApprovalCancelled(contractId);
break;
}
}
}
```
---
## API 接口设计
### 基础 CRUD 接口
```
POST /asset/contract/create - 创建合同
PUT /asset/contract/update - 更新合同
DELETE /asset/contract/delete - 删除合同
GET /asset/contract/get - 获取合同详情
GET /asset/contract/page - 分页查询合同
```
### 审批相关接口
```
POST /asset/contract/submit - 提交审批
POST /asset/contract/withdraw - 撤回合同
GET /asset/contract/approval-history - 审批历史
```
### 特殊业务接口
```
POST /asset/contract/terminate - 终止合同
POST /asset/contract/renew - 续签合同
POST /asset/contract/convert-formal - 转正式合同
POST /asset/contract/convert-tripartite - 变更为三方合同
POST /asset/contract/add-vehicle - 新增车辆
```
### 查询接口
```
GET /asset/contract/change-history - 变更历史
GET /asset/contract/vehicle/list - 合同车辆列表
GET /asset/contract/authorized/list - 被授权人列表
GET /asset/contract/attachment/list - 合同附件列表
```
---
## 状态机设计
### 审批状态approval_status
```
0 - 草稿DRAFT
1 - 审批中APPROVING
2 - 审批通过APPROVED
3 - 审批拒绝REJECTED
4 - 已撤回WITHDRAWN
```
### 合同状态contract_status
```
0 - 草稿DRAFT
1 - 待生效PENDING
2 - 进行中IN_PROGRESS
3 - 已到期EXPIRED
4 - 已终止TERMINATED
5 - 已续签RENEWED
```
### 状态流转规则
**审批状态流转:**
```
草稿 → 审批中 → 审批通过
↓ ↓
已撤回 审批拒绝
```
**合同状态流转:**
```
草稿 → 待生效 → 进行中 → 已到期 → 已续签
已终止
```
### 状态与操作关系矩阵
| 操作 | 草稿 | 审批中 | 审批通过 | 审批拒绝 | 已撤回 |
|------|------|--------|----------|----------|--------|
| 编辑 | ✓ | ✗ | ✗ | ✓ | ✓ |
| 删除 | ✓ | ✗ | ✗ | ✓ | ✓ |
| 提交审批 | ✓ | ✗ | ✗ | ✓ | ✓ |
| 撤回 | ✗ | ✓ | ✗ | ✗ | ✗ |
| 终止 | ✗ | ✗ | ✓ | ✗ | ✗ |
| 续签 | ✗ | ✗ | ✓ | ✗ | ✗ |
| 转正式 | ✗ | ✗ | ✓ | ✗ | ✗ |
| 变更三方 | ✗ | ✗ | ✓ | ✗ | ✗ |
| 新增车辆 | ✗ | ✗ | ✓ | ✗ | ✗ |
---
## 实现分阶段计划
### 阶段1基础合同管理不含审批
**目标**: 实现合同的基础 CRUD 功能
**关键文件**:
1. 数据库表创建脚本
2. DO 类6个
3. Mapper 接口6个
4. VO 类8个
5. Convert 接口
6. Service 接口和实现
7. Controller
8. 枚举类3个
**功能清单**:
- ✓ 创建合同(包含车辆订单、服务项目、被授权人)
- ✓ 更新合同
- ✓ 删除合同
- ✓ 查询合同详情
- ✓ 分页查询合同列表
- ✓ 查询变更历史
**验证方式**:
- 使用 Postman 测试所有 CRUD 接口
- 验证数据库数据正确性
- 验证多表关联查询
### 阶段2BPM 审批集成
**目标**: 集成 Flowable 工作流引擎,实现审批流程
**关键文件**:
1. BPM 流程定义文件BPMN 2.0 XML
2. ContractBpmListener.java事件监听器
3. 审批相关 Service 方法
4. 审批相关 Controller 接口
**功能清单**:
- ✓ 提交审批(启动 BPM 流程)
- ✓ 撤回审批
- ✓ 监听审批结果(通过/驳回)
- ✓ 更新合同审批状态
- ✓ 查询审批历史
**验证方式**:
- 创建合同并提交审批
- 在 BPM 管理界面审批
- 验证合同状态自动更新
- 验证审批历史记录
### 阶段3特殊业务流程
**目标**: 实现续签、转正式、变更三方、新增车辆等特殊业务
**关键文件**:
1. 特殊业务 Service 方法
2. 特殊业务 Controller 接口
3. 特殊业务 VO 类
**功能清单**:
- ✓ 终止合同
- ✓ 续签合同(创建新合同,关联原合同)
- ✓ 转正式合同(试用→正式)
- ✓ 变更为三方合同(增加丙方客户)
- ✓ 新增车辆(合同执行中新增车辆)
**验证方式**:
- 测试每个特殊业务流程
- 验证数据关联正确性
- 验证状态流转正确性
---
## 关键文件清单
### 数据库脚本
- `/Users/kkfluous/Projects/ai-coding/ln-oneos/oneos-backend/yudao-module-asset/sql/mysql/contract.sql`
### DO 类
- `ContractDO.java` - 合同主表
- `ContractVehicleDO.java` - 车辆订单
- `ContractVehicleServiceDO.java` - 服务项目
- `ContractAuthorizedDO.java` - 被授权人
- `ContractAttachmentDO.java` - 合同附件
- `ContractChangeHistoryDO.java` - 变更历史
### Mapper 接口
- `ContractMapper.java`
- `ContractVehicleMapper.java`
- `ContractVehicleServiceMapper.java`
- `ContractAuthorizedMapper.java`
- `ContractAttachmentMapper.java`
- `ContractChangeHistoryMapper.java`
### VO 类
- `ContractBaseVO.java` - 基础 VO
- `ContractSaveReqVO.java` - 创建/更新请求 VO
- `ContractRespVO.java` - 响应 VO
- `ContractPageReqVO.java` - 分页查询请求 VO
- `ContractDetailRespVO.java` - 详情响应 VO
- `ContractVehicleVO.java` - 车辆订单 VO
- `ContractVehicleServiceVO.java` - 服务项目 VO
- `ContractAuthorizedVO.java` - 被授权人 VO
### Service
- `ContractService.java` - Service 接口
- `ContractServiceImpl.java` - Service 实现
- `ContractBpmListener.java` - BPM 事件监听器
### Controller
- `ContractController.java` - REST API 控制器
### Convert
- `ContractConvert.java` - MapStruct 转换器
### 枚举类
- `ContractTypeEnum.java` - 合同类型枚举
- `ContractApprovalStatusEnum.java` - 审批状态枚举
- `ContractStatusEnum.java` - 合同状态枚举
### BPM 流程定义
- `rental_contract_approval.bpmn20.xml` - 租赁合同审批流程定义
---
## 注意事项
1. **数据一致性**: 合同、车辆订单、服务项目、被授权人等多表操作需要使用事务
2. **状态校验**: 每个操作前需要校验当前状态是否允许该操作
3. **权限控制**: 使用 `@PreAuthorize` 控制接口权限
4. **多租户**: 所有表都包含 `tenant_id` 字段,自动过滤
5. **软删除**: 使用 `deleted` 字段实现软删除
6. **审计字段**: 自动记录 `creator``create_time``updater``update_time`
7. **BPM 集成**: 流程实例 ID 需要保存到合同表,便于查询审批状态
8. **文件上传**: 使用 `FileApi` 上传附件,保存文件 ID 和 URL
9. **变更历史**: 每次重要操作都需要记录变更历史
10. **合同编码**: 自动生成格式HT-YYYY-NNNHT=合同YYYY=年份NNN=序号)
---
## 时间估算
- **阶段1**: 基础合同管理 - 2天
- 数据库表设计和创建 - 4小时
- DO/Mapper/VO/Convert - 4小时
- Service 实现 - 6小时
- Controller 实现 - 2小时
- 测试验证 - 2小时
- **阶段2**: BPM 审批集成 - 1天
- BPM 流程定义 - 2小时
- 事件监听器 - 2小时
- 审批相关接口 - 2小时
- 测试验证 - 2小时
- **阶段3**: 特殊业务流程 - 1天
- 续签/转正式/变更三方 - 4小时
- 新增车辆/终止合同 - 2小时
- 测试验证 - 2小时
**总计:约 4 天**

View File

@@ -0,0 +1,107 @@
# 简化账单架构 — 删除3张bill表
## Context
当前能源模块有3层架构原始记录 → energy_bill_detail(统一明细) → 3张bill表(按费用类型拆分的账单)。
实际业务中扣费发生在审核通过创建 bill_detail 时(立即扣),不需要月度账单汇总。
ETC 和电费的 bill 服务是未实现的 stub`throw UnsupportedOperationException`),氢费 bill 服务虽然实现了但当前阶段不需要账单汇总、站方确认、提交财务等功能。
简化为:原始记录 → 审核 → energy_bill_detail终态表直接扣费。billing 页面直接查 bill_detail 按 feeType 过滤。
## 改动范围
### 删除文件(~30个
**Controller (3):**
- `modules/energy/controller/EnergyBillController.java`
- `modules/etc/controller/EtcBillController.java`
- `modules/electricity/controller/ElectricityBillController.java`
**Service (4-6):**
- `modules/energy/service/IEnergyBillService.java` + `impl/EnergyBillServiceImpl.java`
- `modules/etc/service/IEtcBillService.java` + `impl/EtcBillServiceImpl.java`(如存在)
- `modules/electricity/service/IElectricityBillService.java` + `impl/ElectricityBillServiceImpl.java`(如存在)
- `modules/energy/service/IBillAdjustmentService.java` + `impl/BillAdjustmentServiceImpl.java`(如存在)
**Mapper (3-4):**
- `modules/energy/mapper/EnergyHydrogenBillMapper.java`
- `modules/energy/mapper/EnergyBillAdjustmentMapper.java`(如存在)
- `modules/etc/mapper/EnergyEtcBillMapper.java`
- `modules/electricity/mapper/EnergyElectricityBillMapper.java`
**Entity PO (4):**
- `modules/energy/entity/bill/po/EnergyHydrogenBill.java`
- `modules/energy/entity/bill/po/EnergyBillAdjustment.java`(如存在)
- `modules/etc/entity/bill/po/EnergyEtcBill.java`
- `modules/electricity/entity/bill/po/EnergyElectricityBill.java`
**Entity VO/Query/Req整个 bill 子包):**
- `modules/energy/entity/bill/vo/` — 所有 VOEnergyBillVO, EnergyBillDetailVO, BillPreviewVO, BillStatisticsVO, BillAdjustmentVO
- `modules/energy/entity/bill/query/BillQuery.java`
- `modules/energy/entity/bill/req/` — 所有 ReqBillGenerateReq, BillReviewReq, AddAdjustmentReq
- `modules/etc/entity/bill/vo/EtcBillVO.java`
- `modules/etc/entity/bill/req/EtcBillGenerateReq.java`
- `modules/electricity/entity/bill/vo/ElectricityBillVO.java`
- `modules/electricity/entity/bill/req/ElectricityBillGenerateReq.java`
**State Machine如存在:**
- `modules/energy/statemachine/BillStatusMachine.java`
**Finance 相关(仅删 bill 部分):**
- `modules/energy/service/finance/model/BillSubmitRequest.java`
- FinanceService 接口中的 `submitBill` 方法需移除
### 修改文件
| 文件 | 改动 |
|---|---|
| `entity/billdetail/po/EnergyBillDetail.java` | 删除 `billId` 字段 |
| `entity/billdetail/vo/EnergyBillDetailVO.java` | 删除 `billId` 字段 |
| `service/impl/EnergyBillDetailServiceImpl.java` | toVO 中删除 `.billId()` |
| `entity/detail/po/EnergyHydrogenDetail.java` | 删除 `billId` 字段 |
| `entity/detail/vo/EnergyDetailVO.java` | 删除 `billId` 字段 |
| `entity/account/po/EnergyAccountTransaction.java` | 删除 `relatedBillId` 字段 |
| `entity/account/vo/TransactionVO.java` | 删除 `relatedBillId` 字段 |
| `service/impl/EnergyAccountTransactionServiceImpl.java` | toVO 中删除 `.relatedBillId()` |
| `service/finance/FinanceService.java` | 删除 `submitBill` 方法签名 |
| `service/finance/LocalFinanceServiceImpl.java` | 删除 `submitBill` 实现 |
### SQL 迁移
新建 `db/energy/V3__drop_bill_rollup_tables.sql`:
```sql
DROP TABLE IF EXISTS energy_bill_adjustment;
DROP TABLE IF EXISTS energy_hydrogen_bill;
DROP TABLE IF EXISTS energy_etc_bill;
DROP TABLE IF EXISTS energy_electricity_bill;
ALTER TABLE energy_bill_detail DROP COLUMN IF EXISTS bill_id;
ALTER TABLE energy_hydrogen_detail DROP COLUMN IF EXISTS bill_id;
ALTER TABLE etc_toll_record DROP COLUMN IF EXISTS bill_id;
ALTER TABLE energy_account_transaction DROP COLUMN IF EXISTS related_bill_id;
```
## 执行顺序
1. **删除所有 bill 相关 Java 文件**
2. **修改 8 个文件**(删除 billId/relatedBillId 字段引用)
3. **编译验证**(确保无断裂引用)
4. **提交并推送**
5. **部署后执行 SQL 迁移**
## 不动的部分
- energy_bill_detail 表 + Controller + Service核心保留
- 所有原始记录表和相关代码station/etc/electricity 模块的 record 部分)
- 事件系统和监听器
- 审核流程RawRecordReviewServiceImpl
- 扣款服务DeductionService
- 前端(用户自行调整 API 路径)
## 验证
1. 编译通过:`mvn compile` 无错误
2. 直接访问 `GET http://localhost:8702/energy/bill-detail/page?feeType=1` 返回数据
3. 原有的 `/energy/bill/page``/etc/bill/page``/electricity/bill/page` 返回 404已删除
4. 审核+扣费流程不受影响

View File

@@ -0,0 +1,192 @@
# 加氢站对账单功能
## Context
加氢记录审核通过后,需要定期和加氢站对账结算。当前只有一个 `settlement_status` 字段0未对账/1已出账/2已付款但没有对账单实体。需要新建对账单模块支持生成对账单 → 查看/下载 → 确认付款 → 状态同步到氢费记录。
## 核心概念
```
一张对账单 = 一个加氢站 + 一个时间段内的所有已审核订单
```
- **生成条件**时间范围内该站所有订单必须已审核reviewStatus=1
- **付款含义**:我们向加氢站付款(成本结算)
- **状态流转**:生成(已出账) → 付款(已付款) / 删除(回退未对账)
- **关联订单**:对账单关联的订单 snapshot 快照,生成后订单 settlementStatus 同步为已出账
- **下载格式**Excel + PDF
---
## 数据模型
### 新建表:`hydrogen_settlement_bill`(加氢站对账单)
| 字段 | 类型 | 说明 |
|------|------|------|
| id | BIGINT PK | 主键 |
| bill_code | VARCHAR(32) UNIQUE | 对账单编号SB+日期+序号) |
| station_id | BIGINT | 加氢站ID |
| station_name | VARCHAR(128) | 加氢站名称(冗余) |
| period_start | DATE | 对账起始日期 |
| period_end | DATE | 对账截止日期 |
| order_count | INT | 订单笔数 |
| total_gas_weight | DECIMAL(12,4) | 总加注量(kg) |
| total_amount | DECIMAL(12,2) | 总金额(元) — 成本金额 |
| status | TINYINT | 0-已出账 1-已付款 |
| paid_time | DATETIME | 付款时间 |
| paid_by | BIGINT | 付款确认人 |
| remark | VARCHAR(500) | 备注 |
| create_by/create_time/update_by/update_time/del_flag | | 审计字段 |
### 新建表:`hydrogen_settlement_bill_item`(对账单明细快照)
| 字段 | 类型 | 说明 |
|------|------|------|
| id | BIGINT PK | 主键 |
| bill_id | BIGINT | 关联对账单ID |
| order_id | BIGINT | 关联氢费记录ID |
| order_number | VARCHAR(64) | 订单编号 |
| plate_number | VARCHAR(16) | 车牌号 |
| fill_end_time | DATETIME | 加注时间 |
| gas_weight | DECIMAL(10,4) | 加注重量(kg) |
| gas_price | DECIMAL(10,4) | 气价(元/kg) |
| total_amount | DECIMAL(10,2) | 金额(元) |
| create_time | DATETIME | 创建时间 |
> 注:明细快照不含客户/项目信息——加氢站对账只关心站方数据(车牌、时间、重量、气价、金额)。
---
## API 设计
### 后端接口
| 方法 | 路径 | 说明 |
|------|------|------|
| POST | `/station/settlement-bill/generate` | 生成对账单(传 stationId, periodStart, periodEnd |
| GET | `/station/settlement-bill/page` | 对账单分页列表 |
| GET | `/station/settlement-bill/{id}` | 对账单详情(含明细) |
| PUT | `/station/settlement-bill/pay/{id}` | 确认付款 |
| DELETE | `/station/settlement-bill/{id}` | 删除对账单(仅已出账可删) |
| GET | `/station/settlement-bill/export-excel/{id}` | 导出 Excel |
| GET | `/station/settlement-bill/export-pdf/{id}` | 导出 PDF |
### 生成对账单逻辑
```
1. 校验:时间范围内该站所有订单 reviewStatus 必须全部为 1已审核
2. 校验:时间范围内该站不能有 settlementStatus != 0未对账的订单避免重复出账
3. 查询:该站 + 时间范围 + reviewStatus=1 + settlementStatus=0 的所有订单
4. 汇总:计算 orderCount, totalGasWeight, totalAmount
5. 创建hydrogen_settlement_bill + 逐条 snapshot 到 bill_item
6. 更新:所有关联订单的 settlementStatus → 1已出账
7. 返回对账单ID
```
### 确认付款逻辑
```
1. 校验:对账单 status 必须为 0已出账
2. 更新:对账单 status → 1已付款记录 paidTime, paidBy
3. 批量更新:关联订单的 settlementStatus → 2已付款
```
### 删除对账单逻辑
```
1. 校验:对账单 status 必须为 0已出账未付款才可删
2. 批量回退:关联订单的 settlementStatus → 0未对账
3. 删除:对账单 + 明细(逻辑删除)
```
---
## 前端设计
### 加氢记录页面 — 增加「生成对账单」按钮
工具栏新增按钮,点击弹出对话框:
- 选择加氢站(下拉)
- 选择对账时间范围(日期区间选择器)
- 点击「生成」→ 调用后端 → 成功后自动跳转对账单详情页
### 对账单页面(/energy/order/settlement-bill/:id
不单独设菜单,通过加氢记录页面跳转。
**页面结构:**
```
┌─ 对账单详情 ─────────────────────────────┐
│ │
│ 对账单号SB20260325001 │
│ 加氢站:嘉兴站 状态:[已出账] │
│ 对账周期2026-03-01 ~ 2026-03-31 │
│ │
│ ┌────────┬────────┬────────┐ │
│ │ 笔数 │ 总加注量│ 总金额 │ │
│ │ 25 │ 150 kg │ ¥4,500 │ │
│ └────────┴────────┴────────┘ │
│ │
│ [确认付款] [删除] [导出Excel] [导出PDF] │
│ │
│ ── 明细列表 ── │
│ 订单编号 | 车牌 | 加注时间 | 重量(kg) | 气价(元/kg) | 金额(元) │
│ ... │
└──────────────────────────────────────────┘
```
### 对账单列表页(可选,也可嵌在加氢记录页面作为 Tab
暂不独立列表页,通过加氢记录页面「已出账/已付款」筛选 + 点击跳转查看。
---
## 文件清单
### 后端 — ln-energy
| 操作 | 文件 |
|------|------|
| Create | `db/station/V6__create_settlement_bill.sql` |
| Create | `entity/settlement/po/HydrogenSettlementBill.java` |
| Create | `entity/settlement/po/HydrogenSettlementBillItem.java` |
| Create | `entity/settlement/vo/SettlementBillVO.java` |
| Create | `entity/settlement/vo/SettlementBillDetailVO.java` |
| Create | `entity/settlement/req/GenerateSettlementBillReq.java` |
| Create | `mapper/HydrogenSettlementBillMapper.java` |
| Create | `mapper/HydrogenSettlementBillItemMapper.java` |
| Create | `service/ISettlementBillService.java` |
| Create | `service/impl/SettlementBillServiceImpl.java` |
| Create | `controller/SettlementBillController.java` |
| Modify | `db/init/V1__init_ln_energy.sql` — 补建表语句 |
### 前端 — ln-one-os-web
| 操作 | 文件 |
|------|------|
| Create | `views/station/settlement/bill-detail.vue` — 对账单详情页 |
| Create | `views/station/settlement/generate-modal.vue` — 生成对账单对话框 |
| Create | `api/station/settlement/index.ts` — 对账单 API |
| Modify | `views/station/order/index.vue` — 工具栏加「生成对账单」按钮 |
| Modify | 后端动态菜单 — 隐藏菜单注册hideInMenu |
### 数据库
| 操作 | 说明 |
|------|------|
| Execute DDL | hydrogen_settlement_bill + hydrogen_settlement_bill_item |
| Menu | 注册隐藏菜单(对账单详情页路由) |
---
## 验证
1. `mvn clean compile` 编译通过
2. DDL 执行到数据库
3. 测试:选择加氢站 + 时间范围 → 生成对账单 → 跳转详情
4. 测试:时间范围内有未审核订单 → 生成失败提示
5. 测试:确认付款 → 对账单状态+订单状态同步
6. 测试:删除对账单 → 订单状态回退
7. 测试:已付款的对账单不可删除
8. 测试:导出 Excel / PDF

View File

@@ -0,0 +1,234 @@
# 合同管理全功能实现计划
## Context
原型有7个页面列表、新增、查看、新增车辆、续签、变更三方、转正式当前前端只实现了列表+新增/编辑的基础框架。需要补全所有功能,包括后端缺失字段/接口。
## 架构决策
1. **单组件多模式**: form.vue 通过 `mode` 支持 7 种模式create/edit/view/renew/convertThirdParty/convertFormal/addVehicle不创建额外组件
2. **服务费项目**: 在车辆表格中使用可展开行(expandedRowRender)显示服务费明细
3. **附件上传**: 使用现有 `uploadFile` API (`api/infra/file/index.ts`)
---
## Phase 1: 后端变更
### 1.1 SQL 迁移 — `asset_contract` 新增6字段
```sql
ALTER TABLE asset_contract
ADD COLUMN hydrogen_bearer VARCHAR(20) COMMENT '氢费承担方',
ADD COLUMN hydrogen_payment_method VARCHAR(20) COMMENT '氢气付款方式',
ADD COLUMN hydrogen_prepay DECIMAL(12,2) COMMENT '氢气预付款',
ADD COLUMN hydrogen_return_price DECIMAL(12,2) COMMENT '退还车氢气单价',
ADD COLUMN billing_method VARCHAR(50) COMMENT '账单计算方式',
ADD COLUMN main_vehicle_type VARCHAR(50) COMMENT '主车型';
```
### 1.2 更新 ContractDO
文件: `dal/dataobject/contract/ContractDO.java`
添加: `hydrogenBearer`, `hydrogenPaymentMethod`, `hydrogenPrepay`(BigDecimal), `hydrogenReturnPrice`(BigDecimal), `billingMethod`, `mainVehicleType`
### 1.3 更新 ContractBaseVO
文件: `controller/admin/contract/vo/ContractBaseVO.java`
添加同样6个字段自动流入 SaveReqVO/RespVO/DetailRespVO
### 1.4 更新 ContractPageReqVO
文件: `controller/admin/contract/vo/ContractPageReqVO.java`
添加: `signingCompany`(String), `businessManagerId`(Long), `creator`(String)
### 1.5 更新 ContractMapper.selectPage
文件: `dal/mysql/contract/ContractMapper.java`
添加3个查询条件: `.eqIfPresent(signingCompany)`, `.eqIfPresent(businessManagerId)`, `.likeIfPresent(creator)`
### 1.6 ContractRespVO 增加 vehicleCount/deliveredCount
文件: `controller/admin/contract/vo/ContractRespVO.java`
添加: `Integer vehicleCount`, `Integer deliveredCount`
在 Controller 的 page 方法中查询填充
### 1.7 新增 ContractAttachmentVO
新文件: `controller/admin/contract/vo/ContractAttachmentVO.java`
字段: `id`, `attachmentType`, `fileName`, `fileUrl`, `fileSize`, `uploadTime`
### 1.8 更新 ContractDetailRespVO
添加 `List<ContractAttachmentVO> attachments` 字段
### 1.9 新增错误码
文件: `enums/ErrorCodeConstants.java` (从 1_008_005_010 开始)
- CONTRACT_STATUS_NOT_ALLOW_CONVERT
- CONTRACT_STATUS_NOT_ALLOW_ADD_VEHICLE
- CONTRACT_TYPE_NOT_TRIAL
### 1.10 新增4个 Controller 端点
文件: `controller/admin/contract/ContractController.java`
| 端点 | 方法 | 说明 |
|------|------|------|
| POST /convert-to-third-party | convertToThirdParty(id, ContractSaveReqVO) | 从原合同创建三方合同 |
| POST /convert-to-formal | convertToFormal(id, ContractSaveReqVO) | 试用合同转正式 |
| POST /add-vehicle | addVehicle(id, List<VehicleSaveVO>) | 往现有合同追加车辆 |
| POST /upload-seal | uploadSeal(id, fileUrl, fileName) | 上传盖章附件 |
### 1.11 ContractService 新增方法
文件: `service/contract/ContractService.java` + `ContractServiceImpl.java`
- `convertToThirdParty`: 验证合同进行中 → 创建新合同(thirdPartyEnabled=true, originalContractId) → 记录变更历史
- `convertToFormal`: 验证试用合同+进行中 → 创建正式合同(type=2) → 原合同标记终止 → 记录变更历史
- `addVehiclesToContract`: 验证合同进行中 → 追加车辆+服务费 → 记录变更历史
- `uploadSealedContract`: 创建 ContractAttachmentDO(type=2)
---
## Phase 2: 前端 API
文件: `api/asset/contract.ts`
### 2.1 新增接口类型
```typescript
ContractAuthorized { id?, name, phone, idCard }
ContractAttachment { id?, attachmentType, fileName, fileUrl, fileSize?, uploadTime? }
ContractVehicleService { id?, serviceName, serviceFee, effectiveDate? }
```
### 2.2 更新 Contract 接口
添加: `hydrogenBearer`, `hydrogenPaymentMethod`, `hydrogenPrepay`, `hydrogenReturnPrice`, `billingMethod`, `mainVehicleType`, `authorizedPersons`, `attachments`
### 2.3 更新 ContractVehicle 接口
添加: `services?: ContractVehicleService[]`
### 2.4 新增 API 函数
`convertToThirdParty`, `convertToFormal`, `addVehicle`, `uploadSeal`
---
## Phase 3: 前端 data.ts
文件: `views/asset/contract/data.ts`
### 3.1 搜索表单增加字段
- `businessDeptId` — Select (业务部门)
- `businessManagerId` — Select (业务负责人)
- `creator` — Input (创建人)
### 3.2 新增服务费类型常量
`SERVICE_TYPE_OPTIONS` — 42项代处理费用、罚款、违章处理违约金...
---
## Phase 4: 前端 index.vue
文件: `views/asset/contract/index.vue`
### 4.1 新增操作处理函数
- `handleView(row)` → 打开 form modal, mode='view'
- `handleRenew(row)` → 打开 form modal, mode='renew'(替换占位)
- `handleConvertThirdParty(row)` → 打开 form modal, mode='convertThirdParty'
- `handleConvertFormal(row)` → 打开 form modal, mode='convertFormal'
- `handleAddVehicle(row)` → 打开 form modal, mode='addVehicle'
### 4.2 扩展 getRowActions
| 条件 | 操作 |
|------|------|
| 始终 | 查看 → handleView |
| draft/rejected/withdrawn | 编辑、提交审批、删除 |
| approving | 撤回审批 |
| contractStatus=2 (进行中) | 新增车辆、续签、终止 |
| contractStatus=2 且 !thirdPartyEnabled | 变更为三方 |
| contractStatus=2 且 contractType=1 (试用) | 转正式 |
| contractStatus=3 (已到期) | 续签 |
---
## Phase 5: 前端 form.vue 重构
文件: `views/asset/contract/modules/form.vue`
### 5.1 模式管理
```typescript
type FormMode = 'create' | 'edit' | 'view' | 'renew' | 'convertThirdParty' | 'convertFormal' | 'addVehicle'
const mode = ref<FormMode>('create')
const isReadOnly = computed(() => mode.value === 'view')
const isContractReadOnly = computed(() => ['view', 'addVehicle'].includes(mode.value))
```
### 5.2 Modal 标题
根据 mode 显示: 新增合同 / 编辑合同 / 查看合同 / 续签合同 / 变更为三方合同 / 转正式合同 / 新增车辆
### 5.3 onOpenChange 分模式加载
- `create`: 清空所有表单
- `edit`: 获取详情 → 填充全部
- `view`: 获取详情 → 填充全部 → 禁用
- `renew`: 获取详情 → 清ID → 修改contractCode后缀 → 可编辑
- `convertThirdParty`: 获取详情 → 清ID → 设thirdPartyEnabled=true
- `convertFormal`: 获取详情 → 清ID → 设contractType=2 → 固定
- `addVehicle`: 获取详情 → 合同信息只读 → 清空车辆列表
### 5.4 onConfirm 分模式提交
各模式调用对应 API (create/update/renew/convertToThirdParty/convertToFormal/addVehicle)
### 5.5 表单禁用控制
- view模式: 所有表单 disabled, 隐藏确认按钮
- addVehicle模式: 客户/合同/三方表单 disabled, 仅车辆可编辑
- convertFormal模式: contractType 字段 disabled
### 5.6 车辆服务费项目
使用 Ant Design Table 的 `expandedRowRender` slot:
- 展开行显示子表格: 服务名称(Select), 服务费(InputNumber), 生效日期(DatePicker)
- 支持新增/删除行
- serviceCost 列自动计算 = sum(services[].serviceFee)
### 5.7 附件上传集成
- 上传时调用 `uploadFile` 获取 fileUrl
- 提交时将 attachments 数组type=1 原件)随合同数据保存
- 查看模式展示附件列表,可下载
- 续签/转正式:原合同附件标记 isOriginal=true 不可删除
### 5.8 氢气/计费数据绑定
`hydrogenBearer`, `hydrogenPaymentMethod`, `hydrogenPrepay`, `hydrogenReturnPrice`, `billingMethod` 纳入保存数据;编辑时回显
---
## 实施顺序
1. **后端**: SQL迁移 → DO/VO更新 → Mapper更新 → Service新增方法 → Controller新增端点 → 编译验证
2. **前端API**: 更新接口和函数
3. **前端data.ts**: 搜索表单 + 服务费常量
4. **前端form.vue**: 模式管理 → 禁用控制 → 分模式加载/提交 → 服务费展开行 → 附件上传 → 氢气/计费绑定
5. **前端index.vue**: 新操作函数 → getRowActions 扩展
6. **验证**: `mvn compile` + `pnpm run build:antd`
---
## 验证
- 后端: `mvn compile` 无报错
- 前端: `pnpm run build:antd` 无报错
- SQL: 通过 PyMySQL 执行 ALTER TABLE
- 功能: 7种模式逐一打开/提交验证

View File

@@ -0,0 +1,701 @@
# Phase 2 ETC 模块完成计划Controller + API Client + 前端)
## Context
Phase 1 基础设施层FeeType 枚举、DeductionRequest、IDeductionService 重构、ContractMatchService、ReviewConfig 扩展)已完成并编译通过。
Phase 2 ETC 模块后端核心Entity/Mapper/Service/Event/Listener已完成并编译通过。
**本次任务:** 完成 Phase 2 剩余部分:
1. ETC 后端 Controller 层3 个 Controller
2. EtczjApiClientetczj.com HTTP 客户端 + OCR 验证码)
3. 前端 ETC 配置页面 + ETC 账单查询页面
**用户决策:**
- 账户结构:共用一个能源账户(三种费用从同一余额扣款)
- 账单模式:各费用类型独立账单
- 业务流程ETC/电费与氢费完全一致(合同匹配 → 审核 → 扣款)
- 数据源:先设计通用框架,具体 API/Excel 格式后续对接
---
## 架构方案:独立明细表 + 共享账户层
```
modules/
station/ ← 已有:氢费数据源(不动)
etc/ ← 新增ETC 数据源
electricity/ ← 新增:电费数据源
energy/ ← 已有:共享结算层(小幅扩展)
payment/ ← 已有:款项管理(小幅扩展)
```
**核心原则:** 三种费用的明细/账单各自独立(字段差异大),但扣款引擎、账户、流水、充值、款项管理完全共享。
---
## Phase 1: 基础设施层改造energy 域)
### 1.1 新增 `FeeType` 枚举
**文件:** `modules/energy/enums/FeeType.java`(新建)
```java
public enum FeeType {
HYDROGEN(1, "氢费"),
ETC(2, "高速通行费"),
ELECTRICITY(3, "电费");
private final int code;
private final String label;
}
```
### 1.2 重构 `IDeductionService` → 接受通用扣款请求
**改动文件:**
- `modules/energy/service/IDeductionService.java`
- `modules/energy/service/impl/DeductionServiceImpl.java`
**改动内容:**
新增通用 DTO `DeductionRequest`
```java
@Data @Builder
public class DeductionRequest {
private FeeType feeType;
private Long detailId; // 明细ID在各自的明细表中
private Long customerId;
private String customerName;
private Long contractId;
private BigDecimal amount; // 扣款金额
private Integer paymentMode; // 1-预充值 2-月结 3-自行结算
private Integer deductionStatus; // 当前扣款状态(幂等守卫用)
}
```
接口签名变更:
```java
// 原DeductionResult deduct(EnergyHydrogenDetail detail);
// 新:
DeductionResult deduct(DeductionRequest request);
void reDeduct(DeductionRequest request, BigDecimal newAmount);
```
`DeductionServiceImpl` 改动点:
- `deduct()` 方法参数从 `EnergyHydrogenDetail` 改为 `DeductionRequest`,逻辑不变(读 customerId/amount/contractId/paymentMode
- **移除** `detailMapper.updateById(detail)` —— 扣款服务不再直接更新明细表,改为返回 `DeductionResult`,由调用方负责更新各自的明细表扣款状态
- 流水 description 加入 `feeType.getLabel()`(如 "ETC扣款" / "电费扣款"
- `reDeduct()` 同理,不再直接操作 `detailMapper`
**向后兼容:**`EnergyDetailImportListener`(氢费监听器)中构造 `DeductionRequest.fromHydrogenDetail(detail)`,调用新接口后手动更新氢费明细扣款状态。
### 1.3 `energy_account_transaction` 表加 `fee_type` 列
**DDL 迁移脚本:** `db/energy/V2__add_fee_type_to_transaction.sql`
```sql
ALTER TABLE energy_account_transaction
ADD COLUMN fee_type TINYINT NOT NULL DEFAULT 1 COMMENT '费用类型1-氢费 2-ETC 3-电费';
ALTER TABLE energy_account_transaction
ADD INDEX idx_eat_fee_type (fee_type);
```
`EnergyAccountTransaction.java` 新增 `feeType` 字段。
### 1.4 `customer_payment_item` 款项解构扩展
`fee_type` 枚举扩展(已有字段):
- 现有1-租赁费 2-氢费充值 3-押金 4-违约金 5-其他
- 新增6-ETC充值 7-电费充值
`PaymentFlowService` 的流转逻辑需根据新 fee_type 创建对应充值单。
### 1.5 通用合同匹配抽取
现有合同匹配逻辑在 `EnergyDetailImportListener` 中(车牌+时间→合同)。
抽取为共享服务:
**新建:** `modules/energy/service/IContractMatchService.java`
```java
public interface IContractMatchService {
ContractMatchResult matchContract(String plateNumber, Date eventTime);
}
```
返回 `ContractMatchResult`customerId, customerName, contractId, contractCode, costType, paymentMode
ETC/电费/氢费三个监听器共用此服务。
### 1.6 审核配置扩展
`energy_review_config` 表加 `fee_type` 列:
```sql
ALTER TABLE energy_review_config
ADD COLUMN fee_type TINYINT NOT NULL DEFAULT 1 COMMENT '费用类型1-氢费 2-ETC 3-电费';
DROP INDEX uk_erc_level_customer ON energy_review_config;
CREATE UNIQUE INDEX uk_erc_level_customer_fee ON energy_review_config (config_level, customer_id, fee_type);
```
每种费用类型可独立配置"是否需要审核后才扣款"。
---
## Phase 2: ETC 模块(`modules/etc/`
### 2.0 ETC 数据源逆向分析etczj.com - 浙江货车ETC
**平台信息:** https://www.etczj.com Nuxt.js + Element UI 前端nginx/1.27.2 后端)
**认证流程:**
```
1. GET /createCaptcha?verifyCodeSize=null&tmp={random}&secret={guid} → 图形验证码图片
2. POST /member/toLogin?secret={guid}
Body: { acctId: "手机号", password: "Base64编码密码", checkCode: "验证码", msgCode: "", secret: guid }
Response: { state: "1", payload: { randomVal: "xxx", signState: "1" } }
3. 后续请求 Header: Randomval: {randomVal}(存 localStorage
```
**核心业务 API**
| API | 方法 | 说明 | 关键参数 |
|-----|------|------|----------|
| `/vehicleManage/queryVehicleConsumptionDetailPage` | POST | **通行记录分页** | transTimeBegin/End, postingTimeBegin/End, vehicleCode, licenseColor, vcEnStation, vcExStation, currPage, pageSize |
| `/vehicleManage/sumVehicleToll` | POST | **通行费汇总** | 同上(返回 totalPassAmount, totalServiceAmount |
| `/vehicleManage/viewVehiclePage` | POST | **车辆列表** | vehicleCode, currPage, pageSize |
| `/largeFileExport/billDetails` | POST | **通行记录导出** | postingTimeBegin/End, transTimeBegin/End, vehicleCode |
| `/member/flowList` | POST | **账单流水** | - |
| `/member/viewMember` | POST | **用户信息** | - |
**通行记录响应字段映射 → `etc_toll_record` 表:**
| etczj 字段 | 含义 | → 表字段 |
|------------|------|----------|
| vehicleCode | 车牌号码 | plate_number |
| licenseColor | 车牌颜色(0-6) | license_color |
| cardType | 卡类型 | card_type |
| cardCode | 通行卡号 | etc_card_number |
| postingTimeStr | 记账日期 | posting_time |
| transDate + transTime | 通行日期+时间 | trans_time |
| enStation | 入口站 | entry_station_name |
| exStation | 出口站 | exit_station_name |
| dToll | 通行费用(元) | toll_amount |
| serviceFee | 服务费(元) | service_fee |
| appealStatus | 申诉状态 | appeal_status |
**同步策略设计要点:**
- 登录需图形验证码 → 需 OCR 或人工介入。建议:首次登录获取 session 后缓存 `randomVal`session 过期时告警人工重新登录
- 增量同步:按 `postingTimeBegin/End`(记账日期)拉取,每次从 `lastSyncTime` 开始
- 去重键:`vehicleCode + transDate + transTime + enStation + exStation`(平台无唯一订单号)
- 分页:`currPage` + `pageSize`,默认 pageSize=5建议调大到 100
- 日期限制:时间范围最多 31 天
### 2.1 数据库表
**`etc_toll_record`ETC通行记录表**
| 字段 | 类型 | 说明 |
|------|------|------|
| id | BIGINT | 主键 |
| record_code | VARCHAR(32) | 记录编码(系统生成,唯一) |
| etc_card_number | VARCHAR(64) | 通行卡号(cardCode) |
| card_type | VARCHAR(32) | 卡类型(cardType) |
| plate_number | VARCHAR(16) | 车牌号 |
| license_color | TINYINT | 车牌颜色(0-6) |
| vehicle_id | BIGINT | 车辆ID匹配后 |
| contract_id | BIGINT | 合同ID匹配后 |
| customer_id | BIGINT | 客户ID匹配后 |
| customer_name | VARCHAR(128) | 客户名称 |
| entry_station_name | VARCHAR(128) | 入口站 |
| exit_station_name | VARCHAR(128) | 出口站 |
| trans_time | DATETIME | 通行时间transDate+transTime |
| posting_time | DATETIME | 记账日期(postingTimeStr) |
| toll_amount | DECIMAL(10,2) | 通行费(元)(dToll) |
| service_fee | DECIMAL(10,2) | 服务费(元)(serviceFee) |
| total_amount | DECIMAL(10,2) | 合计金额(toll+service) |
| appeal_status | TINYINT | 申诉状态 |
| cost_type | TINYINT | 费用承担方 |
| payment_mode | TINYINT | 付款模式 |
| contract_matched | TINYINT | 合同匹配状态 |
| review_status | TINYINT | 审核状态 |
| deduction_status | TINYINT | 扣款状态 |
| bill_id | BIGINT | 关联账单ID |
| source_type | TINYINT | 来源1-API同步 |
| source_dedup_key | VARCHAR(128) | 去重键(车牌+通行时间+入口+出口) |
| is_oneos_vehicle | TINYINT | 是否OneOS车辆 |
| + BaseEntity 审计字段 | | |
**`etc_sync_config`** 和 **`etc_sync_log`**:复用 station 的 sync_config/sync_log 结构,`provider_type` 固定为 `ETCZJ`。额外字段:`acct_id`(手机号)、`password`(加密存储)。
**`energy_etc_bill`ETC账单表**
| 字段 | 类型 | 说明 |
|------|------|------|
| id | BIGINT | 主键 |
| bill_code | VARCHAR(32) | 账单编码 |
| customer_id | BIGINT | 客户ID |
| bill_period_start/end | DATE | 账单周期 |
| total_toll_count | INT | 通行笔数 |
| total_toll_amount | DECIMAL(12,2) | 通行总金额 |
| total_service_fee | DECIMAL(12,2) | 服务费总额 |
| receivable_amount | DECIMAL(12,2) | 应收金额 |
| actual_amount | DECIMAL(12,2) | 实收金额 |
| adjustment_amount | DECIMAL(12,2) | 调整额 |
| payment_status | TINYINT | 支付状态 |
| review_status | TINYINT | 审核状态 |
| + 审核/财务/审计字段 | | |
### 2.2 代码结构
```
modules/etc/
entity/
record/po/EtcTollRecord.java
record/vo/EtcTollRecordVO.java
record/query/EtcTollRecordQuery.java
record/req/UpdateEtcRecordReq.java
sync/po/EtcSyncConfig.java
sync/po/EtcSyncLog.java
bill/po/EnergyEtcBill.java
bill/vo/EtcBillVO.java
bill/req/EtcBillGenerateReq.java
mapper/
EtcTollRecordMapper.java
EtcSyncConfigMapper.java
EtcSyncLogMapper.java
EnergyEtcBillMapper.java
service/
IEtcTollRecordService.java
IEtcSyncConfigService.java
IEtcBillService.java
impl/EtcTollRecordServiceImpl.java
impl/EtcSyncConfigServiceImpl.java
impl/EtcBillServiceImpl.java
sync/EtcSyncStrategy.java ← 策略接口
sync/EtczjSyncStrategy.java ← etczj.com 具体实现(登录+分页拉取+字段映射)
sync/EtczjApiClient.java ← HTTP 客户端(登录/session管理/通行记录查询)
sync/EtczjApiResponse.java ← 响应 DTO
ingest/EtcIngestTemplate.java ← 模板方法extract→validate→dedup→match→persist→event
controller/
EtcTollRecordController.java
EtcSyncConfigController.java
EtcBillController.java
event/
EtcRecordImportedEvent.java
listener/
EtcDetailImportListener.java ← 监听 EtcRecordImportedEvent执行合同匹配+扣款
validation/
EtcValidationChain.java
```
### 2.3 数据流
```
EtczjSyncStrategy 定时/手动同步:
1. EtczjApiClient.login(acctId, password) → 获取 randomVal session
2. 按日期范围分页调用 /vehicleManage/queryVehicleConsumptionDetailPage
- 从 lastSyncTime 开始每次最多31天
- pageSize=100循环翻页直到无更多数据
3. 字段映射etczj 响应 → EtcTollRecordDTO
4. → EtcIngestTemplate.ingest()
→ extract → validate → dedup(source_dedup_key) → matchVehicle → persist(etc_toll_record)
→ publishEvent(EtcRecordImportedEvent)
5. → EtcDetailImportListener
→ 筛选 is_oneos_vehicle=1
→ contractMatchService.matchContract(plateNumber, transTime)
→ 审核配置检查FeeType.ETC
→ deductionService.deduct(DeductionRequest)
→ 更新 etc_toll_record.deduction_status
Session 管理:
- randomVal 缓存在 Rediskey: `etc:session:{configId}`,过期后自动重新登录
- 图形验证码处理OCR 自动识别Tesseract/百度OCR
- GET /createCaptcha?secret={guid} → 图片
- OCR 识别 → 4位数字
- 识别失败重试刷新验证码重试最多3次
- 3次均失败 → 告警通知人工介入
- 去重策略vehicleCode + transTime + enStation + exStation 组合键
```
### 2.4 API 接口(约 20 个)
| 模块 | 接口数 |
|------|--------|
| ETC 通行记录 CRUD + 审核 + 导出 | 8 |
| ETC 同步配置 + 手动触发 + 日志 | 7 |
| ETC 账单生成 + 审核 + 调整项 | 8 |
---
## Phase 3: 电费模块(`modules/electricity/`
### 3.1 数据库表
**`electricity_charge_record`(充电记录表):**
| 字段 | 类型 | 说明 |
|------|------|------|
| id | BIGINT | 主键 |
| record_code | VARCHAR(32) | 记录编码 |
| charging_station_id | BIGINT | 充电站ID关联已有 charging_station 表) |
| charging_station_name | VARCHAR(128) | 充电站名称 |
| plate_number | VARCHAR(16) | 车牌号 |
| vehicle_id / contract_id / customer_id | BIGINT | 匹配后填充 |
| charging_start_time | DATETIME | 充电开始 |
| charging_end_time | DATETIME | 充电结束 |
| charging_duration | INT | 时长(分钟) |
| kwh | DECIMAL(10,4) | 充电量(度) |
| unit_price | DECIMAL(10,4) | 单价(元/度) |
| charge_amount | DECIMAL(10,2) | 电费(元) |
| service_fee | DECIMAL(10,2) | 服务费(元) |
| total_amount | DECIMAL(10,2) | 合计金额 |
| cost_type / payment_mode / contract_matched / review_status / deduction_status / bill_id | | 与氢费/ETC 一致 |
| source_type | TINYINT | 来源1-Excel导入 2-RPA导入 |
| source_row_key | VARCHAR(64) | 源Excel行唯一键去重 |
| is_oneos_vehicle | TINYINT | 是否OneOS车辆 |
| + BaseEntity 审计字段 | | |
**`energy_electricity_bill`(电费账单表):** 结构类似 ETC 账单,特有字段:`total_kwh`, `total_charge_amount`, `total_service_fee`
### 3.2 代码结构
与 ETC 模块镜像,核心差异:
- **无 API 同步**(暂时),数据入口为 Excel 导入(手动上传 / RPA 落盘后系统扫描)
- `ElectricityExcelIngestStrategy` 实现 `ElectricityIngestTemplate`
- 关联已有 `chargingstation/` 模块的充电站主数据
- 校验链包含:电量/金额一致性校验、充电站匹配校验
### 3.3 数据流
```
Excel 手动导入 / RPA 定期落盘
→ ElectricityIngestTemplate.ingest()
→ extract(解析Excel) → validate → dedup(source_row_key) → matchVehicle → persist
→ publishEvent(ElectricityRecordImportedEvent)
→ ElectricityDetailImportListener
→ 合同匹配 → 审核配置检查FeeType.ELECTRICITY→ 扣款 → 更新状态
```
### 3.4 API 接口(约 15 个)
| 模块 | 接口数 |
|------|--------|
| 充电记录 CRUD + 审核 + 导入/导出 | 8 |
| 电费账单生成 + 审核 + 调整项 | 7 |
---
## Phase 4: 账户层统一视图
### 4.1 流水查询增加 fee_type 过滤
**改动文件:**
- `TransactionQuery.java` — 新增 `feeType` 可选参数
- `EnergyAccountController.java` — 流水分页接口支持按费用类型筛选
### 4.2 账户汇总接口
**新增接口:** `GET /energy/account/{id}/fee-summary`
返回各费用类型的扣款/充值汇总:
```json
{
"hydrogen": { "totalDeducted": 50000, "count": 320 },
"etc": { "totalDeducted": 12000, "count": 580 },
"electricity": { "totalDeducted": 8000, "count": 150 }
}
```
---
## Phase 5: 前端菜单与页面
### 5.1 菜单结构
```
能源管理
├── 加氢明细(已有)
├── ETC 通行记录(新增)
├── 充电记录(新增)
├── 能源账户(已有,增加 fee_type 筛选)
├── 充值单管理(已有)
├── 氢费账单(已有)
├── ETC 账单(新增)
├── 电费账单(新增)
└── 审核配置(已有,增加 fee_type 维度)
加氢站管理(已有,不动)
ETC 管理(新增)
├── ETC 同步配置
└── ETC 同步日志
款项管理(已有)
└── fee_type 解构类型新增 ETC充值/电费充值
```
---
## 实施顺序
| 步骤 | 内容 | 依赖 |
|------|------|------|
| **1** | Phase 1 基础设施FeeType枚举、DeductionRequest、IDeductionService重构、DB迁移、ContractMatchService抽取、ReviewConfig扩展 | 无 |
| **2** | Phase 1 向后兼容(更新氢费监听器/服务适配新接口) | Step 1 |
| **3** | Phase 2 ETC 模块实体→Mapper→Service→Controller→事件→监听器→测试 | Step 2 |
| **4** | Phase 3 电费模块(同上) | Step 2可与 Step 3 并行 |
| **5** | Phase 4 账户层统一视图 | Step 3 & 4 |
| **6** | Phase 5 前端页面 | Step 3 & 4 |
| **7** | Payment 域扩展fee_type 解构+流转) | Step 1 |
---
## 关键改动文件清单
### 修改已有文件
| 文件 | 改动 |
|------|------|
| `energy/service/IDeductionService.java` | 接口签名改为 `DeductionRequest` |
| `energy/service/impl/DeductionServiceImpl.java` | 实现适配,移除 detailMapper 直接操作 |
| `energy/entity/account/po/EnergyAccountTransaction.java` | 新增 `feeType` 字段 |
| `energy/listener/EnergyDetailImportListener.java` | 适配新 DeductionRequest抽取合同匹配逻辑 |
| `energy/entity/review/po/EnergyReviewConfig.java` | 新增 `feeType` 字段 |
| `energy/service/impl/ReviewConfigServiceImpl.java` | 查询时加 feeType 条件 |
| `energy/controller/EnergyAccountController.java` | 流水查询加 feeType 过滤 |
| `energy/entity/account/query/TransactionQuery.java` | 新增 feeType 参数 |
| `payment/entity/po/CustomerPaymentItem.java` | fee_type 枚举扩展文档 |
| `payment/service/impl/PaymentFlowServiceImpl.java` | 支持 ETC/电费充值流转 |
### 新建文件(按 Phase
- Phase 1: ~5 文件FeeType, DeductionRequest, ContractMatchService 接口+实现, V2迁移脚本
- Phase 2: ~20 文件ETC 全套 entity/mapper/service/controller/event/listener+ 3 张表 DDL
- Phase 3: ~18 文件(电费全套)+ 2 张表 DDL
- Phase 4: ~2 文件改动
---
## 验证方案
### 单元测试
- `DeductionServiceTest` —— 验证新接口支持三种 FeeType
- `EtcIngestTemplateTest` —— ETC 数据导入全流程
- `ElectricityIngestTemplateTest` —— 电费 Excel 导入全流程
- `ContractMatchServiceTest` —— 合同匹配逻辑
### 集成测试
- `EtcModuleIntegrationTest` —— ETC 同步 → 导入 → 合同匹配 → 审核 → 扣款 → 账单生成
- `ElectricityModuleIntegrationTest` —— Excel 导入 → 合同匹配 → 审核 → 扣款 → 账单生成
- `MultiFeeBillingIntegrationTest` —— 三种费用共享账户扣款 → 流水 fee_type 正确 → 各自独立出账单
### 回归验证
- 现有氢费全链路测试通过(`EnergyModuleIntegrationTest`
- 现有款项管理测试通过(`PaymentModuleIntegrationTest`
---
## Phase 2 剩余实施计划(本次任务)
### 已完成
- [x] Phase 1 基础设施层FeeType/DeductionRequest/IDeductionService/ContractMatchService/ReviewConfig
- [x] Phase 2 DDL`db/etc/V1__create_etc_tables.sql` - 4 张表)
- [x] Phase 2 Entity POEtcTollRecord/EtcSyncConfig/EtcSyncLog/EnergyEtcBill
- [x] Phase 2 DTOEtcTollRecordVO/EtcTollRecordQuery/EtcBillVO/EtcBillGenerateReq
- [x] Phase 2 Mapper4 个)
- [x] Phase 2 Service 接口 + 实现IEtcTollRecordService/IEtcSyncConfigService/IEtcBillService
- [x] Phase 2 EventEtcRecordImportedEvent
- [x] Phase 2 ListenerEtcDetailImportListener
- [x] 编译通过
### Step 1: ETC 后端 Controller 层
**3 个 Controller沿用 `EnergyBillController` 的精确模式:**
#### 1.1 `EtcTollRecordController.java`
**路径:** `modules/etc/controller/EtcTollRecordController.java`
**前缀:** `@RequestMapping("/etc/record")`
**Tag** `@Tag(name = "ETC通行记录管理")`
| 方法 | 路径 | 说明 | 调用 |
|------|------|------|------|
| GET | /page | 分页查询 | service.pageList(query) |
| GET | /detail/{id} | 详情 | service.getDetail(id) |
| PUT | /review | 单条审核 | service.review(id, approved, remark) |
| PUT | /batch-review | 批量审核 | service.batchReview(ids, approved) |
| PUT | /manual-match | 手动匹配合同 | service.manualMatch(detailId, contractId) |
| POST | /export | 导出 | TODO |
| GET | /statistics | 统计数据 | service.getStatistics() |
#### 1.2 `EtcSyncConfigController.java`
**路径:** `modules/etc/controller/EtcSyncConfigController.java`
**前缀:** `@RequestMapping("/etc/sync-config")`
**Tag** `@Tag(name = "ETC同步配置")`
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | /page | 分页查询 |
| GET | /detail/{id} | 详情 |
| POST | / | 新增 |
| PUT | / | 编辑 |
| DELETE | /{id} | 删除 |
| PUT | /toggle/{id} | 启用/停用 |
| POST | /trigger/{id} | 手动触发同步 |
| GET | /log/{configId} | 同步日志 |
#### 1.3 `EtcBillController.java`
**路径:** `modules/etc/controller/EtcBillController.java`
**前缀:** `@RequestMapping("/etc/bill")`
**Tag** `@Tag(name = "ETC账单管理")`
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | /page | 分页查询 |
| GET | /detail/{id} | 详情 |
| POST | /generate/preview | 生成预览 |
| POST | /generate | 确认生成 |
| PUT | /review | 审核 |
| PUT | /submit-finance/{id} | 提交财务 |
| DELETE | /{id} | 删除 |
| POST | /export | 导出 |
| GET | /statistics | 统计 |
| GET | /record/page/{billId} | 关联通行记录分页 |
**参考文件:**
- `ln-asset-management/src/main/java/com/ln/asset/modules/energy/controller/EnergyBillController.java`
- `ln-asset-management/src/main/java/com/ln/asset/modules/station/controller/SyncConfigController.java`
### Step 2: EtczjApiClientetczj.com HTTP 客户端)
**新建文件:**
- `modules/etc/service/sync/EtczjApiClient.java` — HTTP 客户端
- `modules/etc/service/sync/EtczjApiResponse.java` — 响应 DTO
- `modules/etc/service/sync/EtczjTollRecordDTO.java` — 通行记录 DTO
- `modules/etc/service/sync/EtczjSyncStrategy.java` — 同步策略实现
**EtczjApiClient 核心方法:**
```java
@Slf4j
@Component
public class EtczjApiClient {
private final RestTemplate restTemplate;
private final ObjectMapper objectMapper;
// 登录(获取 randomVal session
public String login(String baseUrl, String acctId, String password) {
// 1. GET /createCaptcha?secret={guid} → 验证码图片
// 2. OCR 识别验证码Tesseract
// 3. POST /member/toLogin?secret={guid}
// Body: { acctId, password: Base64(password), checkCode, secret }
// 4. 返回 randomVal
}
// 查询通行记录
public EtczjApiResponse<List<EtczjTollRecordDTO>> queryTollRecords(
String baseUrl, String randomVal,
String transTimeBegin, String transTimeEnd,
int currPage, int pageSize) {
// POST /vehicleManage/queryVehicleConsumptionDetailPage
// Header: Randomval: {randomVal}
}
// 查询通行费汇总
public EtczjApiResponse<Map<String, Object>> sumToll(
String baseUrl, String randomVal,
String transTimeBegin, String transTimeEnd) {
// POST /vehicleManage/sumVehicleToll
}
}
```
**参考文件:**
- `ln-asset-management/src/main/java/com/ln/asset/modules/station/feign/HecriApiClient.java`RestTemplate + ObjectMapper 模式)
### Step 3: 前端 ETC 页面
**技术栈:** Vben Admin (Vue 3 + TypeScript + Ant Design Vue + VxeTable)
**模式参考:**
- 路由:`playground/src/router/routes/modules/system.ts`
- API`playground/src/api/system/role.ts`
- 列表页:`playground/src/views/system/role/list.vue`
- 列定义:`playground/src/views/system/role/data.ts`
#### 3.1 API 层
**新建文件:** `playground/src/api/etc/index.ts`
```typescript
export namespace EtcApi {
export interface EtcSyncConfig { id: string; providerType: string; acctId: string; syncEnabled: 0|1; syncCron: string; lastSyncTime: string; lastSyncStatus: 0|1; }
export interface EtcTollRecord { id: string; recordCode: string; plateNumber: string; entryStationName: string; exitStationName: string; transTime: string; tollAmount: number; serviceFee: number; totalAmount: number; reviewStatus: number; deductionStatus: number; }
export interface EtcBill { id: string; billCode: string; customerName: string; billPeriodStart: string; billPeriodEnd: string; totalTollCount: number; totalTollAmount: number; receivableAmount: number; actualAmount: number; reviewStatus: number; paymentStatus: number; }
}
// ETC 同步配置 API
async function getEtcSyncConfigPage(params) { return requestClient.get('/etc/sync-config/page', { params }); }
async function addEtcSyncConfig(data) { return requestClient.post('/etc/sync-config', data); }
async function updateEtcSyncConfig(data) { return requestClient.put('/etc/sync-config', data); }
async function deleteEtcSyncConfig(id) { return requestClient.delete(`/etc/sync-config/${id}`); }
async function toggleEtcSync(id) { return requestClient.put(`/etc/sync-config/toggle/${id}`); }
async function triggerEtcSync(id) { return requestClient.post(`/etc/sync-config/trigger/${id}`); }
async function getEtcSyncLog(configId, params) { return requestClient.get(`/etc/sync-config/log/${configId}`, { params }); }
// ETC 账单 API
async function getEtcBillPage(params) { return requestClient.get('/etc/bill/page', { params }); }
async function getEtcBillDetail(id) { return requestClient.get(`/etc/bill/detail/${id}`); }
async function reviewEtcBill(data) { return requestClient.put('/etc/bill/review', data); }
// ... 其他
```
#### 3.2 ETC 同步配置页面
**新建文件:**
- `playground/src/views/etc/sync-config/list.vue` — 配置列表useVbenVxeGrid
- `playground/src/views/etc/sync-config/data.ts` — 列定义 + 表单 schema
- `playground/src/views/etc/sync-config/modules/form.vue` — 新增/编辑表单useVbenDrawer
**页面功能:**
- 表格列供应商类型、登录账号、API地址、同步频率、上次同步时间/状态、启用状态
- 操作:编辑、删除、启用/停用切换、手动触发同步
- 新增/编辑抽屉账号、密码、API地址、同步 cron 表达式
#### 3.3 ETC 账单查询页面
**新建文件:**
- `playground/src/views/etc/bill/list.vue` — 账单列表
- `playground/src/views/etc/bill/data.ts` — 列定义 + 搜索表单
- `playground/src/views/etc/bill/modules/detail.vue` — 账单详情抽屉
**页面功能:**
- 搜索条件:客户名称、账单周期、审核状态、支付状态
- 表格列:账单编码、客户名称、账期、通行笔数、通行费总额、服务费总额、应收、实收、审核状态、支付状态
- 操作:查看详情、审核、提交财务、删除
- 详情抽屉:账单信息 + 关联通行记录分页列表
#### 3.4 路由配置
**新建文件:** `playground/src/router/routes/modules/etc.ts`
```typescript
const routes: RouteRecordRaw[] = [{
meta: { icon: 'mdi:highway', order: 30, title: 'ETC管理' },
name: 'Etc',
path: '/etc',
children: [
{ path: '/etc/sync-config', name: 'EtcSyncConfig', meta: { icon: 'mdi:sync', title: 'ETC同步配置' }, component: () => import('#/views/etc/sync-config/list.vue') },
{ path: '/etc/bill', name: 'EtcBill', meta: { icon: 'mdi:file-document-outline', title: 'ETC账单' }, component: () => import('#/views/etc/bill/list.vue') },
],
}];
```
### 实施顺序
| 步骤 | 内容 | 文件数 |
|------|------|--------|
| 1 | 3 个 ETC Controller | 3 |
| 2 | EtczjApiClient + DTO + SyncStrategy | 4 |
| 3 | 前端 API 层 | 1 |
| 4 | 前端 ETC 同步配置页面 | 3 |
| 5 | 前端 ETC 账单查询页面 | 3 |
| 6 | 前端路由配置 | 1 |
| **合计** | | **15 个文件** |
### 验证方案
- 后端:`mvn compile` 编译通过
- 前端:`cd ln-one-os-web/playground && pnpm dev` 启动无报错
- ETC 同步配置页面:可访问 /etc/sync-config表格加载正常
- ETC 账单页面:可访问 /etc/bill表格加载正常
- Swagger`/swagger-ui.html` 可看到 ETC 相关 3 组 API

View File

@@ -0,0 +1,242 @@
# 数据迁移计划lingniu_prod → 新系统
## Context
老系统 `lingniu_prod`(单库 255 张表)需要全量迁移到新系统的 3 个库。新系统已做架构重构:拆分为 `ln_asset_management`(资产管理)、`ln_energy`(能源计费)、`ry-cloud`(系统核心/RuoYi 框架)。新库中现有数据为测试数据,迁移前清空。
## 源与目标
| | 源 | 目标 |
|---|---|---|
| **Host** | rm-uf65w5v2r77n674x2ko.mysql.rds.aliyuncs.com | 47.100.22.206:3306 |
| **用户** | oneos_read (只读) | root |
| **数据库** | lingniu_prod (255 表) | ln_asset_management (114), ln_energy (19), ry-cloud (29) |
## ID 策略
- `ln_asset_management` / `ln_energy``IdType.AUTO`MySQL 自增),插入时不指定 ID获取 `LAST_INSERT_ID`
- `ry-cloud`:雪花算法 ID已有数据如 sys_user ID = 2038797628680523778
所有迁移记录保存到 `ln_migration.id_mapping` 表,维护 `(old_table, old_id) → (new_table, new_id)` 映射。
## 通用字段转换规则
| 老字段 | 新字段 | 转换 |
|---|---|---|
| `is_deleted` (tinyint 0/1) | `del_flag` (char '0'/'1') | `str(val)` |
| `creater_id` (bigint) | `create_by` (bigint) | 查 user id_mapping |
| `updater_id` (bigint) | `update_by` (bigint) | 查 user id_mapping |
| int 枚举 (如 `truck_type`) | varchar 字典码 | 查 dict_mapping |
| `datetime` | `date` | 取日期部分 |
| `double` | `decimal` | `Decimal(str(val))` |
---
## 迁移分阶段执行
### Phase 0: 准备
1. 在目标库创建 `ln_migration` 数据库和 `id_mapping`
2. **清空**目标库 3 个库中所有业务表数据(保留 ry-cloud 框架种子数据如 sys_config, sys_oss_config, sys_tenant
3. 读取老库 `tab_dic` 构建枚举映射字典 → `dict_mapping.py`
### Phase 1: 基础数据(无 FK 依赖)
| 老表 | 新表 (库) | 行数 | 说明 |
|---|---|---|---|
| `tab_dic` | `sys_dict_type` + `sys_dict_data` (ry-cloud) | 681 | 按 dic_type 分组为 type+data |
| `tab_region` | `common_district` (asset) | 3,359 | 直接映射 |
| `tab_vehicle_model` | `vehicle_model` (asset) | 39 | 字段对齐 |
| `tab_insurance_company` | `insurance_company` (asset) | 20 | 直接映射 |
| `tab_hydrogen_site` | `hydrogen_station` (asset) | 101 | int→varchar 枚举 |
| `tab_parking` | `parking_lot_info` (asset) | 65 | 直接映射 |
| `tab_maintain_site` | `repair_station` (asset) | 64 | 直接映射 |
| `tab_rescue_site` | `rescue_team` (asset) | ~14 | 直接映射 |
| `tab_annual_review_service_station` | `inspection_station` (asset) | 14 | 直接映射 |
| `tab_truck_check_item` | `vehicle_check_item` (asset) | 294 | 直接映射 |
| `tab_contract_templates` | `contract_template` (asset) | 13 | 直接映射 |
| `tab_charge_station` | `charging_station` (asset) | ~数条 | 直接映射 |
### Phase 2: 系统数据(用户/组织/角色)
| 老表 | 新表 (ry-cloud) | 行数 | 说明 |
|---|---|---|---|
| `tab_org` | `sys_dept` + `sys_organization` | 19 | 重建 ancestors 字段 |
| `tab_user` | `sys_user` | 405 | 密码需重置为 BCrypt 临时密码 |
| `tab_role` | `sys_role` | 69 | 雪花 ID |
| `tab_user_role` | `sys_user_role` | 201 | 查 user+role id_mapping |
| `tab_menu` | `sys_menu` | 464 | **评估**:新系统菜单已重新定义,可能跳过 |
| `tab_role_menu` | `sys_role_menu` | 6,236 | 依赖菜单决策 |
> **密码处理**:老系统使用自定义 salt+hash新系统使用 BCrypt。迁移时统一设置临时密码如 `Abc@123456` 的 BCrypt 值),首次登录强制修改。
### Phase 3: 核心业务实体
| 老表 | 新表 (asset) | 行数 | 复杂度 |
|---|---|---|---|
| `tab_truck` | `vehicle_info` | 1,203 | 高47→29 列,大量字段重组 |
| `tab_truck_status_info` | `vehicle_status` | 1,385 | 中int→varchar 枚举 |
| `tab_driver` | `driver_info` | 212 | 低 |
| `tab_customer` | `customer_info` | 310 | 中 |
| `tab_truck_licence` | `vehicle_license` | 1,712 | 中truck_id→vehicle_id FK |
| `tab_truck_insure` | `insurance_procurement` | 740 | 中15→32 列扩展 |
| `tab_equipment_info` | `aftermarket_device` | 1,562 | 中 |
| `tab_violation_management` | `traffic_violation` | 1,285 | 低 |
| `tab_accident` + `tab_accident_cost_bearing` | `accident_info` + `accident_expense` | 293+823 | 中 |
| `tab_failure` | `vehicle_fault_manage` | 5,140 | 低 |
| `tab_vehicle_annual_inspection` | `vehicle_annual_inspection` | 353 | 低 |
| `tab_vehicle_preparation` | `prepare_car` | 1,736 | 低 |
| `tab_customer_invoice` | `invoice_info` | 219 | 低 |
| `tab_truck_device_info` | `aftermarket_device``vehicle_realtime_location` | 1,227 | 评估 |
| `tab_training_materials` | `training_material` (asset) | 3 | 低 |
**vehicle_info 字段映射(核心):**
```
tab_truck.plate_number → vehicle_info.plate_number
tab_truck.vin → vehicle_info.vin
tab_truck.truck_num → vehicle_info.vehicle_code
tab_truck.model (int) → vehicle_info.vehicle_model_id (FK, 查 vehicle_model id_mapping)
tab_truck.color → vehicle_info.body_color
tab_truck.buy_time → vehicle_info.purchase_date (datetime→date)
tab_truck.stock_area (int) → vehicle_info.packing_lot_id (FK, 查 parking_lot_info id_mapping)
tab_truck.mandatory_retirement_period → vehicle_info.mandatory_scrap_date
tab_truck.remarks → vehicle_info.remark
tab_truck.address → vehicle_info.province (提取省份)
```
### Phase 4: 合同域
| 老表 | 新表 (asset) | 行数 | 说明 |
|---|---|---|---|
| `tab_contract` | `vehicle_lease_contract_info` | 681 | 38→67 列,大量新字段 NULL |
| `tab_contract_rent_order` | `vehicle_lease_order` | 679 | 关联合同 |
| `tab_contract_rent_truck` | `vehicle_lease_order_detail` | 3,865 | 关联 order + vehicle |
| `tab_contract_rent_truck_service_cost` | `vehicle_lease_order_service_item` | 1,645 | 关联 detail |
| `tab_contract_thirty_party` | `contract_authorized_person` | 682 | 直接映射 |
| `tab_contract_authorizer_information` | `contract_authorized_person` | 666 | 合并 |
| `tab_contract_costs` | `template_*` 系列表 | 15,059 | 按费用类型拆分 |
| `tab_contract_hydrogen_fees` | `template_hydrogen_fee` | 13 | 直接映射 |
### Phase 5: 交付/退车/换车
| 老表 | 新表 (asset) | 行数 | 说明 |
|---|---|---|---|
| `tab_truck_rent_task` | `delivery_task_subject` | 3,104 | 任务容器 |
| `tab_truck_rent_take` | `delivery_order` + `delivery_vehicle` | 1,956 | 一拆多 |
| `tab_truck_rent_return` | `return_vehicle_task` | 1,079 | 重构 |
| `tab_truck_rent_return_cost` | `return_fees` | 73 | 直接映射 |
| `tab_truck_rent_return_dep_cost` | `return_settlement_*` 子表 | 4,634 | 按类型拆分 |
| `tab_truck_rent_replace` | `vehicle_replacement` | 247 | 直接映射 |
| `tab_standby_vehicle_main/detail` | `vehicle_abnormal_move` | 377+2,171 | 评估映射 |
### Phase 6: 账单/财务
| 老表 | 新表 (asset) | 行数 | 说明 |
|---|---|---|---|
| `tab_rent_contract_bill` | `bills` | 25,625 | 合同账单 |
| `tab_rent_contract_bill_truck` | `vehicle_bills` + `vehicle_bill_service_items` | **270,283** | **大表,批量处理** |
| `tab_rent_contract_bill_other_cost` | 合并到 `vehicle_bills` | ~少量 | |
| `tab_finance_receivable` | `receivable_subject` + `receivable_vehicle` | 10,308 | 拆分 |
| `tab_finance_deposit_receive` | `customer_payment_receipt` | 2,078 | 映射 |
| `tab_finance_deposit_deduction` | `customer_payment_item` | 52 | 映射 |
### Phase 7: 能源域 → ln_energy
| 老表 | 新表 (ln_energy) | 行数 | 说明 |
|---|---|---|---|
| `tab_energy_account` | `energy_account` | 201 | 结构重组 |
| `tab_energy_project_account` | `energy_account_project` | 307 | 直接映射 |
| `tab_energy_account_recharge` | `energy_recharge_order` | 942 | 字段扩展 |
| `tab_import_hydrogen_order` | `hydrogen_station_order` | 58,642 | 加氢原始订单 |
| `tab_energy_hydrogen_bill` | `energy_hydrogen_detail` | **58,554** | **大表** |
| `tab_import_ele_charge_order` | `electricity_charge_record` | 4,405 | 充电原始订单 |
| `tab_energy_electricity_bill` | `energy_bill_detail` | 4,355 | fee_type=electricity |
### Phase 8: 附件
| 老表 | 新表 (asset) | 行数 | 说明 |
|---|---|---|---|
| `tab_data_attachment` | `tab_data_attachment` | **247,447** | **最大表之一**data_id 需 FK 重映射 |
| `tab_image_attachment` | 合并到 `tab_data_attachment` 或保留 | 51,516 | 评估 |
### Phase 9: 其他
| 老表 | 新表 | 行数 | 处理 |
|---|---|---|---|
| `tab_maintain_maintenance_project` | 评估 | 243,474 | 新系统无直接对应,可能跳过 |
| `tab_truck_rent_form_data` | 评估 | 456,061 | 表单数据,新系统结构不同 |
| `tab_preparation_form_data` | 评估 | 171,497 | 同上 |
| `tab_standby_vehicle_form_data` | 评估 | 143,034 | 同上 |
---
## 暂不迁移的表(无新系统对应)
- 审批流:`tab_approve_instance*`, `tab_approve_template_node`
- 工作流:`tab_flow_task*`, `tab_flow_template*`
- G7 车联网:`tab_g7s_org`, `tab_g7s_truck_mileage` (289K), `tab_g7s_in_out_event` (159K)
- 培训考试:`tab_train_*`, `tab_safety_training`
- 系统日志:`tab_api_access_log` (4.6M), `tab_user_log`, `tab_user_message`, `tab_short_message`
- 调度记录:`tab_schedule_execute_result` (347K), `tab_data_sync_task_record`
- 应用版本:`tab_app_version`, `tab_version_user_check`, `tab_release_version_log`
- 临时表:所有 `tab_aa_temp_*`, `*_copy*`, `temp_*`
- 视图:所有 `view_*`, `v_*`
- 汇总表:`truck_info`, `truck_equipment_info`, `truck_hydrogen_info`, `truck_mileage`, `truck_parking`
---
## 实现方案Python 脚本
### 目录结构
```
/Users/kkfluous/Projects/lingniu/oneos-corp/migration/
config.py # 数据库连接配置
id_mapping.py # ID 映射表 CRUD
transform.py # 通用字段转换函数
dict_mapping.py # 老 int 枚举 → 新 varchar 编码映射
migrator.py # 基础迁移器批量读写、mapping、日志
phase0_prepare.py # 建 mapping 表、清空目标库
phase1_reference.py # 字典、区域、车型等基础数据
phase2_system.py # 组织、用户、角色
phase3_core.py # 车辆、司机、客户、证照、保险
phase4_contract.py # 合同、租赁订单
phase5_delivery.py # 交付、退车、换车
phase6_billing.py # 账单、财务
phase7_energy.py # 能源账户、加氢、充电
phase8_attachment.py # 附件
phase9_misc.py # 其他
verify.py # 行数校验、FK 完整性、抽样比对
run_all.py # 按顺序执行所有 phase
```
### 关键技术点
1. **批量处理**大表27 万+ 行)使用 `SSCursor` 服务端游标 + `executemany` 批量写入(每批 1000 行)
2. **ID 映射**`ln_migration.id_mapping(source_table, source_id, target_db, target_table, target_id)`FK 字段通过查映射表解析
3. **枚举映射**:从 `tab_dic` 读取所有字典项,预构建 `{(dic_type, int_value): new_dict_code}` 映射
4. **密码处理**:所有用户密码统一设为 BCrypt(`Abc@123456`),首次登录强制修改
5. **事务**:每个 Phase 按表粒度 commit失败可单表重试
## 验证策略
1. **行数校验**:每张表迁移后对比源/目标行数
2. **抽样比对**:每张核心表随机取 10 条,比对关键业务字段
3. **FK 完整性**:检查所有外键列无孤儿记录
4. **业务逻辑**:能源账户余额一致、合同-车辆关联完整
5. **登录测试**:使用临时密码验证 sys_user 登录
## 回滚方案
1. 迁移前对目标 3 库做 `mysqldump` 备份
2. 回滚 = truncate 所有目标表 + 从备份恢复 + drop `ln_migration`
3. 单 Phase 回滚 = 根据 `id_mapping` 删除该 Phase 写入的记录
## 关键文件
- `ln-asset-management/.../BaseEntity.java` — IdType.AUTO, del_flag char(1)
- `ln-asset-management/.../VehicleInfo.java` — 新车辆实体 29 字段
- `ln-cloud/ruoyi-common/.../BaseEntity.java` — RuoYi 基础实体
- 老后端代码 `/Users/kkfluous/Projects/lingniu/ln_asset/lingniu_asset_server/lingniu-manager/src/main/java/org/lingniu/manager/model/` — 所有老实体类

View File

@@ -0,0 +1,51 @@
# 两项修复:结转不依赖考核记录 + 补发查对应月盈亏
## Context
两个bug需要修复
1. 1月有结转但2月无考核记录 → 结转丢失当前只遍历G[2]有记录的车)
2. 3月补发1月的奖金 → 应查1月盈亏当前查的是3月盈亏
## 修改1结转不依赖考核记录
### 规则
- 1月多跑够结转2月即使没考核记录也发放结转奖金完整月
- 无考核记录时:考核里程=满月目标如3000km实际里程=0视为消耗了一个月目标
- 无客户关联(因无考核记录)→ 不查盈亏,正常发放
### 修改 `calc_engine.py`
`calc_feb()` 中,遍历完 `G[2].items()` 后,额外遍历 `G[1]` 中有结转但不在 `G[2]` 中的key
```
for k, g1 in G1.items():
if k not in G2 and g1['可结转'] >= 1:
# 创建虚拟的2月group考核里程=满月目标,实际=0
# 发放结转奖金(完整月)
# 结转剩余月数 -= 1传递给3月
```
`calc_mar()` 同理:遍历 `G[2]` 中有结转但不在 `G[3]` 中的key。
### 修改 `excel_writer.py`
- `build_payment_records`无客户关联的结转loss_status设为'否'(正常发放)
## 修改2补发查对应月盈亏
### 规则
| 发放类型 | 查哪月盈亏 |
|---------|----------|
| 当月达标/累计补发/结转 | 当月 |
| 补发1月 | 1月 |
| 补发2月 | 2月 |
### 修改 `main.py`
- 将所有月份的 `loss_data` 都传入 `build_payment_records`(不只当月的)
### 修改 `excel_writer.py`
- `build_payment_records` 接收 `all_loss_data={1:..., 2:..., 3:...}`
- 根据发放类型确定查哪个月的盈亏表
- 奖金发放记录表新增列"盈亏查询月",标明查的是哪个月的盈亏
## 验证
- 找一辆1月有结转但2月无记录的车确认2月正常发放结转
- 找一辆3月补发1月的车确认查的是1月盈亏而非3月
- 金额总数合理

View File

@@ -0,0 +1,90 @@
# 用户权限校验实现计划
## Context
当前系统零认证,所有 API 完全开放。需要接入公司现有的 jumpToken 认证体系,并根据用户角色实现数据权限过滤。用户从资产管理平台跳转进入本系统,携带 jumpToken。
## 认证流程
```
用户带 jumpToken 访问 → 前端提取 jumpToken
→ 后端代理调用外部API换取 sessionToken
→ 后端调用外部API获取用户信息(roles, depCode)
→ 后端签发 JWT 返回前端
→ 前端所有请求带 JWT → 后端中间件验证
```
## 三级权限模型
| 级别 | 角色条件 | 数据范围 |
|------|---------|---------|
| **full** | roleName 含 "所有权限" / "数智中心" / "BI-Leader" | 全部数据 |
| **department** | roleName 含 "BI-Leader-Dep" | 自己部门的全部数据 |
| **personal** | 无以上角色 | 仅自己负责的车辆bd=userId |
部门匹配:用户 depCode → 查 tab_department 得 dep_name → 过滤车辆数据中的 department 字段
个人匹配:添加 managerId 字段c.bd按 userId 精确匹配(不用 userName 避免重名)
## 新增/修改文件
### 后端新增
- `src/server/auth/types.ts` — AuthUser、PermissionLevel、JwtPayload 类型
- `src/server/auth/login.ts``POST /api/auth/login`接收外部token调外部API获取用户信息签发JWT+ `GET /api/auth/exchange`(代理 jumpToken 换取 sessionToken避免前端 CORS 问题)
- `src/server/auth/middleware.ts` — Hono 中间件,验证 JWT跳过 /api/health 和 /api/auth/*
- `src/server/auth/permissions.ts``filterByPermission<T>(items, user)` 通用过滤函数
### 后端修改
- `src/server/index.ts` — 挂载 auth 路由和中间件
- `src/server/routes/mileage/vehicle-info.ts` — SQL 添加 `c.bd AS manager_id`
- `src/server/routes/mileage/types.ts` — CachedVehicle 添加 `managerId: number | null`
- `src/server/routes/mileage/cache.ts` — 传递 managerId
- `src/server/routes/mileage/monitoring.ts` — 请求时 filterByPermission + 重算筛选选项
- `src/server/routes/mileage/targets.ts` — 按权限过滤
- `src/server/routes/mileage/trend.ts` — 按权限限定车牌范围
- `src/server/routes/vehicles.ts` — 所有端点用 `getVehiclesForUser(c)` 替代 `getVehicles()`
- `src/server/types.ts` — Vehicle 类型添加 managerId
### 前端新增
- `src/auth/AuthProvider.tsx` — 认证上下文,管理 jumpToken 交换和 JWT 存储
- `src/auth/useAuth.ts` — 认证状态 hook
- `src/auth/api-client.ts` — 全局 fetchJson自动附加 Authorization header
- `src/auth/UnauthorizedPage.tsx` — 未授权页面(图标 + 提示文字)
### 前端修改
- `src/App.tsx` — 包裹 AuthProvider条件渲染 Shell / UnauthorizedPage
- `src/modules/mileage/api.ts` — fetchJson 改用 auth/api-client
- `src/modules/assets/api.ts` — fetchJson 改用 auth/api-client
### 环境配置
- `.env` 添加 `JWT_SECRET``EXTERNAL_API_BASE`
- `package.json` 添加 `jsonwebtoken``@types/jsonwebtoken`
## 关键设计决策
1. **缓存全局,请求时过滤** — 监控缓存保持全量数据,每次请求根据用户权限过滤,筛选选项也从过滤后数据重算
2. **jumpToken 交换走后端代理** — 避免前端 CORS 问题,外部 API 调用全部在服务端
3. **用 managerId数字ID匹配而非 userName** — 避免重名问题
4. **JWT 存 sessionStorage** — 刷新页面不丢失,关闭标签页自动清除
5. **filterByPermission 泛型函数** — 同时适配 Vehicle 和 CachedVehicle 类型
## 实施顺序
1. 安装依赖 (`jsonwebtoken`)
2. 后端 auth 模块types → login → middleware → permissions
3. 数据模型添加 managerIdvehicle-info → types → cache
4. 挂载中间件到 server/index.ts
5. 集成权限过滤到各路由vehicles.ts, monitoring, targets, trend
6. 前端 auth 模块AuthProvider, useAuth, api-client, UnauthorizedPage
7. 前端 API 模块切换到 auth fetch
8. App.tsx 添加认证网关
9. 端到端测试三个权限级别
## 验证方式
1. 无 jumpToken 访问 → 显示"未授权访问"页面
2. 带有效 jumpToken 访问 → 正常进入,检查 JWT 签发
3. full 角色用户 → 看到全部 1004 辆车数据
4. department 角色用户 → 仅看到自己部门的车辆
5. personal 用户 → 仅看到自己负责的车辆
6. JWT 过期后请求 → 返回 401前端显示未授权
7. 全屏监控筛选选项 → 仅显示用户权限范围内的部门/客户

View File

@@ -0,0 +1,124 @@
# ETC & 电费审核 → 能源账单对齐氢费模式
## Context
氢费的完整流程已跑通API 导入 → 设置 review_status=PENDING → 用户审核通过 → 调用 `RawRecordReviewServiceImpl.processReviewedRecord()` → 创建 `energy_bill_detail` → 触发扣款。
ETC 和电费目前的 review 方法只更新了审核状态,**没有创建 energy_bill_detail没有走统一审核服务**。需要对齐。
## 改动范围
### 1. ETC: EtcTollRecordServiceImpl.review()
**文件:** `modules/etc/service/impl/EtcTollRecordServiceImpl.java`
当前 review() 只设 reviewStatus + remark。改为
- 设 reviewStatus
- 如果 approved → 构建 `ReviewedRecordContext`,调用 `reviewService.processReviewedRecord(context)`
- 把返回的 `EnergyBillDetail.id` 写回 `EtcTollRecord.billDetailId`
- batchReview() 同理(逐条调用 review
ReviewedRecordContext 构建:
```java
ReviewedRecordContext.builder()
.feeType(FeeType.ETC)
.rawRecordId(record.getId())
.rawTableType("etc_toll_record")
.sourceType(record.getSourceType())
.plateNumber(record.getPlateNumber())
.eventTime(record.getTransTime())
.rawUnitPrice(null) // ETC 无单价
.rawAmount(record.getTollFee())
.quantity(null) // ETC 无数量
.stationId(null)
.stationName(null)
.tenantId(...)
.build()
```
需要注入 `IRawRecordReviewService`
### 2. 电费: ElectricityRecordServiceImpl.review()
**文件:** `modules/electricity/service/impl/ElectricityRecordServiceImpl.java`
当前 review() 设 reviewStatus + 直接调 executeDeduction错误位置。改为
- 设 reviewStatus
- 如果 approved → 构建 `ReviewedRecordContext`,调用 `reviewService.processReviewedRecord(context)`
- 把返回的 `EnergyBillDetail.id` 写回 `ElectricityChargeRecord.billDetailId`
- **删除** `executeDeduction()`(扣款由 reviewService 统一处理)
- **删除** `triggerDeduction()` 方法
- **删除** `IDeductionService` 依赖
ReviewedRecordContext 构建:
```java
ReviewedRecordContext.builder()
.feeType(FeeType.ELECTRICITY)
.rawRecordId(record.getId())
.rawTableType("electricity_charge_record")
.sourceType(record.getSourceType())
.plateNumber(record.getPlateNumber())
.eventTime(record.getChargingEndTime())
.rawUnitPrice(null)
.rawAmount(record.getTotalAmount())
.quantity(record.getKwh())
.stationId(null)
.stationName(null)
.tenantId(...)
.build()
```
### 3. 电费 PO 精简 (同 ETC 模式)
**文件:** `modules/electricity/entity/record/po/ElectricityChargeRecord.java`
electricity_charge_record 也是原始账单表,应像 ETC 一样删除关联字段:
- 删除: contract_id, contract_code, customer_id, customer_name, cost_type, payment_mode, contract_matched, deduction_status, bill_id, is_oneos_vehicle
- 保留: bill_detail_id关联统一账单
- 对应删除 DB 列
### 4. 电费 Listener 简化
**文件:** `modules/electricity/listener/ElectricityDetailImportListener.java`
当前 listener 做了合同匹配并存到原始记录。因为原始记录不再存这些字段,简化为:
- 仅设 review_status = PENDING
- 移除合同匹配逻辑(交给 reviewService 统一处理)
### 5. ETC Listener 已OK
当前 EtcDetailImportListener 已经只设 review_status = PENDING无需改动。
### 6. 电费 Service 接口清理
**文件:** `modules/electricity/service/IElectricityRecordService.java`
- 删除 `manualMatch()`, `triggerDeduction()` 方法签名
**文件:** `modules/electricity/controller/ElectricityRecordController.java`
- 删除 `/manual-match` 端点
### 7. 电费前端 VO/页面同步
同 ETC 的精简模式VO 删除关联字段,前端 data.ts 列定义同步更新。
## 关键文件
| 文件 | 操作 |
|------|------|
| `etc/service/impl/EtcTollRecordServiceImpl.java` | 改 — review() 接入 reviewService |
| `electricity/service/impl/ElectricityRecordServiceImpl.java` | 改 — review() 接入 reviewService, 删除 deduction 逻辑 |
| `electricity/entity/record/po/ElectricityChargeRecord.java` | 改 — 删除关联字段 |
| `electricity/listener/ElectricityDetailImportListener.java` | 改 — 简化,去掉合同匹配 |
| `electricity/service/IElectricityRecordService.java` | 改 — 删除 manualMatch/triggerDeduction |
| `electricity/controller/ElectricityRecordController.java` | 改 — 删除 /manual-match |
| `electricity/entity/record/vo/ElectricityChargeRecordVO.java` | 改 — 删除关联字段 |
| `electricity/entity/record/query/ElectricityRecordQuery.java` | 改 — 删除关联查询条件 |
| 前端 `views/electricity/record/data.ts` | 改 — 对齐 VO |
| 前端 `api/electricity/index.ts` | 改 — 对齐类型 |
| 数据库 `electricity_charge_record` | ALTER — 删除列 |
## 验证
1. ETC: 导入 → 列表展示 → 审核通过 → energy_bill_detail 有新记录 → bill_detail_id 回填
2. 电费: 导入 → 列表展示 → 审核通过 → energy_bill_detail 有新记录 → 扣款由 reviewService 处理
3. 三种费用的 energy_bill_detail 表中都有数据feeType 分别为 1/2/3