feat(P2): 实现质量保障类5项功能(无障碍/视觉回归/通知渠道/漏洞扫描/灾备)

## 新增功能

### 1. 屏幕阅读器兼容性增强(a11y)
- 无障碍工具库:src/shared/lib/a11y.ts
- aria-live Hook:src/shared/hooks/use-aria-live.ts
- a11y 组件:skip-link/visually-hidden/focus-trap/aria-status
- 增强 UI:table.tsx 系统性 ARIA role,dialog.tsx aria-modal
- 审计文档:docs/accessibility/a11y-audit.md(WCAG 2.1 AA 清单)

### 2. 视觉回归测试
- 测试套件:tests/visual/(homepage + 3 个 dashboard)
- 3 视口(desktop/tablet/mobile)× 2 主题(light/dark)
- 动态元素遮罩,避免误报
- playwright.config.ts 新增 visual-chromium 项目
- 文档:docs/testing/visual-regression.md

### 3. 短信/微信推送渠道集成
- 新模块:src/modules/notifications/
- 4 个渠道:SMS(阿里云/腾讯云)、WeChat(公众号)、Email(SMTP)、In-App
- 分发器按用户偏好并行多渠道发送
- 外部 SDK 动态 import,Mock 模式开发可用
- 文档:docs/notifications/channels.md

### 4. 漏洞扫描 CI 集成
- CI security-scan job:npm audit + Snyk + Trivy FS + OWASP ZAP
- 独立工作流 security.yml:每周一深度扫描 + 容器镜像扫描
- 配置:suppressions.json + .trivyignore
- 本地脚本:security-scan.sh/ps1
- 文档:docs/security/scanning.md(SLA 分级)

### 5. 灾备方案
- 脚本:backup-verify/backup-offsite-sync/dr-drill/failover/health-check
- CI 增强:备份后校验+异地同步,每周灾备演练
- 独立工作流 dr-drill.yml:每周一凌晨 4 点自动演练
- 文档:docs/dr/dr-plan.md(RTO 4h/RPO 24h)+ dr-runbook.md(6 故障场景)

## 验证
- npx tsc --noEmit:0 错误
- npm run lint:0 错误 0 警告
This commit is contained in:
SpecialX
2026-06-17 20:18:29 +08:00
parent b86255f0ea
commit 6585e10c6f
53 changed files with 7491 additions and 37 deletions

View File

@@ -214,6 +214,30 @@
- 功能:默认存储 Provider 单例,替换此实例可迁移到 OSS/S3
- 被以下模块使用:`app/api/files/batch-delete/route.ts`
#### `useA11yId`
- 签名:`useA11yId(prefix: string): string`
- 功能:基于 `React.useId` 生成 SSR 安全的唯一 ID用于 `aria-describedby``aria-labelledby`
- 依赖:`react`
- 被以下模块使用待扩展表单组件、a11y 组件)
#### `mergeA11yProps`
- 签名:`mergeA11yProps<T extends Record<string, unknown>>(...props: (T | undefined | null | false)[]): T`
- 功能:合并多组 aria/data 属性,普通属性后者覆盖前者,`aria-*`/`data-*` 字符串属性以空格拼接
- 依赖:无
- 被以下模块使用:待扩展
#### `describeInput`
- 签名:`describeInput(describedBy?: string, error?: string, hint?: string): { ariaDescribedBy?: string; ariaInvalid?: boolean }`
- 功能:计算输入框的 `aria-describedby`(合并多个 ID`aria-invalid`error 存在则为 true
- 依赖:无
- 被以下模块使用:待扩展(表单组件)
#### `loadingAria`
- 签名:`loadingAria(isLoading: boolean): { ariaBusy: boolean; ariaLive: "polite" | "assertive" }`
- 功能:提供加载状态的 `aria-busy``aria-live=polite` 属性
- 依赖:无
- 被以下模块使用:待扩展
### 导出常量与实例
#### `Permissions` (常量对象)
@@ -300,7 +324,31 @@
- 基于:`@radix-ui/react-switch`
- Props: Radix Switch Root props`checked`, `onCheckedChange`, `disabled`, `id`, `aria-label` 等)
- 功能:开关切换 UI 组件shadcn 风格checked/unchecked 两态)
- 被使用:`settings/components/notification-preferences-form.tsx`
- 被使用settings/components/notification-preferences-form.tsx
#### `SkipLink`
- 文件:`components/a11y/skip-link.tsx`
- Props: `{ href?, children?, ...AnchorHTMLAttributes }`,默认 href=`#main-content`,默认文字"跳转到主内容"
- 功能:跳转链接组件,视觉隐藏,获得焦点时高对比度显示,供键盘用户跳过导航直达主内容
- 被使用:待替换 `app/(dashboard)/layout.tsx` 中的内联 skip-link
#### `VisuallyHidden`
- 文件:`components/a11y/visually-hidden.tsx`
- Props: `{ children?, ...HTMLAttributes }`
- 功能:视觉隐藏但屏幕阅读器可读,用于图标按钮文字描述、表单辅助说明
- 被使用:待扩展
#### `FocusTrap`
- 文件:`components/a11y/focus-trap.tsx`
- Props: `{ children, active?, initialFocusRef?, restoreFocus?, className? }`
- 功能:焦点陷阱组件,捕获 Tab/Shift+Tab 在容器内循环,支持初始焦点与焦点恢复,用于模态框/对话框
- 被使用待扩展Dialog/Sheet 自定义场景)
#### `AriaStatus`
- 文件:`components/a11y/aria-status.tsx`
- Props: `{ children?, politeness?, atomic?, ...HTMLAttributes }`,默认 politeness=`polite`atomic=`true`
- 功能ARIA 状态通知区域,渲染 `aria-live` 区域role=status用于页面级状态通知如"加载中"、"已保存"
- 被使用:待扩展
### 导出 Hooks
@@ -2411,6 +2459,112 @@
---
## 模块notifications
### 模块职责
通知渠道集成层:基于用户通知偏好(`notification_preferences` 表)将通知分发到站内消息 / SMS / 微信公众号 / 邮件多渠道。所有渠道实现统一 `NotificationChannelSender` 接口dispatcher 按偏好并行发送。支持 Mock 模式(开发环境无需外部服务即可运行)。
### 模块路径
`src/modules/notifications`
### 依赖关系
- 依赖 `shared`db, auth-guard, types
- 依赖 `messaging`(复用 `notification-preferences.getNotificationPreferences``data-access.createNotification`
- 所有渠道文件首行 `import "server-only"`,外部 SDK 使用动态 import
### 导出函数 (actions.ts)
> 使用 `requirePermission(MESSAGE_SEND)` 校验权限(项目无独立 NOTIFICATION_SEND 权限点,复用 MESSAGE_SEND
| 函数 | 权限 | 核心功能 |
|------|------|---------|
| `sendNotificationAction` | MESSAGE_SEND | 发送通知给指定用户(按偏好多渠道分发) |
| `sendClassNotificationAction` | MESSAGE_SEND | 发送班级通知(批量发送给班级所有学生;教师只能给自己所教班级发送,通过 dataScope 校验) |
### 导出函数 (dispatcher.ts)
> 文件标记 `"server-only"`。
#### `sendNotification`
- 签名:`sendNotification(payload: NotificationPayload): Promise<ChannelSendResult[]>`
- 功能:读取用户通知偏好 + 联系方式按偏好选择渠道in_app 总是启用sms 需 smsEnabled+phoneemail 需 emailEnabled+emailwechat 需 pushEnabled+openId并行发送记录日志
- 依赖:`data-access.getUserNotificationPreferences`, `data-access.getUserContactInfo`, `data-access.logNotificationSendBatch`, 各渠道 `createXxxSender`
- 被使用:`sendNotificationAction`, `sendClassNotificationAction`
#### `sendBatchNotifications`
- 签名:`sendBatchNotifications(payloads: NotificationPayload[]): Promise<ChannelSendResult[][]>`
- 功能:批量发送通知(每个用户独立选择渠道,并行发送)
- 依赖:`sendNotification`
- 被使用:`sendClassNotificationAction`
### 导出函数 (data-access.ts)
> 文件标记 `"server-only"`。
| 函数 | 签名 | 核心功能 |
|------|------|---------|
| `getUserNotificationPreferences` | `(userId: string) => Promise<NotificationPreferences>` | 获取用户通知偏好(复用 messaging.notification-preferences |
| `getUserContactInfo` | `(userId: string) => Promise<ChannelRecipient>` | 获取用户联系方式phone/emailwechatOpenId 暂不支持users 表无此字段React cache 包装) |
| `logNotificationSend` | `(result: ChannelSendResult) => void` | 记录单条发送日志(当前 console.info未来可扩展 notification_logs 表) |
| `logNotificationSendBatch` | `(results: ChannelSendResult[]) => void` | 批量记录发送日志 |
### 渠道实现 (channels/)
> 所有渠道文件首行 `import "server-only"`,外部 SDK 使用动态 import 避免增加构建体积。
| 文件 | 渠道 | 工厂函数 | 说明 |
|------|------|---------|------|
| `sms-channel.ts` | sms | `createSmsSender()` | 支持 aliyun/tencent/mock根据 `SMS_PROVIDER` 环境变量选择);模板变量替换 title/content |
| `wechat-channel.ts` | wechat | `createWechatSender()` | 微信公众号模板消息access_token 带缓存(提前 5 分钟刷新);配置完整用真实发送器,否则 Mock |
| `email-channel.ts` | email | `createEmailSender()` | Nodemailer SMTPHTML 模板按 type 着色;配置 EMAIL_HOST 启用,否则 Mock |
| `in-app-channel.ts` | in_app | `createInAppSender()` | 复用 messaging.data-access.createNotification 写入 message_notifications 表;总是启用 |
| `types.ts` | - | - | 渠道接口定义NotificationChannelSender, ChannelRecipient |
### 环境变量
| 变量 | 默认值 | 说明 |
|------|--------|------|
| `SMS_PROVIDER` | `mock` | SMS 渠道 provideraliyun/tencent/mock |
| `SMS_ACCESS_KEY_ID` | - | SMS AccessKey ID |
| `SMS_ACCESS_KEY_SECRET` | - | SMS AccessKey Secret |
| `SMS_SIGN_NAME` | - | SMS 签名 |
| `SMS_TEMPLATE_CODE` | - | SMS 模板 ID |
| `WECHAT_APP_ID` | - | 微信公众号 AppID |
| `WECHAT_APP_SECRET` | - | 微信公众号 AppSecret |
| `WECHAT_TEMPLATE_ID` | - | 微信模板消息 ID |
| `EMAIL_HOST` | - | SMTP 主机(配置后启用真实发送) |
| `EMAIL_PORT` | `587` | SMTP 端口 |
| `EMAIL_USER` | - | SMTP 用户名 |
| `EMAIL_PASS` | - | SMTP 密码 |
| `EMAIL_FROM` | `noreply@example.com` | 发件人地址 |
### 类型/接口
#### `NotificationChannel`
- 定义:`"in_app" | "email" | "sms" | "wechat"`
- 被使用:所有渠道文件, dispatcher
#### `NotificationPayload`
- 定义:`{ userId, title, content, type: "info"|"warning"|"error"|"success", metadata?, actionUrl? }`
- 被使用dispatcher, actions, 所有渠道
#### `ChannelSendResult`
- 定义:`{ channel, success, messageId?, error?, sentAt }`
- 被使用dispatcher, actions, 所有渠道
#### `NotificationChannelSender`(接口)
- 定义:`{ channel: NotificationChannel, send(payload, recipient): Promise<ChannelSendResult>, sendBatch(items): Promise<ChannelSendResult[]> }`
- 被使用:所有渠道实现, dispatcher
#### `ChannelRecipient`(接口)
- 定义:`{ userId, phone?, email?, wechatOpenId? }`
- 被使用:所有渠道, data-access.getUserContactInfo
### 文档
- `docs/notifications/channels.md`通知渠道配置说明、Mock 模式、生产环境配置、扩展新渠道指南
---
## 模块attendance
### 模块职责
@@ -2871,6 +3025,7 @@
| **course-plans** | db,auth-guard.requirePermission,types | auth | - | - | - | - | - | data-access.getAdminClasses,getStaffOptions | data-access.getAcademicYears | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
| **parent** | db,auth-guard(requireAuth),types | auth | - | data-access.getStudentHomeworkAssignments,getStudentDashboardGrades | - | - | data-access.getStudentClasses,getStudentSchedule | - | - | - | - | - | - | - | - | data-access.getStudentGradeSummary | - | - | - | - | - | - |
| **messaging** | db,auth-guard(requirePermission,requireAuth),types | auth | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
| **notifications** | db,auth-guard.requirePermission,types | auth | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | notification-preferences,data-access.createNotification | - | - | - | - | - |
| **attendance** | db,auth-guard.requirePermission,types | auth | - | - | - | - | - | data-access.getTeacherClasses,getAdminClasses | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
| **scheduling** | db,auth-guard(requirePermission,getAuthContext),types | auth | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
| **proctoring** | db,auth-guard(requirePermission,requireAuth),types,components.ui,hooks.usePermission | auth | schema.exams,examSubmissions,examProctoringEvents | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
@@ -3143,8 +3298,37 @@
| Job | 触发条件 | 说明 |
|-----|---------|------|
| `build-deploy` | push/PR to main | 构建、测试、部署到 Docker自托管 runner CDCD |
| `security-audit` | push/PR to main | 依赖安全审计:`npm audit` moderate/critical 检查,上传 audit-report.json artifact |
| `scheduled-backup` | schedule cron `0 2 * * *` | 每天凌晨 2 点执行数据库备份上传 backups/ artifact保留 30 天) |
| `security-scan` | push/PR to mainneeds: build-deploy | 完整安全扫描npm audit + Snyk + Trivy FS + OWASP ZAP 基线扫描,所有步骤 continue-on-error上传 security-reports artifactaudit-report.json/trivy-fs-report.json/snyk.sarif |
| `scheduled-backup` | schedule cron `0 2 * * *` | 每天凌晨 2 点执行数据库备份→校验完整性→异地同步,上传 backups/ artifact保留 30 天) |
| `backup-verify` | scheduleneeds: scheduled-backup | 下载备份 artifact,独立校验备份完整性,运行健康检查,上传 backup-verify-report artifact保留 7 天) |
| `weekly-dr-drill` | scheduleneeds: backup-verify,每周触发) | 灾备演练:从备份恢复到测试数据库,验证数据完整性,上传 dr-drill-report artifact保留 90 天) |
### 灾备演练工作流 (`.gitea/workflows/dr-drill.yml`)
独立灾备演练工作流,触发方式:定时 `cron 0 4 * * 1`(每周一凌晨 4 点)/ 手动 `workflow_dispatch`(可指定 backup_file、no_cleanup
| 步骤 | 说明 |
|------|------|
| 安装 MySQL 客户端 | apt-get install mysql-client |
| 准备备份 | 下载 db-backup artifact 或现场执行 backup-db.sh |
| 执行演练 | 运行 scripts/dr-drill.sh创建测试库→恢复→完整性检查→冒烟测试→清理→报告 |
| 上传报告 | dr-drill-report artifact保留 90 天) |
| 失败通知 | webhook 通知运维团队DR_NOTIFICATION_WEBHOOK |
### 安全扫描工作流 (`.gitea/workflows/security.yml`)
独立深度安全扫描工作流,触发方式:定时 `cron 0 3 * * 1`(每周一凌晨 3 点)/ 手动 `workflow_dispatch`(可指定 target_url、skip_dast
| 步骤 | 工具 | 类型 | 输出 |
|------|------|------|------|
| 依赖扫描 | npm audit | 依赖 | audit-report.json |
| 深度依赖 + 静态分析 | Snykseverity-threshold=medium | 依赖 + 代码 | snyk.sarif |
| 文件系统扫描 | Trivy fs | 代码 + 依赖 | trivy-fs-report.json |
| 容器镜像扫描 | Trivy image构建 nextjs-app:scan 镜像) | 容器 | trivy-image-report.json |
| DAST | OWASP ZAP baseline | 动态 | 控制台报告 |
| 汇总报告 | shell + jq | 汇总 | security-summary.md |
所有报告上传为 artifact `security-reports-full`。安全扫描配置文件:`.gitea/suppressions.json`Snyk 漏洞抑制)、`.trivyignore`Trivy CVE 忽略列表)。
### 运维脚本 (`scripts/`)
@@ -3152,9 +3336,17 @@
|------|------|
| `scripts/audit.sh` | Bash 依赖审计脚本,运行 `npm audit --audit-level=moderate`,失败时生成 audit-report.json |
| `scripts/audit.ps1` | PowerShell 版本依赖审计脚本Windows 环境) |
| `scripts/security-scan.sh` | Bash 本地安全扫描脚本npm audit + Trivy fs彩色报告退出码 0=无高危/1=有高危 |
| `scripts/security-scan.ps1` | PowerShell 版本本地安全扫描脚本Windows 环境) |
| `scripts/backup-db.sh` | MySQL 数据库备份脚本,从 DATABASE_URL 解析连接信息gzip 压缩备份,保留 30 天 |
| `scripts/restore-db.sh` | MySQL 数据库恢复脚本,从指定备份文件恢复 |
| `scripts/test-backup.sh` | 备份流程测试脚本,执行一次备份并验证 |
| `scripts/backup-verify.sh` | 备份完整性校验脚本:检查文件存在/大小/gzip 完整性/SQL 内容结构/SQL 语法(可选,需 DATABASE_URL,退出码 0=通过/1=失败 |
| `scripts/backup-offsite-sync.sh` | 异地备份同步脚本:支持 S3/OSS/NFS 后端,同步后校验文件数量,清理远程过期备份(保留 90 天),使用 aws-cli/rclone/ossutil/rsync |
| `scripts/dr-drill.sh` | 灾备演练脚本Bash创建测试库→从备份恢复→数据完整性检查→冒烟测试→清理→生成报告到 docs/dr/reports/,退出码 0=成功/1=失败 |
| `scripts/dr-drill.ps1` | 灾备演练脚本Windows PowerShell 5.1+):功能同 Bash 版本 |
| `scripts/failover.sh` | 故障切换脚本:检测主库健康→提升备库→更新应用配置→重启应用→验证切换,支持手动/半自动/演练模式 |
| `scripts/health-check.sh` | 健康检查脚本:检查应用 HTTP/数据库连接/磁盘空间/备份新鲜度,输出 JSON 报告,退出码 0=健康/1=异常 |
### package.json 脚本
@@ -3162,8 +3354,41 @@
|------|------|------|
| `audit` | `npm audit --audit-level=moderate` | 依赖安全审计 |
| `audit:report` | `npm audit --json > audit-report.json` | 生成 JSON 审计报告 |
| `security:audit` | `npm audit --audit-level=moderate` | 依赖安全审计security 别名) |
| `security:scan` | `bash scripts/security-scan.sh` | 本地完整安全扫描npm audit + Trivy fs |
| `backup` | `bash scripts/backup-db.sh` | 执行数据库备份 |
| `restore` | `bash scripts/restore-db.sh` | 执行数据库恢复 |
| `dr:backup-verify` | `bash scripts/backup-verify.sh` | 校验备份完整性 |
| `dr:offsite-sync` | `bash scripts/backup-offsite-sync.sh` | 异地备份同步 |
| `dr:drill` | `bash scripts/dr-drill.sh` | 灾备演练Bash |
| `dr:drill:ps1` | `powershell -ExecutionPolicy Bypass -File scripts/dr-drill.ps1` | 灾备演练PowerShell |
| `dr:health-check` | `bash scripts/health-check.sh` | 健康检查JSON 报告) |
| `dr:failover` | `bash scripts/failover.sh` | 故障切换 |
### 灾备文档 (`docs/dr/`)
| 文档 | 用途 |
|------|------|
| `docs/dr/dr-plan.md` | 灾备计划文档RTO/RPO 定义4h/24h、备份策略、故障切换流程、联系人列表、恢复步骤 |
| `docs/dr/dr-runbook.md` | 灾备操作手册:数据库故障/应用故障/备份失败/异地同步失败/演练失败/磁盘不足场景的诊断与处理 |
| `docs/dr/reports/` | 灾备演练报告存档目录Markdown 格式,由 dr-drill.sh 生成) |
| `docs/dr/logs/` | 故障切换日志目录(由 failover.sh 生成) |
### 灾备环境变量 (`.env.example`)
| 变量 | 用途 |
|------|------|
| `BACKUP_OFFSITE_BACKEND` | 异地备份后端类型: s3/oss/nfs/none |
| `BACKUP_OFFSITE_REMOTE` | 远程存储路径 |
| `BACKUP_OFFSITE_BUCKET` | 存储桶名称(仅 s3/oss |
| `BACKUP_OFFSITE_ACCESS_KEY` | 访问密钥 |
| `BACKUP_OFFSITE_SECRET_KEY` | 秘密密钥 |
| `BACKUP_OFFSITE_REGION` | 区域(默认 us-east-1 |
| `BACKUP_OFFSITE_RETENTION_DAYS` | 远程备份保留天数(默认 90 |
| `DR_DRILL_TEST_DB` | 演练测试数据库名(默认 next_edu_dr_drill |
| `HEALTH_CHECK_URL` | 应用健康检查 URL默认 http://localhost:8015 |
| `DATABASE_URL_STANDBY` | 备库连接 URL故障切换时使用 |
| `FAILOVER_APP_NAME` | 应用容器名(默认 nextjs-app |
---
@@ -3181,10 +3406,40 @@
### Playwright 配置 (`playwright.config.ts`)
- `testDir`: `./tests/e2e`
- `testDir`: `./tests`(顶层,由各 project 通过 `testDir` 限定范围)
- `baseURL`: `http://127.0.0.1:3000`
- `webServer`: 自动启动 `npm run dev`,端口 3000超时 180s
- `webServer.env`: 注入 `SKIP_ENV_VALIDATION=1``NEXTAUTH_SECRET``NEXTAUTH_URL``DATABASE_URL`(测试库)
- `projects`: chromiumCI 通道为 undefined本地为 chrome
- `projects`:
- `chromium`E2E 测试,`testDir: ./tests/e2e`CI 通道为 undefined本地为 chrome
- `visual-chromium`(视觉回归测试,`testDir: ./tests/visual`CI 通道为 undefined本地为 chrome
- `snapshotPathTemplate`: `{testDir}/__screenshots__/{testFilePath}/{arg}{ext}`
- `expect.toHaveScreenshot`: `maxDiffPixelRatio: 0.01``animations: "disabled"``caret: "hide"`
- `retries`: CI 2 次,本地 0 次
- `workers`: CI 2 个,本地默认
---
## 视觉回归测试 (`tests/visual/`)
| 测试文件 | 覆盖范围 | 依赖 |
|---------|---------|------|
| `homepage.spec.ts` | 登录页在 desktop/tablet/mobile × light/dark 下的快照 | 无需 DB |
| `admin-dashboard.spec.ts` | 管理员仪表盘整页 + 侧边栏/主内容区组件快照 | DATABASE_URL + admin 账号 |
| `teacher-dashboard.spec.ts` | 教师仪表盘整页 + 侧边栏/主内容区组件快照 | DATABASE_URL + teacher 账号 |
| `student-dashboard.spec.ts` | 学生仪表盘整页 + 侧边栏/主内容区组件快照 | DATABASE_URL + student 账号 |
### 视觉测试辅助 (`tests/visual/helpers/`)
| 文件 | 用途 |
|------|------|
| `auth.ts` | 登录辅助 `setupAuthState(role)``loginByUI(page, role)`,测试账号默认 admin@xiaoxue.edu.cn / 123456 |
| `visual-helpers.ts` | `setViewport``setTheme``waitForPageReady``maskDynamicElements``buildMaskOption` |
### 视觉测试配置 (`tests/visual/visual.config.ts`)
- 视口: desktop 1920×1080、tablet 768×1024、mobile 375×812
- 主题: light、dark
- 快照目录: `tests/visual/__screenshots__/`
- storageState 目录: `tests/visual/.auth/`(已加入 .gitignore
- 默认容差: `maxDiffPixelRatio: 0.01`

View File

@@ -283,6 +283,38 @@
"purpose": "生成导入模板 Buffer表头加粗+第二行填写说明+示例行)",
"deps": ["exceljs"],
"usedBy": ["users/import-export.generateUserImportTemplate"]
},
{
"name": "useA11yId",
"file": "lib/a11y.ts",
"signature": "useA11yId(prefix: string): string",
"purpose": "基于React.useId生成SSR安全的唯一ID用于aria-describedby、aria-labelledby等",
"deps": ["react"],
"usedBy": ["待扩展表单组件、a11y组件"]
},
{
"name": "mergeA11yProps",
"file": "lib/a11y.ts",
"signature": "mergeA11yProps<T extends Record<string, unknown>>(...props: (T | undefined | null | false)[]): T",
"purpose": "合并多组aria/data属性普通属性后者覆盖前者aria-*/data-*字符串属性以空格拼接",
"deps": [],
"usedBy": ["待扩展"]
},
{
"name": "describeInput",
"file": "lib/a11y.ts",
"signature": "describeInput(describedBy?: string, error?: string, hint?: string): { ariaDescribedBy?: string; ariaInvalid?: boolean }",
"purpose": "计算输入框的aria-describedby合并多个ID与aria-invaliderror存在则为true",
"deps": [],
"usedBy": ["待扩展(表单组件)"]
},
{
"name": "loadingAria",
"file": "lib/a11y.ts",
"signature": "loadingAria(isLoading: boolean): { ariaBusy: boolean; ariaLive: 'polite' | 'assertive' }",
"purpose": "提供加载状态的aria-busy与aria-live=polite属性",
"deps": [],
"usedBy": ["待扩展"]
}
],
"hooks": [
@@ -1318,6 +1350,40 @@
]
}
},
"notifications": {
"path": "src/modules/notifications",
"description": "通知渠道集成层基于用户通知偏好notification_preferences将通知分发到站内消息/SMS/微信公众号/邮件多渠道。所有渠道实现统一 NotificationChannelSender 接口dispatcher 按偏好并行发送。支持 Mock 模式(开发环境无需外部服务)。",
"exports": {
"actions": [
{ "name": "sendNotificationAction", "permission": "MESSAGE_SEND", "signature": "(payload: NotificationPayload) => Promise<ActionState<ChannelSendResult[]>>", "purpose": "发送通知给指定用户(按偏好多渠道分发)", "deps": ["requirePermission", "dispatcher.sendNotification"], "usedBy": ["待扩展"] },
{ "name": "sendClassNotificationAction", "permission": "MESSAGE_SEND", "signature": "(classId: string, payload: Omit<NotificationPayload, 'userId'>) => Promise<ActionState<ChannelSendResult[][]>>", "purpose": "发送班级通知(批量发送给班级所有学生;教师只能给自己所教班级发送)", "deps": ["requirePermission", "db.schema.classEnrollments", "db.schema.classes", "dispatcher.sendBatchNotifications"], "usedBy": ["待扩展"] }
],
"dispatcher": [
{ "name": "sendNotification", "signature": "(payload: NotificationPayload) => Promise<ChannelSendResult[]>", "file": "dispatcher.ts", "purpose": "发送单条通知:读取用户偏好+联系方式,按偏好选择渠道并行发送,记录日志", "deps": ["data-access.getUserNotificationPreferences", "data-access.getUserContactInfo", "data-access.logNotificationSendBatch", "channels.sms-channel.createSmsSender", "channels.wechat-channel.createWechatSender", "channels.email-channel.createEmailSender", "channels.in-app-channel.createInAppSender"], "usedBy": ["sendNotificationAction", "sendClassNotificationAction"] },
{ "name": "sendBatchNotifications", "signature": "(payloads: NotificationPayload[]) => Promise<ChannelSendResult[][]>", "file": "dispatcher.ts", "purpose": "批量发送通知(每个用户独立选择渠道,并行发送)", "deps": ["sendNotification"], "usedBy": ["sendClassNotificationAction"] }
],
"dataAccess": [
{ "name": "getUserNotificationPreferences", "signature": "(userId: string) => Promise<NotificationPreferences>", "file": "data-access.ts", "purpose": "获取用户通知偏好(复用 messaging.notification-preferences.getNotificationPreferences", "deps": ["messaging.notification-preferences.getNotificationPreferences"], "usedBy": ["dispatcher.sendNotification"] },
{ "name": "getUserContactInfo", "signature": "(userId: string) => Promise<ChannelRecipient>", "file": "data-access.ts", "purpose": "获取用户联系方式phone/emailwechatOpenId 暂不支持users 表无此字段)", "deps": ["shared.db", "shared.db.schema.users", "react.cache"], "usedBy": ["dispatcher.sendNotification"] },
{ "name": "logNotificationSend", "signature": "(result: ChannelSendResult) => void", "file": "data-access.ts", "purpose": "记录单条发送日志(当前使用 console.info未来可扩展 notification_logs 表)", "deps": [], "usedBy": ["logNotificationSendBatch"] },
{ "name": "logNotificationSendBatch", "signature": "(results: ChannelSendResult[]) => void", "file": "data-access.ts", "purpose": "批量记录发送日志", "deps": ["logNotificationSend"], "usedBy": ["dispatcher.sendNotification", "dispatcher.sendBatchNotifications"] }
],
"channels": [
{ "name": "createSmsSender", "file": "channels/sms-channel.ts", "purpose": "创建 SMS 渠道发送器aliyun/tencent/mock根据 SMS_PROVIDER 环境变量选择SDK 动态 import", "deps": ["环境变量: SMS_PROVIDER, SMS_ACCESS_KEY_ID, SMS_ACCESS_KEY_SECRET, SMS_SIGN_NAME, SMS_TEMPLATE_CODE"] },
{ "name": "createWechatSender", "file": "channels/wechat-channel.ts", "purpose": "创建微信渠道发送器(配置完整用真实发送器,否则 Mockaccess_token 带缓存)", "deps": ["环境变量: WECHAT_APP_ID, WECHAT_APP_SECRET, WECHAT_TEMPLATE_ID"] },
{ "name": "createEmailSender", "file": "channels/email-channel.ts", "purpose": "创建邮件渠道发送器(配置 EMAIL_HOST 用 Nodemailer SMTP否则 MockHTML 模板按 type 着色)", "deps": ["环境变量: EMAIL_HOST, EMAIL_PORT, EMAIL_USER, EMAIL_PASS, EMAIL_FROM"] },
{ "name": "createInAppSender", "file": "channels/in-app-channel.ts", "purpose": "创建站内消息渠道发送器(复用 messaging.data-access.createNotification 写入 message_notifications 表;总是启用)", "deps": ["messaging.data-access.createNotification"] }
],
"types": [
{ "name": "NotificationChannel", "type": "type", "file": "types.ts", "definition": "'in_app' | 'email' | 'sms' | 'wechat'", "usedBy": ["所有渠道文件", "dispatcher"] },
{ "name": "NotificationPayload", "type": "interface", "file": "types.ts", "definition": "{ userId, title, content, type: 'info'|'warning'|'error'|'success', metadata?, actionUrl? }", "usedBy": ["dispatcher", "actions", "所有渠道"] },
{ "name": "ChannelSendResult", "type": "interface", "file": "types.ts", "definition": "{ channel, success, messageId?, error?, sentAt }", "usedBy": ["dispatcher", "actions", "所有渠道"] },
{ "name": "NotificationChannelConfig", "type": "interface", "file": "types.ts", "definition": "{ enabled, sms?, wechat?, email? }", "usedBy": ["类型定义"] },
{ "name": "NotificationChannelSender", "type": "interface", "file": "channels/types.ts", "definition": "{ channel: NotificationChannel, send(payload, recipient), sendBatch(items) }", "usedBy": ["所有渠道实现", "dispatcher"] },
{ "name": "ChannelRecipient", "type": "interface", "file": "channels/types.ts", "definition": "{ userId, phone?, email?, wechatOpenId? }", "usedBy": ["所有渠道", "data-access.getUserContactInfo"] }
]
}
},
"attendance": {
"path": "src/modules/attendance",
"description": "学生考勤管理:教师按班级/日期点名(单条/批量)、查询考勤记录、统计出勤率/迟到率,学生/家长查看本人/子女考勤汇总,管理员查看全校考勤记录。支持班级考勤规则配置。",
@@ -1610,6 +1676,7 @@
"grades": {"dependsOn": ["shared", "auth"], "uses": {"shared": ["db", "auth-guard.requirePermission", "types.permissions", "types.action-state", "db.schema.gradeRecords", "db.schema.classes", "db.schema.classEnrollments", "db.schema.subjects", "db.schema.users", "lib.excel"], "auth": ["auth"]}},
"parent": {"dependsOn": ["shared", "auth", "homework", "classes", "grades"], "uses": {"shared": ["db", "auth-guard.requireAuth", "db.schema.parentStudentRelations", "db.schema.users", "db.schema.grades", "db.schema.classEnrollments", "db.schema.classes", "types"], "auth": ["auth"], "homework": ["data-access.getStudentHomeworkAssignments", "data-access.getStudentDashboardGrades"], "classes": ["data-access.getStudentClasses", "data-access.getStudentSchedule"], "grades": ["data-access.getStudentGradeSummary"]}},
"messaging": {"dependsOn": ["shared", "auth"], "uses": {"shared": ["db", "auth-guard.requirePermission", "auth-guard.requireAuth", "db.schema.messages", "db.schema.messageNotifications", "db.schema.notificationPreferences", "db.schema.users", "db.schema.classEnrollments", "db.schema.classes", "db.schema.grades", "types.permissions", "types.action-state"], "auth": ["auth"]}},
"notifications": {"dependsOn": ["shared", "auth", "messaging"], "uses": {"shared": ["db", "auth-guard.requirePermission", "db.schema.users", "db.schema.classEnrollments", "db.schema.classes", "types.permissions", "types.action-state"], "auth": ["auth"], "messaging": ["notification-preferences.getNotificationPreferences", "data-access.createNotification"]}},
"attendance": {"dependsOn": ["shared", "auth", "classes"], "uses": {"shared": ["db", "auth-guard.requirePermission", "db.schema.attendanceRecords", "db.schema.attendanceRules", "db.schema.classEnrollments", "db.schema.users", "db.schema.classes", "types.permissions", "types.action-state", "types.DataScope"], "auth": ["auth"], "classes": ["data-access.getTeacherClasses", "data-access.getAdminClasses"]}},
"scheduling": {"dependsOn": ["shared", "auth", "classes"], "uses": {"shared": ["db", "auth-guard.requirePermission", "auth-guard.getAuthContext", "db.schema.schedulingRules", "db.schema.scheduleChanges", "db.schema.classSchedule", "db.schema.classes", "db.schema.users", "db.schema.classSubjectTeachers", "db.schema.subjects", "db.schema.classrooms", "types.permissions", "types.action-state"], "auth": ["auth"], "classes": []}},
"diagnostic": {"dependsOn": ["shared", "auth"], "uses": {"shared": ["db", "auth-guard.requirePermission", "auth-guard.getAuthContext", "db.schema.knowledgePointMastery", "db.schema.learningDiagnosticReports", "db.schema.knowledgePoints", "db.schema.questionsToKnowledgePoints", "db.schema.examSubmissions", "db.schema.submissionAnswers", "db.schema.classEnrollments", "db.schema.classes", "db.schema.users", "types.permissions", "types.action-state", "hooks.usePermission", "components.ui.*"], "auth": ["auth"]}},
@@ -1823,19 +1890,74 @@
"trigger": "push/PR to main",
"steps": ["checkout", "cache npm", "configure npm proxy", "npm ci", "lint", "typecheck", "install playwright chromium", "integration tests", "e2e tests", "cache next.js build", "build", "prepare standalone", "deploy to docker"]
},
"security-audit": {
"security-scan": {
"runsOn": "ubuntu-latest",
"trigger": "push/PR to main",
"steps": ["checkout", "setup node 20", "npm ci", "npm audit --audit-level=moderate (continue-on-error)", "npm audit --audit-level=critical", "upload audit-report.json artifact"]
"needs": "build-deploy",
"continueOnError": true,
"steps": ["checkout", "setup node 20", "npm ci", "npm audit --audit-level=moderate + 生成 audit-report.json (continue-on-error)", "Snyk scan --severity-threshold=high --sarif-file-output=snyk.sarif (env SNYK_TOKEN, continue-on-error)", "Trivy fs scan json+table (continue-on-error)", "OWASP ZAP baseline scan target=NEXTAUTH_URL||localhost:8015 cmd_options='-a -j' (continue-on-error)", "upload security-reports artifact (audit-report.json, trivy-fs-report.json, snyk.sarif)"]
},
"scheduled-backup": {
"runsOn": "ubuntu-latest",
"trigger": "schedule cron 0 2 * * *",
"condition": "github.event_name == 'schedule'",
"steps": ["checkout", "run scripts/backup-db.sh (env DATABASE_URL, BACKUP_DIR)", "upload backups/ artifact (retention 30 days)"]
"steps": ["checkout", "run scripts/backup-db.sh (env DATABASE_URL, BACKUP_DIR)", "run scripts/backup-verify.sh (校验备份完整性)", "run scripts/backup-offsite-sync.sh (异地同步, env BACKUP_OFFSITE_*, 失败不阻塞)", "upload backups/ artifact (retention 30 days)"]
},
"backup-verify": {
"runsOn": "ubuntu-latest",
"trigger": "schedule",
"condition": "github.event_name == 'schedule'",
"needs": "scheduled-backup",
"steps": ["checkout", "download db-backup artifact", "run scripts/backup-verify.sh (独立校验)", "run scripts/health-check.sh > health-report.json", "upload backup-verify-report artifact (backups/, health-report.json, retention 7 days)"]
},
"weekly-dr-drill": {
"runsOn": "ubuntu-latest",
"trigger": "schedule (每周触发, github.run_attempt % 7 == 0)",
"condition": "github.event_name == 'schedule' && github.run_attempt % 7 == 0",
"needs": "backup-verify",
"steps": ["checkout", "run scripts/dr-drill.sh (env DATABASE_URL, DR_DRILL_TEST_DB=next_edu_dr_drill)", "upload dr-drill-report artifact (docs/dr/reports/, retention 90 days)"]
}
}
},
"drDrillWorkflow": {
"configFile": ".gitea/workflows/dr-drill.yml",
"triggers": ["schedule cron 0 4 * * 1 (每周一凌晨 4 点)", "workflow_dispatch (inputs: backup_file, no_cleanup)"],
"job": "dr-drill",
"runsOn": "ubuntu-latest",
"timeoutMinutes": 30,
"steps": [
"checkout",
"install mysql-client",
"prepare backup directory (mkdir backups docs/dr/reports)",
"download db-backup artifact (continue-on-error) 或现场执行 backup-db.sh",
"run scripts/dr-drill.sh (支持 --backup/--no-cleanup 参数)",
"upload dr-drill-report-${{ github.run_id }} artifact (docs/dr/reports/, retention 90 days)",
"on failure: webhook 通知运维团队 (DR_NOTIFICATION_WEBHOOK)"
]
},
"securityWorkflow": {
"configFile": ".gitea/workflows/security.yml",
"triggers": ["schedule cron 0 3 * * 1 (每周一凌晨 3 点)", "workflow_dispatch (inputs: target_url, skip_dast)"],
"job": "deep-security-scan",
"runsOn": "ubuntu-latest",
"continueOnError": true,
"steps": [
"checkout",
"setup node 20",
"npm ci",
"npm audit + 生成 audit-report.json (依赖扫描)",
"Snyk scan --severity-threshold=medium --sarif-file-output=snyk.sarif (env SNYK_TOKEN, 深度依赖+静态分析)",
"Trivy fs scan json+table (文件系统扫描, trivy-fs-report.json)",
"Build Next.js standalone + docker build nextjs-app:scan + Trivy image scan (容器镜像扫描, trivy-image-report.json)",
"OWASP ZAP baseline scan (DAST, target=inputs.target_url||NEXTAUTH_URL||localhost:8015, 可通过 skip_dast 跳过)",
"Generate security-summary.md (jq 汇总各报告漏洞计数)",
"upload security-reports-full artifact (audit-report.json, trivy-fs-report.json, trivy-image-report.json, snyk.sarif, security-summary.md)"
],
"configFiles": {
"suppressions": ".gitea/suppressions.json (Snyk 漏洞抑制, 每条含 id/package/severity/reason/expires/owner)",
"trivyignore": ".trivyignore (Trivy CVE 忽略列表, 每行一个 CVE 带注释)"
}
},
"scripts": {
"scripts/audit.sh": {
"type": "bash",
@@ -1862,22 +1984,93 @@
"scripts/test-backup.sh": {
"type": "bash",
"purpose": "备份流程测试,执行一次备份并验证最新备份文件"
},
"scripts/backup-verify.sh": {
"type": "bash",
"purpose": "备份完整性校验:检查文件存在/大小/gzip 完整性/SQL 内容结构/SQL 语法(可选,需 DATABASE_URL",
"env": ["BACKUP_DIR", "DATABASE_URL", "BACKUP_VERIFY_MIN_SIZE"],
"exitCodes": {"0": "校验通过", "1": "校验失败"},
"options": ["--min-size BYTES", "--no-sql-check", "--help"]
},
"scripts/backup-offsite-sync.sh": {
"type": "bash",
"purpose": "异地备份同步:支持 S3/OSS/NFS 后端,同步后校验文件数量,清理远程过期备份(保留 90 天)",
"env": ["BACKUP_DIR", "BACKUP_OFFSITE_BACKEND", "BACKUP_OFFSITE_REMOTE", "BACKUP_OFFSITE_BUCKET", "BACKUP_OFFSITE_ACCESS_KEY", "BACKUP_OFFSITE_SECRET_KEY", "BACKUP_OFFSITE_REGION", "BACKUP_OFFSITE_RETENTION_DAYS"],
"tools": ["aws-cli (s3)", "rclone (s3/oss)", "ossutil (oss)", "rsync (nfs)"],
"exitCodes": {"0": "同步成功", "1": "同步失败"},
"options": ["--backend TYPE", "--no-cleanup", "--no-verify", "--help"]
},
"scripts/dr-drill.sh": {
"type": "bash",
"purpose": "灾备演练:创建测试库→从备份恢复→数据完整性检查→冒烟测试→清理→生成报告到 docs/dr/reports/",
"env": ["DATABASE_URL", "BACKUP_DIR", "DR_DRILL_TEST_DB", "DR_DRILL_REPORT_DIR"],
"exitCodes": {"0": "演练成功", "1": "演练失败"},
"options": ["--backup FILE", "--test-db NAME", "--no-cleanup", "--report-dir DIR", "--help"]
},
"scripts/dr-drill.ps1": {
"type": "powershell",
"purpose": "灾备演练Windows PowerShell 5.1+ 版本),功能同 Bash 版本",
"env": ["DATABASE_URL", "BACKUP_DIR", "DR_DRILL_TEST_DB", "DR_DRILL_REPORT_DIR"],
"platform": "Windows",
"options": ["-BackupFile FILE", "-TestDb NAME", "-NoCleanup", "-ReportDir DIR", "-Help"]
},
"scripts/failover.sh": {
"type": "bash",
"purpose": "故障切换:检测主库健康→提升备库→更新应用配置→重启应用→验证切换",
"env": ["DATABASE_URL", "DATABASE_URL_STANDBY", "FAILOVER_APP_URL", "FAILOVER_APP_NAME", "FAILOVER_CONFIG_FILE", "FAILOVER_LOG_FILE"],
"exitCodes": {"0": "切换成功", "1": "切换失败"},
"options": ["--auto", "--primary URL", "--standby URL", "--app-url URL", "--no-restart", "--dry-run", "--help"]
},
"scripts/health-check.sh": {
"type": "bash",
"purpose": "健康检查:检查应用 HTTP/数据库连接/磁盘空间/备份新鲜度,输出 JSON 报告",
"env": ["DATABASE_URL", "HEALTH_CHECK_URL", "BACKUP_DIR", "HEALTH_CHECK_DISK_THRESHOLD", "HEALTH_CHECK_BACKUP_MAX_AGE"],
"exitCodes": {"0": "健康", "1": "异常"},
"options": ["--app-url URL", "--no-app", "--no-db", "--no-disk", "--no-backup", "--disk-threshold PCT", "--backup-max-age HRS", "--help"]
}
},
"packageJsonScripts": {
"audit": "npm audit --audit-level=moderate",
"audit:report": "npm audit --json > audit-report.json",
"security:audit": "npm audit --audit-level=moderate",
"security:scan": "bash scripts/security-scan.sh",
"backup": "bash scripts/backup-db.sh",
"restore": "bash scripts/restore-db.sh"
"restore": "bash scripts/restore-db.sh",
"dr:backup-verify": "bash scripts/backup-verify.sh",
"dr:offsite-sync": "bash scripts/backup-offsite-sync.sh",
"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"
},
"drDocs": {
"docs/dr/dr-plan.md": "灾备计划文档RTO/RPO 定义4h/24h、备份策略、故障切换流程、联系人列表、恢复步骤",
"docs/dr/dr-runbook.md": "灾备操作手册:数据库故障/应用故障/备份失败/异地同步失败/演练失败/磁盘不足场景的诊断与处理",
"docs/dr/reports/": "灾备演练报告存档目录Markdown 格式,由 dr-drill.sh 生成)",
"docs/dr/logs/": "故障切换日志目录(由 failover.sh 生成)"
},
"drEnvVars": {
"BACKUP_OFFSITE_BACKEND": "异地备份后端类型: s3|oss|nfs|none",
"BACKUP_OFFSITE_REMOTE": "远程存储路径",
"BACKUP_OFFSITE_BUCKET": "存储桶名称(仅 s3/oss",
"BACKUP_OFFSITE_ACCESS_KEY": "访问密钥",
"BACKUP_OFFSITE_SECRET_KEY": "秘密密钥",
"BACKUP_OFFSITE_REGION": "区域(默认 us-east-1",
"BACKUP_OFFSITE_RETENTION_DAYS": "远程备份保留天数(默认 90",
"DR_DRILL_TEST_DB": "演练测试数据库名(默认 next_edu_dr_drill",
"HEALTH_CHECK_URL": "应用健康检查 URL默认 http://localhost:8015",
"DATABASE_URL_STANDBY": "备库连接 URL故障切换时使用",
"FAILOVER_APP_NAME": "应用容器名(默认 nextjs-app"
},
"gitignore": {
"added": ["/backups/", "/audit-report.json", "/playwright-report/", "/test-results/"]
"added": ["/backups/", "/audit-report.json", "/trivy-fs-report.json", "/trivy-image-report.json", "/snyk.sarif", "/security-summary.md", "/playwright-report/", "/test-results/", "/tests/visual/.auth/"],
"exceptions": [".env.example (灾备环境变量示例,允许提交)"]
}
},
"testing": {
"e2e": {
"configFile": "playwright.config.ts",
"testDir": "./tests/e2e",
"testDir": "./tests",
"baseURL": "http://127.0.0.1:3000",
"webServer": {
"command": "npm run dev",
@@ -1891,7 +2084,15 @@
"DATABASE_URL": "mysql://test:test@127.0.0.1:3306/test_db"
}
},
"projects": [{"name": "chromium", "channel": "CI: undefined, local: chrome"}],
"projects": [
{"name": "chromium", "testDir": "./tests/e2e", "channel": "CI: undefined, local: chrome"},
{"name": "visual-chromium", "testDir": "./tests/visual", "channel": "CI: undefined, local: chrome"}
],
"snapshotPathTemplate": "{testDir}/__screenshots__/{testFilePath}/{arg}{ext}",
"expect": {
"toHaveScreenshot": {"maxDiffPixelRatio": 0.01, "animations": "disabled", "caret": "hide"},
"toMatchSnapshot": {"maxDiffPixelRatio": 0.01}
},
"retries": "CI: 2, local: 0",
"workers": "CI: 2, local: default",
"testFiles": {
@@ -1903,6 +2104,37 @@
"announcements.spec.ts": {"coverage": "公告页面未认证重定向 + 登录后渲染", "requiresDb": "partial"},
"grades.spec.ts": {"coverage": "成绩页面未认证重定向 + 登录后渲染", "requiresDb": "partial"}
}
},
"visual": {
"configFile": "tests/visual/visual.config.ts",
"snapshotDir": "tests/visual/__screenshots__",
"storageStateDir": "tests/visual/.auth/",
"viewports": {
"desktop": {"width": 1920, "height": 1080},
"tablet": {"width": 768, "height": 1024},
"mobile": {"width": 375, "height": 812}
},
"themes": ["light", "dark"],
"defaultMaxDiffPixelRatio": 0.01,
"testAccounts": {
"admin": {"email": "admin@xiaoxue.edu.cn", "envVars": ["VISUAL_ADMIN_EMAIL", "VISUAL_ADMIN_PASSWORD"]},
"teacher": {"email": "admin@xiaoxue.edu.cn", "envVars": ["VISUAL_TEACHER_EMAIL", "VISUAL_TEACHER_PASSWORD"]},
"student": {"email": "admin@xiaoxue.edu.cn", "envVars": ["VISUAL_STUDENT_EMAIL", "VISUAL_STUDENT_PASSWORD"]}
},
"testFiles": {
"homepage.spec.ts": {"coverage": "登录页在 desktop/tablet/mobile × light/dark 下的快照", "requiresDb": false},
"admin-dashboard.spec.ts": {"coverage": "管理员仪表盘整页 + 侧边栏/主内容区组件快照", "requiresDb": true, "role": "admin"},
"teacher-dashboard.spec.ts": {"coverage": "教师仪表盘整页 + 侧边栏/主内容区组件快照", "requiresDb": true, "role": "teacher"},
"student-dashboard.spec.ts": {"coverage": "学生仪表盘整页 + 侧边栏/主内容区组件快照", "requiresDb": true, "role": "student"}
},
"helpers": {
"auth.ts": ["setupAuthState(role)", "loginByUI(page, role)", "storageStatePath(role)"],
"visual-helpers.ts": ["setViewport(page, size)", "setTheme(page, theme)", "waitForPageReady(page)", "maskDynamicElements(page, selectors)", "buildMaskOption(masks)"]
},
"scripts": {
"test:visual": "playwright test --project=visual-chromium",
"test:visual:update": "playwright test --project=visual-chromium --update-snapshots"
}
}
}
}