# 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<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
