Files
mileage-bonus/.claude_plans/stateful-waddling-elephant.md
kkfluous 573f8397a6 chore: 添加输入输出文件 + .claude记忆和计划
输入文件:
- 租赁任务考核_2026年{1,2,3}月.xlsx (考核源数据)
- {1,2}月.xlsx (客户盈亏表)
- 车辆里程考核与奖金发放规则(V.1.2).docx

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

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

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 14:09:24 +08:00

702 lines
28 KiB
Markdown
Raw Blame History

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