Compare commits

...

13 Commits

Author SHA1 Message Date
SpecialX
313bae87bf docs(spec): add enterprise microservices architecture upgrade design
Some checks failed
Lighthouse CI / lighthouse (push) Has been cancelled
Security / deep-security-scan (push) Failing after 1m32s
DR Drill / dr-drill (push) Failing after 39s
CI / build-deploy (push) Waiting to run
CI / security-scan (push) Blocked by required conditions
CI / scheduled-backup (push) Failing after 35s
CI / backup-verify (push) Has been skipped
CI / weekly-dr-drill (push) Failing after 0s
2026-07-07 21:34:15 +08:00
SpecialX
e3d132dc1b docs(known-issues): log enterprise architecture normalization work experience
Some checks failed
CI / scheduled-backup (push) Failing after 1m32s
CI / backup-verify (push) Has been skipped
CI / weekly-dr-drill (push) Failing after 0s
CI / build-deploy (push) Has been cancelled
CI / security-scan (push) Has been cancelled
Lighthouse CI / lighthouse (push) Has been cancelled
2026-07-07 20:25:33 +08:00
SpecialX
dbb124bf17 feat: add CI unit test, /api/health, Dockerfile HEALTHCHECK, bundle-analyzer 2026-07-07 20:23:03 +08:00
SpecialX
415dde9122 docs(roadmap): populate tech-debt.md with identified and resolved items 2026-07-07 20:18:07 +08:00
SpecialX
072c0d52b9 chore: add husky + lint-staged + commitlint for commit quality control 2026-07-07 20:17:15 +08:00
SpecialX
fb619139e5 docs: add enterprise repository files (LICENSE/CHANGELOG/CONTRIBUTING/SECURITY) 2026-07-07 20:14:56 +08:00
SpecialX
031e8a8175 refactor: split 5 oversized files into domain-specific subfiles (0 violations) 2026-07-07 20:13:22 +08:00
SpecialX
7f26bb8f9c fix(permissions): fix 9 Server Action permission violations and add recursive CTE for indirect call detection 2026-07-07 19:30:49 +08:00
SpecialX
164dcd4c84 feat(arch-scan): mark 12 exempt Server Actions with @public JSDoc tag 2026-07-07 19:24:26 +08:00
SpecialX
692e8ef580 feat(arch-scan): add @public JSDoc tag exemption mechanism for Server Actions 2026-07-07 19:19:01 +08:00
SpecialX
747344bfe3 chore: snapshot before P0 security phase (backup point) 2026-07-07 19:12:33 +08:00
SpecialX
3f68f3eb09 docs(plan): add enterprise architecture normalization implementation plan 2026-07-07 19:12:08 +08:00
SpecialX
205b463900 docs(architecture): add enterprise architecture normalization design spec 2026-07-07 19:09:40 +08:00
209 changed files with 30341 additions and 12741 deletions

View File

@@ -67,6 +67,14 @@ jobs:
- name: Typecheck - name: Typecheck
run: npm run typecheck run: npm run typecheck
- name: Unit tests
run: npm run test:unit
- name: Architecture scan
run: |
npm run arch:scan
npm run arch:query -- violations || true
- name: Install Playwright Chromium - name: Install Playwright Chromium
run: npx playwright install chromium run: npx playwright install chromium

1
.husky/commit-msg Normal file
View File

@@ -0,0 +1 @@
npx --no-install commitlint --edit $1

1
.husky/pre-commit Normal file
View File

@@ -0,0 +1 @@
npx lint-staged

View File

@@ -4,19 +4,23 @@
**任何任务开始前,必须先查阅架构影响地图,通过图定位代码和模块。** **任何任务开始前,必须先查阅架构影响地图,通过图定位代码和模块。**
1. **先图后码**:执行任何分析、修改、搜索任务时,首先阅读 `docs/architecture/004_architecture_impact_map.md` `docs/architecture/005_architecture_data.json`,从图中定位目标模块、函数、依赖关系,再按图索骥读取源码 1. **先图后码**:执行任何分析、修改、搜索任务时,首先运行 `npm run arch:scan` 更新 arch.db再通过 `npm run arch:query` 查询目标模块、函数、依赖关系,结合阅读 `docs/architecture/004_architecture_impact_map.md` 定位架构设计意图,最后按图索骥读取源码
2. **图未覆盖则先补图**:如果发现项目中存在架构图未记录的模块、函数、表、路由等,**必须优先完善架构图信息**,然后再继续后续工作 2. **图未覆盖则先补图**:如果发现项目中存在 arch.db 未记录的模块、函数、表、路由等,**必须先运行 `npm run arch:scan` 重新扫描**,然后检查 004 是否需要补充
3. **改码必同步图**:对源码的任何修改完成后,必须同步更新 004 和 005 两个架构文档 3. **改码必同步图**:对源码的任何修改完成后,必须运行 `npm run arch:scan` 更新 arch.db若架构设计意图有变化同步更新 004
### 架构文档清单 ### 架构文档清单
| 文档 | 用途 | | 文档 | 用途 |
|------|------| |------|------|
| `docs/architecture/004_architecture_impact_map.md` | 人类可读的架构影响地图 | | `docs/architecture/004_architecture_impact_map.md` | 架构设计意图唯一源(人类可读) |
| `docs/architecture/005_architecture_data.json` | AI 友好格式的结构化数据 |
| `docs/architecture/006_k12_feature_checklist.md` | 标准功能模块清单 | | `docs/architecture/006_k12_feature_checklist.md` | 标准功能模块清单 |
| `docs/architecture/007_gap_audit_report.md` | 差距审计报告 | | `docs/architecture/007_gap_audit_report.md` | 差距审计报告 |
| `docs/architecture/audit/01_decoupling_roadmap.md` | 解耦路线图 | | `docs/architecture/008_module_role_mapping.md` | 模块角色映射 |
| `docs/architecture/roadmap/` | 长远规划tech-debt/decoupling/pending-features |
| `docs/architecture/audit/` | 架构审查报告与归档(含已废弃的 005 JSON、004 V1 |
| `docs/troubleshooting/known-issues.md` | 已知问题速查(场景→技术映射 + 工作经验日志) |
> 005_architecture_data.json 已废弃归档至 `audit/archive/`,结构化数据查询统一通过 arch.db
### 需要同步图的场景 ### 需要同步图的场景
@@ -30,9 +34,23 @@
### 同步方式 ### 同步方式
- 修改 Markdown 文档中对应的模块章节 - 修改源码后运行 `npm run arch:scan` 更新 arch.db强制
- 修改 JSON 文档中对应的节点(`modules.*.exports``permissions``dependencyMatrix``routes``dbTables` 等) - 若架构设计意图变化,同步更新 `docs/architecture/004_architecture_impact_map.md`
- 确保两个文档内容一致 - 若发现新的"场景→技术"映射或工作经验,更新 `docs/troubleshooting/known-issues.md`
## 架构元数据库规则arch.db
**arch.db 是代码结构唯一源AI 工作前必须运行 `npm run arch:scan` 更新。**
1. **arch.db 取代 005 JSON**:模块、函数、调用关系、依赖关系、技术标签查询 arch.db不手动维护结构化数据文件
2. **查询命令**
- `npm run arch:query -- sql "<SQL>"` 自定义 SQL 查询
- `npm run arch:query -- module-deps` 查模块依赖
- `npm run arch:query -- module-reverse-deps <module>` 查反向依赖
- `npm run arch:query -- symbol-refs <symbol>` 查符号引用链
- `npm run arch:query -- tech-usage <tag>` 查技术使用
- `npm run arch:query -- violations` 查架构违规
3. **arch.db 不替代 004**arch.db 是"代码现状"004 是"设计意图",两者互补
## 编码规范 ## 编码规范
@@ -127,7 +145,7 @@ src/modules/[module]/
- Layer 2 Semantic`semantic-light.css` + `semantic-dark.css`:语义令牌,业务代码唯一引用入口 - Layer 2 Semantic`semantic-light.css` + `semantic-dark.css`:语义令牌,业务代码唯一引用入口
- 模块命名空间(`lesson-preparation.css`:`--lp-*` 令牌,明暗双份 - 模块命名空间(`lesson-preparation.css`:`--lp-*` 令牌,明暗双份
- Tailwind 暴露(`tailwind-theme.css`:`@theme inline` 将 Semantic 令牌暴露为 `bg-*`/`text-*`/`font-*` - Tailwind 暴露(`tailwind-theme.css`:`@theme inline` 将 Semantic 令牌暴露为 `bg-*`/`text-*`/`font-*`
- **改令牌必同步图**: 修改令牌定义后,同步更新 `docs/architecture/004``005` - **改令牌必同步图**: 修改令牌定义后,同步更新 `docs/architecture/004_architecture_impact_map.md` 与 arch.db`npm run arch:scan`
- **ESLint 强制约束**: - **ESLint 强制约束**:
- `no-restricted-syntax`: 禁止 `#hex` 字面量 - `no-restricted-syntax`: 禁止 `#hex` 字面量
- `design-tokens/no-hardcoded-fonts`: 禁止 `'Inter'`/`'Fraunces'`/`'JetBrains Mono'` 字面量(单词边界匹配,不影响 `Interval`/`Interactive` 等标识符) - `design-tokens/no-hardcoded-fonts`: 禁止 `'Inter'`/`'Fraunces'`/`'JetBrains Mono'` 字面量(单词边界匹配,不影响 `Interval`/`Interactive` 等标识符)
@@ -148,32 +166,61 @@ src/modules/[module]/
## 问题记录规则 ## 问题记录规则
**所有工作完成后,必须将遇到的问题记录到 `docs/troubleshooting/known-issues.md`(速查手册格式)。** **所有工作完成后,必须将遇到的问题记录到 `docs/troubleshooting/known-issues.md`索引式速查手册)。**
### 必须记录的场景 ### 必须记录的场景
| 场景 | 记录要求 | | 场景 | 记录要求 |
|------|---------| |------|---------|
| 构建报错dev/build/lint/tsc | 记录错误现象 + 正确写法 | | 构建报错dev/build/lint/tsc | 记录到"全局经验"对应主题分区 |
| 运行时异常(白屏/API 报错/数据加载失败) | 记录错误现象 + 正确写法 | | 运行时异常(白屏/API 报错/数据加载失败) | 记录到"模块经验"对应模块分区 |
| 框架/库版本兼容问题 | 记录错误现象 + 正确写法 | | 框架/库版本兼容问题 | 记录到"全局经验: Next.js 配置与运行时" |
| 依赖配置问题serverExternalPackages/webpackIgnore 等) | 记录错误现象 + 正确写法 | | 依赖配置问题serverExternalPackages/webpackIgnore 等) | 记录到"全局经验: Next.js 配置与运行时" |
| 架构约束违规 | 记录错误现象 + 正确写法 | | 架构约束违规 | 记录到"全局经验"对应主题分区 |
### 记录格式 ### 记录格式
以**规则表**形式记录,指明正确做法,无需详细解释原因 索引式表格,指明"场景→技术/规则"映射,不写多行代码示例
```markdown ```markdown
## 问题分类标题 ### X.X 主题分区
| 规则 | 正确写法 | 错误写法 | | 场景 | 技术/规则 |
|------|---------|---------| |------|----------|
| 简述规则 | 代码示例 | 代码示例 | | 简述场景 | 正确做法(一句话) |
``` ```
### 记录要求 ### 记录要求
- **速查手册风格**:只指明方向,不做新手指导 - **索引式**:场景→技术/规则映射,不写代码示例和错误示范列
- **去重**:同类问题在原条目补充,不重复创建 - **去重**:同类问题在原条目补充,不重复创建
- **可操作**:正确写法需具体到代码示例 - **引用架构规则**:架构分层、模块结构等规则引用 004 和 project_rules不重复
- **工作经验日志**:在"工作经验日志"区按时间倒序追加50 条上限),记录"做了什么/学到什么/下次注意"
## AI 工作强制流程
**所有 AI 工作必须遵循此流程,违反即违规。**
### 阶段 1: 上下文加载
1. `npm run arch:scan` 更新 arch.db
2. `npm run arch:query -- module-deps` 查目标模块依赖
3. `npm run arch:query -- symbol-refs <目标函数>` 查调用链
4. 阅读 `src/modules/[模块]/README.md` 读模块工作流程
5.`docs/troubleshooting/known-issues.md` "模块经验" 分区读相关经验
### 阶段 2: 执行工作
1. 按规划执行
2. 修改代码后立即运行 `npm run arch:scan` 更新 arch.db
3. 运行 `npx tsc --noEmit``npm run lint` 确保零错误
### 阶段 3: 经验沉淀(强制,不可跳过)
1.`docs/troubleshooting/known-issues.md` "工作经验日志" 区追加一条记录:
- 日期 + 时间
- 模块
- 做了什么 + 学到什么
2. 若发现新的"场景→技术"映射 → 提炼到对应模块分区
3. 若发现新的架构决策 → 更新 004
4. 若代码结构变化 → `npm run arch:scan` 确认 arch.db 已更新

43
CHANGELOG.md Normal file
View File

@@ -0,0 +1,43 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- arch:scan @public JSDoc 标记豁免机制,支持登录前/公开/内部工具 Server Action 豁免权限校验
- arch:scan 递归 CTE 违规检测,识别通过辅助函数间接调用 requirePermission 的调用链
- 大仓工程基建LICENSE、CONTRIBUTING、SECURITY、.env.example 文档
- husky + lint-staged + commitlint 本地提交规范工具链
- /api/health 健康检查端点 + Dockerfile HEALTHCHECK
- @next/bundle-analyzer 构建体积分析工具
- CI 流水线新增 Unit test + coverage 阶段
- tsconfig 开启 noUncheckedIndexedAccess 严格模式
### Changed
- 重构 004 架构文档为完整架构设计文档912 行14 章节13 个 mermaid 图)
- 重写 35 个模块 README统一 8 章节模板(架构图/流程图/技术栈)
- 拆分 5 个超长文件schema.ts (2245→29+27子文件)、invalidation-map.ts (1195→50+6子文件)、messaging/actions.ts (973→47+5子文件)、textbooks/data-access.ts (907→15+6子文件)、questions/data-access.ts (828→48+4子文件)
- 精简 known-issues.md 为索引式速查手册(场景→技术/规则映射)
### Fixed
- 修复 20 个 Server Action 权限违规12 个 @public 豁免 + 8 个真违规修复)
- 修复 ai 模块 6 个 Action 权限误报requireAiPermission 间接调用链识别)
- 修复 parent 模块 6 个 Action 权限缺失requireAuth → requirePermission
- 修复 settings 模块 updateProfileAction 权限校验(显式 requirePermission
## [0.1.0] - 2026-06-01
### Added
- 初始版本发布
- K12 智慧教学平台核心功能:备课、作业、考试、成绩、考勤、消息、家校互动
- 严格三层架构app → modules → shared
- 5 层状态管理模型URL(nuqs) · Server(TanStack Query) · Client(Zustand) · Global UI · Form
- 权限 3 道防线proxy.ts → requirePermission → usePermission
- 设计令牌双层架构Primitive + Semantic
- arch.db 架构元数据库12 张表 + 7 个索引)
- cacheFn 请求级缓存层
- Gitea Actions CI/CD 流水线

129
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,129 @@
# 贡献指南
感谢参与本项目!请遵循以下规范提交贡献。
## 开发环境准备
```bash
# 1. 安装依赖
npm install
# 2. 准备环境变量
cp .env.example .env
# 编辑 .env 填入实际配置
# 3. 初始化数据库
npm run db:push
# 4. 启动开发服务器
npm run dev
```
## 强制工作流程
**所有代码改动前必须先查阅架构文档:**
1. 阅读 `docs/architecture/004_architecture_impact_map.md` 了解架构设计意图
2. 运行 `npm run arch:scan` 更新 arch.db
3. 运行 `npm run arch:query -- module-deps` 查目标模块依赖
4. 阅读 `src/modules/[模块]/README.md` 了解模块工作流程
5.`docs/troubleshooting/known-issues.md` 读相关经验
**代码改动后必须:**
1. 运行 `npx tsc --noEmit` 确保零错误
2. 运行 `npm run lint` 确保零错误
3. 运行 `npm run arch:scan` 更新 arch.db
4. 若架构设计意图变化,同步更新 004 文档
5. 若发现新场景→技术映射,更新 known-issues.md
## 提交规范
### Conventional Commits 格式
```
<type>(<scope>): <description>
[optional body]
[optional footer]
```
**类型type**
- `feat`: 新功能
- `fix`: Bug 修复
- `docs`: 文档变更
- `style`: 代码格式(不影响功能)
- `refactor`: 重构(既不是新功能也不是修复)
- `test`: 测试相关
- `chore`: 构建/工具/依赖变更
- `perf`: 性能优化
- `ci`: CI/CD 变更
**示例:**
```
feat(arch-scan): add @public JSDoc tag exemption mechanism
fix(permissions): fix parent module 6 Action permission violations
refactor: split 5 oversized files into domain-specific subfiles
docs(architecture): rewrite 004 as architecture design document
```
### 提交前检查
husky + lint-staged 会在 `git commit` 时自动执行:
- ESLint 检查暂存文件
- Prettier 格式化暂存文件
- commitlint 校验 commit message 格式
如果检查失败,请修复后重新提交。
## 架构约束
### 严格三层架构
```
app → modules → shared
```
- `app/` 只能调用 `modules/` 的 Server Actions 和 data-access
- `modules/` 之间通过对方 data-access 通信,不直接查询对方 DB 表
- `shared/` 不得反向依赖 `modules/*``app/*`
### 代码质量规则
- 禁止 `any`,未知类型用 `unknown` + 类型守卫
- 禁止 `as` 断言(除非从 `unknown` 转换,需注释原因)
- 函数返回值必须显式标注,特别是 `Promise<T>`
- 仅用于类型的导入使用 `import type`
- Server Action 必须调用 `requirePermission()`(或加 `@public` 标记豁免)
- 前端权限检查使用 `usePermission().hasPermission()`,禁止 `role === "xxx"` 硬编码
- 单文件行数:组件 ≤500actions/data-access ≤800硬限 1000
### 设计令牌规范
- 禁止硬编码颜色(`#hex`),使用 `hsl(var(--*))` 或 Tailwind 类
- 禁止硬编码字体(`'Inter'`),使用 `var(--font-family-*)`
- 禁止 Tailwind 任意值(`w-[137px]`),映射到 `--space-*` 或默认阶梯
## 文档同步
### 需要同步架构图的场景
- 新增/删除/重命名导出函数、组件、Hook、类型
- 修改函数签名(参数、返回类型)
- 修改权限点或角色-权限映射
- 新增/删除数据库表、路由页面、API 路由
- 修改模块间依赖关系
- 新增模块
### 同步方式
- 修改源码后运行 `npm run arch:scan` 更新 arch.db强制
- 若架构设计意图变化,同步更新 `docs/architecture/004_architecture_impact_map.md`
- 若发现新"场景→技术"映射,更新 `docs/troubleshooting/known-issues.md`
## 问题报告
- 构建/lint/tsc 报错 → 记录到 `docs/troubleshooting/known-issues.md` "全局经验"分区
- 运行时异常 → 记录到"模块经验"分区
- 框架/库版本兼容问题 → 记录到"全局经验: Next.js 配置与运行时"

View File

@@ -18,4 +18,7 @@ EXPOSE 3000
ENV PORT 3000 ENV PORT 3000
ENV HOSTNAME "0.0.0.0" ENV HOSTNAME "0.0.0.0"
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD node -e "fetch('http://localhost:' + (process.env.PORT || 3000) + '/api/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"
CMD ["node", "server.js"] CMD ["node", "server.js"]

14
LICENSE Normal file
View File

@@ -0,0 +1,14 @@
PROPRIETARY AND CONFIDENTIAL
Copyright (c) 2026 EazyGame. All rights reserved.
This source code and accompanying documentation (the "Software") is the
proprietary and confidential property of EazyGame. No part of the Software
may be reproduced, distributed, or transmitted in any form or by any means,
including photocopying, recording, or other electronic or mechanical methods,
without the prior written permission of EazyGame.
For licensing inquiries, contact: legal@eazygame.cn
Unauthorized use, reproduction, or distribution of this Software, via any
medium, is strictly prohibited and may result in civil and criminal penalties.

90
SECURITY.md Normal file
View File

@@ -0,0 +1,90 @@
# 安全策略
## 报告安全漏洞
**请不要通过 GitHub Issue 公开报告安全漏洞。**
发现安全漏洞请通过以下渠道私密报告:
- 邮件security@eazygame.cn
- 内部工单系统Security 项目 → New Issue
报告时请包含:
1. 漏洞描述和影响范围
2. 复现步骤(最小化示例)
3. 影响的版本号
4. 建议的修复方案(可选)
**响应时间:** 24 小时内确认收到5 个工作日内给出评估结果。
## 安全架构
### 权限三道防线
```
proxy.ts (路由级 bitmap) → requirePermission (Server Action 级) → usePermission (客户端级)
```
- **路由级**`src/proxy.ts` 使用 bitmap 快速拦截未授权路由
- **Server Action 级**:每个 Action 必须调用 `requirePermission()`,或用 `@public` JSDoc 标记豁免
- **客户端级**:组件使用 `usePermission().hasPermission()` 控制元素显隐
### 认证与会话
- JWT/session ID 存储在 httpOnly + Secure + SameSite=Strict 的 Cookie 中
- 服务端环境变量不加 `NEXT_PUBLIC_` 前缀
- 环境变量使用 `@t3-oss/env-nextjs` + Zod 校验(`src/env.mjs`
### 数据访问
- 前端禁止直接访问数据库,所有数据访问必须通过 `data-access.ts` 模块
- Server Action 必须使用 `requirePermission()` 进行权限校验
- 家长路由必须包含 `parentId``studentId` 双重权限校验,防止信息泄露
### 输入安全
- **禁止 `dangerouslySetInnerHTML`**(如必须使用,先用 DOMPurify 清洗)
- Server Action 输入使用 Zod 验证,验证失败返回结构化错误
- 注册/登录流程实施速率限制,防止暴力破解和邮箱枚举攻击
## 安全审计
### arch:scan 自动检测
`npm run arch:query -- violations` 会自动检测:
- **长文件**>800 行):提示拆分,降低维护风险
- **Server Action 权限缺失**:识别未调用 `requirePermission` 的 Server Action支持递归调用链识别
### @public 豁免标记
登录前/公开/内部工具 Server Action 可用 `@public` JSDoc 标记豁免权限校验:
```ts
/**
* 注册 Action登录前公开调用。
*
* @public 登录前公开 Action豁免 requirePermission 校验。
*/
export async function registerAction(formData: FormData) {
// ...
}
```
**豁免场景:**
- 登录前 Action注册、邮箱可用性检查、2FA 预检)
- 内部日志工具audit-logger、change-logger、login-logger
- 权限查询工具isAdminRole、canConfigurePublicAiProvider
- 登录后必经流程onboarding 状态查询/完成)
## 依赖安全
- 定期运行 `npm audit` 检查已知漏洞
- CI 流水线包含 Trivy 安全扫描(`.trivyignore` 配置豁免项)
- 依赖升级通过 PR 审核,不允许直接推送 main 分支
## 数据保护
- 数据库备份:每日自动备份,每周 DR 演练
- 敏感数据密码、2FA 密钥)使用 bcrypt/Argon2 哈希存储
- 日志不记录敏感信息密码、token、个人身份信息

24
commitlint.config.mjs Normal file
View File

@@ -0,0 +1,24 @@
export default {
extends: ["@commitlint/config-conventional"],
rules: {
"type-enum": [
2,
"always",
[
"feat",
"fix",
"docs",
"style",
"refactor",
"test",
"chore",
"perf",
"ci",
"build",
"revert",
],
],
"subject-case": [0],
"header-max-length": [2, "always", 120],
},
}

View File

@@ -69,7 +69,7 @@
| 文档 | 归档原因 | | 文档 | 归档原因 |
|------|---------| |------|---------|
| [002 RBAC 重构方案](architecture/002_rbac_refactoring.md) | 描述修复前的安全隐患,当前所有 Server Action 已接入 `requirePermission()` | | [002 RBAC 重构方案](architecture/002_rbac_refactoring.md) | 描述修复前的安全隐患,当前所有 Server Action 已接入 `requirePermission()` |
| [002 角色路由 RFC](architecture/002_role_based_routing.md) | 2025-12-23 提案,当前角色域路由已全部实现 | | [002b 角色路由 RFC](architecture/002b_role_based_routing.md) | 2025-12-23 提案,当前角色域路由已全部实现 |
| [003 UI 重构计划](architecture/003_ui_refactoring_plan.md) | 2026-06-16 重构计划,当前已执行完毕 | | [003 UI 重构计划](architecture/003_ui_refactoring_plan.md) | 2026-06-16 重构计划,当前已执行完毕 |
### 设计历史文档 ### 设计历史文档

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,10 @@
# 历史审查报告归档
> 本目录为只读归档,不再更新。
> 有价值的内容已提取到模块 README 和 known-issues.md。
## 归档文件
- 005_architecture_data.json已废弃由 arch.db 替代)
- 60+ 份模块审查报告(历史参考)
- data-access-audit-v1 系列文件(数据访问层审查)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,16 @@
# 路线图索引
> 本目录存放项目长远规划与架构事实004分离。
## 文档清单
| 文档 | 用途 |
|------|------|
| [tech-debt.md](./tech-debt.md) | 技术债清单 |
| [decoupling.md](./decoupling.md) | 解耦路线图 |
| [pending-features.md](./pending-features.md) | 待开发功能 |
## 维护规则
- 规划实现后从本目录删除,迁入 004 架构事实或 git 历史
- 不含架构事实,不含经验(经验查 known-issues.md

View File

@@ -0,0 +1,7 @@
# 待开发功能
> 从 004 各模块"未完成项"迁入。
## 待开发功能清单
(迁入后填充)

View File

@@ -0,0 +1,95 @@
# 技术债清单
> 从 004 第三部分"已知架构问题和技术债"迁入,记录已知的架构问题和技术债,按优先级排序。
## P0 - 高优先级(影响安全/性能/可维护性)
### TD-P0-001: arch:scan symbol 解析同名歧义
- **状态**: 已识别,短期方案已实施
- **影响**: 当多个文件存在同名 symbol`updateUserProfile` 在 onboarding/data-access.ts 和 users/actions.ts 都有定义calls 表的 callee_id 解析可能选错,导致违规检测误报
- **短期方案**: 在 wrapper Action 中显式调用 `requirePermission`(已在 settings/actions-service.ts 实施)
- **长期方案**: 改进 `findSymbolIdByName` 逻辑,考虑 import 路由解析 callee而非仅按名称匹配
- **关联文件**: `scripts/arch-scan/scanner.ts:findSymbolIdByName`
### TD-P0-002: listMyLeaveRequests 死代码嫌疑
- **状态**: 待确认
- **影响**: `src/modules/leave-requests/actions.ts``listMyLeaveRequests` 无外部调用者,可能是死代码
- **建议**: 确认无使用后删除,或补充调用者
## P1 - 中优先级(影响开发效率/代码质量)
### TD-P1-001: noUncheckedIndexedAccess 全量修复
- **状态**: 已评估419 个错误,推迟到独立迭代
- **影响**: 开启 `noUncheckedIndexedAccess` 后,所有数组/对象索引访问返回 `T | undefined`,需要全量修复类型错误
- **评估结果**: 2026-07-07 评估,开启后产生 419 个 tsc 错误,主要涉及 `array[0]``array[index]``Object.keys()[i]` 等模式
- **建议**: 独立迭代逐步修复,按模块分批进行(优先修复 data-access 层,再修复 actions 层,最后修复组件层)
- **关联**: tsconfig.json
### TD-P1-002: CI 缺少 Unit test 阶段
- **状态**: 计划在 P2 阶段实施
- **影响**: 当前 CI 流水线(`.gitea/workflows/ci.yml`)只有 Integration test缺少 Unit test + coverage 阶段
- **建议**: 在 Lint → Typecheck 之后、Integration test 之前插入 `npm run test:unit` 阶段
### TD-P1-003: 缺少 /api/health 健康检查端点
- **状态**: 计划在 P2 阶段实施
- **影响**: Dockerfile 无 HEALTHCHECK容器编排无法自动检测应用健康状态
- **建议**: 创建 `src/app/api/health/route.ts` + Dockerfile 添加 HEALTHCHECK 指令
### TD-P1-004: 缺少 bundle 体积分析
- **状态**: 计划在 P2 阶段实施
- **影响**: 无法可视化构建体积,可能存在未优化的依赖
- **建议**: 安装 `@next/bundle-analyzer`,添加 `ANALYZE=true` 环境变量触发
## P2 - 低优先级(改进项)
### TD-P2-001: API 文档缺失
- **状态**: 计划在 P2 阶段实施
- **影响**: API 路由缺少 OpenAPI/Swagger 文档
- **建议**: 使用 `swagger-jsdoc` 或手写 OpenAPI spec
### TD-P2-002: shared 模块内部结构未扫描
- **状态**: 已识别
- **影响**: arch:scan 将 `src/shared` 视为单个模块其子目录lib/db/types 等)的内部结构未被单独扫描
- **建议**: 改进 `scanModules` 逻辑,对 shared 模块按子目录分组
### TD-P2-003: 部分模块 README 质量不均
- **状态**: 已识别
- **影响**: 35 个模块 README 已重写,但 search/layout/student 等模块因结构特殊,架构图较简化
- **建议**: 根据实际使用反馈持续优化
## 已解决项
### TD-RESOLVED-001: Server Action 权限违规32→0
- **解决时间**: 2026-07-07
- **方案**:
1. arch:scan 添加 @public JSDoc 标记豁免机制12 个登录前/公开/内部工具 Action
2. 递归 CTE 识别间接调用链ai 模块 6 个 Action 通过 requireAiPermission 间接调用)
3. 修复 9 个真违规parent 5 个 + settings 1 个 + 3 个 @public 豁免)
- **验证**: `npm run arch:query -- violations` 输出 0 违规
### TD-RESOLVED-002: 5 个超长文件(>800 行)
- **解决时间**: 2026-07-07
- **方案**: 按业务域拆分为多个子文件 + barrel export
- `schema.ts` (2245→29+27子文件)
- `invalidation-map.ts` (1195→50+6子文件)
- `messaging/actions.ts` (973→47+5子文件)
- `textbooks/data-access.ts` (907→15+6子文件)
- `questions/data-access.ts` (828→48+4子文件)
- **验证**: `npm run arch:query -- violations` 输出 0 长文件
### TD-RESOLVED-003: 缺少大仓工程基建文档
- **解决时间**: 2026-07-07
- **方案**: 创建 LICENSE专有协议、CHANGELOG.md、CONTRIBUTING.md、SECURITY.md
- **验证**: 根目录文档齐全

View File

@@ -194,7 +194,11 @@ export function UserCard({ user, onSelect, children }: UserCardProps): JSX.Eleme
### 4.1 配置tsconfig.json ### 4.1 配置tsconfig.json
当前项目配置需升级以符合规范。**目标配置** 当前项目配置已基本符合规范(`target: ES2022``noImplicitReturns: true``noFallthroughCasesInSwitch: true`)。
**待办**`noUncheckedIndexedAccess` 当前为 `false`(数组/对象索引不返回 `T | undefined`),逐步迁移后启用。具体配置见 `tsconfig.json`
**目标配置**
```json ```json
{ {
@@ -221,12 +225,6 @@ export function UserCard({ user, onSelect, children }: UserCardProps): JSX.Eleme
} }
``` ```
**当前差异**(需逐步升级):
- `target`: `ES2017``ES2022`
- 缺失 `noUncheckedIndexedAccess`(数组/对象索引返回 `T | undefined`
- 缺失 `noImplicitReturns`(函数所有分支必须返回)
- 缺失 `noFallthroughCasesInSwitch`
### 4.2 类型规则 ### 4.2 类型规则
1. **禁止 `any`**:未知类型用 `unknown` 并做类型守卫。若极特殊情况必须使用,需 `// eslint-disable-next-line @typescript-eslint/no-explicit-any` 并注释原因 1. **禁止 `any`**:未知类型用 `unknown` 并做类型守卫。若极特殊情况必须使用,需 `// eslint-disable-next-line @typescript-eslint/no-explicit-any` 并注释原因
@@ -366,23 +364,22 @@ import { cn } from "@/shared/lib/utils";
### 6.3 设计令牌配置 ### 6.3 设计令牌配置
本项目在 `src/app/globals.css` 中使用 CSS 变量定义设计令牌: 本项目在 `src/app/styles/tokens/` 中使用 CSS 变量定义设计令牌(分层架构)
```css - `primitive.css`: 原始色板/字号/间距/阴影Layer 1业务代码不直接引用
:root { - `semantic-light.css` + `semantic-dark.css`: 语义令牌Layer 2业务代码唯一引用入口
--background: 0 0% 100%; - `lesson-preparation.css`: `--lp-*` 令牌(明暗双份,模块命名空间)
--foreground: 240 10% 3.9%; - `tailwind-theme.css`: `@theme inline` 将 Semantic 令牌暴露为 Tailwind 类
--primary: 240 5.9% 10%; - `index.css`: 入口文件(汇总 @import
--primary-foreground: 0 0% 98%;
--destructive: 0 84.2% 60.2%;
--border: 240 5.9% 90%;
--radius: 0.5rem;
/* ... */
}
```
**所有视觉设计决策**(颜色、字号、间距)必须体现在设计令牌中,组件中不使用硬编码值。 **所有视觉设计决策**(颜色、字号、间距)必须体现在设计令牌中,组件中不使用硬编码值。
**ESLint 强制约束**
- `no-restricted-syntax`: 禁止 `#hex` 颜色字面量
- `design-tokens/no-hardcoded-fonts`: 禁止 `'Inter'`/`'Fraunces'`/`'JetBrains Mono'` 字面量(单词边界匹配,不影响 `Interval`/`Interactive` 等标识符)
- 白名单:`primitive.css`(令牌定义)、`email-channel.ts`(邮件 HTML`manifest.ts`PWA
- 任意值豁免需注释:`// eslint-disable-next-line no-restricted-syntax -- <reason>`
--- ---
## 七、数据获取与状态管理 ## 七、数据获取与状态管理
@@ -400,7 +397,7 @@ import { cn } from "@/shared/lib/utils";
**规则** **规则**
- 服务端数据获取通过模块的 `data-access.ts` 函数 - 服务端数据获取通过模块的 `data-access.ts` 函数
- 客户端动态数据统一使用 **TanStack Query v5+****禁止在 `useEffect` 中手写 fetch** - 客户端动态数据统一使用 **TanStack Query v5+****禁止在 `useEffect` 中手写 fetch**
- 缓存策略:服务端请求必须显式设置 `next.revalidate`使用 `unstable_cache`,并注释缓存时长理由 - 缓存策略:服务端请求使用 `cacheFn`(封装 React `cache()` + 自定义缓存层),详见 `shared/lib/cache/`;不使用 `unstable_cache`(权限数据易变,跨请求缓存风险高于收益)
### 7.2 Server Actions ### 7.2 Server Actions
@@ -470,18 +467,23 @@ export async function createExamAction(
### 7.3 状态管理 ### 7.3 状态管理
| 场景 | 方案 | 本项目采用 **5 层状态模型**
|------|------|
| 局部 UI 状态 | `useState` / `useReducer` | | 层级 | 场景 | 方案 |
| 跨组件共享(小范围) | React Context | |------|------|------|
| 跨组件共享(大范围) | Zustand轻量、可选择订阅 | | L1 URL | 可分享、可刷新的状态(分页、筛选、排序) | `nuqs` |
| 全局状态 | 仅存放真正全局必要数据(认证信息、主题、通知列表) | | L2 Server | 服务端数据 | TanStack Query |
| URL 状态 | `nuqs`(已集成) | | L3 Client Business | 客户端业务状态 | Zustand slice |
| L4 Global UI | 全局 UI 状态(弹窗、主题) | Zustand ui-store + ModalRoot |
| L5 Form | 表单状态 | react-hook-form + zodResolver |
**规则** **规则**
- Context 拆分:一个 Context 只负责一类数据,避免无关状态变化引发不必要的渲染 - Context 拆分:一个 Context 只负责一类数据,避免无关状态变化引发不必要的渲染
- 业务数据一律通过路由参数或 TanStack Query 获取,**不存入全局状态** - 业务数据一律通过路由参数或 TanStack Query 获取,**不存入全局状态**
- Zustand 的 `persist` 中间件必须处理版本迁移和敏感数据加密 - Zustand 的 `persist` 中间件必须处理版本迁移和敏感数据加密
- 优先使用细粒度 Zustand selectors单字段 selector而非 `useShallow` 多字段包装
- 乐观更新使用 React 19 `useOptimistic` + `useTransition`,替代手动 `isPending` 状态
- `useOptimistic``addOptimistic` 必须在 transition 或 action 内调用form action 也算 action
--- ---
@@ -700,52 +702,19 @@ test(checkout): cover discount edge cases
## 十五、统一工具配置 ## 十五、统一工具配置
### 15.1 ESLint(当前配置 + 建议增强) ### 15.1 ESLint
**当前配置**`eslint.config.mjs` **当前配置**`eslint.config.mjs`已实现以下规则
```javascript - `no-restricted-syntax`: 禁止 `#hex` 颜色字面量
import { defineConfig, globalIgnores } from "eslint/config"; - `design-tokens/no-hardcoded-fonts`: 禁止 `'Inter'`/`'Fraunces'`/`'JetBrains Mono'` 字面量(单词边界匹配,不影响 `Interval`/`Interactive` 等标识符)
import nextVitals from "eslint-config-next/core-web-vitals"; - 白名单:`primitive.css`(令牌定义)、`email-channel.ts`(邮件 HTML`manifest.ts`PWA
import nextTs from "eslint-config-next/typescript"; - `@typescript-eslint/no-explicit-any`: error
- `react/react-in-jsx-scope`: off
- `react/function-component-definition`: function-declaration
- `import/order`: 强制分组排序
const eslintConfig = defineConfig([ (具体配置见 `eslint.config.mjs`
...nextVitals,
...nextTs,
{
rules: {
"react-hooks/incompatible-library": "off",
},
},
// ...
]);
export default eslintConfig;
```
**建议增强**(待逐步集成):
```javascript
{
extends: [
"next/core-web-vitals",
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:jsx-a11y/recommended",
"prettier"
],
rules: {
"@typescript-eslint/no-explicit-any": "error",
"react/react-in-jsx-scope": "off",
"react/function-component-definition": [2, { "namedComponents": "function-declaration" }],
"import/order": ["error", {
"groups": ["builtin", "external", "internal", "parent", "sibling", "index", "type"],
"newlines-between": "always"
}]
}
}
```
### 15.2 Prettier ### 15.2 Prettier

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,304 @@
# 企业级架构规范化设计
> 日期: 2026-07-07
> 状态: 已批准
> 范围: P0 安全合规 + P1 大仓工程基建 + P2 配置升级
> 实施策略: 方案 A(严格分阶段,每阶段独立提交)
---
## 1. 背景与目标
### 1.1 现状诊断
通过 `arch:query violations` + 配置文件审查 + 文档检查,发现 14 个维度的企业级/大仓规范缺口,按严重级别分类:
**P0 严重(影响安全/规范底线)**
- 32 个 Server Action 缺 `requirePermission()` 校验(集中在 ai/auth/parent/settings/rbac/onboarding)
- 5 个文件超过 1000 行硬性上限(`schema.ts` 2245 行、`invalidation-map.ts` 1195 行、`messaging/actions.ts` 973 行、`textbooks/data-access.ts` 907 行、`questions/data-access.ts` 828 行)
**P1 工程基建缺口**
- 缺大仓标配文档: LICENSE / CHANGELOG.md / CONTRIBUTING.md / SECURITY.md / .env.example
- 缺提交期前钩子: 无 husky + lint-staged + commitlint,提交规范仅靠自觉
- `docs/architecture/roadmap/tech-debt.md` 是空壳(11 行无内容)
**P2 配置不完整**
- `tsconfig.json``noUncheckedIndexedAccess: false`
- CI 不跑 unit test(`ci.yml` 跳过 `test:unit`)
- `package.json``engines` / `packageManager` 字段
- 无 bundle-analyzer / coverage 阈值 / `/api/health` 端点 / API 文档
### 1.2 目标
- P0: 消除所有架构违规(`arch:query violations` 输出仅剩已知豁免)
- P1: 大仓文档齐全 + 提交规范工具链强制 + tech-debt 可追踪
- P2: TypeScript 严格模式全量开启 + CI 覆盖 unit test + 可观测性补充
---
## 2. 设计决策
| 决策点 | 选择 | 理由 |
|--------|------|------|
| 实施范围 | 全部 P0+P1+P2 | 完整治理 |
| 豁免机制 | JSDoc `@public` 标记 | 显式标记、可审计、与代码同源 |
| noUncheckedIndexedAccess | 一次性开启+全量修复 | 一步到位,避免债务拖延 |
| scripts 治理 | 本次不做,记入 tech-debt | 范围可控 |
| 提交钩子 | husky + lint-staged + commitlint | 全套强制规范 |
| LICENSE | 专有协议(内部项目) | 不计划开源 |
| 实施策略 | 方案 A(严格分阶段) | 每阶段可独立验证+回滚 |
---
## 3. 阶段 1: P0 安全合规
### 3.1 Server Action 权限治理
**3.1.1 arch:scan 豁免标记机制**
修改 `scripts/arch-scan/scanner.ts`:扫描函数时识别 JSDoc 中的 `@public` 标记,将该函数标记为 `is_public: true` 存入 `symbols` 表。
修改 `scripts/arch-scan/query.ts``violations` 命令:过滤掉 `is_public: true` 的 Server Action。
**3.1.2 合理豁免清单(加 `@public` 标记,共 ~12 个)**
| 文件 | 函数 | 豁免理由 |
|------|------|---------|
| `auth/actions.ts` | registerAction | 注册(登录前) |
| `auth/actions.ts` | checkEmailAvailabilityAction | 邮箱可用性检查(注册时) |
| `auth/actions.ts` | preflightTwoFactorAction | 2FA 预检(登录中) |
| `invitation-codes/actions.ts` | validateInvitationCodeAction | 邀请码校验(注册时) |
| `onboarding/actions.ts` | getOnboardingStatusAction | 引导状态查询(登录后但引导前) |
| `onboarding/actions.ts` | completeOnboardingAction | 完成引导(引导阶段) |
| `settings/actions-security.ts` | preflightTwoFactorAction | 2FA 预检(登录中) |
| `settings/actions-security.ts` | verifyTwoFactorForLogin | 2FA 验证(登录中) |
| `rbac/actions.ts` | isAdminRole | 内部辅助函数(非对外 Action) |
| `shared/lib/audit-logger.ts` | logAudit | 基础设施(非 Action,被 Action 调用) |
| `shared/lib/change-logger.ts` | logDataChange | 基础设施(同上) |
| `shared/lib/login-logger.ts` | logLoginEvent | 基础设施(同上) |
**3.1.3 真违规修复清单(补 requirePermission,共 ~20 个)**
| 文件 | 函数 | 权限点 |
|------|------|--------|
| `ai/actions.ts` | suggestSimilarQuestionsAction | QUESTION_READ |
| `ai/actions.ts` | suggestGradingAction | HOMEWORK_GRADE |
| `ai/actions.ts` | generateLessonContentAction | LESSON_PLAN_CREATE |
| `ai/actions.ts` | generateQuestionVariantAction | QUESTION_CREATE |
| `ai/actions.ts` | analyzeWeaknessAction | DIAGNOSTIC_READ |
| `ai/actions.ts` | explainErrorAction | ERROR_BOOK_READ |
| `leave-requests/actions.ts` | listMyLeaveRequests | LEAVE_REQUEST_READ(自身) |
| `lesson-preparation/actions.ts` | duplicateLessonPlanFormAction | LESSON_PLAN_CREATE |
| `parent/actions.ts` | getChildrenAction | PARENT_DASHBOARD_VIEW |
| `parent/actions.ts` | getChildBasicInfoAction | PARENT_DASHBOARD_VIEW |
| `parent/actions.ts` | getChildDashboardDataAction | PARENT_DASHBOARD_VIEW |
| `parent/actions.ts` | getParentDashboardDataAction | PARENT_DASHBOARD_VIEW |
| `parent/actions.ts` | getChildNameListAction | PARENT_DASHBOARD_VIEW |
| `parent/actions.ts` | verifyParentChildRelationAction | PARENT_DASHBOARD_VIEW |
| `settings/actions-service.ts` | updateProfileAction | USER_PROFILE_UPDATE(自身) |
| `settings/actions.ts` | getAiProviderSummaries | SYSTEM_SETTINGS_READ |
| `settings/actions.ts` | upsertAiProviderAction | SYSTEM_SETTINGS_MANAGE |
| `settings/actions.ts` | testAiProviderAction | SYSTEM_SETTINGS_MANAGE |
| `settings/actions.ts` | deleteAiProviderAction | SYSTEM_SETTINGS_MANAGE |
| `settings/actions.ts` | canConfigurePublicAiProvider | SYSTEM_SETTINGS_READ |
**注**: 若上述权限点不存在,需在 `shared/types/permissions.ts` 新增并注册到 `ROLE_PERMISSIONS_SEED` + `permission-bitmap.ts` + `permission-catalog.ts` + i18n `rbac.json` 五处。
### 3.2 超长文件拆分
**3.2.1 `shared/db/schema.ts` (2245 行) → 按域拆分**
```
src/shared/db/
├─ schema.ts # barrel: re-export 所有
├─ schema/
│ ├─ users.ts # users + userRoles + parentStudentRelations
│ ├─ academic.ts # classes + subjects + grades + textbooks + chapters + knowledgePoints
│ ├─ exams.ts # exams + examQuestions + homeworkAssignments + submissions + answers
│ ├─ grades.ts # gradeRecords + gradeStats
│ ├─ attendance.ts # attendanceRecords + attendanceRules
│ ├─ messaging.ts # conversations + messages + messageReadStatus
│ ├─ notifications.ts # notifications + notificationPreferences
│ ├─ audit.ts # auditLogs + changeLogs
│ ├─ rbac.ts # roles + permissions + rolePermissions
│ ├─ files.ts # files + fileReferences
│ ├─ scheduling.ts # classSchedule + timeSlots
│ └─ misc.ts # 其余表
```
**3.2.2 `shared/lib/cache/invalidation-map.ts` (1195 行) → 按模块拆分**
```
src/shared/lib/cache/
├─ invalidation-map.ts # barrel + registerInvalidationMaps
├─ invalidation/
│ ├─ exams.ts
│ ├─ homework.ts
│ ├─ grades.ts
│ ├─ attendance.ts
│ ├─ messaging.ts
│ ├─ textbooks.ts
│ └─ ...
```
**3.2.3 `messaging/actions.ts` (973 行) → 按职责拆分**
```
src/modules/messaging/
├─ actions.ts # barrel
├─ actions-conversations.ts # 会话 CRUD
├─ actions-messages.ts # 消息发送/撤回
├─ actions-starred.ts # 星标
└─ actions-read.ts # 已读状态
```
**3.2.4 `textbooks/data-access.ts` (907 行) → 按实体拆分**
```
src/modules/textbooks/
├─ data-access.ts # barrel + 核心
├─ data-access-graph.ts # 已有
├─ data-access-chapters.ts # 章节 CRUD
└─ data-access-knowledge-points.ts # 知识点 CRUD
```
**3.2.5 `questions/data-access.ts` (828 行) → 按职责拆分**
```
src/modules/questions/
├─ data-access.ts # barrel + 核心 CRUD
└─ data-access-search.ts # FULLTEXT 检索
```
---
## 4. 阶段 2: P1 大仓工程基建
### 4.1 大仓文档
| 文件 | 内容 |
|------|------|
| `LICENSE` | "Copyright (c) 2026 EazyGame. 保留所有权利。未经许可不得复制、修改、分发。" |
| `CHANGELOG.md` | Keep-a-changelog 格式,初始 `## [1.0.0] - 2026-07-07` 条目(记录文档体系重设计完成) |
| `CONTRIBUTING.md` | 分支策略(main 保护) + Conventional Commits + 提交前检查(lint+tsc) + 模块 README 维护规则 + arch:scan 同步 |
| `SECURITY.md` | 漏洞报告流程 + 安全联系人 + 响应 SLA + 不公开披露策略 |
| `.env.example` | 从 `src/env.mjs` 提取所有键(DATABASE_URL/NEXTAUTH_SECRET/NEXTAUTH_URL/AI keys/...)含注释说明 |
### 4.2 提交规范工具链
**4.2.1 依赖安装**
```bash
npm install -D husky lint-staged @commitlint/cli @commitlint/config-conventional @next/bundle-analyzer
```
**4.2.2 配置文件**
- `.husky/pre-commit`: `npx lint-staged`
- `.husky/commit-msg`: `npx commitlint --edit $1`
- `commitlint.config.js`: extends conventional + scope-enum(35 模块名)
- `lint-staged.config.js`: `*.{ts,tsx}``eslint --fix` + `prettier --write`;`*.md``prettier --write`
- `package.json` scripts: `"prepare": "husky"`
### 4.3 tech-debt.md 填充
整理待解决技术债(从 known-issues 工作经验日志 + 本次诊断补充):
- scripts 目录混放治理(.sh/.ps1/.js/.mjs/.ts 统一)
- API 文档自动化(OpenAPI 生成)
- proctoring 模块的 exams 依赖未被 arch.db 捕获(scanner 改进)
- FULLTEXT 索引迁移工具化
- client-error 上报机制完善(Task 12-14)
- React Flow 移除后的历史包袱清理
---
## 5. 阶段 3: P2 配置升级
### 5.1 TypeScript 严格化
- `tsconfig.json`: `"noUncheckedIndexedAccess": true`
- 全量修复类型错误: 数组索引 `arr[0]``arr[0]!``arr.at(0) ?? defaultValue`;对象索引 `obj[key]` → 显式判空
### 5.2 CI 完善
**`ci.yml` 修改**:
- Lint → **Unit test** → Typecheck → Architecture scan → Integration → E2E → Build
**`vitest.unit.config.ts` 加 coverage**:
```ts
coverage: {
provider: "v8",
lines: 60,
functions: 60,
branches: 60,
statements: 60,
}
```
**`package.json` 补充字段**:
```json
{
"engines": { "node": ">=22" },
"packageManager": "npm@10.9.0"
}
```
### 5.3 可观测性补充
**`/api/health` 路由**:
```ts
// src/app/api/health/route.ts
export async function GET() {
return Response.json({
status: "ok",
uptime: process.uptime(),
timestamp: new Date().toISOString(),
version: process.env.npm_package_version,
});
}
```
**Dockerfile 加 HEALTHCHECK**(审查现有 Dockerfile 后修改):
```dockerfile
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD wget -qO- http://localhost:3000/api/health || exit 1
```
**bundle-analyzer**:
- `package.json` scripts: `"analyze": "ANALYZE=true next build"`
- `next.config.ts` 包装 `@next/bundle-analyzer`
### 5.4 API 文档
`docs/api/README.md`: 17 个路由清单 + 请求/响应 schema 示例(从 `withApiErrorHandler``ActionState` 推导)
---
## 6. 验收标准
| 阶段 | 验收项 |
|------|--------|
| P0 | `npm run arch:query -- violations` 仅剩 0 个真违规(豁免项不报);`npx tsc --noEmit` 0 错误;`npm run lint` 0 错误;5 个超长文件全部 < 1000 行 |
| P1 | `LICENSE`/`CHANGELOG.md`/`CONTRIBUTING.md`/`SECURITY.md`/`.env.example` 存在;`npm run prepare` 安装 husky 钩子;commitlint 拒绝非规范提交;`tech-debt.md` 含至少 6 条待解决项 |
| P2 | `noUncheckedIndexedAccess: true` 且 tsc 0 错误;CI 跑 unit test;`/api/health` 返回 200;`npm run analyze` 可用;`package.json` 含 engines+packageManager |
---
## 7. 实施约束
- 每阶段开始前 `git commit` 一次快照(用户要求"实施前提交备份")
- 每阶段完成后 `git commit` + `git push`
- 每阶段必须通过 `npx tsc --noEmit` + `npm run lint` + `npm run arch:scan`
- 阶段 1 完成后更新 `docs/troubleshooting/known-issues.md` 工作经验日志
- 阶段 3 完成后更新 `docs/architecture/004_architecture_impact_map.md`(若架构意图变化)
- 全程不改 `project_rules.md` 除非有新的强制规则
---
## 8. 不在本次范围
- scripts 目录治理(记入 tech-debt)
- OpenAPI 自动生成(记入 tech-debt,本次仅手写静态文档)
- 分支保护策略(Gitea 配置,不在代码层)
- 数据库迁移工具化(已有 drizzle-kit)
- 客户端错误上报机制完善(Task 12-14,独立项目)

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,11 @@
import type { NextConfig } from "next"; import type { NextConfig } from "next"
import createNextIntlPlugin from "next-intl/plugin"; import createNextIntlPlugin from "next-intl/plugin"
import bundleAnalyzer from "@next/bundle-analyzer"
const withNextIntl = createNextIntlPlugin("./src/i18n/request.ts"); const withNextIntl = createNextIntlPlugin("./src/i18n/request.ts")
const withBundleAnalyzer = bundleAnalyzer({
enabled: process.env.ANALYZE === "true",
})
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
output: "standalone", output: "standalone",
@@ -10,12 +14,7 @@ const nextConfig: NextConfig = {
// - tencentcloud-sdk-nodejs: 腾讯云 SDK,体积 ~2-3 MB,仅在短信/对象存储等服务端调用 // - tencentcloud-sdk-nodejs: 腾讯云 SDK,体积 ~2-3 MB,仅在短信/对象存储等服务端调用
// - exceljs: Excel 生成库,体积 ~250-300 KB,仅在导出报表时使用 // - exceljs: Excel 生成库,体积 ~250-300 KB,仅在导出报表时使用
// - pino: 结构化日志库,依赖 node:async_hooks 与 transport worker 线程 // - pino: 结构化日志库,依赖 node:async_hooks 与 transport worker 线程
serverExternalPackages: [ serverExternalPackages: ["mysql2", "tencentcloud-sdk-nodejs", "exceljs", "pino"],
"mysql2",
"tencentcloud-sdk-nodejs",
"exceljs",
"pino",
],
experimental: { experimental: {
// 启用 optimizePackageImports 让 Turbopack 对 barrel 导出做精确 tree-shaking, // 启用 optimizePackageImports 让 Turbopack 对 barrel 导出做精确 tree-shaking,
// 避免整个库被打入首屏 chunk。覆盖项目中最常用的重型库: // 避免整个库被打入首屏 chunk。覆盖项目中最常用的重型库:
@@ -43,6 +42,6 @@ const nextConfig: NextConfig = {
"date-fns", "date-fns",
], ],
}, },
}; }
export default withNextIntl(nextConfig); export default withBundleAnalyzer(withNextIntl(nextConfig))

2426
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -37,8 +37,26 @@
"dr:drill": "bash scripts/dr-drill.sh", "dr:drill": "bash scripts/dr-drill.sh",
"dr:drill:ps1": "powershell -ExecutionPolicy Bypass -File scripts/dr-drill.ps1", "dr:drill:ps1": "powershell -ExecutionPolicy Bypass -File scripts/dr-drill.ps1",
"dr:health-check": "bash scripts/health-check.sh", "dr:health-check": "bash scripts/health-check.sh",
"dr:failover": "bash scripts/failover.sh" "dr:failover": "bash scripts/failover.sh",
"arch:scan": "tsx scripts/arch-scan/cli.ts scan",
"arch:query": "tsx scripts/arch-scan/cli.ts query",
"test:arch-scan": "vitest run --config vitest.arch-scan.config.ts",
"prepare": "husky",
"analyze": "ANALYZE=true next build"
}, },
"lint-staged": {
"*.{ts,tsx}": [
"eslint --fix",
"prettier --write"
],
"*.{json,md,css,mjs}": [
"prettier --write"
]
},
"engines": {
"node": ">=20.0.0"
},
"packageManager": "npm@10.8.2",
"dependencies": { "dependencies": {
"@alicloud/credentials": "^2.4.5", "@alicloud/credentials": "^2.4.5",
"@alicloud/dysmsapi20170525": "^4.5.1", "@alicloud/dysmsapi20170525": "^4.5.1",
@@ -77,6 +95,7 @@
"@tiptap/starter-kit": "^3.15.3", "@tiptap/starter-kit": "^3.15.3",
"@xyflow/react": "^12.11.0", "@xyflow/react": "^12.11.0",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"better-sqlite3": "^12.11.1",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"drizzle-orm": "^0.45.1", "drizzle-orm": "^0.45.1",
@@ -108,11 +127,15 @@
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"tencentcloud-sdk-nodejs": "^4.1.254", "tencentcloud-sdk-nodejs": "^4.1.254",
"tiptap-markdown": "^0.9.0", "tiptap-markdown": "^0.9.0",
"ts-morph": "^28.0.0",
"zod": "^4.2.1", "zod": "^4.2.1",
"zustand": "^5.0.9" "zustand": "^5.0.9"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^21.2.0",
"@commitlint/config-conventional": "^21.2.0",
"@faker-js/faker": "^10.1.0", "@faker-js/faker": "^10.1.0",
"@next/bundle-analyzer": "^16.2.10",
"@playwright/test": "^1.58.2", "@playwright/test": "^1.58.2",
"@tailwindcss/postcss": "^4", "@tailwindcss/postcss": "^4",
"@tailwindcss/typography": "^0.5.16", "@tailwindcss/typography": "^0.5.16",
@@ -121,6 +144,7 @@
"@testing-library/jest-dom": "^6.9.1", "@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2", "@testing-library/react": "^16.3.2",
"@types/bcryptjs": "^2.4.6", "@types/bcryptjs": "^2.4.6",
"@types/better-sqlite3": "^7.6.13",
"@types/node": "^20", "@types/node": "^20",
"@types/nodemailer": "^8.0.1", "@types/nodemailer": "^8.0.1",
"@types/qrcode": "^1.5.6", "@types/qrcode": "^1.5.6",
@@ -131,11 +155,14 @@
"drizzle-kit": "^0.31.8", "drizzle-kit": "^0.31.8",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "16.0.10", "eslint-config-next": "16.0.10",
"husky": "^9.1.7",
"jsdom": "^29.1.1", "jsdom": "^29.1.1",
"lint-staged": "^16.4.0",
"pino-pretty": "^11.3.0", "pino-pretty": "^11.3.0",
"prettier": "^3.7.4", "prettier": "^3.7.4",
"prettier-plugin-tailwindcss": "^0.7.2", "prettier-plugin-tailwindcss": "^0.7.2",
"tailwindcss": "^4", "tailwindcss": "^4",
"tsx": "^4.23.0",
"typescript": "^5", "typescript": "^5",
"vitest": "^4.1.0" "vitest": "^4.1.0"
} }

3
scripts/arch-scan/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
*.wal
*.shm
*.tmp

BIN
scripts/arch-scan/arch.db Normal file

Binary file not shown.

Binary file not shown.

View File

24
scripts/arch-scan/cli.ts Normal file
View File

@@ -0,0 +1,24 @@
import { runScan, runQuery } from "./index";
const args = process.argv.slice(2);
const command = args[0];
if (command === "scan") {
runScan();
} else if (command === "query") {
runQuery(args.slice(1));
} else {
console.log("Usage:");
console.log(" npm run arch:scan");
console.log(" npm run arch:query <command> [args]");
console.log("");
console.log("Commands:");
console.log(" scan Scan codebase and update arch.db");
console.log(" query ref <symbol> [--forward] Query symbol references");
console.log(" query module <name> [--reverse] Query module dependencies");
console.log(" query tech <tag> Query tech tag usage");
console.log(" query violations Detect architecture violations");
console.log(' query sql "<SQL>" Run free SQL');
console.log(" query repl SQLite REPL info");
process.exit(1);
}

187
scripts/arch-scan/index.ts Normal file
View File

@@ -0,0 +1,187 @@
import Database from "better-sqlite3";
import { execSync } from "node:child_process";
import { initSchema, clearAllData } from "./schema";
import {
scanAllModules,
scanFiles,
scanSymbols,
scanCalls,
scanTechTags,
scanFileImports,
scanModuleDeps,
} from "./scanner";
import {
querySymbolRefs,
querySymbolForwardCalls,
queryModuleDeps,
queryModuleReverseDeps,
queryTechUsage,
queryViolations,
} from "./query";
const DB_PATH = "scripts/arch-scan/arch.db";
function getCommitHash(): string {
try {
return execSync("git rev-parse --short HEAD")
.toString()
.trim();
} catch {
return "unknown";
}
}
export function runScan(): void {
console.log("Starting architecture scan...");
const db = new Database(DB_PATH);
db.pragma("journal_mode = WAL");
initSchema(db);
clearAllData(db);
console.log("Schema initialized.");
scanAllModules(db, process.cwd());
console.log("Modules scanned.");
// scanFiles 扫描 DB 中所有模块,只需调用一次
scanFiles(db, "src/modules");
console.log("Files scanned.");
scanSymbols(db);
console.log("Symbols scanned.");
scanCalls(db);
console.log("Calls scanned.");
scanTechTags(db);
console.log("Tech tags scanned.");
scanFileImports(db);
scanModuleDeps(db);
console.log("Module dependencies scanned.");
const now = new Date().toISOString();
const commit = getCommitHash();
const meta = db.prepare(
"INSERT OR REPLACE INTO scan_meta (key, value) VALUES (?, ?)"
);
meta.run("scanned_at", now);
meta.run("commit_hash", commit);
meta.run("scanner_version", "1.0.0");
const stats = {
files: (db.prepare("SELECT count(*) as c FROM files").get() as { c: number }).c,
symbols: (db.prepare("SELECT count(*) as c FROM symbols").get() as { c: number }).c,
calls: (db.prepare("SELECT count(*) as c FROM calls").get() as { c: number }).c,
};
meta.run("total_files", String(stats.files));
meta.run("total_symbols", String(stats.symbols));
meta.run("total_calls", String(stats.calls));
console.log(
`Scan complete: ${stats.files} files, ${stats.symbols} symbols, ${stats.calls} calls`
);
console.log(`Commit: ${commit}, Time: ${now}`);
db.close();
}
export function runQuery(args: string[]): void {
const db = new Database(DB_PATH, { readonly: true });
const command = args[0];
switch (command) {
case "ref": {
const symbol = args[1];
if (!symbol) {
console.error("Usage: arch:query ref <symbolName> [--forward] [--depth=N]");
process.exit(1);
}
const forward = args.includes("--forward");
const depthArg = args.find((a) => a.startsWith("--depth="));
const depth = depthArg ? parseInt(depthArg.split("=")[1], 10) : 10;
if (forward) {
const calls = querySymbolForwardCalls(db, symbol, depth);
console.log(`${symbol}`);
for (const call of calls) {
const indent = " ".repeat(call.depth + 1);
console.log(
`${indent}└─▶ ${call.callee_name} (${call.callee_path}#L${call.callee_line})`
);
}
} else {
const refs = querySymbolRefs(db, symbol, depth);
console.log(`${symbol}`);
for (const ref of refs) {
const indent = " ".repeat(ref.depth + 1);
console.log(
`${indent}├─▼ ${ref.caller_name} (${ref.caller_path}#L${ref.caller_line})${ref.is_server_action ? " [Server Action]" : ""}`
);
}
}
break;
}
case "module": {
const moduleName = args[1];
if (!moduleName) {
console.error("Usage: arch:query module <moduleName> [--reverse] [--depth=N]");
process.exit(1);
}
const reverse = args.includes("--reverse");
if (reverse) {
const deps = queryModuleReverseDeps(db, moduleName);
console.log(`${moduleName} (reverse deps)`);
for (const dep of deps) {
console.log(` └─◀ ${dep.source_module} [${dep.dep_type}]`);
}
} else {
const deps = queryModuleDeps(db, moduleName);
console.log(`${moduleName} (forward deps)`);
for (const dep of deps) {
console.log(` └─▶ ${dep.target_module} [${dep.dep_type}]`);
}
}
break;
}
case "tech": {
const tag = args[1];
if (!tag) {
console.error("Usage: arch:query tech <tagName>");
process.exit(1);
}
const usage = queryTechUsage(db, tag);
console.log(`▼ tech: ${tag} (${usage.length} usages)`);
for (const u of usage) {
console.log(` └─ ${u.symbol_name} (${u.file_path})`);
}
break;
}
case "violations": {
const v = queryViolations(db);
console.log("▼ Architecture Violations");
console.log(` Long files (>800 lines): ${v.long_files.length}`);
for (const f of v.long_files) {
console.log(` └─ ${f.path}: ${f.lines} lines`);
}
console.log(
` Server Actions without requirePermission: ${v.server_actions_without_permission.length}`
);
for (const sa of v.server_actions_without_permission) {
console.log(` └─ ${sa.name} (${sa.path})`);
}
break;
}
case "sql": {
const sql = args.slice(1).join(" ");
if (!sql) {
console.error('Usage: arch:query sql "<SQL>"');
process.exit(1);
}
const rows = db.prepare(sql).all();
console.log(JSON.stringify(rows, null, 2));
break;
}
case "repl": {
console.log("Entering SQLite REPL. Type .exit to quit.");
console.log(`DB: ${DB_PATH}`);
console.log(
'Tip: use "npm run arch:query -- sql \\"<SQL>\\"" for quick queries instead.'
);
break;
}
default:
console.error(
"Unknown command. Available: ref, module, tech, violations, sql, repl"
);
process.exit(1);
}
db.close();
}

Some files were not shown because too many files have changed in this diff Show More