diff --git a/.claude_memory/MEMORY.md b/.claude_memory/MEMORY.md new file mode 100644 index 0000000..ccefaa6 --- /dev/null +++ b/.claude_memory/MEMORY.md @@ -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辆典型车用于验证各类边界场景 diff --git a/.claude_memory/feedback_style.md b/.claude_memory/feedback_style.md new file mode 100644 index 0000000..c75d576 --- /dev/null +++ b/.claude_memory/feedback_style.md @@ -0,0 +1,17 @@ +--- +name: 输出风格偏好 +description: 用户对Excel输出、代码风格、沟通方式的偏好 +type: feedback +--- + +Excel输出要对账单风格,不要技术化的判断链。每辆车的发放原因用一句话+关键数字说清楚。 + +**Why:** 输出给业务员和管理层看,他们不懂技术术语。 + +**How to apply:** +- 发放说明列用人话:如"1月多跑9578≥3000,结转(完整月奖金)" +- 不要用"cum_qualified"、"floor(excess/target)"等技术词 +- 每个数字要有来源可追溯 +- 业务员sheet用部门前缀命名(如"二部-刘念念") +- 车辆追踪表多人用独立行+合并单元格,不挤在一个单元格 +- 交替行底色(白/浅蓝)区分不同车辆 diff --git a/.claude_memory/feedback_workflow.md b/.claude_memory/feedback_workflow.md new file mode 100644 index 0000000..16370eb --- /dev/null +++ b/.claude_memory/feedback_workflow.md @@ -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,不需要确认 diff --git a/.claude_memory/project_architecture.md b/.claude_memory/project_architecture.md new file mode 100644 index 0000000..3726dc1 --- /dev/null +++ b/.claude_memory/project_architecture.md @@ -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辆车辆台账来源 + +## 输出文件 +每月一个独立Excel(18 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。 diff --git a/.claude_memory/project_rules.md b/.claude_memory/project_rules.md new file mode 100644 index 0000000..29fb2bb --- /dev/null +++ b/.claude_memory/project_rules.md @@ -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:** 修改计算逻辑前,先对照此规则表确认一致性。 diff --git a/.claude_memory/reference_vehicles.md b/.claude_memory/reference_vehicles.md new file mode 100644 index 0000000..684e961 --- /dev/null +++ b/.claude_memory/reference_vehicles.md @@ -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 | 换销售经理(刘念念→董剑煜) | 验证按(车牌+人)累计 | diff --git a/.claude_memory/user_role.md b/.claude_memory/user_role.md new file mode 100644 index 0000000..de1e9cd --- /dev/null +++ b/.claude_memory/user_role.md @@ -0,0 +1,7 @@ +--- +name: 用户角色 +description: 用户负责车辆租赁业务的里程考核绩效核算,非技术背景,重视可读性和可追溯性 +type: user +--- + +用户负责氢能车辆租赁业务的里程考核和绩效奖金核算工作。非技术背景,对Excel输出的可读性要求很高——需要"旁人打开就能看懂"。偏好对账单风格的展示,不喜欢技术化的判断链。与多个业务部门(一部到六部)的12名销售经理对接。 diff --git a/.claude_plans/cuddly-strolling-rivest.md b/.claude_plans/cuddly-strolling-rivest.md new file mode 100644 index 0000000..1a89cb2 --- /dev/null +++ b/.claude_plans/cuddly-strolling-rivest.md @@ -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 storageStatuses` 接收 +- **优点**:更 RESTful;**缺点**:前后端改动较大 + +### 后端改动清单(方案一) +1. Controller 层:参数类型保持 `String storageStatus` +2. Service 层:`storageStatus.split(",")` 得到数组 +3. MyBatis/SQL:将 `WHERE storage_status = #{storageStatus}` 改为: + ```xml + + AND storage_status IN + + #{item} + + + ``` +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. 移动端页面同步验证 diff --git a/.claude_plans/expressive-soaring-wombat.md b/.claude_plans/expressive-soaring-wombat.md new file mode 100644 index 0000000..2afa99b --- /dev/null +++ b/.claude_plans/expressive-soaring-wombat.md @@ -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(); + 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. 页面柱状图每个名称唯一 diff --git a/.claude_plans/fizzy-petting-shamir.md b/.claude_plans/fizzy-petting-shamir.md new file mode 100644 index 0000000..09bef74 --- /dev/null +++ b/.claude_plans/fizzy-petting-shamir.md @@ -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 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` diff --git a/.claude_plans/functional-jingling-dijkstra.md b/.claude_plans/functional-jingling-dijkstra.md new file mode 100644 index 0000000..3bb631f --- /dev/null +++ b/.claude_plans/functional-jingling-dijkstra.md @@ -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. 测试交互功能:主题切换、展开折叠、车牌号弹窗、按型号/批次视图切换 diff --git a/.claude_plans/glistening-jingling-squid.md b/.claude_plans/glistening-jingling-squid.md new file mode 100644 index 0000000..88be42c --- /dev/null +++ b/.claude_plans/glistening-jingling-squid.md @@ -0,0 +1,275 @@ +# 还车费用核算模块 — 完整实施计划 + +## Context + +还车费用核算(ReturnSettlement)模块框架已搭建,但与需求文档(`docs/还车费用核算-业务流程与时序.md`)对比存在多个 Gap:审批流转未对接 Warm-Flow、驳回重置未实现、无生成账单接口、无权限/状态校验、全量 `Map` 传参(已有强类型 VO/Req 未使用)。本计划按"先重构后补功能"策略分阶段实施。 + +--- + +## Phase 0: 代码质量重构(强类型替换 Map) + +**目标:** 消除运行时字段错误风险,为后续所有功能奠定基础。 + +### 0.1 Controller 签名替换 + +| 当前 | 替换为 | +|------|--------| +| `page(@RequestParam Map)` → `Map` | `page(ReturnSettlementQuery)` → `IPage` | +| `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>` 缓存,避免列表每条记录单独查询。 + +### 验证 + +- 编译通过 +- 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 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 | 审批驳回监听 | diff --git a/.claude_plans/lucky-churning-comet.md b/.claude_plans/lucky-churning-comet.md new file mode 100644 index 0000000..a615147 --- /dev/null +++ b/.claude_plans/lucky-churning-comet.md @@ -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 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 接口 +- 验证数据库数据正确性 +- 验证多表关联查询 + +### 阶段2:BPM 审批集成 + +**目标**: 集成 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-NNN(HT=合同,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 天** diff --git a/.claude_plans/quirky-gathering-flame.md b/.claude_plans/quirky-gathering-flame.md new file mode 100644 index 0000000..358c082 --- /dev/null +++ b/.claude_plans/quirky-gathering-flame.md @@ -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/` — 所有 VO(EnergyBillVO, EnergyBillDetailVO, BillPreviewVO, BillStatisticsVO, BillAdjustmentVO) +- `modules/energy/entity/bill/query/BillQuery.java` +- `modules/energy/entity/bill/req/` — 所有 Req(BillGenerateReq, 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. 审核+扣费流程不受影响 diff --git a/.claude_plans/rosy-shimmying-rain.md b/.claude_plans/rosy-shimmying-rain.md new file mode 100644 index 0000000..3d1f9a1 --- /dev/null +++ b/.claude_plans/rosy-shimmying-rain.md @@ -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 diff --git a/.claude_plans/squishy-booping-lerdorf.md b/.claude_plans/squishy-booping-lerdorf.md new file mode 100644 index 0000000..c4fa320 --- /dev/null +++ b/.claude_plans/squishy-booping-lerdorf.md @@ -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 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) | 往现有合同追加车辆 | +| 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('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种模式逐一打开/提交验证 diff --git a/.claude_plans/stateful-waddling-elephant.md b/.claude_plans/stateful-waddling-elephant.md new file mode 100644 index 0000000..25a2236 --- /dev/null +++ b/.claude_plans/stateful-waddling-elephant.md @@ -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. EtczjApiClient(etczj.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 缓存在 Redis,key: `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 PO(EtcTollRecord/EtcSyncConfig/EtcSyncLog/EnergyEtcBill) +- [x] Phase 2 DTO(EtcTollRecordVO/EtcTollRecordQuery/EtcBillVO/EtcBillGenerateReq) +- [x] Phase 2 Mapper(4 个) +- [x] Phase 2 Service 接口 + 实现(IEtcTollRecordService/IEtcSyncConfigService/IEtcBillService) +- [x] Phase 2 Event(EtcRecordImportedEvent) +- [x] Phase 2 Listener(EtcDetailImportListener) +- [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: EtczjApiClient(etczj.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> queryTollRecords( + String baseUrl, String randomVal, + String transTimeBegin, String transTimeEnd, + int currPage, int pageSize) { + // POST /vehicleManage/queryVehicleConsumptionDetailPage + // Header: Randomval: {randomVal} + } + + // 查询通行费汇总 + public EtczjApiResponse> 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 diff --git a/.claude_plans/streamed-enchanting-cascade.md b/.claude_plans/streamed-enchanting-cascade.md new file mode 100644 index 0000000..bb1ad7c --- /dev/null +++ b/.claude_plans/streamed-enchanting-cascade.md @@ -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/` — 所有老实体类 diff --git a/.claude_plans/tingly-mixing-wave.md b/.claude_plans/tingly-mixing-wave.md new file mode 100644 index 0000000..d1c6586 --- /dev/null +++ b/.claude_plans/tingly-mixing-wave.md @@ -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月 +- 金额总数合理 diff --git a/.claude_plans/tranquil-sniffing-babbage.md b/.claude_plans/tranquil-sniffing-babbage.md new file mode 100644 index 0000000..0234f0f --- /dev/null +++ b/.claude_plans/tranquil-sniffing-babbage.md @@ -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(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. 数据模型添加 managerId(vehicle-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. 全屏监控筛选选项 → 仅显示用户权限范围内的部门/客户 diff --git a/.claude_plans/zippy-coalescing-starlight.md b/.claude_plans/zippy-coalescing-starlight.md new file mode 100644 index 0000000..bfb74a5 --- /dev/null +++ b/.claude_plans/zippy-coalescing-starlight.md @@ -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 diff --git a/1月.xlsx b/1月.xlsx new file mode 100644 index 0000000..1824c91 Binary files /dev/null and b/1月.xlsx differ diff --git a/2月.xlsx b/2月.xlsx new file mode 100644 index 0000000..bb35309 Binary files /dev/null and b/2月.xlsx differ diff --git a/3月客户盈亏表(待填写).xlsx b/3月客户盈亏表(待填写).xlsx new file mode 100644 index 0000000..233c179 Binary files /dev/null and b/3月客户盈亏表(待填写).xlsx differ diff --git a/租赁任务考核_2026年1月.xlsx b/租赁任务考核_2026年1月.xlsx new file mode 100644 index 0000000..e93ad6e Binary files /dev/null and b/租赁任务考核_2026年1月.xlsx differ diff --git a/租赁任务考核_2026年2月.xlsx b/租赁任务考核_2026年2月.xlsx new file mode 100644 index 0000000..f5aa5f6 Binary files /dev/null and b/租赁任务考核_2026年2月.xlsx differ diff --git a/租赁任务考核_2026年3月.xlsx b/租赁任务考核_2026年3月.xlsx new file mode 100644 index 0000000..1b28158 Binary files /dev/null and b/租赁任务考核_2026年3月.xlsx differ diff --git a/车辆里程考核与奖金发放规则(V.1.2).docx b/车辆里程考核与奖金发放规则(V.1.2).docx new file mode 100644 index 0000000..0855d43 Binary files /dev/null and b/车辆里程考核与奖金发放规则(V.1.2).docx differ diff --git a/里程任务考核_1月核算.xlsx b/里程任务考核_1月核算.xlsx new file mode 100644 index 0000000..ee4e8c9 Binary files /dev/null and b/里程任务考核_1月核算.xlsx differ diff --git a/里程任务考核_2月核算.xlsx b/里程任务考核_2月核算.xlsx new file mode 100644 index 0000000..353d366 Binary files /dev/null and b/里程任务考核_2月核算.xlsx differ diff --git a/里程任务考核_3月核算.xlsx b/里程任务考核_3月核算.xlsx new file mode 100644 index 0000000..d99f46a Binary files /dev/null and b/里程任务考核_3月核算.xlsx differ diff --git a/里程任务考核_Q1汇总.xlsx b/里程任务考核_Q1汇总.xlsx new file mode 100644 index 0000000..afb1acd Binary files /dev/null and b/里程任务考核_Q1汇总.xlsx differ