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:
@@ -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-invalid(error存在则为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/email;wechatOpenId 暂不支持,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": "创建微信渠道发送器(配置完整用真实发送器,否则 Mock;access_token 带缓存)", "deps": ["环境变量: WECHAT_APP_ID, WECHAT_APP_SECRET, WECHAT_TEMPLATE_ID"] },
|
||||
{ "name": "createEmailSender", "file": "channels/email-channel.ts", "purpose": "创建邮件渠道发送器(配置 EMAIL_HOST 用 Nodemailer SMTP,否则 Mock;HTML 模板按 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user