Files
ln-bi/docs/superpowers/specs/2026-04-01-mileage-module-design.md
T
dsh-agent 9a9c9d08e1 refactor(stage4): 集中配置与启动校验,权限守卫 fail-closed,凭据移出仓库
配置分层
- 新增 src/server/config.ts:环境变量集中读取 + 启动期校验。
  删除公开兜底密钥 ln-bi-default-secret:SSO 模式下 JWT_SECRET 缺失或
  短于 32 字符时 assertRuntimeConfig() 直接拒绝启动(已实测)。
- index.ts 不再自己读端口/环境;auth/config.ts 与 auth/login.ts 改为引用集中配置。

权限守卫
- energy / scheduling / hydrogen-heatmap 的守卫由 “user 存在才校验” 改为
  “无角色即拒绝”:user 缺失时按无权限处理,不再静默放行(fail-closed)。
- /api/ele/* 此前完全无鉴权,任何已登录用户都能写入电费表;现按能源域
  (BI-LEADER-ENERGY) 守卫。

接口契约
- 未匹配的 /api/* 由 200 text/html(SPA) 改为 404 application/json;
  未认证时仍是 401,避免向未授权调用方暴露路由是否存在。
- 新增全局 onError 返回 JSON 500。

凭据治理(此前均为 git 跟踪文件中的明文)
- Dockerfile 删除烧进镜像的 JWT_SECRET,改为必须运行时注入。
- docker-compose.yml 删除生产库口令/JWT 密钥/失效的 MILEAGE_DB_*,
  改为强制注入写法;补齐 OSS_* 与 NODE_ENV/DEV_BYPASS_AUTH/BI_AUTH_*。
- woodpecker.yml 删除 Harbor base64 凭据改用 secret,pull_request 不再推镜像。
- 删除 scripts-tmp/(含生产库 root 口令)与已跟踪的 .DS_Store;.gitignore 补全。
- 文档中残留的里程库口令改为占位符。

lint / test(128) / build 全绿。
2026-09-11 10:19:07 +08:00

243 lines
8.5 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.
# 里程管理模块设计
## 背景
在模块化重构的基础上,实现里程管理 BI 模块,1:1 复刻原型 `/Users/kkfluous/Projects/ai-coding/ln-yuanxing/lnoneos-1` 中的里程管理部分。包含 3 个子 Tab:实时监控、统计报表、每日汇报(占位)。
## 目标
- 1:1 复刻原型 UI(样式、动画、交互细节完全一致)
- 接入真实数据源(两个数据库)
- 每日汇报 Tab 暂做占位
## 数据源
### 数据库 1lingniu_prod(已有连接)
- `tab_mileage_assessment_target` — 5 个考核项目定义(目标名称、车辆数、年考核里程、考核年限等)
- `tab_mileage_assessment_vehicle` — 492 辆考核车辆(今日里程、累计里程、完成率、达标状态等)
- `tab_truck``tab_truck_status_info``tab_contract``tab_customer` / `tab_user``tab_department` — 车辆关联客户名、部门、经理
### 数据库 2hydrogen_energy(新增连接)
- 连接信息:`101.133.130.65:3306`,用户 `bi_reader_02`,密码 `<见环境变量 MILEAGE_DB_PASSWORD>`,库名 `hydrogen_energy`
- `v_vehicle_daily_stats` — 1004 辆车的每日里程明细(plate, vin, stat_date, daily_km, total_km, day_hydrogen, daily_run_secs, source
## 架构
### 后端
新增 `src/server/mileage-db.ts` — hydrogen_energy 数据库连接池。
新增 `src/server/routes/mileage.ts` — 里程管理 API 路由。
修改 `src/server/index.ts` — 注册新路由 `/api/mileage`
### 前端
```
src/modules/mileage/
├── MileageModule.tsx # 主组件:3个子Tab切换(实时监控/统计报表/每日汇报)
├── MonitoringView.tsx # 实时监控视图
├── StatisticsView.tsx # 统计报表视图
├── DailyReportView.tsx # 每日汇报(占位)
├── api.ts # API 客户端
└── types.ts # 类型定义
```
### 数据流
```
前端 MileageModule → fetch /api/mileage/*
后端 mileage.ts 路由
├── lingniu_prod 池:考核目标/车辆、车辆关联信息(客户/部门/经理)
└── hydrogen_energy 池:v_vehicle_daily_stats(日里程/趋势)
↓ 内存合并
前端渲染(Recharts 图表 + 列表)
```
## API 端点
### `GET /api/mileage/monitoring`
实时监控数据:全部 1004 辆车的今日里程 + 关联信息。
**查询逻辑:**
1.`v_vehicle_daily_stats` 取最新日期的所有车辆数据(plate, daily_km, total_km, source
2.`lingniu_prod` 取车辆关联信息(客户名、部门、经理),使用现有的 `MAIN_SQL` 关联链
3. 内存按 plate 合并
**返回:**
```ts
{
vehicles: Array<{
plate: string;
vin: string;
dailyKm: number;
totalKm: number | null;
source: string; // TBOX / G7S / NONE
isOnline: boolean; // source !== 'NONE' && dailyKm > 0
isDataSynced: boolean; // source !== 'NONE'
customer: string | null;
department: string | null;
manager: string | null;
}>;
updatedAt: string;
}
```
### `GET /api/mileage/targets`
考核项目列表 + 每个项目的汇总统计。
**查询逻辑:**
1.`tab_mileage_assessment_target` 取全部未删除项目
2.`tab_mileage_assessment_vehicle` 按 target_id 聚合统计
**返回:**
```ts
Array<{
id: number;
targetName: string;
vehicleCount: number;
totalMileagePerVehicle: number;
annualMileagePerVehicle: number;
assessmentYears: number;
period: string; // "YYYY-MM-DD ~ YYYY-MM-DD"
todayTotal: number; // SUM(today_mileage)
cumulativeTotal: number; // SUM(current_mileage)
avgCompletion: number; // AVG(completion_rate) * 100
qualifiedCount: number; // SUM(is_qualified)
yearQualifiedCount: number; // SUM(current_year_is_qualified)
halfQualifiedCount: number; // completion_rate >= 0.5 的车辆数
currentYearTarget: number; // SUM(current_year_mileage_task)
currentYearCompleted: number; // SUM(current_year_mileage)
remaining: number; // currentYearTarget - currentYearCompleted
daysLeft: number; // current_year_assessment_end_date - today
dailyTarget: number; // remaining / daysLeft
}>
```
### `GET /api/mileage/target/:id/vehicles`
某考核项目的车辆明细列表。
**查询逻辑:**
`tab_mileage_assessment_vehicle` WHERE target_id = :id AND is_deleted = 0
**返回:**
```ts
Array<{
plateNumber: string;
todayMileage: number;
totalMileage: number;
completionRate: number;
isQualified: boolean;
currentYearIsQualified: boolean;
dailyRequiredMileage: number;
}>
```
### `GET /api/mileage/trend?targetId=...&days=7`
7天里程趋势,按考核项目筛选。
**查询逻辑:**
1. 若有 targetId:从 `tab_mileage_assessment_vehicle` 取该项目的所有 plate_number
2.`v_vehicle_daily_stats` WHERE plate IN (...) AND stat_date >= (today - days) GROUP BY stat_date
**返回:**
```ts
Array<{
date: string; // "MM-DD"
mileage: number; // SUM(daily_km)
}>
```
## 前端组件设计
### MileageModule.tsx
主组件,管理子 Tab 切换(monitoring / statistics / report),包含:
- 子导航栏(实时监控/统计报表/每日汇报),带 motion layoutId 动画下划线
- 条件渲染对应 View 组件
### MonitoringView.tsx
1:1 复刻原型实时监控视图。
**状态:**
- activeSubTab 由父组件管理
- searchTerm, filterDept, filterPlate, filterProject, filterEntity, filterRegionCode, filterYear, filterDate, filterDateRange, filterMileageRange
- sortBy ('today' | 'total'), sortOrder ('asc' | 'desc')
- isFilterOpen, isFullscreen
**UI 结构:**
1. 看板头部(标题 + 全屏按钮 + 排序切换)
2. 快捷筛选栏(3 个 SearchableSelect + 高级筛选图标)
3. 可展开高级筛选面板
4. KPI 卡片网格(4列:总里程深色卡、平均单车、监控台数)
5. 车辆详情清单(motion.div 列表)
6. 全屏叠加层(AnimatePresence
**SearchableSelect 组件:** 在 MonitoringView 内部定义(原型中的实现与公共 SearchSelect 不同,它使用 motion 动画、"无限制"默认选项、不同样式)。
### StatisticsView.tsx
1:1 复刻原型统计报表视图。
**状态:**
- selectedProject, chartType ('bar' | 'line' | 'area')
- isTableFullscreen, expandedModel, viewAllModel, viewAllSearch, viewAllSort
**UI 结构:**
1. 项目选择器(横向滚动按钮组)
2. 左侧:7天趋势图(Recharts BarChart/LineChart/AreaChart 切换)+ landscape KPI 卡片
3. 右侧:车型考核里程汇总卡片列表(可展开详情 + 车辆明细前5台)
4. 全屏表格叠加层(15列明细表)
5. 查看全部侧滑面板(搜索 + 排序 + 车辆列表)
### DailyReportView.tsx
占位组件,显示"每日汇报 - 开发中"。
## 数据映射
### 实时监控
| UI 字段 | 数据来源 |
|---------|---------|
| 车牌号 | `v_vehicle_daily_stats.plate` |
| 今日里程 | `daily_km`(最新日期) |
| 累计里程 | `total_km`(最近非空值,用用户提供的变量填充 SQL) |
| 在线状态 | `source !== 'NONE' && daily_km > 0` |
| 数据同步 | `source !== 'NONE'` |
| 客户名 | `lingniu_prod`: tab_truck → tab_truck_status_info → tab_contract → tab_customer.customer_name |
| 部门 | `lingniu_prod`: → tab_user → tab_department.dep_name |
### 统计报表
| UI 字段 | 数据来源 |
|---------|---------|
| 项目列表 | `tab_mileage_assessment_target`target_name, vehicle_count 等) |
| 今日总里程 | `SUM(tab_mileage_assessment_vehicle.today_mileage)` by target_id |
| 累计总里程 | `SUM(current_mileage)` by target_id |
| 平均完成率 | `AVG(completion_rate) * 100` by target_id |
| 达标车辆数 | `SUM(current_year_is_qualified)` by target_id |
| 50%达标数 | `COUNT(completion_rate >= 0.5)` by target_id |
| 考核区间 | `default_start_date ~ default_end_date` |
| 年考核任务/辆 | `annual_mileage_per_vehicle` |
| 本年需完成 | `SUM(current_year_mileage_task)` |
| 已完成 | `SUM(current_year_mileage)` |
| 未完成总数 | 本年需完成 - 已完成 |
| 剩余天数 | `current_year_assessment_end_date - today`(取 vehicle 中的值) |
| 日均需完成 | 未完成 / 剩余天数 |
| 7天趋势 | `v_vehicle_daily_stats` 按项目车牌过滤聚合 |
| 车辆明细 | `tab_mileage_assessment_vehicle` 的 plate_number, today_mileage, total_mileage 等 |
## 不在范围内
- 每日汇报 Tab 具体实现(占位)
- landscape 适配(原型中有 landscape: 前缀样式,照搬即可但不做额外适配工作)
- 后端缓存
- 新增依赖