refactor(stage9): 后端业务域目录形状统一

- 每个域现在都是 src/server/routes/<domain>/ 目录:
  vehicles/routes.ts(原 routes/vehicles.ts)、vehicle-heatmap/{routes,model}.ts、
  hydrogen-heatmap/{routes,model}.ts、ele/routes.ts、feedback/routes.ts。
- 命名约定:index.ts 是聚合器(挂子路由),routes.ts 是叶子路由。mileage/energy/
  scheduling 保持 index.ts 聚合,其余统一为 routes.ts。
- 纯移动与改名,未改动任何 SQL、参数顺序、路由挂载前缀或处理器逻辑。
  import 改写由一次性 codemod 完成;vehicles/repository.test.ts 读取的源码路径同步更新。
- docs/ARCHITECTURE.md 补充"后端业务域形状"表,并如实标注 ele / feedback 尚未拆出
  repository(这两个域缺 SQL 指纹测试,建议先补测试再拆)。

lint / test(139) / build 全绿,可达性 0 未引用文件。
This commit is contained in:
dsh-agent
2026-09-11 10:30:54 +08:00
parent 954d626afc
commit 795fb207cb
14 changed files with 48 additions and 33 deletions
+18 -3
View File
@@ -82,9 +82,23 @@ server/
db/schema/ 运行时建表/改表的唯一位置(只读模式下整体跳过) db/schema/ 运行时建表/改表的唯一位置(只读模式下整体跳过)
middleware/ authJWT → 注入 user)、read-only middleware/ authJWT → 注入 user)、read-only
auth/ 登录换票、固定密码、权限过滤与脱敏 auth/ 登录换票、固定密码、权限过滤与脱敏
routes/ 按业务域的 HTTP 路由 routes/<domain>/ 按业务域的 HTTP 路由
``` ```
### 后端业务域的形状(`server/routes/<domain>/`
命名约定:`index.ts` 是**聚合器**(把子路由挂到前缀上),`routes.ts` 是**叶子路由**。
| 域 | 文件 | 说明 |
| --- | --- | --- |
| `vehicles/` | `routes.ts` `repository.ts` `model.ts` `utils.ts` `types.ts` | 唯一已完整分层到 repository 的域,SQL 有指纹测试 |
| `mileage/` | `index.ts`(聚合)+ `monitoring.ts` `targets.ts` `trend.ts` `daily-report.ts` `vehicle-recent.ts` + `*-model.ts` + `cache.ts` `oneos-api.ts` `daily-report-{service,store,scheduler}.ts` | 路由 / 模型 / 服务已分开 |
| `energy/` | `index.ts`(聚合)+ `hydrogen-bi-v2.ts` `hydrogen-station-board.ts` `electric.ts` `etc.ts` + `query-model.ts` `cache.ts` `constants.ts` | 路由 / 模型已分开,SQL 仍在路由内 |
| `scheduling/` | `index.ts`(聚合)+ `suggestions.ts` `notify.ts` + `algorithm.ts` `notification-model.ts` | 同上 |
| `hydrogen-heatmap/``vehicle-heatmap/` | `routes.ts` + `model.ts` | 路由 / 模型已分开 |
| `ele/``feedback/` | `routes.ts`+ `oss.ts` | **尚未拆出 repository**SQL 与处理器在同一文件 |
### 中间件顺序(在 `app.ts` 中显式体现) ### 中间件顺序(在 `app.ts` 中显式体现)
``` ```
@@ -118,8 +132,9 @@ cors → read-only → /api/auth(公开) → authMiddleware → 各业务域
诚实记录,避免后来者以为已经做完: 诚实记录,避免后来者以为已经做完:
- **后端路由尚未全部拆成 routes / service / repository**`routes/vehicles/` 已有 - **后端仍有两个域没拆出 repository**`ele/routes.ts``feedback/routes.ts` 仍把 SQL
`repository.ts` + `model.ts` 的雏形,其余域仍是单文件路由;拆分时不要改变 SQL 与参数顺序 与处理器放在同一文件(形状见上表)。拆分时**不要改变 SQL 与参数顺序**——这两个域目前
没有 SQL 指纹测试,建议先补契约测试再动。
- **运行时建表已集中到 `server/db/schema/`**,并在 `DB_READ_ONLY=1` 时整体跳过(由架构测试守护, - **运行时建表已集中到 `server/db/schema/`**,并在 `DB_READ_ONLY=1` 时整体跳过(由架构测试守护,
已实测不触碰数据库)。它仍由业务接口在首次调用时触发,而不是只由 `bootstrap.ts` 调用—— 已实测不触碰数据库)。它仍由业务接口在首次调用时触发,而不是只由 `bootstrap.ts` 调用——
要彻底改成显式迁移,需要先建立数据库变更脚本流程,避免"代码里偷偷建表"。 要彻底改成显式迁移,需要先建立数据库变更脚本流程,避免"代码里偷偷建表"。
+5 -5
View File
@@ -4,14 +4,14 @@ import { readOnlyMiddleware } from './middleware/read-only.js';
import { cors } from 'hono/cors'; import { cors } from 'hono/cors';
import authRouter from './auth/login.js'; import authRouter from './auth/login.js';
import { authMiddleware } from './middleware/auth.js'; import { authMiddleware } from './middleware/auth.js';
import eleRouter from './routes/ele/index.js'; import eleRouter from './routes/ele/routes.js';
import energyRouter from './routes/energy/index.js'; import energyRouter from './routes/energy/index.js';
import feedbackRouter from './routes/feedback/index.js'; import feedbackRouter from './routes/feedback/routes.js';
import hydrogenHeatmapRouter from './routes/hydrogen-heatmap.js'; import hydrogenHeatmapRouter from './routes/hydrogen-heatmap/routes.js';
import mileageRouter from './routes/mileage/index.js'; import mileageRouter from './routes/mileage/index.js';
import schedulingRouter from './routes/scheduling/index.js'; import schedulingRouter from './routes/scheduling/index.js';
import vehicleHeatmapRouter from './routes/vehicle-heatmap.js'; import vehicleHeatmapRouter from './routes/vehicle-heatmap/routes.js';
import vehiclesRouter from './routes/vehicles.js'; import vehiclesRouter from './routes/vehicles/routes.js';
/** /**
* 创建 HTTP 应用,但不监听端口、不访问数据库,也不启动后台任务。 * 创建 HTTP 应用,但不监听端口、不访问数据库,也不启动后台任务。
+1 -1
View File
@@ -1,4 +1,4 @@
import pool from '../../db/mysql.js'; import pool from '../mysql.js';
import { ddlAllowed, logDdlSkipped } from './guard.js'; import { ddlAllowed, logDdlSkipped } from './guard.js';
const CREATE_TABLE_SQL = ` const CREATE_TABLE_SQL = `
@@ -10,8 +10,8 @@ import {
rankHydrogenStations, rankHydrogenStations,
serializeHydrogenStation, serializeHydrogenStation,
type HydrogenStationRecord, type HydrogenStationRecord,
} from './hydrogen-heatmap-model.js'; } from './model.js';
import { recentDayRange } from '../../shared/date-range.js'; import { recentDayRange } from '../../../shared/date-range.js';
// 默认区间是"近 30 天"滚动窗口,注入固定时钟让断言可复现。 // 默认区间是"近 30 天"滚动窗口,注入固定时钟让断言可复现。
const NOW = new Date('2026-07-13T12:00:00'); const NOW = new Date('2026-07-13T12:00:00');
@@ -1,4 +1,4 @@
import { recentDayRange } from '../../shared/date-range.js'; import { recentDayRange } from '../../../shared/date-range.js';
export type HydrogenHeatmapMetric = 'kg' | 'refuels' | 'vehicles'; export type HydrogenHeatmapMetric = 'kg' | 'refuels' | 'vehicles';
export type HydrogenPayer = 'all' | 'lingniu' | 'customer'; export type HydrogenPayer = 'all' | 'lingniu' | 'customer';
@@ -1,9 +1,9 @@
import { Hono } from 'hono'; import { Hono } from 'hono';
import type { RowDataPacket } from 'mysql2'; import type { RowDataPacket } from 'mysql2';
import pool from '../db/mysql.js'; import pool from '../../db/mysql.js';
import type { AuthUser } from '../auth/types.js'; import type { AuthUser } from '../../auth/types.js';
import { canAccessEnergy } from '../auth/types.js'; import { canAccessEnergy } from '../../auth/types.js';
import { recentDayRange } from '../../shared/date-range.js'; import { recentDayRange } from '../../../shared/date-range.js';
import { import {
buildHydrogenPoints, buildHydrogenPoints,
filterHydrogenStationsByRadius, filterHydrogenStationsByRadius,
@@ -14,7 +14,7 @@ import {
serializeHydrogenStation, serializeHydrogenStation,
type HydrogenPayer, type HydrogenPayer,
type HydrogenStationRecord, type HydrogenStationRecord,
} from './hydrogen-heatmap-model.js'; } from './model.js';
type StationRow = RowDataPacket & HydrogenStationRecord; type StationRow = RowDataPacket & HydrogenStationRecord;
+1 -1
View File
@@ -2,7 +2,7 @@ import { Hono } from 'hono';
import pool from '../../db/mysql.js'; import pool from '../../db/mysql.js';
import { fetchVehicleInfoMap } from '../mileage/vehicle-info.js'; import { fetchVehicleInfoMap } from '../mileage/vehicle-info.js';
import { fetchOneOsMileageDates } from '../mileage/oneos-api.js'; import { fetchOneOsMileageDates } from '../mileage/oneos-api.js';
import { mapRegion } from '../vehicles.js'; import { mapRegion } from '../vehicles/routes.js';
import { filterByPermission, maskCustomerNames } from '../../auth/permissions.js'; import { filterByPermission, maskCustomerNames } from '../../auth/permissions.js';
import { classifyVehicle, generateSuggestions } from './algorithm.js'; import { classifyVehicle, generateSuggestions } from './algorithm.js';
import { fetchActiveNotificationMap, fetchRecentInterventionCount } from './notify.js'; import { fetchActiveNotificationMap, fetchRecentInterventionCount } from './notify.js';
@@ -11,8 +11,8 @@ import {
summarizeVehicleRecords, summarizeVehicleRecords,
vehicleGridPrecision, vehicleGridPrecision,
type VehicleHeatmapRecord, type VehicleHeatmapRecord,
} from './vehicle-heatmap-model.js'; } from './model.js';
import { recentDayRange } from '../../shared/date-range.js'; import { recentDayRange } from '../../../shared/date-range.js';
// 默认区间是"近 30 天"滚动窗口,注入固定时钟让断言可复现。 // 默认区间是"近 30 天"滚动窗口,注入固定时钟让断言可复现。
const NOW = new Date('2026-07-13T12:00:00'); const NOW = new Date('2026-07-13T12:00:00');
@@ -1,4 +1,4 @@
import { recentDayRange } from '../../shared/date-range.js'; import { recentDayRange } from '../../../shared/date-range.js';
export type VehicleHeatmapMetric = 'locations' | 'vehicles'; export type VehicleHeatmapMetric = 'locations' | 'vehicles';
export type VehicleHeatmapRecord = { export type VehicleHeatmapRecord = {
@@ -1,7 +1,7 @@
import { Hono } from 'hono'; import { Hono } from 'hono';
import pool from '../db/mysql.js'; import pool from '../../db/mysql.js';
import heatmapPool from '../db/heatmap.js'; import heatmapPool from '../../db/heatmap.js';
import { getCache } from './mileage/cache.js'; import { getCache } from '../mileage/cache.js';
import { import {
buildVehicleGrid, buildVehicleGrid,
buildVehicleRanking, buildVehicleRanking,
@@ -11,7 +11,7 @@ import {
summarizeVehicleRecords, summarizeVehicleRecords,
vehicleGridPrecision, vehicleGridPrecision,
type VehicleHeatmapRecord, type VehicleHeatmapRecord,
} from './vehicle-heatmap-model.js'; } from './model.js';
type MetaRow = { type MetaRow = {
start_date: string; start_date: string;
@@ -26,7 +26,7 @@ function vehicle(id: string): Vehicle {
} }
test('keeps all vehicle GET endpoints in their established order', () => { test('keeps all vehicle GET endpoints in their established order', () => {
const source = readFileSync(new URL('../vehicles.ts', import.meta.url), 'utf8'); const source = readFileSync(new URL('./routes.ts', import.meta.url), 'utf8');
const paths = Array.from(source.matchAll(/app\.get\('([^']+)'/g), (match) => match[1]); const paths = Array.from(source.matchAll(/app\.get\('([^']+)'/g), (match) => match[1]);
assert.deepEqual(paths, [ assert.deepEqual(paths, [
@@ -7,9 +7,9 @@ import type {
BatchSummary, BatchSummary,
BatchGroup, BatchGroup,
InventoryTypeSummary, InventoryTypeSummary,
} from '../types.js'; } from '../../types.js';
import { filterByPermission, maskCustomerNames, maskCustomerName } from '../auth/permissions.js'; import { filterByPermission, maskCustomerNames, maskCustomerName } from '../../auth/permissions.js';
import type { AuthUser } from '../auth/types.js'; import type { AuthUser } from '../../auth/types.js';
import type { Context } from 'hono'; import type { Context } from 'hono';
import { import {
INVENTORY_REGIONS, INVENTORY_REGIONS,
@@ -21,7 +21,7 @@ import {
mapInventoryRegion, mapInventoryRegion,
mapMacroRegion, mapMacroRegion,
resolveCity, resolveCity,
} from './vehicles/model.js'; } from './model.js';
import { import {
compareDepartmentNames, compareDepartmentNames,
countByType, countByType,
@@ -29,10 +29,10 @@ import {
getStats, getStats,
listDateRange, listDateRange,
normalizeDateRange, normalizeDateRange,
} from './vehicles/utils.js'; } from './utils.js';
import { vehicleRepository, type FlowDetailRow, type WeeklyDetailType } from './vehicles/repository.js'; import { vehicleRepository, type FlowDetailRow, type WeeklyDetailType } from './repository.js';
export { mapRegion } from './vehicles/model.js'; export { mapRegion } from './model.js';
const app = new Hono(); const app = new Hono();