Compare commits
12 Commits
5d9981fd7d
...
e3d132dc1b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e3d132dc1b | ||
|
|
dbb124bf17 | ||
|
|
415dde9122 | ||
|
|
072c0d52b9 | ||
|
|
fb619139e5 | ||
|
|
031e8a8175 | ||
|
|
7f26bb8f9c | ||
|
|
164dcd4c84 | ||
|
|
692e8ef580 | ||
|
|
747344bfe3 | ||
|
|
3f68f3eb09 | ||
|
|
205b463900 |
@@ -67,6 +67,14 @@ jobs:
|
||||
- name: 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
|
||||
run: npx playwright install chromium
|
||||
|
||||
|
||||
1
.husky/commit-msg
Normal file
1
.husky/commit-msg
Normal file
@@ -0,0 +1 @@
|
||||
npx --no-install commitlint --edit $1
|
||||
1
.husky/pre-commit
Normal file
1
.husky/pre-commit
Normal file
@@ -0,0 +1 @@
|
||||
npx lint-staged
|
||||
@@ -4,19 +4,23 @@
|
||||
|
||||
**任何任务开始前,必须先查阅架构影响地图,通过图定位代码和模块。**
|
||||
|
||||
1. **先图后码**:执行任何分析、修改、搜索任务时,首先阅读 `docs/architecture/004_architecture_impact_map.md` 或 `docs/architecture/005_architecture_data.json`,从图中定位目标模块、函数、依赖关系,再按图索骥读取源码
|
||||
2. **图未覆盖则先补图**:如果发现项目中存在架构图未记录的模块、函数、表、路由等,**必须优先完善架构图信息**,然后再继续后续工作
|
||||
3. **改码必同步图**:对源码的任何修改完成后,必须同步更新 004 和 005 两个架构文档
|
||||
1. **先图后码**:执行任何分析、修改、搜索任务时,首先运行 `npm run arch:scan` 更新 arch.db,再通过 `npm run arch:query` 查询目标模块、函数、依赖关系,结合阅读 `docs/architecture/004_architecture_impact_map.md` 定位架构设计意图,最后按图索骥读取源码
|
||||
2. **图未覆盖则先补图**:如果发现项目中存在 arch.db 未记录的模块、函数、表、路由等,**必须先运行 `npm run arch:scan` 重新扫描**,然后检查 004 是否需要补充
|
||||
3. **改码必同步图**:对源码的任何修改完成后,必须运行 `npm run arch:scan` 更新 arch.db;若架构设计意图有变化,同步更新 004
|
||||
|
||||
### 架构文档清单
|
||||
|
||||
| 文档 | 用途 |
|
||||
|------|------|
|
||||
| `docs/architecture/004_architecture_impact_map.md` | 人类可读的架构影响地图 |
|
||||
| `docs/architecture/005_architecture_data.json` | AI 友好格式的结构化数据 |
|
||||
| `docs/architecture/004_architecture_impact_map.md` | 架构设计意图唯一源(人类可读) |
|
||||
| `docs/architecture/006_k12_feature_checklist.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 文档中对应的模块章节
|
||||
- 修改 JSON 文档中对应的节点(`modules.*.exports`、`permissions`、`dependencyMatrix`、`routes`、`dbTables` 等)
|
||||
- 确保两个文档内容一致
|
||||
- 修改源码后运行 `npm run arch:scan` 更新 arch.db(强制)
|
||||
- 若架构设计意图变化,同步更新 `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`):语义令牌,业务代码唯一引用入口
|
||||
- 模块命名空间(`lesson-preparation.css`):`--lp-*` 令牌,明暗双份
|
||||
- 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 强制约束**:
|
||||
- `no-restricted-syntax`: 禁止 `#hex` 字面量
|
||||
- `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) | 记录错误现象 + 正确写法 |
|
||||
| 运行时异常(白屏/API 报错/数据加载失败) | 记录错误现象 + 正确写法 |
|
||||
| 框架/库版本兼容问题 | 记录错误现象 + 正确写法 |
|
||||
| 依赖配置问题(serverExternalPackages/webpackIgnore 等) | 记录错误现象 + 正确写法 |
|
||||
| 架构约束违规 | 记录错误现象 + 正确写法 |
|
||||
| 构建报错(dev/build/lint/tsc) | 记录到"全局经验"对应主题分区 |
|
||||
| 运行时异常(白屏/API 报错/数据加载失败) | 记录到"模块经验"对应模块分区 |
|
||||
| 框架/库版本兼容问题 | 记录到"全局经验: Next.js 配置与运行时" |
|
||||
| 依赖配置问题(serverExternalPackages/webpackIgnore 等) | 记录到"全局经验: Next.js 配置与运行时" |
|
||||
| 架构约束违规 | 记录到"全局经验"对应主题分区 |
|
||||
|
||||
### 记录格式
|
||||
|
||||
以**规则表**形式记录,指明正确做法,无需详细解释原因:
|
||||
索引式表格,指明"场景→技术/规则"映射,不写多行代码示例:
|
||||
|
||||
```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
43
CHANGELOG.md
Normal 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
129
CONTRIBUTING.md
Normal 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"` 硬编码
|
||||
- 单文件行数:组件 ≤500,actions/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 配置与运行时"
|
||||
@@ -18,4 +18,7 @@ EXPOSE 3000
|
||||
ENV PORT 3000
|
||||
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"]
|
||||
|
||||
14
LICENSE
Normal file
14
LICENSE
Normal 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
90
SECURITY.md
Normal 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
24
commitlint.config.mjs
Normal 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],
|
||||
},
|
||||
}
|
||||
@@ -69,7 +69,7 @@
|
||||
| 文档 | 归档原因 |
|
||||
|------|---------|
|
||||
| [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 重构计划,当前已执行完毕 |
|
||||
|
||||
### 设计历史文档
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
4901
docs/architecture/audit/archive/004_architecture_impact_map_v1.md
Normal file
4901
docs/architecture/audit/archive/004_architecture_impact_map_v1.md
Normal file
File diff suppressed because it is too large
Load Diff
10
docs/architecture/audit/archive/README.md
Normal file
10
docs/architecture/audit/archive/README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# 历史审查报告归档
|
||||
|
||||
> 本目录为只读归档,不再更新。
|
||||
> 有价值的内容已提取到模块 README 和 known-issues.md。
|
||||
|
||||
## 归档文件
|
||||
|
||||
- 005_architecture_data.json(已废弃,由 arch.db 替代)
|
||||
- 60+ 份模块审查报告(历史参考)
|
||||
- data-access-audit-v1 系列文件(数据访问层审查)
|
||||
1682
docs/architecture/audit/archive/known-issues_v1.md
Normal file
1682
docs/architecture/audit/archive/known-issues_v1.md
Normal file
File diff suppressed because it is too large
Load Diff
16
docs/architecture/roadmap/README.md
Normal file
16
docs/architecture/roadmap/README.md
Normal 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)
|
||||
7
docs/architecture/roadmap/pending-features.md
Normal file
7
docs/architecture/roadmap/pending-features.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# 待开发功能
|
||||
|
||||
> 从 004 各模块"未完成项"迁入。
|
||||
|
||||
## 待开发功能清单
|
||||
|
||||
(迁入后填充)
|
||||
95
docs/architecture/roadmap/tech-debt.md
Normal file
95
docs/architecture/roadmap/tech-debt.md
Normal 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
|
||||
- **验证**: 根目录文档齐全
|
||||
@@ -194,7 +194,11 @@ export function UserCard({ user, onSelect, children }: UserCardProps): JSX.Eleme
|
||||
|
||||
### 4.1 配置(tsconfig.json)
|
||||
|
||||
当前项目配置需升级以符合规范。**目标配置**:
|
||||
当前项目配置已基本符合规范(`target: ES2022`、`noImplicitReturns: true`、`noFallthroughCasesInSwitch: true`)。
|
||||
|
||||
**待办**:`noUncheckedIndexedAccess` 当前为 `false`(数组/对象索引不返回 `T | undefined`),逐步迁移后启用。具体配置见 `tsconfig.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 类型规则
|
||||
|
||||
1. **禁止 `any`**:未知类型用 `unknown` 并做类型守卫。若极特殊情况必须使用,需 `// eslint-disable-next-line @typescript-eslint/no-explicit-any` 并注释原因
|
||||
@@ -366,23 +364,22 @@ import { cn } from "@/shared/lib/utils";
|
||||
|
||||
### 6.3 设计令牌配置
|
||||
|
||||
本项目在 `src/app/globals.css` 中使用 CSS 变量定义设计令牌:
|
||||
本项目在 `src/app/styles/tokens/` 中使用 CSS 变量定义设计令牌(分层架构):
|
||||
|
||||
```css
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 240 10% 3.9%;
|
||||
--primary: 240 5.9% 10%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--border: 240 5.9% 90%;
|
||||
--radius: 0.5rem;
|
||||
/* ... */
|
||||
}
|
||||
```
|
||||
- `primitive.css`: 原始色板/字号/间距/阴影(Layer 1,业务代码不直接引用)
|
||||
- `semantic-light.css` + `semantic-dark.css`: 语义令牌(Layer 2,业务代码唯一引用入口)
|
||||
- `lesson-preparation.css`: `--lp-*` 令牌(明暗双份,模块命名空间)
|
||||
- `tailwind-theme.css`: `@theme inline` 将 Semantic 令牌暴露为 Tailwind 类
|
||||
- `index.css`: 入口文件(汇总 @import)
|
||||
|
||||
**所有视觉设计决策**(颜色、字号、间距)必须体现在设计令牌中,组件中不使用硬编码值。
|
||||
|
||||
**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` 函数
|
||||
- 客户端动态数据统一使用 **TanStack Query v5+**,**禁止在 `useEffect` 中手写 fetch**
|
||||
- 缓存策略:服务端请求必须显式设置 `next.revalidate` 或使用 `unstable_cache`,并注释缓存时长理由
|
||||
- 缓存策略:服务端请求使用 `cacheFn`(封装 React `cache()` + 自定义缓存层),详见 `shared/lib/cache/`;不使用 `unstable_cache`(权限数据易变,跨请求缓存风险高于收益)
|
||||
|
||||
### 7.2 Server Actions
|
||||
|
||||
@@ -470,18 +467,23 @@ export async function createExamAction(
|
||||
|
||||
### 7.3 状态管理
|
||||
|
||||
| 场景 | 方案 |
|
||||
|------|------|
|
||||
| 局部 UI 状态 | `useState` / `useReducer` |
|
||||
| 跨组件共享(小范围) | React Context |
|
||||
| 跨组件共享(大范围) | Zustand(轻量、可选择订阅) |
|
||||
| 全局状态 | 仅存放真正全局必要数据(认证信息、主题、通知列表) |
|
||||
| URL 状态 | `nuqs`(已集成) |
|
||||
本项目采用 **5 层状态模型**:
|
||||
|
||||
| 层级 | 场景 | 方案 |
|
||||
|------|------|------|
|
||||
| L1 URL | 可分享、可刷新的状态(分页、筛选、排序) | `nuqs` |
|
||||
| L2 Server | 服务端数据 | TanStack Query |
|
||||
| L3 Client Business | 客户端业务状态 | Zustand slice |
|
||||
| L4 Global UI | 全局 UI 状态(弹窗、主题) | Zustand ui-store + ModalRoot |
|
||||
| L5 Form | 表单状态 | react-hook-form + zodResolver |
|
||||
|
||||
**规则**:
|
||||
- Context 拆分:一个 Context 只负责一类数据,避免无关状态变化引发不必要的渲染
|
||||
- 业务数据一律通过路由参数或 TanStack Query 获取,**不存入全局状态**
|
||||
- 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
|
||||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||
import nextTs from "eslint-config-next/typescript";
|
||||
- `no-restricted-syntax`: 禁止 `#hex` 颜色字面量
|
||||
- `design-tokens/no-hardcoded-fonts`: 禁止 `'Inter'`/`'Fraunces'`/`'JetBrains Mono'` 字面量(单词边界匹配,不影响 `Interval`/`Interactive` 等标识符)
|
||||
- 白名单:`primitive.css`(令牌定义)、`email-channel.ts`(邮件 HTML)、`manifest.ts`(PWA)
|
||||
- `@typescript-eslint/no-explicit-any`: error
|
||||
- `react/react-in-jsx-scope`: off
|
||||
- `react/function-component-definition`: function-declaration
|
||||
- `import/order`: 强制分组排序
|
||||
|
||||
const eslintConfig = defineConfig([
|
||||
...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"
|
||||
}]
|
||||
}
|
||||
}
|
||||
```
|
||||
(具体配置见 `eslint.config.mjs`)
|
||||
|
||||
### 15.2 Prettier
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -1,7 +1,11 @@
|
||||
import type { NextConfig } from "next";
|
||||
import createNextIntlPlugin from "next-intl/plugin";
|
||||
import type { NextConfig } from "next"
|
||||
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 = {
|
||||
output: "standalone",
|
||||
@@ -10,12 +14,7 @@ const nextConfig: NextConfig = {
|
||||
// - tencentcloud-sdk-nodejs: 腾讯云 SDK,体积 ~2-3 MB,仅在短信/对象存储等服务端调用
|
||||
// - exceljs: Excel 生成库,体积 ~250-300 KB,仅在导出报表时使用
|
||||
// - pino: 结构化日志库,依赖 node:async_hooks 与 transport worker 线程
|
||||
serverExternalPackages: [
|
||||
"mysql2",
|
||||
"tencentcloud-sdk-nodejs",
|
||||
"exceljs",
|
||||
"pino",
|
||||
],
|
||||
serverExternalPackages: ["mysql2", "tencentcloud-sdk-nodejs", "exceljs", "pino"],
|
||||
experimental: {
|
||||
// 启用 optimizePackageImports 让 Turbopack 对 barrel 导出做精确 tree-shaking,
|
||||
// 避免整个库被打入首屏 chunk。覆盖项目中最常用的重型库:
|
||||
@@ -43,6 +42,6 @@ const nextConfig: NextConfig = {
|
||||
"date-fns",
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default withNextIntl(nextConfig);
|
||||
export default withBundleAnalyzer(withNextIntl(nextConfig))
|
||||
|
||||
2426
package-lock.json
generated
2426
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
29
package.json
29
package.json
@@ -37,8 +37,26 @@
|
||||
"dr:drill": "bash scripts/dr-drill.sh",
|
||||
"dr:drill:ps1": "powershell -ExecutionPolicy Bypass -File scripts/dr-drill.ps1",
|
||||
"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": {
|
||||
"@alicloud/credentials": "^2.4.5",
|
||||
"@alicloud/dysmsapi20170525": "^4.5.1",
|
||||
@@ -77,6 +95,7 @@
|
||||
"@tiptap/starter-kit": "^3.15.3",
|
||||
"@xyflow/react": "^12.11.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"better-sqlite3": "^12.11.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"drizzle-orm": "^0.45.1",
|
||||
@@ -108,11 +127,15 @@
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tencentcloud-sdk-nodejs": "^4.1.254",
|
||||
"tiptap-markdown": "^0.9.0",
|
||||
"ts-morph": "^28.0.0",
|
||||
"zod": "^4.2.1",
|
||||
"zustand": "^5.0.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^21.2.0",
|
||||
"@commitlint/config-conventional": "^21.2.0",
|
||||
"@faker-js/faker": "^10.1.0",
|
||||
"@next/bundle-analyzer": "^16.2.10",
|
||||
"@playwright/test": "^1.58.2",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
@@ -121,6 +144,7 @@
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
"@types/node": "^20",
|
||||
"@types/nodemailer": "^8.0.1",
|
||||
"@types/qrcode": "^1.5.6",
|
||||
@@ -131,11 +155,14 @@
|
||||
"drizzle-kit": "^0.31.8",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.0.10",
|
||||
"husky": "^9.1.7",
|
||||
"jsdom": "^29.1.1",
|
||||
"lint-staged": "^16.4.0",
|
||||
"pino-pretty": "^11.3.0",
|
||||
"prettier": "^3.7.4",
|
||||
"prettier-plugin-tailwindcss": "^0.7.2",
|
||||
"tailwindcss": "^4",
|
||||
"tsx": "^4.23.0",
|
||||
"typescript": "^5",
|
||||
"vitest": "^4.1.0"
|
||||
}
|
||||
|
||||
3
scripts/arch-scan/.gitignore
vendored
Normal file
3
scripts/arch-scan/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
*.wal
|
||||
*.shm
|
||||
*.tmp
|
||||
BIN
scripts/arch-scan/arch.db
Normal file
BIN
scripts/arch-scan/arch.db
Normal file
Binary file not shown.
BIN
scripts/arch-scan/arch.db-shm
Normal file
BIN
scripts/arch-scan/arch.db-shm
Normal file
Binary file not shown.
0
scripts/arch-scan/arch.db-wal
Normal file
0
scripts/arch-scan/arch.db-wal
Normal file
24
scripts/arch-scan/cli.ts
Normal file
24
scripts/arch-scan/cli.ts
Normal 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
187
scripts/arch-scan/index.ts
Normal 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();
|
||||
}
|
||||
119
scripts/arch-scan/query.test.ts
Normal file
119
scripts/arch-scan/query.test.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import Database from "better-sqlite3";
|
||||
import { initSchema } from "./schema";
|
||||
import {
|
||||
querySymbolRefs,
|
||||
querySymbolForwardCalls,
|
||||
queryModuleDeps,
|
||||
queryModuleReverseDeps,
|
||||
queryViolations,
|
||||
} from "./query";
|
||||
|
||||
function setupTestDb(): Database.Database {
|
||||
const db = new Database(":memory:");
|
||||
initSchema(db);
|
||||
// 插入测试数据
|
||||
db.prepare(
|
||||
"INSERT INTO modules (name, path, layer, created_at) VALUES (?, ?, ?, ?)"
|
||||
).run("exams", "src/modules/exams", "modules", "2026-07-07");
|
||||
db.prepare(
|
||||
"INSERT INTO modules (name, path, layer, created_at) VALUES (?, ?, ?, ?)"
|
||||
).run("grades", "src/modules/grades", "modules", "2026-07-07");
|
||||
db.prepare(
|
||||
"INSERT INTO files (module_id, path, kind, lines) VALUES (?, ?, ?, ?)"
|
||||
).run(1, "src/modules/exams/actions.ts", "actions", 100);
|
||||
db.prepare(
|
||||
"INSERT INTO files (module_id, path, kind, lines) VALUES (?, ?, ?, ?)"
|
||||
).run(1, "src/modules/exams/data-access.ts", "data-access", 200);
|
||||
db.prepare(
|
||||
"INSERT INTO symbols (file_id, name, kind, is_exported, is_async, is_server_action, start_line, end_line) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
|
||||
).run(1, "createExamAction", "function", 1, 1, 1, 10, 50);
|
||||
db.prepare(
|
||||
"INSERT INTO symbols (file_id, name, kind, is_exported, is_async, is_server_action, start_line, end_line) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
|
||||
).run(2, "createExam", "function", 1, 1, 0, 20, 80);
|
||||
db.prepare(
|
||||
"INSERT INTO calls (caller_id, callee_id, call_line, count) VALUES (?, ?, ?, ?)"
|
||||
).run(1, 2, 30, 1); // createExamAction 调用 createExam
|
||||
db.prepare(
|
||||
"INSERT INTO module_deps (source_module_id, target_module_id, dep_type) VALUES (?, ?, ?)"
|
||||
).run(1, 2, "data-access-call"); // exams 依赖 grades
|
||||
return db;
|
||||
}
|
||||
|
||||
describe("querySymbolRefs", () => {
|
||||
it("should find callers of a symbol (reverse)", () => {
|
||||
const db = setupTestDb();
|
||||
const refs = querySymbolRefs(db, "createExam");
|
||||
expect(refs).toHaveLength(1);
|
||||
expect(refs[0].caller_name).toBe("createExamAction");
|
||||
});
|
||||
});
|
||||
|
||||
describe("querySymbolForwardCalls", () => {
|
||||
it("should find callees of a symbol (forward)", () => {
|
||||
const db = setupTestDb();
|
||||
const calls = querySymbolForwardCalls(db, "createExamAction");
|
||||
expect(calls).toHaveLength(1);
|
||||
expect(calls[0].callee_name).toBe("createExam");
|
||||
});
|
||||
});
|
||||
|
||||
describe("queryModuleDeps", () => {
|
||||
it("should find module dependencies (forward)", () => {
|
||||
const db = setupTestDb();
|
||||
const deps = queryModuleDeps(db, "exams");
|
||||
expect(deps).toContainEqual({
|
||||
target_module: "grades",
|
||||
dep_type: "data-access-call",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("queryModuleReverseDeps", () => {
|
||||
it("should find module reverse dependencies", () => {
|
||||
const db = setupTestDb();
|
||||
const deps = queryModuleReverseDeps(db, "grades");
|
||||
expect(deps).toContainEqual({
|
||||
source_module: "exams",
|
||||
dep_type: "data-access-call",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("queryViolations", () => {
|
||||
it("should detect files over 800 lines", () => {
|
||||
const db = setupTestDb();
|
||||
// 插入一个超长文件
|
||||
db.prepare(
|
||||
"INSERT INTO files (module_id, path, kind, lines) VALUES (?, ?, ?, ?)"
|
||||
).run(1, "src/modules/exams/huge.ts", "lib", 900);
|
||||
const violations = queryViolations(db);
|
||||
expect(violations.long_files).toContainEqual({
|
||||
path: "src/modules/exams/huge.ts",
|
||||
lines: 900,
|
||||
});
|
||||
});
|
||||
|
||||
it("should detect Server Actions without requirePermission", () => {
|
||||
const db = setupTestDb();
|
||||
const violations = queryViolations(db);
|
||||
// createExamAction 是 Server Action 但没有调用 requirePermission
|
||||
const missing = violations.server_actions_without_permission.find(
|
||||
(v) => v.name === "createExamAction"
|
||||
);
|
||||
expect(missing).toBeDefined();
|
||||
});
|
||||
|
||||
it("should exclude @public Server Actions from violations", () => {
|
||||
const db = setupTestDb();
|
||||
// 插入一个带 @public 标记的 Server Action(豁免权限校验)
|
||||
db.prepare(
|
||||
"INSERT INTO symbols (file_id, name, kind, is_exported, is_async, is_server_action, is_public, start_line, end_line) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
||||
).run(1, "publicLoginAction", "function", 1, 1, 1, 1, 60, 100);
|
||||
const violations = queryViolations(db);
|
||||
const missing = violations.server_actions_without_permission.find(
|
||||
(v) => v.name === "publicLoginAction"
|
||||
);
|
||||
expect(missing).toBeUndefined();
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user