diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..368a4dc --- /dev/null +++ b/.prettierrc @@ -0,0 +1,9 @@ +{ + "semi": false, + "singleQuote": false, + "tabWidth": 2, + "trailingComma": "all", + "printWidth": 100, + "arrowParens": "always", + "plugins": ["prettier-plugin-tailwindcss"] +} diff --git a/.trae/rules/project_rules.md b/.trae/rules/project_rules.md index 93ffd5f..7183ddd 100644 --- a/.trae/rules/project_rules.md +++ b/.trae/rules/project_rules.md @@ -16,6 +16,7 @@ | `docs/architecture/005_architecture_data.json` | AI 友好格式的结构化数据 | | `docs/architecture/006_k12_feature_checklist.md` | 标准功能模块清单 | | `docs/architecture/007_gap_audit_report.md` | 差距审计报告 | +| `docs/architecture/audit/01_decoupling_roadmap.md` | 解耦路线图 | ### 需要同步图的场景 @@ -33,7 +34,11 @@ - 修改 JSON 文档中对应的节点(`modules.*.exports`、`permissions`、`dependencyMatrix`、`routes`、`dbTables` 等) - 确保两个文档内容一致 -## 代码质量规则 +## 编码规范 + +**详细规范见 `docs/standards/coding-standards.md`,以下为核心强制规则。** + +### 代码质量规则 - 每次修改后运行 `npm run lint` 和 `npx tsc --noEmit` 确保零错误 - Server Action 必须使用 `requirePermission()` 进行权限校验 @@ -42,5 +47,77 @@ - 配置文件、常量文件、类型定义文件:无限制 - React 组件:建议 ≤ 500 行(复杂表单/大型表格可放宽至 800 行) - Server Actions / Data Access 模块:建议 ≤ 800 行 + - 工具函数:建议 ≤ 40 行 + - 自定义 Hook:建议 ≤ 80 行 - 超过建议行数时应考虑拆分(如 data-access 拆分为多个按职责划分的文件) - 硬性上限:任何文件不超过 1000 行,超过必须拆分 + +### 架构分层规则 + +- 严格三层架构,依赖方向单向:`app → modules → shared` +- `app/` 只能调用 `modules/` 的 Server Actions 和 data-access,不直接访问 DB +- `modules/` 之间通过对方 data-access 通信,**不直接查询对方 DB 表** +- `shared/` 是被依赖方,**不得反向依赖** `@/auth`、`@/proxy` 或任何 `modules/*` + +### 模块标准结构 + +``` +src/modules/[module]/ +├─ actions.ts # Server Actions(编排层) +├─ data-access.ts # 数据访问层(可拆分为 data-access-*.ts) +├─ schema.ts # Zod 验证(可选) +├─ types.ts # 类型定义 +├─ components/ # 模块专属组件 +└─ hooks/ # 模块专属 Hook(可选) +``` + +### TypeScript 规则 + +- **禁止 `any`**:未知类型用 `unknown` 并做类型守卫 +- **禁止 `as` 断言**(除非从 `unknown` 转换或测试中,需注释原因) +- **函数返回值必须显式标注**,特别是 `Promise` +- **仅用于类型的导入必须使用 `import type`** +- **可选链后禁止跟非空断言 `!`** + +### 命名规范 + +- 目录:kebab-case(`user-profile/`) +- 组件文件:PascalCase(`UserProfile.tsx`) +- Hook 文件:camelCase(`useAuth.ts`) +- 变量/函数:camelCase,布尔值用 `is/has/can/should` 前缀 +- 常量:UPPER_SNAKE_CASE(`MAX_RETRY_COUNT`) +- 类/接口:PascalCase,接口不加 `I` 前缀 + +### 组件规范 + +- 组件必须为纯函数,使用 `function` 声明 +- 页面组件(`page.tsx`)使用默认导出;其余组件使用具名导出 +- 默认服务端组件,需要交互时才添加 `"use client"`(必须位于文件第一行) +- **不使用 `React.FC`**,直接用函数声明 + 显式标注 props 类型 + +### Server Action 规范 + +- 每个 Action 必须调用 `requirePermission()` 进行权限校验 +- 输入使用 Zod 验证,验证失败返回结构化错误 +- 返回值统一采用 `ActionState` 类型 +- 使用 `revalidatePath` 精确刷新缓存 + +### Tailwind 规范 + +- 使用 `cn()` 工具函数管理条件类名 +- **禁止**字符串拼接动态类名(`bg-${color}-500`) +- **禁止**使用任意值(`w-[137px]`),除非有充分理由并注释 +- 设计令牌在 `src/app/globals.css` 中使用 CSS 变量定义 + +### 安全规范 + +- **禁止 `dangerlySetInnerHTML`**(如必须使用,先用 DOMPurify 清洗) +- JWT/session ID 存储在 httpOnly + Secure + SameSite=Strict 的 Cookie 中 +- 服务端环境变量不加 `NEXT_PUBLIC_` 前缀 +- 环境变量使用 `@t3-oss/env-nextjs` + Zod 校验(已实现于 `src/env.mjs`) + +### 提交规范 + +- 使用 Conventional Commits 格式:`feat(scope): description` +- 类型:`feat`, `fix`, `chore`, `docs`, `style`, `refactor`, `test`, `perf`, `ci` +- 提交前必须运行 `npm run lint` 和 `npx tsc --noEmit` 确保零错误 diff --git a/docs/README.md b/docs/README.md index d537be1..e4428b4 100644 --- a/docs/README.md +++ b/docs/README.md @@ -26,6 +26,13 @@ | [管理模块群审查](architecture/audit/management-modules-audit.md) | school/classes/users/audit 等管理模块审查 | | [新增模块和其他模块审查](architecture/audit/new-and-other-modules-audit.md) | diagnostic/elective/proctoring/notifications 等新增模块审查 | +## 编码规范 + +| 文档 | 用途 | +|------|------| +| [编码规范](standards/coding-standards.md) | 适配当前项目的企业级编码规范(TypeScript/React/Next.js/Tailwind/安全/测试/CI) | +| [项目规则](../.trae/rules/project_rules.md) | AI 助手项目规则(架构图优先 + 核心强制规则) | + ## 专题文档(活跃维护) | 文档 | 用途 | diff --git a/docs/standards/coding-standards.md b/docs/standards/coding-standards.md new file mode 100644 index 0000000..dcb51c8 --- /dev/null +++ b/docs/standards/coding-standards.md @@ -0,0 +1,828 @@ +# Next_Edu 编码规范 + +> 版本:1.0(2026-06-17 适配当前项目) +> 依据:Google TypeScript Style + Airbnb React + Next.js 16 + Tailwind v4 最佳实践 +> 适用范围:Next_Edu K12 智慧教务系统(单应用 + 模块化架构) +> 关联文档: +> - [项目规则](../../.trae/rules/project_rules.md) +> - [架构影响地图](../architecture/004_architecture_impact_map.md) +> - [解耦路线图](../architecture/audit/01_decoupling_roadmap.md) + +--- + +## 目录 + +1. [项目原则与理念](#一项目原则与理念) +2. [项目结构](#二项目结构) +3. [命名规范](#三命名规范) +4. [TypeScript 强制规范](#四typescript-强制规范) +5. [React 与 Next.js 组件规范](#五react-与-nextjs-组件规范) +6. [Tailwind CSS 规范](#六tailwind-css-规范) +7. [数据获取与状态管理](#七数据获取与状态管理) +8. [路由、代理与安全](#八路由代理与安全) +9. [错误处理与可观测性](#九错误处理与可观测性) +10. [测试规范](#十测试规范) +11. [Git 工作流与提交规范](#十一git-工作流与提交规范) +12. [CI/CD 流水线](#十二cicd-流水线) +13. [可访问性(A11y)规范](#十三可访问性a11y规范) +14. [文档与交付物](#十四文档与交付物) +15. [统一工具配置](#十五统一工具配置) +16. [代码审查清单](#十六代码审查清单) + +--- + +## 一、项目原则与理念 + +1. **可读性优先于机巧**:代码首先是写给队友看的 +2. **显式优于隐式**:避免魔法值、隐式类型转换、隐式全局副作用 +3. **单一职责**:每个文件、函数、组件只做一件事,衡量标准是"能否用一句话描述它" +4. **防御性编程**:永远假设输入可能是 null/undefined 或非法格式 +5. **工具强制一致性**:风格、格式、类型由 ESLint、Prettier、TypeScript 自动保证 +6. **架构图优先**:任何任务开始前先查阅 [004 架构影响地图](../architecture/004_architecture_impact_map.md),按图索骥 +7. **模块封装**:模块间不直接查询对方 DB 表,必须通过 data-access 函数 + +--- + +## 二、项目结构 + +### 2.1 顶层结构(单应用 + 模块化) + +本项目**不是 Monorepo**,采用单 Next.js 应用 + 严格模块化架构: + +``` +root/ +├─ src/ +│ ├─ app/ # App Router 路由层 +│ │ ├─ (auth)/ # 路由组:认证页面 +│ │ ├─ (dashboard)/ # 路由组:业务页面(admin/teacher/student/parent) +│ │ ├─ api/ # REST API 路由 +│ │ ├─ globals.css # Tailwind v4 指令 + CSS 变量设计令牌 +│ │ ├─ layout.tsx # 根布局(Provider 组合) +│ │ └─ page.tsx # 首页 +│ ├─ modules/ # 业务模块层(26 个模块) +│ │ ├─ exams/ # 每个模块标准结构见 2.2 +│ │ ├─ homework/ +│ │ ├─ classes/ +│ │ └─ ... +│ ├─ shared/ # 基础设施层(被依赖方,不反向依赖) +│ │ ├─ components/ # 共享组件(ui/ + a11y/ + 顶层) +│ │ ├─ db/ # Drizzle ORM(schema.ts + relations.ts + index.ts) +│ │ ├─ hooks/ # 全局自定义 Hook +│ │ ├─ lib/ # 纯工具函数(auth-guard, ai, permissions, ...) +│ │ └─ types/ # 公共类型定义 +│ ├─ auth.ts # NextAuth 配置(根模块) +│ ├─ proxy.ts # Next.js 16 代理(原 middleware.ts) +│ └─ env.mjs # 环境变量校验(@t3-oss/env-nextjs + Zod) +├─ tests/ # 测试目录 +│ ├─ e2e/ # Playwright E2E 测试 +│ ├─ integration/ # 集成测试 +│ ├─ visual/ # 视觉回归测试 +│ └─ setup/ # 测试 setup +├─ scripts/ # 运维脚本(db/backup/security/dr) +├─ docs/ # 文档(架构/审查/专题/设计) +├─ drizzle/ # 数据库迁移 +└─ .gitea/workflows/ # CI/CD 流水线 +``` + +### 2.2 模块标准结构 + +每个业务模块遵循**统一结构**,职责分离: + +``` +src/modules/[module]/ +├─ actions.ts # Server Actions(编排层:权限校验 + 调用 data-access + revalidate) +├─ data-access.ts # 数据访问层(DB CRUD,仅服务端,可拆分为多个 data-access-*.ts) +├─ schema.ts # Zod 验证 schema(可选,按需) +├─ types.ts # 模块类型定义 +├─ components/ # 模块专属组件 +│ └─ [feature]/ # 复杂功能可分子目录 +└─ hooks/ # 模块专属 Hook(可选) +``` + +**分层规则**(严格单向依赖): + +``` +app/ ──▶ modules/ ──▶ shared/ + ▲ + │ + 禁止反向依赖 +``` + +- `app/` 只能调用 `modules/` 的 Server Actions 和 data-access 函数,**不直接访问 DB** +- `modules/` 之间通过对方 data-access 通信,**不直接查询对方 DB 表** +- `shared/` 是被依赖方,**不得反向依赖** `@/auth`、`@/proxy` 或任何 `modules/*` +- `src/auth.ts` 和 `src/proxy.ts` 位于根目录,属于应用层 + +**文件拆分规则**(当 data-access 过大时): + +``` +src/modules/classes/ +├─ data-access.ts # 核心 CRUD +├─ data-access-stats.ts # 统计查询 +├─ data-access-schedule.ts # 课表查询 +└─ data-access-grades.ts # 成绩汇总 +``` + +### 2.3 路由组织 + +- 使用路由组 `(group)` 组织相同布局的页面,不影响 URL +- 动态路由 `[slug]`、捕获所有 `[...slug]`、可选捕获 `[[...slug]]` 按需使用 +- 路由命名一律使用**小写与连字符**(`user-profile`) +- 每个路由段应提供 `loading.tsx`(骨架屏)和 `error.tsx`(错误边界) + +### 2.4 核心原则 + +- 页面组件(`page.tsx`)必须使用**默认导出**;布局、加载、错误使用**具名导出** +- 每个路由段都必须提供 `error.tsx`,不得出现未捕获异常导致白屏 +- `loading.tsx` 必须提供骨架屏或最小可感知的加载状态,**不得使用全局 spin 遮罩** +- 服务端数据获取通过模块的 `data-access.ts`,标记 `import "server-only"` 以防客户端误用 + +--- + +## 三、命名规范 + +### 3.1 文件与目录 + +| 对象 | 命名风格 | 示例 | +|------|---------|------| +| 目录 | kebab-case | `user-profile/`, `class-detail/` | +| 组件文件 | PascalCase | `UserProfile.tsx`, `ExamForm.tsx` | +| Hook 文件 | camelCase | `useAuth.ts`, `useExamPreview.ts` | +| 工具函数文件 | camelCase | `formatCurrency.ts`, `auditLogger.ts` | +| 类型定义文件 | camelCase | `user.ts`, `permissions.ts` | +| 测试文件 | `*.test.ts` / `*.spec.ts` | `utils.test.ts`, `auth.spec.ts` | +| Server Action | `actions.ts` 或 `xxx-actions.ts` | `actions.ts`, `actions-analytics.ts` | +| Data Access | `data-access.ts` 或 `data-access-*.ts` | `data-access.ts`, `data-access-stats.ts` | +| 代理(Next.js 16) | `proxy.ts` | `src/proxy.ts` | +| 常量文件 | camelCase | `navigation.ts`, `permissions.ts` | +| 环境变量 | UPPER_SNAKE_CASE | `DATABASE_URL`, `NEXTAUTH_SECRET` | + +### 3.2 变量、函数、类 + +- **变量**:camelCase。布尔值用 `is/has/can/should` 前缀(`isVisible`, `hasError`) +- **函数**:camelCase,动词开头(`fetchUser`, `handleSubmit`, `validateForm`) +- **常量**:UPPER_SNAKE_CASE(`MAX_RETRY_COUNT`, `API_BASE_URL`) +- **类与接口**:PascalCase。接口**不加** `I` 前缀(Google 风格) +- **类型别名**:PascalCase,如 `type UserId = string` +- **泛型参数**:使用描述性名称,如 `TData`, `TResponse`(避免单字母 `T`) +- **枚举**:推荐联合类型 + 字符串字面量(tree-shaking 友好)。如必须用枚举,成员名 PascalCase + +### 3.3 组件与 Props + +- 组件名必须为**多词**(`UserProfile` 而非 `Profile`),以免与 HTML 元素冲突 +- Props 类型命名为 `组件名Props`(`UserProfileProps`),定义在组件文件顶部 +- 事件回调 Props 使用 `on` 前缀(`onSave`, `onClose`) +- `children` 必须显式声明类型 `React.ReactNode` +- **不使用 `React.FC`**,直接用函数声明 + 显式标注 props 类型(Google 风格) + +```tsx +// 推荐方式 +interface UserCardProps { + user: User; + onSelect: (id: string) => void; + children?: React.ReactNode; +} + +export function UserCard({ user, onSelect, children }: UserCardProps) { + // ... +} +``` + +--- + +## 四、TypeScript 强制规范 + +### 4.1 配置(tsconfig.json) + +当前项目配置需升级以符合规范。**目标配置**: + +```json +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["dom", "dom.iterable", "ES2022"], + "strict": true, + "noUncheckedIndexedAccess": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "moduleResolution": "bundler", + "esModuleInterop": true, + "skipLibCheck": true, + "module": "ESNext", + "jsx": "react-jsx", + "incremental": true, + "noEmit": true, + "paths": { + "@/*": ["./src/*"] + } + } +} +``` + +**当前差异**(需逐步升级): +- `target`: `ES2017` → `ES2022` +- 缺失 `noUncheckedIndexedAccess`(数组/对象索引返回 `T | undefined`) +- 缺失 `noImplicitReturns`(函数所有分支必须返回) +- 缺失 `noFallthroughCasesInSwitch` + +### 4.2 类型规则 + +1. **禁止 `any`**:未知类型用 `unknown` 并做类型守卫。若极特殊情况必须使用,需 `// eslint-disable-next-line @typescript-eslint/no-explicit-any` 并注释原因 + +2. **优先 `interface` 描述对象形状**,`type` 用于联合、交叉、映射类型 + +3. **不使用 `as` 断言**,除非从 `unknown` 强制转换或在测试中(需注释原因)。可用 `satisfies` 保持类型推导 + +4. **函数返回值必须显式标注**,特别是 `Promise` + +5. **可选链后禁止跟非空断言 `!`**(`x?.y!` 是矛盾的) + +6. **所有仅用于类型的导入必须使用 `import type`** + +```typescript +import type { User, Permission } from "@/shared/types/permissions"; +``` + +7. **避免 `object` 或 `{}` 作为类型**,使用 `Record` 或具体接口 + +8. **泛型使用有意义的名称**;若函数只有一处使用,不一定需要泛型 + +### 4.3 导入顺序(强制执行) + +```typescript +// 1. React 相关 +import React from "react"; +// 2. 第三方库 +import { z } from "zod"; +import { useQuery } from "@tanstack/react-query"; +// 3. 内部绝对路径(使用别名 @/) +import { Button } from "@/shared/components/ui/button"; +import { requirePermission } from "@/shared/lib/auth-guard"; +// 4. 相对路径导入 +import { formatDate } from "../utils"; +import { getExams } from "./data-access"; +// 5. 类型导入 +import type { User } from "@/shared/types/permissions"; +``` + +使用 `eslint-plugin-import` 规则 `import/order` 自动排序,分组间空一行。 + +--- + +## 五、React 与 Next.js 组件规范 + +### 5.1 组件定义 + +- 组件必须为**纯函数**,使用 `function` 声明(非箭头函数,Google 风格) +- 页面组件(`page.tsx`)使用**默认导出**;其余所有组件使用**具名导出** +- **禁止在渲染期间**修改外部变量、执行网络请求、读取/写入 DOM(除 ref 初始化) + +### 5.2 服务端组件 vs 客户端组件 + +- **默认服务端组件**。只有需要交互(事件处理)、状态(`useState`/`useReducer`)、效果(`useEffect`)或浏览器 API 时,才在文件顶部添加 `"use client"` 指令 +- `"use client"` 必须位于文件**第一行**,之后空一行再写代码 +- 客户端组件应尽可能小而聚焦。将需要交互的局部提取为客户端组件,外层容器保持服务端渲染 +- **禁止在服务端组件中使用** `useState`, `useEffect`, `onClick` 等客户端特性 + +```tsx +// 容器页面(服务端) +import { UserList } from "@/modules/users/components/user-list"; +import { getUsers } from "@/modules/users/data-access"; + +export default async function UsersPage() { + const users = await getUsers(); + return ; +} +``` + +### 5.3 组件拆分指南 + +本项目采用**企业级行数规范**(见 [项目规则](../../.trae/rules/project_rules.md)): + +| 文件类型 | 建议行数 | 硬性上限 | +|---------|---------|---------| +| 配置/常量/类型定义文件 | 无限制 | 无限制 | +| React 组件 | ≤ 500 行 | 800 行(复杂表单/大型表格) | +| Server Actions / Data Access | ≤ 800 行 | 1000 行 | +| 工具函数 | ≤ 40 行 | - | +| 自定义 Hook | ≤ 80 行 | - | + +**超过建议行数时的拆分信号**: +1. **语义边界**:子模块能用一个明确名称独立描述其作用 +2. **状态边界**:有独立的 `useState`/`useEffect` 逻辑,或生命周期明显不同 +3. **复用潜力**:某段 UI 或逻辑可能在另一页面使用 +4. **复杂度预警**:Hook 调用超过 3 个,或 JSX 嵌套层级超过 4 层 +5. **可测试性**:若要对组件的一部分逻辑编写单元测试,说明该部分应该独立 + +### 5.4 Hook 规范 + +- 命名以 `use` 开头,驼峰式 +- **单一职责**:一个 Hook 只做一件事 +- 返回值使用**对象形式**(非数组),方便使用者按需提取 + +```ts +const { data, isLoading, error } = useUser(userId); +``` + +- 必须编写 JSDoc,描述用途、参数、返回值和可能副作用 +- 所有 `useEffect` 必须提供**清理函数**(如订阅、定时器) +- `useEffect` 依赖数组必须**完整**,不得遗漏响应式变量。若确实需要忽略,用 `// eslint-disable-next-line react-hooks/exhaustive-deps` 并注释原因 + +--- + +## 六、Tailwind CSS 规范 + +### 6.1 核心策略 + +本项目使用 **Tailwind v4**,采用 **CSS 变量设计令牌**(在 `globals.css` 中定义),而非传统的 `tailwind.config.ts` 扩展。 + +- **移动优先**:所有类名从无前缀(移动端)开始,逐步通过 `sm:`, `md:`, `lg:`, `xl:`, `2xl:` 增强 +- **类名组织顺序**:布局 → 盒模型 → 排版 → 背景 → 边框 → 效果 → 状态 +- **可读性**:当单个元素类名超过 10 个时,考虑提取为组件 + +### 6.2 类名编写最佳实践 + +使用 `cn()` 工具函数(基于 `clsx` + `tailwind-merge`)管理条件类名: + +```tsx +import { cn } from "@/shared/lib/utils"; + +