From b28ca49491b9412721606acf23d8dcebd2c97d02 Mon Sep 17 00:00:00 2001 From: dsh-agent Date: Fri, 11 Sep 2026 10:20:47 +0800 Subject: [PATCH] =?UTF-8?q?refactor(stage7):=20=E5=90=8E=E7=AB=AF=E7=9B=AE?= =?UTF-8?q?=E5=BD=95=E5=88=86=E5=B1=82=E3=80=81=E6=9E=B6=E6=9E=84=E5=AE=88?= =?UTF-8?q?=E6=8A=A4=E6=B5=8B=E8=AF=95=E4=B8=8E=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 后端目录 - db/:mysql / hydrogen / heatmap 连接与氢能只读 SQL 守卫收拢到一处。 - middleware/:auth(JWT → 注入 user)与 read-only 归位。 - 相关测试随文件移动(middleware/read-only.test.ts、db/hydrogen-read-only.test.ts)。 import 改写由一次性 codemod 完成,未改变任何逻辑。 架构守护 - 新增 src/architecture.test.ts,断言 6 条分层铁律: server 不依赖 modules、前端不依赖 server、shared 为叶子层、 无 vendor 引用、model.ts 不依赖 react、@ts-nocheck 仅限已登记的原型快照。 豁免清单只减不增。 测试基建 - npm test 的 glob 同时匹配 .test.ts 与 .test.tsx(此前 .tsx 测试会被静默漏掉)。 文档 - 新增根 README.md:入口、快速开始、命令、目录、部署注意事项 (JWT_SECRET 必须注入且 >=32 字符,否则拒绝启动)。 - 新增 docs/ARCHITECTURE.md:依赖方向、6 条硬规则、业务域形状、 中间件顺序、新增模块步骤,以及"已知未尽事项"的诚实清单。 lint / test(134) / build 全绿。 --- README.md | 87 ++++++++++++ docs/ARCHITECTURE.md | 127 ++++++++++++++++++ package.json | 2 +- src/architecture.test.ts | 126 +++++++++++++++++ .../station-daily/StationDailyDetailView.tsx | 2 +- src/server/app.ts | 4 +- src/server/auth/login.ts | 2 +- src/server/auth/password.test.ts | 4 +- src/server/{heatmap-db.ts => db/heatmap.ts} | 0 .../{ => db}/hydrogen-read-only.test.ts | 0 src/server/{ => db}/hydrogen-read-only.ts | 0 src/server/{hydrogen-db.ts => db/hydrogen.ts} | 0 src/server/{db.ts => db/mysql.ts} | 0 .../middleware.ts => middleware/auth.ts} | 4 +- .../read-only.test.ts} | 2 +- .../read-only.ts} | 0 src/server/routes/ele/index.ts | 2 +- src/server/routes/ele/migration.ts | 2 +- src/server/routes/energy/electric.ts | 2 +- src/server/routes/energy/etc.ts | 2 +- src/server/routes/energy/hydrogen-bi-v2.ts | 2 +- .../routes/energy/hydrogen-station-board.ts | 2 +- src/server/routes/energy/index.ts | 4 +- src/server/routes/feedback/index.ts | 2 +- src/server/routes/hydrogen-heatmap.ts | 2 +- src/server/routes/mileage/cache.ts | 2 +- .../routes/mileage/daily-report-service.ts | 2 +- .../routes/mileage/daily-report-store.ts | 2 +- src/server/routes/mileage/targets.ts | 2 +- src/server/routes/mileage/trend.ts | 2 +- src/server/routes/mileage/vehicle-info.ts | 2 +- src/server/routes/scheduling/db-schema.ts | 2 +- src/server/routes/scheduling/notify.ts | 2 +- src/server/routes/scheduling/suggestions.ts | 2 +- src/server/routes/vehicle-heatmap.ts | 4 +- src/server/routes/vehicles/repository.ts | 2 +- 36 files changed, 372 insertions(+), 32 deletions(-) create mode 100644 README.md create mode 100644 docs/ARCHITECTURE.md create mode 100644 src/architecture.test.ts rename src/server/{heatmap-db.ts => db/heatmap.ts} (100%) rename src/server/{ => db}/hydrogen-read-only.test.ts (100%) rename src/server/{ => db}/hydrogen-read-only.ts (100%) rename src/server/{hydrogen-db.ts => db/hydrogen.ts} (100%) rename src/server/{db.ts => db/mysql.ts} (100%) rename src/server/{auth/middleware.ts => middleware/auth.ts} (93%) rename src/server/{read-only-middleware.test.ts => middleware/read-only.test.ts} (94%) rename src/server/{read-only-middleware.ts => middleware/read-only.ts} (100%) diff --git a/README.md b/README.md new file mode 100644 index 0000000..c23fbea --- /dev/null +++ b/README.md @@ -0,0 +1,87 @@ +# LN-BI + +羚牛业务数据的统一 BI Web 应用。React 19 + Vite 前端,Hono 后端,单进程同时提供 API 与静态页面。 + +## 入口 + +| 入口 | 模块 | 路由 | +| --- | --- | --- | +| 资产 BI | 资产管理、里程管理、车辆/加氢热力图、智能调度 | `/asset` | +| 能源 BI | 氢费 BI、电能、ETC | `/energy` | +| 隐藏页 | 充电记录导入(需能源角色) | `/ele/import` | +| 隐藏页 | 用户反馈管理 | `/admin/feedback` | +| 独立入口 | 氢能经营看板(与 `/energy` 内同一组件) | `/energy/hydrogen-board` | + +页面内部用 Hash 保存子页面状态,例如 `/energy#hydrogen/overview`。入口兼容逻辑见 `src/app/routing.ts`。 + +## 快速开始 + +需要 Node.js 22 与 npm。 + +```bash +npm ci +cp .env.local.example .env.local # 填入数据库等连接配置,切勿提交 +npm run dev:local # 只读本地预览:前端 127.0.0.1:8115,API 127.0.0.1:3001 +``` + +`dev:local` 会强制 `DB_READ_ONLY=1`、`HYDROGEN_DB_READ_ONLY=1`、`MILEAGE_REPORT_AUTO_ARCHIVE=0`、 +`DEV_BYPASS_AUTH=1`,并且**不启动建表与定时任务**,适合看真实数据而不写库。不要把它暴露到公网。 + +常规开发(前端 3000,后端 3001,`/api` 由 Vite 代理): + +```bash +npm run dev +``` + +## 命令 + +| 命令 | 说明 | +| --- | --- | +| `npm run dev` | 前后端开发模式 | +| `npm run dev:local` | 只读本地预览(免登录,禁写库) | +| `npm run lint` | 类型检查(`tsc --noEmit`) | +| `npm test` | Node 内置测试运行器 | +| `npm run build` | 生成 `dist` | +| `npm start` | 生产式启动(需先 build,且必须提供环境变量) | + +发布门禁是 `lint` + `test` + `build` 三者同时通过(CI 见 `woodpecker.yml`)。 + +## 目录 + +``` +src/ +├── app/ 应用外壳:路由解析、导航注册 +├── auth/ 前端认证状态与注入 +├── components/ 跨模块 UI(Shell、反馈、通用控件) +├── modules/ 前端业务域(每个域自带 api / model / components / css) +│ ├── assets/ 资产管理 +│ ├── mileage/ 里程(monitoring / statistics / daily-report) +│ ├── scheduling/智能调度 +│ ├── energy/ 能源(hydrogen / electric / etc) +│ ├── ele/ 电能数据导入 +│ ├── admin/ 反馈后台 +│ └── *-heatmap/ 两类热力图 +├── shared/ 前后端共享的叶子层(角色常量、跨端 DTO) +└── server/ + ├── config.ts 环境变量集中读取 + 启动期校验 + ├── app.ts 应用装配(无副作用,便于测试) + ├── index.ts 进程启动 + ├── local.ts 只读预览启动 + ├── db/ MySQL / PostgreSQL 连接 + ├── middleware/认证、只读 + ├── auth/ 登录(SSO 换票 / 固定密码)、权限 + └── routes/ 按业务域的 HTTP 路由 +``` + +## 架构约定 + +分层规则、依赖方向与「新增一个模块该放哪」见 [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md)。 +其中 6 条硬规则由 `src/architecture.test.ts` 断言,违反即测试失败。 + +## 部署注意事项 + +- `JWT_SECRET` **必须**在运行环境注入且不少于 32 字符;SSO 模式下缺失时服务会拒绝启动。 + 镜像内不再内置默认密钥(历史默认值等同于公开密钥,可被离线伪造令牌)。 +- 数据库口令、Harbor 凭据、OSS / OneOS / 高德密钥一律通过环境变量或 CI Secret 注入, + 仓库内不保留真实值。历史提交中出现过的凭据都需要轮换。 +- 生产环境请显式设置 `NODE_ENV=production` 与 `DEV_BYPASS_AUTH=0`;后者会绕过全部认证。 diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..86f9edf --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,127 @@ +# 架构与分层约定 + +本文是这个仓库的结构契约。**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// + 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 连接 + middleware/ auth(JWT → 注入 user)、read-only + auth/ 登录换票、固定密码、权限过滤与脱敏 + routes/ 按业务域的 HTTP 路由 +``` + +### 中间件顺序(在 `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. 新增一个业务模块的步骤 + +1. `src/modules//`:建 `api.ts`(HTTP)、`model.ts`(纯逻辑)、`index.tsx`(入口)、`components/`。 +2. `src/app/modules.ts`:注册导航项,必要时用 `roles.ts` 的判断控制可见性。 +3. `src/server/routes/.ts`(或 `routes//`):实现接口;需要模块级权限就加 **fail-closed** 守卫。 +4. 数据权限:读取后调用 `filterByPermission` + `maskCustomerNames`,或写等价的 SQL 过滤。 +5. 测试:纯逻辑进 `model.test.ts`;接口口径用**固定夹具**锁 SQL 与参数(参考 `server/routes/energy/routes.test.ts`)。 +6. 跑 `npm run lint && npm test && npm run build`。 + +## 5. 已知未尽事项 + +诚实记录,避免后来者以为已经做完: + +- **后端路由尚未全部拆成 routes / service / repository**。`routes/vehicles/` 已有 + `repository.ts` + `model.ts` 的雏形,其余域仍是单文件路由;拆分时不要改变 SQL 与参数顺序。 +- **运行时建表仍分散在业务代码里**:`ele`、`feedback`、`mileage` 在首次调用时执行 + `CREATE TABLE`,而 `read-only` 中间件只按 HTTP 方法拦截,所以只读预览下 GET 仍可能触发建表。 + 应收敛到显式的迁移函数并只由 `bootstrap.ts` 调用。 +- **重复实现尚未合并**:Excel 导出有 3 套、高德初始化 2 份、数值格式化多处; + 公共实现 `modules/energy/hydrogen/model/display-format.ts` 目前只被自己的测试使用。 +- **前端 `dist` 体积**:氢能看板单个 chunk 约 200 kB(gzip 50 kB),来自原型渲染方式(大量内联样式)。 diff --git a/package.json b/package.json index 3fe42ff..386f092 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "build": "vite build", "start": "node --import tsx src/server/index.ts", "lint": "tsc --noEmit", - "test": "node --import tsx --test $(find src -type f -name '*.test.ts' -print | sort)", + "test": "node --import tsx --test $(find src -type f \\( -name '*.test.ts' -o -name '*.test.tsx' \\) -print | sort)", "test:mileage-report": "node --import tsx --test src/server/routes/mileage/daily-report-model.test.ts" }, "dependencies": { diff --git a/src/architecture.test.ts b/src/architecture.test.ts new file mode 100644 index 0000000..db0c77a --- /dev/null +++ b/src/architecture.test.ts @@ -0,0 +1,126 @@ +import assert from "node:assert/strict"; +import { readFileSync, readdirSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import path from "node:path"; +import test from "node:test"; + +/** + * 架构守护测试。 + * + * 这些不是代码风格偏好,而是这个仓库赖以保持"能读懂"的结构约束。 + * 一旦被打破(尤其是 client/server 互相引用、shared 反向依赖上层), + * 依赖图会重新变成无法追踪的网,所以用断言固化。 + */ + +const srcDir = path.dirname(fileURLToPath(import.meta.url)); + +function walk(dir: string, out: string[] = []): string[] { + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) walk(full, out); + else if (/\.(ts|tsx|css)$/.test(entry.name)) out.push(full); + } + return out; +} + +const allFiles = walk(srcDir); +const rel = (file: string) => path.relative(srcDir, file); + +/** 取出文件里所有 import 说明符(含副作用 import / 动态 import / @import)。 */ +function specifiers(file: string): string[] { + const source = readFileSync(file, "utf8"); + const found: string[] = []; + const patterns = [ + /(?:^|[^\w$])(?:import|export)\s+[\s\S]*?\sfrom\s*['"]([^'"]+)['"]/g, + /(?:^|[^\w$])import\s*['"]([^'"]+)['"]/g, + /(?:^|[^\w$])import\s*\(\s*['"]([^'"]+)['"]\s*\)/g, + /@import\s+['"]([^'"]+)['"]/g, + ]; + for (const re of patterns) { + for (const match of source.matchAll(re)) found.push(match[1]); + } + return found; +} + +/** 把相对 import 解析为 src 下的路径(用于判断它指向哪个分区)。 */ +function targetPath(file: string, spec: string): string | null { + if (!spec.startsWith(".")) return null; + return path.relative(srcDir, path.resolve(path.dirname(file), spec)); +} + +function filesUnder(prefix: string): string[] { + return allFiles.filter((f) => rel(f).startsWith(prefix)); +} + +function fileContains(file: string, re: RegExp): boolean { + return re.test(readFileSync(file, "utf8")); +} + +test("分层铁律:server 不得依赖前端 modules", () => { + const violations: string[] = []; + for (const file of filesUnder("server/")) { + for (const spec of specifiers(file)) { + const target = targetPath(file, spec); + if (target && target.startsWith("modules/")) violations.push(`${rel(file)} -> ${spec}`); + } + } + assert.deepEqual(violations, [], "server 必须独立于前端:请把共享内容下沉到 src/shared"); +}); + +test("分层铁律:前端不得依赖 server", () => { + const violations: string[] = []; + const clientDirs = ["modules/", "components/", "auth/", "app/"]; + for (const dir of clientDirs) { + for (const file of filesUnder(dir)) { + for (const spec of specifiers(file)) { + const target = targetPath(file, spec); + if (target && target.startsWith("server/")) violations.push(`${rel(file)} -> ${spec}`); + } + } + } + assert.deepEqual(violations, [], "前端不得直接引用服务端模块"); +}); + +test("分层铁律:shared 是最底层,不得反向依赖上层", () => { + const violations: string[] = []; + for (const file of filesUnder("shared/")) { + for (const spec of specifiers(file)) { + const target = targetPath(file, spec); + if (target && (target.startsWith("modules/") || target.startsWith("server/") || target.startsWith("components/"))) { + violations.push(`${rel(file)} -> ${spec}`); + } + } + } + assert.deepEqual(violations, [], "src/shared 必须保持为可被前后端共同依赖的叶子层"); +}); + +test("原型代码不再散落在 vendor 目录", () => { + const violations: string[] = []; + for (const file of allFiles) { + for (const spec of specifiers(file)) { + if (spec.includes("vendor/")) violations.push(`${rel(file)} -> ${spec}`); + } + } + assert.deepEqual(violations, [], "生产代码应位于 src/modules 下的业务域目录,而不是 src/vendor"); +}); + +test("纯模型文件不得依赖 React(保证可被 node:test 直接测)", () => { + const violations: string[] = []; + for (const file of allFiles.filter((f) => /(^|\/)model\.tsx?$/.test(rel(f)))) { + for (const spec of specifiers(file)) { + if (spec === "react" || spec.startsWith("react/")) violations.push(`${rel(file)} -> ${spec}`); + } + } + assert.deepEqual(violations, [], "model.ts 应只包含可在 Node 中直接执行的纯函数"); +}); + +test("类型豁免清单只减不增:@ts-nocheck 仅限已登记的 8113 原型快照", () => { + // 这三个文件是逐字节保留的验收原型(UI/CSS 冻结),显式登记为唯一豁免。 + const allowed = [ + "modules/energy/hydrogen/board/EnergyBiBoardApp.tsx", + "modules/energy/hydrogen/station-daily/StationDailyApp.tsx", + "modules/energy/hydrogen/station-daily/StationDailyDetailView.tsx", + ]; + const actual = allFiles.filter((f) => fileContains(f, /^\/\/\s*@ts-nocheck/m)).map(rel).sort(); + assert.deepEqual(actual, [...allowed].sort(), "新增 @ts-nocheck 会让类型门禁失效,请改为修正类型"); +}); diff --git a/src/modules/energy/hydrogen/station-daily/StationDailyDetailView.tsx b/src/modules/energy/hydrogen/station-daily/StationDailyDetailView.tsx index 0354e75..a7772d9 100644 --- a/src/modules/energy/hydrogen/station-daily/StationDailyDetailView.tsx +++ b/src/modules/energy/hydrogen/station-daily/StationDailyDetailView.tsx @@ -10,7 +10,7 @@ import { MobileListFullscreenButton } from '../common/MobileListFullscreenButton import { SPOT_PAY_METHOD_LABEL, type StationCashIntakeDay, -} from '../common/energy-spot-cash-intake'; +} from '../common/energy-spot-cash-intake/index'; import { fetchHydrogenStationBoard } from '../../api'; import { fetchAllH2BiDrillRecords } from '../api'; import { PrototypeDrillModal } from '../drill/prototype-real-drills'; diff --git a/src/server/app.ts b/src/server/app.ts index ba4b554..84f447b 100644 --- a/src/server/app.ts +++ b/src/server/app.ts @@ -1,9 +1,9 @@ import { serveStatic } from '@hono/node-server/serve-static'; import { Hono } from 'hono'; -import { readOnlyMiddleware } from './read-only-middleware.js'; +import { readOnlyMiddleware } from './middleware/read-only.js'; import { cors } from 'hono/cors'; import authRouter from './auth/login.js'; -import { authMiddleware } from './auth/middleware.js'; +import { authMiddleware } from './middleware/auth.js'; import eleRouter from './routes/ele/index.js'; import energyRouter from './routes/energy/index.js'; import feedbackRouter from './routes/feedback/index.js'; diff --git a/src/server/auth/login.ts b/src/server/auth/login.ts index cba7bce..5856112 100644 --- a/src/server/auth/login.ts +++ b/src/server/auth/login.ts @@ -1,6 +1,6 @@ import { Hono } from 'hono'; import jwt from 'jsonwebtoken'; -import pool from '../db.js'; +import pool from '../db/mysql.js'; import type { AuthUser, JwtPayload, PermissionLevel } from './types.js'; import { FULL_ACCESS_ROLES, DEPT_ACCESS_ROLES } from './types.js'; import { authMode, verifyAuthToken } from './config.js'; diff --git a/src/server/auth/password.test.ts b/src/server/auth/password.test.ts index 0d4b2e1..4979623 100644 --- a/src/server/auth/password.test.ts +++ b/src/server/auth/password.test.ts @@ -3,8 +3,8 @@ import test from 'node:test'; import { Hono } from 'hono'; import jwt from 'jsonwebtoken'; import { passwordRouter } from './password.js'; -import { authMiddleware } from './middleware.js'; -import { readOnlyMiddleware } from '../read-only-middleware.js'; +import { authMiddleware } from '../middleware/auth.js'; +import { readOnlyMiddleware } from '../middleware/read-only.js'; import { verifyAuthToken } from './config.js'; test('固定密码模式默认关闭、失败限流、只读权限及换密失效', async () => { diff --git a/src/server/heatmap-db.ts b/src/server/db/heatmap.ts similarity index 100% rename from src/server/heatmap-db.ts rename to src/server/db/heatmap.ts diff --git a/src/server/hydrogen-read-only.test.ts b/src/server/db/hydrogen-read-only.test.ts similarity index 100% rename from src/server/hydrogen-read-only.test.ts rename to src/server/db/hydrogen-read-only.test.ts diff --git a/src/server/hydrogen-read-only.ts b/src/server/db/hydrogen-read-only.ts similarity index 100% rename from src/server/hydrogen-read-only.ts rename to src/server/db/hydrogen-read-only.ts diff --git a/src/server/hydrogen-db.ts b/src/server/db/hydrogen.ts similarity index 100% rename from src/server/hydrogen-db.ts rename to src/server/db/hydrogen.ts diff --git a/src/server/db.ts b/src/server/db/mysql.ts similarity index 100% rename from src/server/db.ts rename to src/server/db/mysql.ts diff --git a/src/server/auth/middleware.ts b/src/server/middleware/auth.ts similarity index 93% rename from src/server/auth/middleware.ts rename to src/server/middleware/auth.ts index d1fb3a8..ff3e6e4 100644 --- a/src/server/auth/middleware.ts +++ b/src/server/middleware/auth.ts @@ -1,6 +1,6 @@ import type { Context, Next } from 'hono'; -import type { AuthUser } from './types.js'; -import { authMode, verifyAuthToken } from './config.js'; +import type { AuthUser } from '../auth/types.js'; +import { authMode, verifyAuthToken } from '../auth/config.js'; // 临时:跳过所有认证(保留完整逻辑便于快速恢复) diff --git a/src/server/read-only-middleware.test.ts b/src/server/middleware/read-only.test.ts similarity index 94% rename from src/server/read-only-middleware.test.ts rename to src/server/middleware/read-only.test.ts index b5a7295..4253ad0 100644 --- a/src/server/read-only-middleware.test.ts +++ b/src/server/middleware/read-only.test.ts @@ -1,7 +1,7 @@ import assert from 'node:assert/strict'; import test from 'node:test'; import { Hono } from 'hono'; -import { readOnlyMiddleware } from './read-only-middleware.js'; +import { readOnlyMiddleware } from './read-only.js'; test('read-only preview blocks write handlers but permits reads; normal mode is unchanged', async () => { const previous = process.env.DB_READ_ONLY; diff --git a/src/server/read-only-middleware.ts b/src/server/middleware/read-only.ts similarity index 100% rename from src/server/read-only-middleware.ts rename to src/server/middleware/read-only.ts diff --git a/src/server/routes/ele/index.ts b/src/server/routes/ele/index.ts index dd8bbbb..535613d 100644 --- a/src/server/routes/ele/index.ts +++ b/src/server/routes/ele/index.ts @@ -1,7 +1,7 @@ import { Hono } from 'hono'; import type { RowDataPacket, ResultSetHeader } from 'mysql2'; import * as XLSX from 'xlsx'; -import pool from '../../db.js'; +import pool from '../../db/mysql.js'; import type { AuthUser } from '../../auth/types.js'; import { canAccessEnergy } from '../../auth/types.js'; import { ensureChargeRecordTable } from './migration.js'; diff --git a/src/server/routes/ele/migration.ts b/src/server/routes/ele/migration.ts index 6e2c6bb..37f5667 100644 --- a/src/server/routes/ele/migration.ts +++ b/src/server/routes/ele/migration.ts @@ -1,4 +1,4 @@ -import pool from '../../db.js'; +import pool from '../../db/mysql.js'; const CREATE_TABLE_SQL = ` CREATE TABLE IF NOT EXISTS bi_ele_charge_record ( diff --git a/src/server/routes/energy/electric.ts b/src/server/routes/energy/electric.ts index a937c1e..9c5f268 100644 --- a/src/server/routes/energy/electric.ts +++ b/src/server/routes/energy/electric.ts @@ -1,6 +1,6 @@ import type { Hono } from 'hono'; import type { RowDataPacket } from 'mysql2'; -import type pool from '../../db.js'; +import type pool from '../../db/mysql.js'; import type { cached } from './cache.js'; import { dateRangeClause, diff --git a/src/server/routes/energy/etc.ts b/src/server/routes/energy/etc.ts index 6ea4ae3..36e2295 100644 --- a/src/server/routes/energy/etc.ts +++ b/src/server/routes/energy/etc.ts @@ -1,6 +1,6 @@ import type { Hono } from 'hono'; import type { RowDataPacket } from 'mysql2'; -import type pool from '../../db.js'; +import type pool from '../../db/mysql.js'; import type { cached } from './cache.js'; export interface EtcOverviewDependencies { diff --git a/src/server/routes/energy/hydrogen-bi-v2.ts b/src/server/routes/energy/hydrogen-bi-v2.ts index ff55453..cc6c071 100644 --- a/src/server/routes/energy/hydrogen-bi-v2.ts +++ b/src/server/routes/energy/hydrogen-bi-v2.ts @@ -1,6 +1,6 @@ import type { Hono } from "hono"; import type { RowDataPacket } from "mysql2"; -import type hydrogenPool from "../../hydrogen-db.js"; +import type hydrogenPool from "../../db/hydrogen.js"; import { HYDROGEN_FUEL_ONLY_WHERE, HYDROGEN_FUEL_ONLY_WHERE_B, diff --git a/src/server/routes/energy/hydrogen-station-board.ts b/src/server/routes/energy/hydrogen-station-board.ts index 727762f..aa73f0d 100644 --- a/src/server/routes/energy/hydrogen-station-board.ts +++ b/src/server/routes/energy/hydrogen-station-board.ts @@ -1,6 +1,6 @@ import type { Hono } from 'hono'; import type { RowDataPacket } from 'mysql2'; -import type hydrogenPool from '../../hydrogen-db.js'; +import type hydrogenPool from '../../db/hydrogen.js'; import type { cached } from './cache.js'; import { HYDROGEN_BASE_WHERE, HYDROGEN_BASE_WHERE_B, HYDROGEN_LOCAL, HYDROGEN_TABLE } from './constants.js'; import { dateRangeClause, enumerateDateRange, resolveDateRange } from './query-model.js'; diff --git a/src/server/routes/energy/index.ts b/src/server/routes/energy/index.ts index eabe40a..ec57f17 100644 --- a/src/server/routes/energy/index.ts +++ b/src/server/routes/energy/index.ts @@ -1,8 +1,8 @@ import { Hono } from 'hono'; import type { AuthUser } from '../../auth/types.js'; import { canAccessEnergy } from '../../auth/types.js'; -import pool from '../../db.js'; -import hydrogenPool from '../../hydrogen-db.js'; +import pool from '../../db/mysql.js'; +import hydrogenPool from '../../db/hydrogen.js'; import { cached } from './cache.js'; import { registerElectricMonthlyRoute, diff --git a/src/server/routes/feedback/index.ts b/src/server/routes/feedback/index.ts index fb779a6..a7e35fe 100644 --- a/src/server/routes/feedback/index.ts +++ b/src/server/routes/feedback/index.ts @@ -1,6 +1,6 @@ import { Hono } from 'hono'; import type { ResultSetHeader, RowDataPacket } from 'mysql2'; -import pool from '../../db.js'; +import pool from '../../db/mysql.js'; import type { AuthUser } from '../../auth/types.js'; import { canManageFeedback } from '../../auth/types.js'; import { uploadFeedbackImage } from './oss.js'; diff --git a/src/server/routes/hydrogen-heatmap.ts b/src/server/routes/hydrogen-heatmap.ts index 8b3d751..029867d 100644 --- a/src/server/routes/hydrogen-heatmap.ts +++ b/src/server/routes/hydrogen-heatmap.ts @@ -1,6 +1,6 @@ import { Hono } from 'hono'; import type { RowDataPacket } from 'mysql2'; -import pool from '../db.js'; +import pool from '../db/mysql.js'; import type { AuthUser } from '../auth/types.js'; import { canAccessEnergy } from '../auth/types.js'; import { diff --git a/src/server/routes/mileage/cache.ts b/src/server/routes/mileage/cache.ts index fe3315c..6f2e054 100644 --- a/src/server/routes/mileage/cache.ts +++ b/src/server/routes/mileage/cache.ts @@ -1,7 +1,7 @@ import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname, join } from 'node:path'; -import pool from '../../db.js'; +import pool from '../../db/mysql.js'; import { fetchVehicleInfoMap } from './vehicle-info.js'; import { fetchOneOsDailyMileage, fetchOneOsMileageDates } from './oneos-api.js'; import { diff --git a/src/server/routes/mileage/daily-report-service.ts b/src/server/routes/mileage/daily-report-service.ts index 7f302bf..70a485b 100644 --- a/src/server/routes/mileage/daily-report-service.ts +++ b/src/server/routes/mileage/daily-report-service.ts @@ -1,5 +1,5 @@ import type { RowDataPacket } from 'mysql2'; -import pool from '../../db.js'; +import pool from '../../db/mysql.js'; import type { DailyMileageReport, MileageReportBreakdown, diff --git a/src/server/routes/mileage/daily-report-store.ts b/src/server/routes/mileage/daily-report-store.ts index ab54637..e0d3310 100644 --- a/src/server/routes/mileage/daily-report-store.ts +++ b/src/server/routes/mileage/daily-report-store.ts @@ -1,5 +1,5 @@ import type { ResultSetHeader, RowDataPacket } from 'mysql2'; -import pool from '../../db.js'; +import pool from '../../db/mysql.js'; import type { DailyMileageReport, MileageReportHistoryItem, diff --git a/src/server/routes/mileage/targets.ts b/src/server/routes/mileage/targets.ts index 3fcd94b..a1aadb1 100644 --- a/src/server/routes/mileage/targets.ts +++ b/src/server/routes/mileage/targets.ts @@ -1,5 +1,5 @@ import { Hono } from 'hono'; -import pool from '../../db.js'; +import pool from '../../db/mysql.js'; import { getCache } from './cache.js'; import { fetchOneOsDailyMileage } from './oneos-api.js'; import { fetchVehicleInfoByPlates } from './vehicle-info.js'; diff --git a/src/server/routes/mileage/trend.ts b/src/server/routes/mileage/trend.ts index 22f22f9..9e74aec 100644 --- a/src/server/routes/mileage/trend.ts +++ b/src/server/routes/mileage/trend.ts @@ -1,5 +1,5 @@ import { Hono } from 'hono'; -import pool from '../../db.js'; +import pool from '../../db/mysql.js'; import { fetchOneOsMileageDates } from './oneos-api.js'; const app = new Hono(); diff --git a/src/server/routes/mileage/vehicle-info.ts b/src/server/routes/mileage/vehicle-info.ts index 64fa258..458d94c 100644 --- a/src/server/routes/mileage/vehicle-info.ts +++ b/src/server/routes/mileage/vehicle-info.ts @@ -1,4 +1,4 @@ -import pool from '../../db.js'; +import pool from '../../db/mysql.js'; import type { VehicleInfoRow } from './types.js'; /** 车辆关联信息 SQL(客户名、部门、经理、租赁状态、主体、项目、品牌) */ diff --git a/src/server/routes/scheduling/db-schema.ts b/src/server/routes/scheduling/db-schema.ts index 0e046e8..a4dc641 100644 --- a/src/server/routes/scheduling/db-schema.ts +++ b/src/server/routes/scheduling/db-schema.ts @@ -1,4 +1,4 @@ -import pool from '../../db.js'; +import pool from '../../db/mysql.js'; const CREATE_NOTIFICATIONS_TABLE = ` CREATE TABLE IF NOT EXISTS tab_scheduling_notifications ( diff --git a/src/server/routes/scheduling/notify.ts b/src/server/routes/scheduling/notify.ts index 1a545dd..6092c4c 100644 --- a/src/server/routes/scheduling/notify.ts +++ b/src/server/routes/scheduling/notify.ts @@ -1,5 +1,5 @@ import { Hono } from 'hono'; -import pool from '../../db.js'; +import pool from '../../db/mysql.js'; import type { AuthUser } from '../../auth/types.js'; import type { NotifyRequest, diff --git a/src/server/routes/scheduling/suggestions.ts b/src/server/routes/scheduling/suggestions.ts index 86bab07..28c1d16 100644 --- a/src/server/routes/scheduling/suggestions.ts +++ b/src/server/routes/scheduling/suggestions.ts @@ -1,5 +1,5 @@ import { Hono } from 'hono'; -import pool from '../../db.js'; +import pool from '../../db/mysql.js'; import { fetchVehicleInfoMap } from '../mileage/vehicle-info.js'; import { fetchOneOsMileageDates } from '../mileage/oneos-api.js'; import { mapRegion } from '../vehicles.js'; diff --git a/src/server/routes/vehicle-heatmap.ts b/src/server/routes/vehicle-heatmap.ts index 23ea794..e838ffc 100644 --- a/src/server/routes/vehicle-heatmap.ts +++ b/src/server/routes/vehicle-heatmap.ts @@ -1,6 +1,6 @@ import { Hono } from 'hono'; -import pool from '../db.js'; -import heatmapPool from '../heatmap-db.js'; +import pool from '../db/mysql.js'; +import heatmapPool from '../db/heatmap.js'; import { getCache } from './mileage/cache.js'; import { buildVehicleGrid, diff --git a/src/server/routes/vehicles/repository.ts b/src/server/routes/vehicles/repository.ts index 1c95284..6dd18ef 100644 --- a/src/server/routes/vehicles/repository.ts +++ b/src/server/routes/vehicles/repository.ts @@ -1,4 +1,4 @@ -import pool from '../../db.js'; +import pool from '../../db/mysql.js'; import type { Vehicle, VehicleRow } from '../../types.js'; import { transformRow } from './model.js'; import type { WeeklyTruckIds } from './types.js';