6 Commits
V3.0.0 ... main

Author SHA1 Message Date
kkfluous
2956496fb0 V3.3.0 补充3月盈亏表 + 更新1月2月盈亏数据
- 3月客户盈亏表已填写,改名为3月.xlsx(84客户,39亏损)
- 3月核算:未匹配0条,考核应发57057,拦截41921,实发15136
- 新增.gitignore排除临时文件
- 新增里程考核绩效核算/目录(整理后的数据和结果)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 16:26:35 +08:00
kkfluous
c6a22d8a48 data: 补充1月2月亏损表缺失客户盈亏数据
从3月核算反馈文件K列提取10个客户盈亏信息,补充到1月(+6)和2月(+10)亏损表。
1月未匹配从30→22条,2月从62→32条。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 16:17:37 +08:00
kkfluous
573f8397a6 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>
2026-04-07 14:09:24 +08:00
kkfluous
da487c41d4 V3.2.0 结转不依赖考核记录 + 补发查对应月盈亏
1. 结转不依赖考核记录:
   - 1月有结转但2月/3月无考核记录 → 创建虚拟group(目标=满月,实际=0)
   - 虚拟记录正常发放结转奖金
   - 无客户关联 → 不查盈亏,正常发放

2. 补发查对应月盈亏:
   - 补发1月 → 查1月亏损表
   - 补发2月 → 查2月亏损表
   - 当月/结转/累计补发 → 查当月亏损表
   - 奖金发放记录新增"盈亏查询月"列

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 13:30:42 +08:00
kkfluous
ee962c97ae fix: 无亏损表的月份全部不发放
3月无亏损表→所有车辆标注"未匹配"→拦截全部考核应发→实发0。
移除了"未匹配且有亏损表"的多余条件判断。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 09:56:03 +08:00
kkfluous
4f48d1986f V3.1.0 新增奖金发放记录sheet,汇总改为从发放记录生成
数据链路:车辆考核追踪 → 奖金发放记录 → 月汇总
- 奖金发放记录:逐条明细,含车牌/业务员/部门/客户/发放类型/考核应发/客户盈亏/亏损拦截/实发
- 亏损→红底,未匹配→黄底,正常发放→绿底
- 月汇总改为从发放记录SUM生成:考核应发→亏损筛选→最终发放
- 业务员列不带部门前缀(部门独立列)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 09:47:06 +08:00
47 changed files with 3709 additions and 9 deletions

6
.claude_memory/MEMORY.md Normal file
View File

@@ -0,0 +1,6 @@
- [用户角色](user_role.md) — 负责车辆租赁里程考核绩效核算,非技术背景,重视可读性
- [输出风格偏好](feedback_style.md) — 对账单风格,人话说明,部门前缀,交替行色
- [工作流程偏好](feedback_workflow.md) — 边讨论边改用具体case验证高频重新生成
- [里程考核规则](project_rules.md) — 完整计算规则:达标/结转/补发/累计/亏损筛选
- [项目架构](project_architecture.md) — 3文件架构18 sheets数据流
- [关键测试车辆](reference_vehicles.md) — 7辆典型车用于验证各类边界场景

View File

@@ -0,0 +1,17 @@
---
name: 输出风格偏好
description: 用户对Excel输出、代码风格、沟通方式的偏好
type: feedback
---
Excel输出要对账单风格不要技术化的判断链。每辆车的发放原因用一句话+关键数字说清楚。
**Why:** 输出给业务员和管理层看,他们不懂技术术语。
**How to apply:**
- 发放说明列用人话:如"1月多跑9578≥3000结转(完整月奖金)"
- 不要用"cum_qualified"、"floor(excess/target)"等技术词
- 每个数字要有来源可追溯
- 业务员sheet用部门前缀命名如"二部-刘念念"
- 车辆追踪表多人用独立行+合并单元格,不挤在一个单元格
- 交替行底色(白/浅蓝)区分不同车辆

View File

@@ -0,0 +1,16 @@
---
name: 工作流程偏好
description: 用户对开发流程、沟通方式的偏好
type: feedback
---
用户喜欢边讨论边改通过具体数据case推动规则确认。不喜欢一次性大规划后再实现。
**Why:** 规则有很多边界情况,只有看到实际数据才能确认逻辑对不对。
**How to apply:**
- 每次改完立即生成Excel让用户验证
- 用具体车辆如粤AGP5797、粤AGP5769举例说明
- 修改后立即"重新生成"是高频操作,保持脚本可随时运行
- 用git tag标记版本V1.0.0 → V3.2.0),方便回退
- 用户说"重新生成"就直接 rm + python3 main.py不需要确认

View File

@@ -0,0 +1,37 @@
---
name: 项目架构和文件结构
description: 代码架构、输入输出文件、Excel sheet结构
type: project
---
## 代码架构V3.2.0
- `calc_engine.py` - 计算引擎:规则/读取/分组/结转/补发/累计/亏损读取
- `excel_writer.py` - Excel输出所有sheet生成函数
- `main.py` - 入口按月循环生成独立Excel文件
- `generate_q1_summary.py` - 旧版单文件脚本(已被拆分,保留)
## 输入文件
- `租赁任务考核_2026年{1,2,3}月.xlsx` - 月度考核源数据
- `{1,2}月.xlsx` - 客户盈亏表3月待补充
- `里程任务考核_Q1汇总.xlsx` - 全量492辆车辆台账来源
## 输出文件
每月一个独立Excel18 sheets
1. 考核奖励规则
2. 里程明细X月
3. X月计算过程
4. 车辆考核追踪全量492辆多人拆行+合并单元格+交替色)
5. X月奖金发放记录逐条明细+亏损筛选)
6. X月汇总从发放记录生成考核应发→亏损筛选→最终发放
7-18. 业务员sheets × 12对账单风格+发放说明)
## 数据流
```
源数据 → 里程明细(逐条达标) → 计算过程(分组+判断链)
→ 车辆考核追踪(考核维度) → 奖金发放记录(+亏损筛选)
→ 月汇总(从发放记录SUM) → 业务员sheets
```
**Why:** 从单文件重构为模块化,支持按月独立核算和多维度展示。
**How to apply:** 新功能加在对应模块中。计算逻辑改calc_engine展示改excel_writer流程改main。

View File

@@ -0,0 +1,53 @@
---
name: 里程考核绩效核算规则
description: 车辆里程考核的完整计算规则,经过多次讨论确认
type: project
---
## 核心规则V1.2规则文档 + 讨论确认)
### 达标判断
- 每条考核记录独立判断达标(不合并同车同人多条记录)
- 达标条件:实际行驶里程 ≥ 应考核里程
### 奖金计算
- 奖金 = 考核天数/当月天数 × 月奖励金额(始终按天折算)
- 不存在"超过整月目标就发全月"的特殊情况
### 结转(多跑)
- floor(多跑里程/月度目标里程) ≥ 1 → 结转,发完整月奖金
- 结转占用当月名额,不叠加当月奖励
- 结转不依赖目标月的考核记录(无记录也发,视为消耗一个月目标)
- 无客户关联的结转不查盈亏,正常发放
### 补发
- 累计全部达标后才补发严格版cum_actual ≥ cum_target for ALL months
- 不是"覆盖到哪补到哪"的progressive模式
- 亏损月被拦截的奖金永久不发,不补发
### 累计补发当月
- 当月未达标 + 无结转 + 累计全部达标 → 按天折算发当月奖金
### 亏损筛选
- 按客户维度:客户亏损 → 该客户下所有车不发
- 补发X月 → 查X月盈亏表不是当月的
- 未匹配亏损表 → 不发放,标注待人工确认
- 无亏损表的月份 → 全部不发放
### 多人共用车辆
- 同车不同销售独立核算
- 累计按(车牌号+销售经理)维度,换人后累计重新开始
- 单车奖金池12期跨销售共享
### 考核奖励规则表
| 考核目标 | 月度目标里程 | 奖励金额 |
|---------|-----------|---------|
| 交投40辆4.5T普货 | 3000km | 150元 |
| 交投190辆4.5T冷链车 | 3000km | 150元 |
| 羚牛136辆4.5T冷链车 | 5000km | 260元 |
| 恒运50辆4.5T普货 | 5000km | 260元 |
| 羚牛100辆18T | 6000km | 1000元 |
**Why:** 规则文档V1.2为基础,多条规则在讨论中逐步确认和修正。
**How to apply:** 修改计算逻辑前,先对照此规则表确认一致性。

View File

@@ -0,0 +1,15 @@
---
name: 关键测试车辆
description: 讨论中反复用到的典型车辆案例,用于验证逻辑正确性
type: reference
---
| 车牌 | 场景 | 用途 |
|------|------|------|
| 粤AGP5797 | 1月达标多跑4134(不够结转5000)2月未达标累计达标 | 验证"累计补发当月" |
| 粤AGP5769 | 1月仅1天(84/161未达标)2月未达标3月大爆发13663 | 验证跨月补发1月+2月+当月 |
| 粤AGR7791 | 1月跑12578多跑9578够结转3个月 | 验证多跑结转 |
| 粤AGP5636 | 同人同月2条记录(达标+未达标) | 验证不合并逻辑 |
| 粤AGE4080 | 同人同月2条记录合并后里程误导 | 验证逐条显示✓/✗ |
| 粤A03350F | 同车多人(刘念念+董剑煜) | 验证多人拆行+合并单元格 |
| 粤AGE8412 | 换销售经理(刘念念→董剑煜) | 验证按(车牌+人)累计 |

View File

@@ -0,0 +1,7 @@
---
name: 用户角色
description: 用户负责车辆租赁业务的里程考核绩效核算,非技术背景,重视可读性和可追溯性
type: user
---
用户负责氢能车辆租赁业务的里程考核和绩效奖金核算工作。非技术背景对Excel输出的可读性要求很高——需要"旁人打开就能看懂"。偏好对账单风格的展示不喜欢技术化的判断链。与多个业务部门一部到六部的12名销售经理对接。

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

8
.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
.DS_Store
__pycache__/
.~*
*.csv
*.jpeg
*.jpg
*.png
.localized

BIN
1月.xlsx Normal file

Binary file not shown.

BIN
2月.xlsx Normal file

Binary file not shown.

BIN
3月.xlsx Normal file

Binary file not shown.

View File

@@ -98,6 +98,26 @@ def calc_feb(G1, G2):
g2['可结转'] = fc + jr g2['可结转'] = fc + jr
g2['2月已发'] = carry>0 or bonus2>0 or cbp2>0 g2['2月已发'] = carry>0 or bonus2>0 or cbp2>0
g2['1月已补发'] = bp1>0 g2['1月已补发'] = bp1>0
# 补充1月有结转但2月无考核记录的车 → 创建虚拟2月group并发放结转
for k, g1 in G1.items():
if k not in G2 and g1['可结转'] >= 1:
bf = g1['奖励额']
carry = bf
feb_data['结转'].append({'车牌':k[0],'销售':g1['销售'],'部门':g1['部门'],'':carry})
# 创建虚拟2月group考核里程=满月目标,实际=0
G2[k] = {
'recs':[], '应考核':g1['目标km'], '实际':0, '奖金':0, '天数':DAYS[2],
'有达标':False, '目标km':g1['目标km'], '奖励额':g1['奖励额'],
'部门':g1['部门'], '销售':g1['销售'], '车牌':k[0],
'cum_t':g1['应考核']+g1['目标km'], 'cum_a':g1['实际'],
'cum_q': g1['实际'] >= g1['应考核']+g1['目标km'],
'结转':carry, '补发1月':0, '补发1月对应':'',
'当月奖金':0, '累计补发2月':0, '结转占位':True,
'可结转': max(0, g1['可结转'] - 1),
'2月已发':True, '1月已补发':False,
'虚拟':True, # 标记为无考核记录
}
return feb_data return feb_data
def calc_mar(G1, G2, G3, feb_data): def calc_mar(G1, G2, G3, feb_data):
@@ -134,6 +154,25 @@ def calc_mar(G1, G2, G3, feb_data):
g3['结转']=carry; g3['补发1月']=bj; g3['补发1月对应']=g1['销售'] if g1 and bj>0 else '' g3['结转']=carry; g3['补发1月']=bj; g3['补发1月对应']=g1['销售'] if g1 and bj>0 else ''
g3['补发2月']=bf2; g3['补发2月对应']=g2['销售'] if g2 and bf2>0 else '' g3['补发2月']=bf2; g3['补发2月对应']=g2['销售'] if g2 and bf2>0 else ''
g3['当月奖金']=bonus3; g3['累计补发3月']=cbp3; g3['结转占位']=carry>0 g3['当月奖金']=bonus3; g3['累计补发3月']=cbp3; g3['结转占位']=carry>0
# 补充2月有结转但3月无考核记录的车
for k, g2 in G2.items():
if k not in G3 and g2.get('可结转', 0) >= 1:
bf = g2['奖励额']
carry = bf
mar_data['结转'].append({'车牌':k[0],'销售':g2['销售'],'部门':g2['部门'],'':carry})
G3[k] = {
'recs':[], '应考核':g2['目标km'], '实际':0, '奖金':0, '天数':DAYS[3],
'有达标':False, '目标km':g2['目标km'], '奖励额':g2['奖励额'],
'部门':g2['部门'], '销售':g2['销售'], '车牌':k[0],
'cum_t':(G1.get(k,{}).get('应考核',0))+g2['应考核']+g2['目标km'],
'cum_a':(G1.get(k,{}).get('实际',0))+g2['实际'],
'cum_q':False,
'结转':carry, '补发1月':0, '补发1月对应':'',
'补发2月':0, '补发2月对应':'',
'当月奖金':0, '累计补发3月':0, '结转占位':True,
'虚拟':True,
}
return mar_data return mar_data
def collect_vehicle_payments(G, feb_data, mar_data): def collect_vehicle_payments(G, feb_data, mar_data):

View File

@@ -199,6 +199,166 @@ def write_summary_jan(wb, records, loss_data=None, plate_client=None):
write_total(ws,rn,1,{'达标':jan_dl}) write_total(ws,rn,1,{'达标':jan_dl})
AW(ws) AW(ws)
def build_payment_records(month, month_data, all_loss_data, plate_client):
"""构建奖金发放记录列表,每条考核应发一行,叠加亏损筛选。
all_loss_data: {1: loss_dict, 2: loss_dict, 3: loss_dict_or_None}
补发X月 → 查X月盈亏其他 → 查当月盈亏;无客户 → 正常发放
"""
# 发放类型→查哪个月盈亏
def get_loss_month(cat, settle_month):
if '补发1月' in cat: return 1
if '补发2月' in cat: return 2
if '补发3月' in cat: return 3
return settle_month
records = []
for cat, dl in month_data.items():
loss_month = get_loss_month(cat, month)
loss_data = all_loss_data.get(loss_month)
for d in dl:
client = (plate_client or {}).get(d['车牌'], '')
# 无客户关联(如虚拟结转记录)→ 正常发放
if not client:
loss_status = ''
elif loss_data:
loss_status = loss_data.get(client, '未匹配')
else:
loss_status = '未匹配' # 无亏损表视为未匹配,不发放
考核应发 = d['']
if loss_status == '':
拦截 = 考核应发; 实发 = 0
elif loss_status == '未匹配':
拦截 = 考核应发; 实发 = 0
else:
拦截 = 0; 实发 = 考核应发
records.append({
'车牌号': d['车牌'],
'业务员': d['销售'],
'部门': d['部门'],
'客户名称': client or '(无客户关联)',
'发放类型': cat,
'盈亏查询月': f'{loss_month}',
'考核应发': 考核应发,
'客户盈亏': loss_status,
'亏损拦截': 拦截,
'实发金额': 实发,
})
return records
def write_payment_record_sheet(wb, month, payment_records):
"""写入奖金发放记录sheet"""
ws = wb.create_sheet(f'{month}月奖金发放记录')
headers = ['车牌号','业务员','部门','客户名称','发放类型','盈亏查询月',
'考核应发','客户盈亏','亏损拦截','实发金额']
WH(ws, headers)
green_fill = PatternFill(start_color='C6EFCE', end_color='C6EFCE', fill_type='solid')
red_fill = PatternFill(start_color='FFC7CE', end_color='FFC7CE', fill_type='solid')
yellow_fill = PatternFill(start_color='FFFFCC', end_color='FFFFCC', fill_type='solid')
rn = 2
for r in sorted(payment_records, key=lambda x: (x['业务员'], x['车牌号'])):
WR(ws, rn, [r['车牌号'], r['业务员'], r['部门'], r['客户名称'], r['发放类型'],
r.get('盈亏查询月',''),
R(r['考核应发']), r['客户盈亏'], R(r['亏损拦截']), R(r['实发金额'])])
if r['客户盈亏'] == '':
for ci in [8, 9, 10]: ws.cell(row=rn, column=ci).fill = red_fill
elif r['客户盈亏'] == '未匹配':
for ci in [8, 9, 10]: ws.cell(row=rn, column=ci).fill = yellow_fill
elif r['实发金额'] > 0:
ws.cell(row=rn, column=10).fill = green_fill
rn += 1
total_应发 = sum(r['考核应发'] for r in payment_records)
total_拦截 = sum(r['亏损拦截'] for r in payment_records)
total_实发 = sum(r['实发金额'] for r in payment_records)
rn += 1
WR(ws, rn, ['', '', '', '', '合计', '', R(total_应发), '', R(total_拦截), R(total_实发)])
for ci in range(5, 11): ws.cell(row=rn, column=ci).font = Font(bold=True)
ws.auto_filter.ref = f"A1:J1"
AW(ws)
return payment_records
def write_summary_from_records(wb, month, payment_records):
"""从奖金发放记录生成月汇总"""
ws = wb.create_sheet(f'{month}月汇总')
rn = 1
# 按发放类型分section
by_type = defaultdict(list)
for r in payment_records:
by_type[r['发放类型']].append({'车牌':r['车牌号'],'销售':r['业务员'],'部门':r['部门'],'':r['实发金额']})
# 考核应发(按类型)
ws.cell(row=rn, column=1, value='一、考核应发明细').font=Font(bold=True, size=12); rn+=2
by_type_应发 = defaultdict(list)
for r in payment_records:
by_type_应发[r['发放类型']].append({'车牌':r['车牌号'],'销售':r['业务员'],'部门':r['部门'],'':r['考核应发']})
for cat in sorted(by_type_应发.keys()):
rn = write_sec(ws, rn, f'考核应发-{cat}', by_type_应发[cat])
total_应发 = sum(r['考核应发'] for r in payment_records)
WR(ws, rn, ['考核应发合计', '', R(total_应发)]); ws.cell(row=rn,column=1).font=Font(bold=True,size=11); rn+=2
# 亏损拦截
blocked = [r for r in payment_records if r['客户盈亏'] == '' and r['考核应发'] > 0]
unmatched = [r for r in payment_records if r['客户盈亏'] == '未匹配' and r['考核应发'] > 0]
if blocked or unmatched:
ws.cell(row=rn, column=1, value='二、亏损筛选').font=Font(bold=True, size=12); rn+=2
if blocked:
bl_dl = [{'车牌':r['车牌号'],'销售':r['业务员'],'部门':r['部门'],'':r['亏损拦截']} for r in blocked]
rn = write_sec(ws, rn, '亏损拦截(客户亏损不发放)', bl_dl)
if unmatched:
um_dl = [{'车牌':r['车牌号'],'销售':r['业务员'],'部门':r['部门'],'':r['亏损拦截']} for r in unmatched]
rn = write_sec(ws, rn, '未匹配亏损表(需人工确认)', um_dl)
total_拦截 = sum(r['亏损拦截'] for r in payment_records)
WR(ws, rn, ['拦截合计', '', R(total_拦截)]); ws.cell(row=rn,column=1).font=Font(bold=True); rn+=2
# 最终发放(按销售人员)
ws.cell(row=rn, column=1, value='三、最终发放').font=Font(bold=True, size=12); rn+=2
passed = [r for r in payment_records if r['实发金额'] > 0]
passed_dl = [{'车牌':r['车牌号'],'销售':r['业务员'],'部门':r['部门'],'':r['实发金额']} for r in passed]
rn = write_sec(ws, rn, '最终发放明细', passed_dl)
# 最终发放合计(按销售/按部门)
total_by_person = defaultdict(lambda: {'部门':'','':0})
for r in passed:
total_by_person[r['业务员']][''] += r['实发金额']
total_by_person[r['业务员']]['部门'] = r['部门']
ws.cell(row=rn, column=1, value=f'{month}月最终发放(按销售人员)').font=Font(bold=True, size=11); rn+=1
WH(ws, ['销售人员','部门名称','最终发放'], rn); rn+=1
gt = 0
for p in sorted(total_by_person.keys()):
d = total_by_person[p]
WR(ws, rn, [p, d['部门'], R(d[''])]); gt += d['']; rn+=1
WR(ws, rn, ['合计','', R(gt)]); ws.cell(row=rn,column=1).font=hf; rn+=2
by_dept = defaultdict(float)
for p, d in total_by_person.items(): by_dept[d['部门']] += d['']
ws.cell(row=rn, column=1, value=f'{month}月最终发放(按部门)').font=Font(bold=True, size=11); rn+=1
WH(ws, ['部门名称','最终发放'], rn); rn+=1
for dept in sorted(by_dept.keys()):
WR(ws, rn, [dept, R(by_dept[dept])]); rn+=1
WR(ws, rn, ['合计', R(gt)]); ws.cell(row=rn,column=1).font=hf; rn+=2
# 汇总数字
total_实发 = sum(r['实发金额'] for r in payment_records)
total_拦截 = sum(r['亏损拦截'] for r in payment_records)
ws.cell(row=rn, column=1, value='总览').font=Font(bold=True, size=12); rn+=1
WR(ws, rn, ['考核应发', R(total_应发)]); rn+=1
WR(ws, rn, ['亏损拦截', R(total_拦截)]); rn+=1
WR(ws, rn, ['最终发放', R(total_实发)]); ws.cell(row=rn,column=1).font=Font(bold=True,size=12)
ws.cell(row=rn,column=2).font=Font(bold=True,size=12)
AW(ws)
# 保留旧函数兼容3月无亏损表时使用
def write_summary_month(wb, month, month_data, section_names, loss_data=None, plate_client=None): def write_summary_month(wb, month, month_data, section_names, loss_data=None, plate_client=None):
ws = wb.create_sheet(f'{month}月汇总') ws = wb.create_sheet(f'{month}月汇总')
rn=1 rn=1
@@ -285,7 +445,10 @@ def write_salesperson_sheet(wb, person, dept, settle_month, D, G, month_data, ve
if g_cur: break if g_cur: break
if not g_cur: continue if not g_cur: continue
if g_cur['recs']:
first = g_cur['recs'][0] first = g_cur['recs'][0]
else:
first = {'合同编号':'','客户名称':'(无考核记录)','考核目标':g_cur.get('考核目标','')}
mkm = g_cur['目标km'] mkm = g_cur['目标km']
# 第1行车辆信息 + 各月里程/目标 # 第1行车辆信息 + 各月里程/目标

15
main.py
View File

@@ -82,17 +82,16 @@ for settle_month in [1, 2, 3]:
else: else:
write_calc_process_mar(wb, G[1], G[2], G[3], feb_data) write_calc_process_mar(wb, G[1], G[2], G[3], feb_data)
# Sheet 4: 汇总
if settle_month == 1:
write_summary_jan(wb, D[1], loss_data[1], plate_client)
elif settle_month == 2:
write_summary_month(wb, 2, feb_data, ['结转','补发1月','当月','累计补发2月'], loss_data[2], plate_client)
else:
write_summary_month(wb, 3, mar_data, ['结转','补发1月','补发2月','当月','累计补发3月'], loss_data[3], plate_client)
# Sheet 5: 车辆考核追踪 # Sheet 5: 车辆考核追踪
write_vehicle_tracking_sheet(wb, settle_month, G, master_vehicles, vehicle_payments, vehicle_info, loss_data[settle_month], plate_client) write_vehicle_tracking_sheet(wb, settle_month, G, master_vehicles, vehicle_payments, vehicle_info, loss_data[settle_month], plate_client)
# Sheet 6: 奖金发放记录(叠加亏损筛选,补发查对应月盈亏)
payment_records = build_payment_records(settle_month, month_data, loss_data, plate_client)
write_payment_record_sheet(wb, settle_month, payment_records)
# Sheet 7: 月汇总(从发放记录生成)
write_summary_from_records(wb, settle_month, payment_records)
# Sheet 6-17: 业务员 # Sheet 6-17: 业务员
for person in sorted(all_persons.keys()): for person in sorted(all_persons.keys()):
write_salesperson_sheet(wb, person, all_persons[person], settle_month, D, G, month_data, vehicle_payments) write_salesperson_sheet(wb, person, all_persons[person], settle_month, D, G, month_data, vehicle_payments)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.