改动 - 新增 scheduling/repository.ts:通知/干预与建议列表的全部 SQL(13 条)。 - notify.ts / suggestions.ts 改为 register*(app, deps) 可注入,保留默认导出与 create*Router 工厂。 - suggestions.ts 的两处跨域依赖(里程车辆信息、OneOS 里程)改为依赖注入: 既让本域可独立测试,也让"建议依赖里程数据"在类型上可见,而不是藏在 import 里。 - 跨域引用修正:mapRegion 原本从 '../vehicles/routes.js' 引入(为一个纯函数把整个 车辆路由拖进依赖图),改为直接从 '../vehicles/model.js' 引入。 契约测试(新增 10 个用例) - 逐条锁定 SQL 与参数顺序:干预登记的三步(查重 → INSERT → 回读)、409 阻断、 批量循环、历史列表的状态过滤与 limit 上限(500/默认 200)、状态更新的 UPDATE 形状、 400/404 分支、活跃映射与近 7 天计数、建议列表五条基础查询的内容与顺序。 - 架构守护升级:不再只看 routes.ts,而是要求该域**除 repository.ts 外的任何非测试文件 都不得含 SQL**;匹配用"语句形状"正则(如 update <table> set)而非裸关键字, 避免把 UpdateNotification 或日志 "update error" 误判。已验证 6/6 个 repository 被识别、 其余文件零误判。 等价性验证 - 把 notify.ts / suggestions.ts 改造前的实现从 git 取出,与改造后跑同一批请求 (9 个场景,含建议列表整条链路 8 次查询),对比落库 SQL、参数、HTTP 状态与响应体: 完全一致。 - 期间修正了两处**验证工具自身**的缺陷(旧文件误引用新 notify;跨域默认实现的调用 被记到另一侧),修正后结论可信。 lint / test(191) / build 全绿,可达性 0 未引用文件。
9.6 KiB
9.6 KiB
架构与分层约定
本文是这个仓库的结构契约。6 条硬规则由 src/architecture.test.ts 断言,破坏即测试失败。
1. 分层与依赖方向
app/ 应用外壳(路由、导航注册)
↓
modules/ 前端业务域 —— 只依赖 shared/
↓
shared/ 叶子层:角色常量、跨端 DTO、纯工具(不得依赖 modules/ 或 server/)
server/ 后端 —— 只依赖 shared/,不得依赖 modules/
依赖只能向下。前端通过 HTTP 调用后端,不通过 import。
6 条硬规则
| # | 规则 | 违反后果 |
|---|---|---|
| 1 | server/** 不得 import modules/** |
服务端被前端类型/组件绑架,无法独立部署与测试 |
| 2 | modules/**、components/**、auth/**、app/** 不得 import server/** |
打包会拖入服务端代码与密钥 |
| 3 | shared/** 不得 import modules/** / server/** / components/** |
叶子层反向依赖会让依赖图成环 |
| 4 | 不得出现 vendor/ 引用 |
生产代码必须位于业务域目录,不能藏在"第三方/参考"目录里 |
| 5 | model.ts / model.tsx 不得 import react |
纯模型必须能在 Node 里直接单测,不依赖 DOM |
| 6 | @ts-nocheck 只允许出现在已登记的原型快照文件 |
类型门禁对最大文件失效 |
关于第 6 条的豁免清单
这三个文件是逐字节保留的 8113 验收原型(UI / CSS 冻结,改动需重新做桌面与移动端截图验收):
modules/energy/hydrogen/board/EnergyBiBoardApp.tsx
modules/energy/hydrogen/station-daily/StationDailyApp.tsx
modules/energy/hydrogen/station-daily/StationDailyDetailView.tsx
豁免清单是只减不增的:新增 @ts-nocheck 会被测试拦下,请改为修正类型。
2. 前端业务域的形状
每个域尽量保持同一形状,便于"进一个目录就知道从哪读起":
modules/<domain>/
index.tsx 域入口组件(导出给 app/modules.ts 注册)
api.ts 只做 HTTP,不做业务判断
types.ts DTO
model.ts 纯函数:筛选 / 排序 / 聚合 / 口径(可被 node:test 直接测)
components/ 无状态展示组件
*.css 域内样式
复杂域可以再分子域,例如氢能:
modules/energy/hydrogen/
index.tsx 导航入口
api.ts types.ts 数据访问与 DTO
model/ 纯逻辑(格式化、承担方标签、按日汇总格式)
board/ 经营看板 UI
station-daily/ 单站日报 UI
drill/ 下钻弹层 UI
common/ 域内共享工具与组件
fonts/ 看板专用字体
判定标准很简单:能在 Node 里直接测的放 model/;需要浏览器/DOM 的放 UI 子目录。
3. 后端结构
server/
config.ts 环境变量集中读取 + assertRuntimeConfig() 启动期校验
app.ts createApp():装配中间件、挂载路由、静态托管(无副作用,可测试)
index.ts 进程启动:config 校验 → app → bootstrap → listen
local.ts 只读预览启动(不建表、不定时任务)
bootstrap.ts 启动副作用总入口
db/ mysql / hydrogen / heatmap 连接
db/schema/ 运行时建表/改表的唯一位置(只读模式下整体跳过)
middleware/ auth(JWT → 注入 user)、read-only
auth/ 登录换票、固定密码、权限过滤与脱敏
routes/<domain>/ 按业务域的 HTTP 路由
后端业务域的形状(server/routes/<domain>/)
命名约定:index.ts 是聚合器(把子路由挂到前缀上),routes.ts 是叶子路由。
| 域 | 文件 | 说明 |
|---|---|---|
vehicles/ |
routes.ts repository.ts model.ts utils.ts types.ts |
✅ 完整分层;SQL 有契约测试 |
ele/ |
routes.ts repository.ts model.ts(+ model.test.ts routes.test.ts) |
✅ 完整分层;改造前后 SQL/参数/响应体已做等价性验证 |
feedback/ |
routes.ts repository.ts oss.ts(+ routes.test.ts) |
✅ 完整分层;同上 |
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 |
路由 / 模型 / 服务已分开,SQL 仍在各路由文件内 |
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 repository.ts + algorithm.ts notification-model.ts |
✅ 完整分层;跨域数据源(里程车辆信息 / OneOS)显式注入 |
hydrogen-heatmap/ |
routes.ts repository.ts model.ts(+ routes.test.ts) |
✅ 完整分层;buildWhere 片段与参数顺序已锁定 |
vehicle-heatmap/ |
routes.ts repository.ts model.ts(+ routes.test.ts) |
✅ 完整分层;同时覆盖 MySQL(考核批次)与 PG(定位点)两个库 |
已完整分层的六个域(vehicles / ele / feedback / vehicle-heatmap / hydrogen-heatmap / scheduling)
由架构测试守护:必须存在 repository.ts,且该域其他任何非测试文件都不得含 SQL。
其余域(energy / mileage)尚未拆出 repository——拆分时不要改变 SQL 与参数顺序,
请按 ele/routes.test.ts 的配方先补契约测试,并用"改造前后同一批请求对比落库 SQL 与响应体"做等价性验证。
中间件顺序(在 app.ts 中显式体现)
cors → read-only → /api/auth(公开) → authMiddleware → 各业务域路由
→ /api/health → /api/* 未匹配返回 JSON 404 → 静态托管 + SPA 回退
两个已经踩过的坑,改动时不要退回去:
- 未匹配的
/api/*必须返回 JSON 404。若落到 SPA 回退,会返回200 text/html, 前端res.ok为真后在res.json()抛解析错误,把"路由不存在"伪装成数据问题。 - 模块守卫必须 fail-closed,写法是
if (!canAccessX(user?.roles)), 不能写成if (user && !canAccessX(user.roles))——后者在user缺失时静默放行。
角色与数据权限
- 角色常量唯一真源:
src/shared/auth/roles.ts(前后端共用)。 - 数据范围过滤与客户名脱敏:
src/server/auth/permissions.ts。 permissionLevel只表示数据范围,不等于模块权限;能源/调度/反馈各自另有白名单。
4. 新增一个业务模块的步骤
src/modules/<domain>/:建api.ts(HTTP)、model.ts(纯逻辑)、index.tsx(入口)、components/。src/app/modules.ts:注册导航项,必要时用roles.ts的判断控制可见性。src/server/routes/<domain>.ts(或routes/<domain>/):实现接口;需要模块级权限就加 fail-closed 守卫。- 数据权限:读取后调用
filterByPermission+maskCustomerNames,或写等价的 SQL 过滤。 - 测试:纯逻辑进
model.test.ts;接口口径用固定夹具锁 SQL 与参数(参考server/routes/energy/routes.test.ts)。 - 跑
npm run lint && npm test && npm run build。
5. 已知未尽事项
诚实记录,避免后来者以为已经做完:
- 后端仍有 2 个域没拆出 repository:
energy/mileage的 SQL 仍在各自的路由文件里 (形状见上表)。已完成的有 6 个域,可作为模板:路由只做校验与组装,SQL 进repository.ts, 纯逻辑进model.ts,并用 mock pool 的契约测试锁定 SQL 与参数。 守卫用的是"语句形状"正则(如update <table> set)而不是裸关键字,避免把UpdateNotification或日志里的 "update error" 误判为 SQL;小写 SQL 同样能被抓到。 - 运行时建表已集中到
server/db/schema/,并在DB_READ_ONLY=1时整体跳过(由架构测试守护, 已实测不触碰数据库)。它仍由业务接口在首次调用时触发,而不是只由bootstrap.ts调用—— 要彻底改成显式迁移,需要先建立数据库变更脚本流程,避免"代码里偷偷建表"。 src/lib/cn.ts刻意不引入tailwind-merge:全仓只有 2 处调用,为此新增一个 传递依赖不划算。若将来条件类覆盖变多,再换clsx+tailwind-merge。- 前端
dist体积:氢能看板单个 chunk 约 200 kB(gzip 50 kB),来自原型渲染方式(大量内联样式)。
已收敛的重复实现(不要退回多份)
| 能力 | 唯一实现 | 由测试守护 |
|---|---|---|
| Excel 导出(拼装与写出) | src/shared/xlsx.ts |
架构测试:modules/** 不得再出现 book_new / book_append_sheet / writeFile( |
| 高德 JSAPI 加载与底图 | src/shared/amap.ts |
架构测试:只有它可 import @amap/amap-jsapi-loader |
| 运行时 DDL(建表/改表) | src/server/db/schema/* |
架构测试:其他 server 文件不得出现 CREATE TABLE / ALTER TABLE;每个 schema 模块必须检查 ddlAllowed() |
| 自然日区间(近 N 天/快捷区间) | src/shared/date-range.ts、modules/energy/daily-range/model.ts |
date-range 暂无独立测试;改动请补 |
| 氢能数值格式化 | modules/energy/hydrogen/model/display-format.ts |
display-format.test.ts |
| 角色常量与模块可见性 | src/shared/auth/roles.ts |
app/modules.test.ts |
| 环境变量读取与校验 | src/server/config.ts |
auth/password.test.ts 覆盖密码模式;JWT 缺失拒绝启动已实测 |