From a303cf2d532477630b64056fefe5605992e9b931 Mon Sep 17 00:00:00 2001 From: WorkBuddy Date: Mon, 17 Aug 2026 12:25:42 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E5=90=8E=E7=AB=AF=E9=A6=96=E6=AC=A1?= =?UTF-8?q?=E5=9F=BA=E7=BA=BF=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 运行必须源码:app/(controller/model/service/middleware/lib/utils/validate/router)、config/、database/(migrations/seeders/sql)、启动脚本、app/public/templates 模板。 已忽略运行非必须:node_modules/、logs/、run/、app/public 运行时上传数据、.env*、构建/缓存产物。 --- .gitignore | 48 + .sequelizerc | 10 + add_word_cols.js | 23 + add_wx_link.js | 23 + app.js | 88 + app/controller/admin.js | 1664 +++ app/controller/auth.js | 610 + app/controller/carousel.js | 35 + app/controller/exam.js | 813 ++ app/controller/health.js | 42 + app/controller/materials.js | 119 + app/controller/news.js | 70 + app/controller/score.js | 230 + app/controller/upload.js | 478 + app/extend/helper.js | 273 + app/lib/file_io_guard.js | 83 + app/lib/file_path_cache.js | 47 + app/middleware/auth.js | 116 + app/middleware/concurrency_guard.js | 53 + app/middleware/loginRateLimit.js | 73 + app/middleware/rateLimit.js | 139 + app/model/account.js | 167 + app/model/admin.js | 57 + app/model/aggregation.js | 135 + app/model/benkeSubmission.js | 115 + app/model/carousel.js | 105 + app/model/choiceQuestion.js | 67 + app/model/competitionInfo.js | 61 + app/model/competitionTime.js | 93 + app/model/contestant.js | 161 + app/model/examQuestion.js | 84 + app/model/examSubmission.js | 110 + app/model/gaozhiQuestion.js | 89 + app/model/gaozhiSubmission.js | 133 + app/model/jiaoshiSubmission.js | 115 + app/model/judgeQuestion.js | 62 + app/model/news.js | 117 + app/model/systemConfig.js | 55 + app/model/team.js | 78 + app/model/wordQuestion.js | 67 + app/model/workSubmission.js | 109 + app/model/zhongzhiQuestion.js | 89 + app/model/zhongzhiSubmission.js | 133 + app/public/templates/.gitkeep | 1 + .../gaozhi/word_template_1785117066172.docx | Bin 0 -> 921 bytes .../gaozhi/word_template_1785207535307.docx | Bin 0 -> 19009 bytes .../gaozhi/word_template_1785207663577.docx | Bin 0 -> 17747 bytes .../gaozhi/word_template_1785207668867.docx | Bin 0 -> 19009 bytes .../zhongzhi/word_template_1785117066098.docx | Bin 0 -> 922 bytes .../zhongzhi/word_template_1785139347982.docx | Bin 0 -> 17656 bytes app/router.js | 158 + app/service/admin.js | 1912 +++ app/service/auth.js | 116 + app/service/exam.js | 745 + app/service/news.js | 132 + app/service/sms.js | 78 + app/service/upload.js | 368 + app/utils/filename.js | 62 + app/utils/sendFile.js | 109 + app/validate/index.js | 86 + auto-patch-egg.js | 65 + config/config.default.js | 409 + config/plugin.js | 32 + database/006-seed-50-20-30.sql | 207 + database/config.json | 71 + database/migrations/001-create-accounts.js | 125 + database/migrations/001-create-contestants.js | 105 + database/migrations/002-create-admins.js | 61 + database/migrations/002-create-teams.js | 56 + .../migrations/003-create-competition-info.js | 42 + database/migrations/004-create-aggregation.js | 94 + .../migrations/004-create-exam-questions.js | 69 + .../migrations/005-create-exam-submissions.js | 67 + database/migrations/005-create-news.js | 68 + database/migrations/006-create-carousel.js | 58 + .../migrations/006-create-work-submissions.js | 85 + .../007-create-zhongzhi-questions.js | 69 + .../migrations/008-create-gaozhi-questions.js | 69 + .../009-create-zhongzhi-submissions.js | 82 + .../010-create-gaozhi-submissions.js | 82 + .../011-create-benke-submissions.js | 77 + .../012-create-jiaoshi-submissions.js | 77 + .../013-create-competition-times.js | 57 + .../migrations/014-create-system-configs.js | 42 + .../migrations/015-create-choice-questions.js | 68 + .../migrations/016-create-judge-questions.js | 63 + .../migrations/017-create-word-questions.js | 68 + .../018-add-test-flag-to-contestants.js | 23 + .../migrations/019-add-title-to-carousel.js | 17 + .../020-add-question-type-to-submissions.js | 40 + .../021-unique-submission-indexes.js | 99 + .../022-add-content-button-to-carousel.js | 56 + database/seeders/001-seed-competition-info.js | 21 + database/seeders/001-seed-contestants.js | 50 + database/seeders/002-seed-admin.js | 36 + database/seeders/003-seed-carousel.js | 36 + database/seeders/003-seed-news.js | 85 + database/seeders/004-seed-exam-questions.js | 23 + .../seeders/005-seed-competition-times.js | 61 + .../006-seed-choice-judge-word-questions.js | 176 + dev.js | 13 + egg-compat-patch.js | 87 + package-lock.json | 11367 ++++++++++++++++ package.json | 70 + scripts/diagnose_upload_download.cjs | 195 + seed-carousel.js | 40 + show_tables.js | 9 + start-dev.js | 8 + unit_auth_catch.cjs | 18 + 109 files changed, 25904 insertions(+) create mode 100644 .gitignore create mode 100644 .sequelizerc create mode 100644 add_word_cols.js create mode 100644 add_wx_link.js create mode 100644 app.js create mode 100644 app/controller/admin.js create mode 100644 app/controller/auth.js create mode 100644 app/controller/carousel.js create mode 100644 app/controller/exam.js create mode 100644 app/controller/health.js create mode 100644 app/controller/materials.js create mode 100644 app/controller/news.js create mode 100644 app/controller/score.js create mode 100644 app/controller/upload.js create mode 100644 app/extend/helper.js create mode 100644 app/lib/file_io_guard.js create mode 100644 app/lib/file_path_cache.js create mode 100644 app/middleware/auth.js create mode 100644 app/middleware/concurrency_guard.js create mode 100644 app/middleware/loginRateLimit.js create mode 100644 app/middleware/rateLimit.js create mode 100644 app/model/account.js create mode 100644 app/model/admin.js create mode 100644 app/model/aggregation.js create mode 100644 app/model/benkeSubmission.js create mode 100644 app/model/carousel.js create mode 100644 app/model/choiceQuestion.js create mode 100644 app/model/competitionInfo.js create mode 100644 app/model/competitionTime.js create mode 100644 app/model/contestant.js create mode 100644 app/model/examQuestion.js create mode 100644 app/model/examSubmission.js create mode 100644 app/model/gaozhiQuestion.js create mode 100644 app/model/gaozhiSubmission.js create mode 100644 app/model/jiaoshiSubmission.js create mode 100644 app/model/judgeQuestion.js create mode 100644 app/model/news.js create mode 100644 app/model/systemConfig.js create mode 100644 app/model/team.js create mode 100644 app/model/wordQuestion.js create mode 100644 app/model/workSubmission.js create mode 100644 app/model/zhongzhiQuestion.js create mode 100644 app/model/zhongzhiSubmission.js create mode 100644 app/public/templates/.gitkeep create mode 100644 app/public/templates/gaozhi/word_template_1785117066172.docx create mode 100644 app/public/templates/gaozhi/word_template_1785207535307.docx create mode 100644 app/public/templates/gaozhi/word_template_1785207663577.docx create mode 100644 app/public/templates/gaozhi/word_template_1785207668867.docx create mode 100644 app/public/templates/zhongzhi/word_template_1785117066098.docx create mode 100644 app/public/templates/zhongzhi/word_template_1785139347982.docx create mode 100644 app/router.js create mode 100644 app/service/admin.js create mode 100644 app/service/auth.js create mode 100644 app/service/exam.js create mode 100644 app/service/news.js create mode 100644 app/service/sms.js create mode 100644 app/service/upload.js create mode 100644 app/utils/filename.js create mode 100644 app/utils/sendFile.js create mode 100644 app/validate/index.js create mode 100644 auto-patch-egg.js create mode 100644 config/config.default.js create mode 100644 config/plugin.js create mode 100644 database/006-seed-50-20-30.sql create mode 100644 database/config.json create mode 100644 database/migrations/001-create-accounts.js create mode 100644 database/migrations/001-create-contestants.js create mode 100644 database/migrations/002-create-admins.js create mode 100644 database/migrations/002-create-teams.js create mode 100644 database/migrations/003-create-competition-info.js create mode 100644 database/migrations/004-create-aggregation.js create mode 100644 database/migrations/004-create-exam-questions.js create mode 100644 database/migrations/005-create-exam-submissions.js create mode 100644 database/migrations/005-create-news.js create mode 100644 database/migrations/006-create-carousel.js create mode 100644 database/migrations/006-create-work-submissions.js create mode 100644 database/migrations/007-create-zhongzhi-questions.js create mode 100644 database/migrations/008-create-gaozhi-questions.js create mode 100644 database/migrations/009-create-zhongzhi-submissions.js create mode 100644 database/migrations/010-create-gaozhi-submissions.js create mode 100644 database/migrations/011-create-benke-submissions.js create mode 100644 database/migrations/012-create-jiaoshi-submissions.js create mode 100644 database/migrations/013-create-competition-times.js create mode 100644 database/migrations/014-create-system-configs.js create mode 100644 database/migrations/015-create-choice-questions.js create mode 100644 database/migrations/016-create-judge-questions.js create mode 100644 database/migrations/017-create-word-questions.js create mode 100644 database/migrations/018-add-test-flag-to-contestants.js create mode 100644 database/migrations/019-add-title-to-carousel.js create mode 100644 database/migrations/020-add-question-type-to-submissions.js create mode 100644 database/migrations/021-unique-submission-indexes.js create mode 100644 database/migrations/022-add-content-button-to-carousel.js create mode 100644 database/seeders/001-seed-competition-info.js create mode 100644 database/seeders/001-seed-contestants.js create mode 100644 database/seeders/002-seed-admin.js create mode 100644 database/seeders/003-seed-carousel.js create mode 100644 database/seeders/003-seed-news.js create mode 100644 database/seeders/004-seed-exam-questions.js create mode 100644 database/seeders/005-seed-competition-times.js create mode 100644 database/seeders/006-seed-choice-judge-word-questions.js create mode 100644 dev.js create mode 100644 egg-compat-patch.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 scripts/diagnose_upload_download.cjs create mode 100644 seed-carousel.js create mode 100644 show_tables.js create mode 100644 start-dev.js create mode 100644 unit_auth_catch.cjs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..250cb78 --- /dev/null +++ b/.gitignore @@ -0,0 +1,48 @@ +# Dependencies +node_modules/ + +# Build output +dist/ +dist-ssr/ + +# Logs +logs/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# Runtime (egg) +run/ +.wb_*/ +.wb_check_state.js +.egg-config.json + +# Env / secrets (defense-in-depth; real .env.production lives outside this repo) +.env* +!.env.example + +# Coverage / cache +coverage/ +.cache/ + +# Agent / tooling state +.workbuddy/ + +# Editor +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store + +# Uploaded / runtime data under app/public (keep templates as source) +app/public/uploads/ +app/public/answers/ +app/public/works/ +app/public/materials/ +app/public/news/ +app/public/carousel/ +app/public/tmp/ +app/public/exports/ +test_dir/ diff --git a/.sequelizerc b/.sequelizerc new file mode 100644 index 0000000..b1ef760 --- /dev/null +++ b/.sequelizerc @@ -0,0 +1,10 @@ +'use strict'; + +const path = require('path'); + +module.exports = { + config: path.resolve(__dirname, 'database', 'config.json'), + 'migrations-path': path.resolve(__dirname, 'database', 'migrations'), + 'seeders-path': path.resolve(__dirname, 'database', 'seeders'), + 'models-path': path.resolve(__dirname, 'app', 'model'), +}; diff --git a/add_word_cols.js b/add_word_cols.js new file mode 100644 index 0000000..da72823 --- /dev/null +++ b/add_word_cols.js @@ -0,0 +1,23 @@ +const mysql = require('mysql2/promise'); +(async () => { + const c = await mysql.createConnection({ + host: 'localhost', user: 'root', password: '74191221', database: 'competition_db' + }); + try { + await c.query("ALTER TABLE exam_submissions ADD COLUMN word_file_path VARCHAR(500) NULL DEFAULT NULL COMMENT 'Word答案文件路径' AFTER answers"); + console.log('Added word_file_path'); + } catch (e) { + if (e.code === 'ER_DUP_FIELDNAME') console.log('word_file_path exists'); + else throw e; + } + try { + await c.query("ALTER TABLE exam_submissions ADD COLUMN word_file_name VARCHAR(255) NULL DEFAULT NULL COMMENT 'Word答案文件名' AFTER word_file_path"); + console.log('Added word_file_name'); + } catch (e) { + if (e.code === 'ER_DUP_FIELDNAME') console.log('word_file_name exists'); + else throw e; + } + const [cols] = await c.query('SHOW COLUMNS FROM exam_submissions'); + cols.forEach(x => console.log(x.Field, x.Type, x.Null)); + await c.end(); +})(); \ No newline at end of file diff --git a/add_wx_link.js b/add_wx_link.js new file mode 100644 index 0000000..62bb7d5 --- /dev/null +++ b/add_wx_link.js @@ -0,0 +1,23 @@ +const mysql = require('mysql2/promise'); +(async () => { + const c = await mysql.createConnection({ + host: 'localhost', user: 'root', password: '74191221', database: 'competition_db' + }); + try { + await c.query("ALTER TABLE news ADD COLUMN wx_link VARCHAR(500) NULL DEFAULT NULL COMMENT '微信公众号文章链接' AFTER content"); + console.log('Added wx_link column'); + } catch (e) { + if (e.code === 'ER_DUP_FIELDNAME') console.log('wx_link already exists'); + else throw e; + } + try { + await c.query("ALTER TABLE news ADD COLUMN news_type VARCHAR(20) NOT NULL DEFAULT 'content' COMMENT '新闻类型' AFTER wx_link"); + console.log('Added news_type column'); + } catch (e) { + if (e.code === 'ER_DUP_FIELDNAME') console.log('news_type already exists'); + else throw e; + } + const [cols] = await c.query('SHOW COLUMNS FROM news'); + cols.forEach(x => console.log(x.Field, x.Type, x.Null)); + await c.end(); +})(); \ No newline at end of file diff --git a/app.js b/app.js new file mode 100644 index 0000000..0352207 --- /dev/null +++ b/app.js @@ -0,0 +1,88 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +/** + * Egg.js 应用启动钩子 - AppBootHook + * 表结构通过 migration/seeder 管理,不在启动时自动同步 + */ +class AppBootHook { + constructor(app) { + this.app = app; + } + + async willReady() { + // 数据库表结构由 migration 管理,不在启动时自动 sync + this.app.logger.info('[AppBoot] 服务启动完成'); + } + + /** + * 进程就绪后,确保 app/public 下所有业务目录存在。 + * + * 关键背景(生产踩坑): + * app/public/{works,answers,materials,tmp,...} 这类上传目录通常被 .gitignore 忽略, + * 每次「整目录重新部署」(rsync --delete / 重新 clone) 都会被清空。 + * 若运行中的代码某条写入路径恰好没有即时 mkdir,就会 ENOENT → 上传失败 / 下载 404, + * 且旧版 auth 中间件会把这类磁盘异常伪装成「401 登录过期」误导排查。 + * 此处统一在启动时把全部已知写入目录递归建好,作为永久兜底, + * 与各处写入点的 fs.mkdirSync(recursive:true) 形成双保险。 + */ + async didReady() { + const publicDir = path.join(this.app.baseDir, 'app/public'); + const groups = ['zhongzhi', 'gaozhi', 'benke', 'jiaoshi']; + + // 需要按「目录/组别」创建的(组别子目录) + const grouped = ['works', 'answers', 'materials', 'templates']; + // 需要直接创建(无组别)的 + const flat = ['tmp', 'tmp/excel', 'tmp/zip', 'carousel', 'news']; + + const targets = []; + for (const d of grouped) { + for (const g of groups) targets.push(path.join(publicDir, d, g)); + } + for (const d of flat) targets.push(path.join(publicDir, d)); + + let created = 0; + let existed = 0; + for (const t of targets) { + try { + if (!fs.existsSync(t)) { + fs.mkdirSync(t, { recursive: true }); + created++; + } else { + existed++; + } + } catch (e) { + this.app.logger.error('[AppBoot] 创建上传目录失败', { target: t, message: e.message }); + } + } + this.app.logger.info( + `[AppBoot] 上传目录就绪:已创建 ${created} 个,已存在 ${existed} 个(根: ${publicDir})` + ); + + // 🔴 连接池容量自举断言(2026-08-13 推进 #16):egg-cluster 按 os.cpus() 起多 worker, + // 总连接数 ≈ worker 数 × pool.max。若超过 MySQL max_connections(默认 151), + // 高峰期会偶发 "connection acquire timeout" / "too many connections"。 + // 启动期只做 WARN(不阻断启动),提醒运维调大 DB 或调小 DB_POOL_MAX。 + try { + const os = require('os'); + const poolMax = parseInt(process.env.DB_POOL_MAX, 10) || 20; + const rows = await this.app.model.query('SELECT @@max_connections AS m', { type: this.app.model.QueryTypes.SELECT }); + const dbMax = parseInt(rows && rows[0] && rows[0].m, 10) || 151; + const workers = os.cpus().length; + const estimated = workers * poolMax; + if (estimated > dbMax) { + this.app.logger.warn( + `[AppBoot][pool] 预估总连接数 ${estimated} (${workers} workers × ${poolMax}) 超过 MySQL max_connections=${dbMax},高峰期可能连接获取超时;建议调大 DB max_connections 或调小 DB_POOL_MAX` + ); + } else { + this.app.logger.info(`[AppBoot][pool] 连接池容量 OK:${estimated} (${workers} × ${poolMax}) ≤ MySQL max_connections=${dbMax}`); + } + } catch (e) { + this.app.logger.warn('[AppBoot][pool] 连接数自举断言跳过(无法查询 MySQL):' + (e && e.message)); + } + } +} + +module.exports = AppBootHook; diff --git a/app/controller/admin.js b/app/controller/admin.js new file mode 100644 index 0000000..3792997 --- /dev/null +++ b/app/controller/admin.js @@ -0,0 +1,1664 @@ +'use strict'; + +const Controller = require('egg').Controller; +const path = require('path'); +const fs = require('fs'); +const { sanitizeUploadFileName } = require('../utils/filename'); +// 🔴 文件 IO 并发护栏(与上传通道共享同一全局配额):限制单 worker 同时进行的文件流, +// 防止「无带宽限制 + 单 worker + 极端并发」下裸崩。生产下载经 X-Accel-Redirect 卸载给 +// nginx(Node 不持有流,仅回头);开发回退流式时由本护栏按流生命周期释放配额。 +const { acquire: acquireFileIo, release: releaseFileIo } = require('../lib/file_io_guard'); +// 🔴 同源 collapse(缺口9 修复):所有文件发送通道统一走 sendFile util +const { sendFile } = require('../utils/sendFile'); + +/** + * 管理控制器 - AdminController + * + * 负责所有后台管理功能,包括数据统计、账号管理、Excel 导入、 + * 提交记录查看、CSV 导出、批量下载、时间设置、评分、题目管理、 + * 成绩发布、新闻管理、轮播图管理等。 + * + * 所有接口均需管理员权限(middleware.auth({ adminOnly: true }))。 + */ +class AdminController extends Controller { + /** + * 验证当前用户是否为管理员 + * @returns {boolean} + */ + _ensureAdmin() { + const { ctx } = this; + const user = ctx.state.currentUser; + if (!user || user.role !== 'admin') { + ctx.body = ctx.helper.error(403, '无权限访问,需要管理员权限'); + ctx.status = 403; + return false; + } + return true; + } + + /** + * 验证组别参数是否有效 + * @param {string} group - 组别 + * @returns {boolean} + */ + _validateGroup(group, allGroups = false) { + const validGroupsExams = ['zhongzhi', 'gaozhi']; + const validGroupsAll = ['zhongzhi', 'gaozhi', 'benke', 'jiaoshi']; + const validGroups = allGroups ? validGroupsAll : validGroupsExams; + + if (!validGroups.includes(group)) { + this.ctx.body = this.ctx.helper.error(400, `无效的组别,仅支持 ${validGroups.join(' / ')}`); + return false; + } + return true; + } + + /** + * 统一「大文件下载」出口(X-Accel-Redirect 根治方案)。 + * + * 生产(config.xAccelRedirect.enabled=true):后端只回一个 X-Accel-Redirect 内部 + * 重定向头,nginx 用 internal location 直接从磁盘流式吐文件 —— Node 不再持有文件流, + * 极端并发下几乎零占用 worker 的 fd / 事件循环,从根上消除雪崩。 + * + * 开发(默认关闭):回退为 Node 直接流式,保持与改造前一致,便于本地调试。 + * + * @param {object} ctx - Egg 上下文 + * @param {object} app - Egg app(读 config.xAccelRedirect) + * @param {string} relativePath - 相对 app/public 的路径(如 works/benke/x.docx) + * @param {string} fileName - 原始文件名(用于 Content-Disposition) + * @param {string} contentType - MIME 类型 + */ + async _sendFile(ctx, app, relativePath, fileName, contentType) { + // 🔴 同源 collapse(2026-08-12 缺口9 修复):所有文件发送统一走 sendFile util, + // 避免各通道自写 fs.createReadStream 漏 X-File-Size 哨兵导致前端完整性校验失效。 + return sendFile(ctx, app, { relativePath, fileName, contentType }); + } + + // ==================== 数据统计 ==================== + + /** + * 数据总览统计 + * GET /api/admin/stats + * + * 返回各组报名人数、激活数、提交数、提交率、总分等统计数据 + */ + async stats() { + const { ctx, service } = this; + if (!this._ensureAdmin()) return; + + try { + // 支持 ?excludeTest=1(或 ?scope=production)仅统计生产数据,排除测试数据 + const excludeTest = ctx.query.excludeTest === '1' || ctx.query.excludeTest === 'true' + || ctx.query.scope === 'production'; + const stats = await service.admin.getStats({ excludeTest }); + ctx.body = ctx.helper.success(stats, '获取统计数据成功'); + } catch (err) { + ctx.logger.error('获取统计数据失败:', err); + ctx.body = ctx.helper.error(500, '获取统计数据失败'); + } + } + + // ==================== 账号管理 ==================== + + /** + * 分页获取账号列表 + * GET /api/admin/accounts?group=xxx&keyword=xxx&page=1&pageSize=20 + * + * 支持按组别和关键词筛选,关联团队信息 + */ + async accounts() { + const { ctx, service } = this; + if (!this._ensureAdmin()) return; + + try { + const { group, keyword, page = 1, pageSize = 20 } = ctx.query; + const result = await service.admin.getAccounts({ group, keyword, page, pageSize }); + ctx.body = ctx.helper.success(result, '获取账号列表成功'); + } catch (err) { + ctx.logger.error('获取账号列表失败:', err); + ctx.body = ctx.helper.error(500, '获取账号列表失败'); + } + } + + /** + * 从 Excel 文件导入账号数据 + * POST /api/admin/import-excel + * + * 上传 xlsx 文件,逐行解析并创建账号+团队+聚合记录 + */ + async importExcel() { + const { ctx, service, app } = this; + if (!this._ensureAdmin()) return; + + try { + // 获取上传的文件流 + const stream = await ctx.getFileStream(); + if (!stream) { + ctx.body = ctx.helper.error(400, '未接收到文件'); + return; + } + + // 验证文件格式 + const ext = path.extname(stream.filename).toLowerCase(); + if (!['.xlsx', '.xls'].includes(ext)) { + ctx.body = ctx.helper.error(400, '仅支持 .xlsx / .xls 格式'); + return; + } + + // 保存临时文件 + const tempDir = path.join(app.baseDir, 'app/public/tmp/excel'); + if (!fs.existsSync(tempDir)) { + fs.mkdirSync(tempDir, { recursive: true }); + } + + const tempFilePath = path.join(tempDir, `import_${Date.now()}${ext}`); + const writeStream = fs.createWriteStream(tempFilePath); + + await new Promise((resolve, reject) => { + stream.pipe(writeStream); + writeStream.on('finish', resolve); + writeStream.on('error', reject); + stream.on('error', reject); + }); + + // 调用服务解析 Excel 并导入 + // testFlag 来自前端导入对话框的「标记为测试数据」勾选(multipart 非文件字段在 stream.fields) + const rawTestFlag = stream.fields && stream.fields.testFlag; + const testFlag = rawTestFlag === '1' || rawTestFlag === 'true' || rawTestFlag === true; + const result = await service.admin.importFromExcel(tempFilePath, { testFlag }); + + // 清理临时文件 + try { fs.unlinkSync(tempFilePath); } catch (e) { /* ignore */ } + + ctx.body = ctx.helper.success(result, `导入完成:成功 ${result.imported} 条,跳过 ${result.skipped} 条`); + } catch (err) { + ctx.logger.error('Excel 导入失败:', err); + const msg = (err && err.message) ? err.message : '未知错误'; + // 注意:前端 onExcelImport 已统一加「Excel 导入失败:」前缀,这里只回传纯错误,避免重复前缀 + ctx.body = ctx.helper.error(500, msg); + } + } + + // ==================== 提交记录管理 ==================== + + /** + * 获取某组的提交记录 + * GET /api/admin/submissions/:group + * + * 中职/高职组:返回答题提交详情(含题目信息) + * 本科/教师组:返回作品提交详情(含文件信息) + */ + async submissions() { + const { ctx, service } = this; + if (!this._ensureAdmin()) return; + + try { + const { group } = ctx.params; + if (!this._validateGroup(group, true)) return; + + // 中职/高职组使用简化版查询(contestants + exam_submissions) + const isExamGroup = group === 'zhongzhi' || group === 'gaozhi'; + const submissions = isExamGroup + ? await service.admin.getExamSubmissions(group) + : await service.admin.getSubmissions(group); + ctx.body = ctx.helper.success({ + group, + list: submissions, + total: submissions.length, + }, '获取提交记录成功'); + } catch (err) { + ctx.logger.error('获取提交记录失败:', err); + ctx.body = ctx.helper.error(500, '获取提交记录失败'); + } + } + + /** + * 作品管理统一列表(覆盖全部 4 个组别) + * GET /api/admin/works/:group + */ + async works() { + const { ctx, service } = this; + if (!this._ensureAdmin()) return; + + try { + const { group } = ctx.params; + if (!this._validateGroup(group, true)) return; + + const list = await service.admin.getWorks(group); + ctx.body = ctx.helper.success({ group, list, total: list.length }, '获取作品列表成功'); + } catch (err) { + ctx.logger.error('获取作品列表失败:', err); + ctx.body = ctx.helper.error(500, '获取作品列表失败'); + } + } + + /** + * 导出提交记录为 CSV + * GET /api/admin/export-csv/:group + * + * 设置 Content-Disposition 头,触发浏览器下载 + */ + async exportCsv() { + const { ctx, service } = this; + if (!this._ensureAdmin()) return; + + try { + const { group } = ctx.params; + if (!this._validateGroup(group, true)) return; + + const csvData = await service.admin.exportToCsv(group); + + // 设置响应头触发下载 + const fileName = `${group}_submissions_${new Date().toISOString().slice(0, 10)}.csv`; + ctx.set('Content-Type', 'text/csv; charset=utf-8'); + ctx.attachment(fileName); + + // 添加 UTF-8 BOM 确保 Excel 正确识别中文 + ctx.body = '\uFEFF' + csvData; + } catch (err) { + ctx.logger.error('导出 CSV 失败:', err); + ctx.body = ctx.helper.error(500, '导出 CSV 失败'); + } + } + + /** + * 批量下载某组所有提交文件为 ZIP + * GET /api/admin/batch-download/:group + * + * 文件重命名为 "组号-学校-姓名-原文件名" 格式,流式下载 ZIP + */ + async batchDownload() { + const { ctx, service } = this; + if (!this._ensureAdmin()) return; + + try { + const { group } = ctx.params; + if (!this._validateGroup(group, true)) return; + + const zipFilePath = await service.admin.batchDownloadZip(group); + + // 🔴 同源 collapse(缺口9 修复):统一走 sendFile(磁盘校验 + X-File-Size 哨兵)。 + // 临时 zip 在 app/public/tmp/zip 下,但用 absolutePath 模式并强制 Node 流(useXAccel:false), + // 避免 X-Accel「发头即 unlink」竞态损坏;清理逻辑保留在流结束后的 finish 事件。 + const fileName = path.basename(zipFilePath); + await sendFile(ctx, this.app, { absolutePath: zipFilePath, fileName, contentType: 'application/zip', useXAccel: false }); + + // 下载完成后清理 ZIP 文件(Node 流模式下 res.finish 在流发完后触发) + ctx.res.on('finish', () => { + try { fs.unlinkSync(zipFilePath); } catch (e) { /* ignore */ } + }); + } catch (err) { + ctx.logger.error('批量下载失败:', err); + + // 如果 archiver 不可用,返回友好错误 + if (err.code === 'MODULE_NOT_FOUND' && err.message.includes('archiver')) { + ctx.body = ctx.helper.error(500, 'ZIP 打包功能需要安装 archiver 依赖,请运行 npm install archiver'); + return; + } + + ctx.body = ctx.helper.error(500, '批量下载失败'); + } + } + + // ==================== 时间管理 ==================== + + /** + * 批量设置比赛时间 + * PUT /api/admin/times + * Body: { times: [{ group, startTime, endTime, deadline }] } + */ + async setTimes() { + const { ctx, service } = this; + if (!this._ensureAdmin()) return; + + try { + const rule = { + times: { type: 'array', required: true, min: 1 }, + }; + try { + ctx.validate(rule); + } catch (validateErr) { + ctx.body = ctx.helper.error(400, '参数校验失败:' + validateErr.errors[0].message); + return; + } + + const { times } = ctx.request.body; + + // 验证每个时间配置 + const validGroups = ['zhongzhi', 'gaozhi', 'benke', 'jiaoshi']; + for (const t of times) { + if (!t.group || !validGroups.includes(t.group)) { + ctx.body = ctx.helper.error(400, `无效的组别: ${t.group}`); + return; + } + } + + await service.admin.setTimes(times); + ctx.body = ctx.helper.success(null, '比赛时间设置成功'); + } catch (err) { + ctx.logger.error('设置比赛时间失败:', err); + ctx.body = ctx.helper.error(500, '设置比赛时间失败'); + } + } + + /** + * 获取比赛时间配置 + * GET /api/admin/times + */ + async getTimes() { + const { ctx, service } = this; + if (!this._ensureAdmin()) return; + + try { + const times = await service.admin.getTimes(); + ctx.body = ctx.helper.success({ times }); + } catch (err) { + ctx.logger.error('获取比赛时间失败:', err); + ctx.body = ctx.helper.error(500, '获取比赛时间失败'); + } + } + + // ==================== 评分管理 ==================== + + /** + * 批量设置评分 + * PUT /api/admin/scores/:group + * Body: { scores: [{ submissionId, score }] } + */ + async setScores() { + const { ctx, service } = this; + if (!this._ensureAdmin()) return; + + try { + const { group } = ctx.params; + if (!this._validateGroup(group, true)) return; + + const rule = { + scores: { type: 'array', required: true, min: 1 }, + }; + try { + ctx.validate(rule); + } catch (validateErr) { + ctx.body = ctx.helper.error(400, '参数校验失败:' + validateErr.errors[0].message); + return; + } + + const { scores } = ctx.request.body; + + // 验证每个评分数据 + for (const item of scores) { + if (!item.contestantId) { + ctx.body = ctx.helper.error(400, '评分数据格式错误,需要 contestantId'); + return; + } + if (group === 'zhongzhi' || group === 'gaozhi') { + if (item.wordScore === undefined && item.examScore === undefined && item.score === undefined) { + ctx.body = ctx.helper.error(400, '评分数据格式错误,需要 wordScore(或 examScore/score)'); + return; + } + } else { + if (item.wordScore === undefined && item.pptScore === undefined && item.score === undefined) { + ctx.body = ctx.helper.error(400, '评分数据格式错误,需要 wordScore/pptScore(或 score)'); + return; + } + } + } + + await service.admin.updateScores(scores, group); + ctx.body = ctx.helper.success(null, '评分设置成功'); + } catch (err) { + ctx.logger.error('设置评分失败:', err); + ctx.body = ctx.helper.error(500, '设置评分失败'); + } + } + + /** + * 获取评分列表 + * GET /api/admin/scores/:group + */ + async getScores() { + const { ctx, service } = this; + if (!this._ensureAdmin()) return; + + try { + const { group } = ctx.params; + if (!this._validateGroup(group, true)) return; + + const scores = await service.admin.getScores(group); + ctx.body = ctx.helper.success({ list: scores }); + } catch (err) { + ctx.logger.error('获取评分列表失败:', err); + ctx.body = ctx.helper.error(500, '获取评分列表失败'); + } + } + + // ==================== 题目管理 ==================== + + /** + * 批量设置/替换题目 + * PUT /api/admin/exam/:group/questions + * Body: { question_type: 'choice'|'judge'|'word', questions: [...], action: 'add'|'update'|'delete' } + * + * 按题型分表管理:选择题(choice)、判断题(judge)、Word题(word) + */ + async setQuestions() { + const { ctx, service } = this; + if (!this._ensureAdmin()) return; + + try { + const { group } = ctx.params; + if (!this._validateGroup(group)) return; + + const { questions, action, question_type, score } = ctx.request.body; + const qType = question_type || 'choice'; + + // 批量设分(setTypeScore)/ 批量删除(bulkDelete)不需要 questions 数组;其余操作均需要 + if (action !== 'setTypeScore' && action !== 'bulkDelete') { + if (!questions || !Array.isArray(questions)) { + ctx.body = ctx.helper.error(400, '参数校验失败:questions 为必填数组'); + return; + } + } + + if (!['choice', 'judge', 'word'].includes(qType)) { + ctx.body = ctx.helper.error(400, '题型参数无效,必须为 choice/judge/word'); + return; + } + + if (action === 'add') { + for (const q of questions) { + // 分值必须为大于 0 的合法数值;0 分题属非法业务数据 + if (q.score == null || !isFinite(Number(q.score)) || Number(q.score) <= 0) { + ctx.body = ctx.helper.error(400, '题目分值(score)必须为大于 0 的合法数值'); + return; + } + await service.admin.addQuestion(q, group, qType); + } + ctx.body = ctx.helper.success({ added: questions.length }, '题目添加成功'); + } else if (action === 'update') { + for (const q of questions) { + if (!q.question_uid && !q.id) { + ctx.body = ctx.helper.error(400, '更新题目需要 question_uid 或 id'); + return; + } + await service.admin.updateQuestion(q.question_uid || q.id, q, qType); + } + ctx.body = ctx.helper.success({ updated: questions.length }, '题目更新成功'); + } else if (action === 'delete') { + for (const q of questions) { + if (!q.question_uid && !q.id) { + ctx.body = ctx.helper.error(400, '删除题目需要 question_uid 或 id'); + return; + } + await service.admin.deleteQuestion(q.question_uid || q.id, qType); + } + ctx.body = ctx.helper.success({ deleted: questions.length }, '题目删除成功'); + } else if (action === 'setTypeScore') { + // 批量按题型设置分值:将该组该题型全部题目统一设为指定分值 + if (score == null || isNaN(parseInt(score, 10))) { + ctx.body = ctx.helper.error(400, '批量设分需要合法的 score 数值'); + return; + } + const updated = await service.admin.setTypeScore(group, qType, parseInt(score, 10)); + const typeLabel = qType === 'choice' ? '选择题' : (qType === 'judge' ? '判断题' : '综合性任务'); + ctx.body = ctx.helper.success({ updated }, `已将${typeLabel}统一设为 ${score} 分`); + } else if (action === 'bulkDelete') { + // 批量删除题目:question_uids 数组 + const ids = (ctx.request.body.question_uids || []) + .map(id => parseInt(id, 10)) + .filter(id => !isNaN(id)); + if (ids.length === 0) { + ctx.body = ctx.helper.error(400, '批量删除需要 question_uids 数组'); + return; + } + const deleted = await service.admin.bulkDeleteQuestions(ids, qType); + ctx.body = ctx.helper.success({ deleted: ids.length }, `已删除 ${ids.length} 道题目`); + } else { + // 默认:替换该题型全部题目 + await service.admin.manageQuestions(questions, group, qType); + ctx.body = ctx.helper.success({ count: questions.length }, '题目设置成功'); + } + } catch (err) { + ctx.logger.error('题目设置失败:', err); + ctx.body = ctx.helper.error(500, '题目设置失败'); + } + } + + /** + * 获取考试题目列表(按题型分表返回) + * GET /api/admin/exam/:group/questions + * 返回: { choice: [...], judge: [...], word: [...] } + */ + async getQuestions() { + const { ctx, service } = this; + if (!this._ensureAdmin()) return; + + try { + const { group } = ctx.params; + if (!this._validateGroup(group)) return; + + const questions = await service.admin.getQuestions(group); + ctx.body = ctx.helper.success(questions); + } catch (err) { + ctx.logger.error('获取题目列表失败:', err); + ctx.body = ctx.helper.error(500, '获取题目列表失败'); + } + } + + /** + * 客观题 Excel 批量导入 + * POST /api/admin/exam/:group/questions/import-excel + * multipart/form-data: file 字段为 .xlsx/.xls + * 单工作表,含列:题型、题目、选项A、选项B、选项C、选项D、正确答案、分值 + */ + async importQuestionsExcel() { + const { ctx, service, app } = this; + if (!this._ensureAdmin()) return; + + const { group } = ctx.params; + if (!this._validateGroup(group)) return; + + try { + const stream = await ctx.getFileStream(); + if (!stream) { + ctx.body = ctx.helper.error(400, '未接收到文件'); + return; + } + + const ext = path.extname(stream.filename).toLowerCase(); + if (!['.xlsx', '.xls'].includes(ext)) { + ctx.body = ctx.helper.error(400, '仅支持 .xlsx / .xls 格式'); + return; + } + + const tempDir = path.join(app.baseDir, 'app/public/tmp/excel'); + if (!fs.existsSync(tempDir)) fs.mkdirSync(tempDir, { recursive: true }); + + const tempFilePath = path.join(tempDir, `questions_import_${Date.now()}${ext}`); + const writeStream = fs.createWriteStream(tempFilePath); + + await new Promise((resolve, reject) => { + stream.pipe(writeStream); + writeStream.on('finish', resolve); + writeStream.on('error', reject); + stream.on('error', reject); + }); + + const result = await service.admin.importQuestionsFromExcel(tempFilePath, { group }); + + try { fs.unlinkSync(tempFilePath); } catch (e) { /* ignore */ } + + ctx.body = ctx.helper.success(result, `导入完成:成功 ${result.imported} 条,跳过 ${result.skipped} 条`); + } catch (err) { + ctx.logger.error('客观题 Excel 导入失败:', err); + const msg = (err && err.message) ? err.message : '未知错误'; + // 前端 onExcelFileChange 已统一加「Excel 导入失败:」前缀,这里只回传纯错误,避免重复前缀 + ctx.body = ctx.helper.error(500, msg); + } + } + + /** + * 下载客观题 Excel 导入模板 + * GET /api/admin/exam/:group/questions/template + */ + async downloadQuestionTemplate() { + const { ctx, service } = this; + if (!this._ensureAdmin()) return; + + try { + const buffer = await service.admin.buildQuestionTemplate(); + ctx.set('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); + ctx.set('Content-Disposition', 'attachment; filename="objective_questions_template.xlsx"'); + ctx.body = buffer; + } catch (err) { + ctx.logger.error('下载题目模板失败:', err); + ctx.body = ctx.helper.error(500, '下载题目模板失败'); + } + } + + // ==================== Word 模板管理 ==================== + + /** + * 上传 Word 模板文件 + * POST /api/admin/upload-word-template/:group + * + * 上传并更新 competition_times 中的 word_template 字段 + */ + async uploadWordTemplate() { + const { ctx, service, app } = this; + if (!this._ensureAdmin()) return; + + try { + const { group } = ctx.params; + if (!this._validateGroup(group)) return; + + // 获取上传的文件流 + const stream = await ctx.getFileStream(); + if (!stream) { + ctx.body = ctx.helper.error(400, '未接收到文件'); + return; + } + + // 验证文件格式 + const ext = path.extname(stream.filename).toLowerCase(); + if (!['.doc', '.docx'].includes(ext)) { + ctx.body = ctx.helper.error(400, '仅支持 .doc / .docx 格式'); + return; + } + + // 保存文件到模板目录 + const templateDir = path.join(app.baseDir, 'app/public/templates', group); + if (!fs.existsSync(templateDir)) { + fs.mkdirSync(templateDir, { recursive: true }); + } + + const timestamp = Date.now(); + const safeFileName = `word_template_${timestamp}${ext}`; + const filePath = path.join(templateDir, safeFileName); + + const writeStream = fs.createWriteStream(filePath); + await new Promise((resolve, reject) => { + stream.pipe(writeStream); + writeStream.on('finish', resolve); + writeStream.on('error', reject); + stream.on('error', reject); + }); + + // 构建相对路径(相对于 app/public/) + const relativePath = path.join('templates', group, safeFileName).replace(/\\/g, '/'); + + // 更新数据库 + await service.admin.uploadWordTemplate(group, { filePath: relativePath }); + + ctx.body = ctx.helper.success({ + group, + filePath: relativePath, + fileUrl: ctx.helper.fileUrl(relativePath), + fileName: stream.filename, + }, 'Word 模板上传成功'); + } catch (err) { + ctx.logger.error('上传 Word 模板失败:', err); + ctx.body = ctx.helper.error(500, '上传 Word 模板失败'); + } + } + + // ==================== 成绩发布管理 ==================== + + /** + * 发布/撤销成绩 + * PUT /api/admin/publish-scores + * Body: { publish: true/false, group?: 'zhongzhi'|'gaozhi'|'benke'|'jiaoshi' } + * + * group 可选:带组别时仅对该组生效(管理后台「发布成绩」按钮即按组操作); + * 不带时对全部组生效,兼容旧调用方。 + */ + async publishScores() { + const { ctx, service } = this; + if (!this._ensureAdmin()) return; + + try { + const rule = { + publish: { type: 'boolean', required: true }, + }; + try { + ctx.validate(rule); + } catch (validateErr) { + ctx.body = ctx.helper.error(400, '参数校验失败:' + validateErr.errors[0].message); + return; + } + + const { publish, group } = ctx.request.body; + const validGroups = [ 'zhongzhi', 'gaozhi', 'benke', 'jiaoshi' ]; + if (group && !validGroups.includes(group)) { + ctx.body = ctx.helper.error(400, '无效的组别'); + return; + } + + await service.admin.publishScores(publish, group); + + const scopeLabel = group ? '该组成绩' : '全部组成绩'; + ctx.body = ctx.helper.success( + { published: publish, group: group || null }, + publish ? `${scopeLabel}已发布` : `${scopeLabel}已撤销发布` + ); + } catch (err) { + ctx.logger.error('发布成绩失败:', err); + ctx.body = ctx.helper.error(500, '操作失败'); + } + } + + /** + * 查询成绩是否已发布 + * GET /api/admin/publish-scores?group=xxx + * + * 带 group 返回该组状态;不带时 published 表示「四个组是否全部已发布」, + * 并额外返回 groups 明细供管理后台一次性渲染各组按钮。 + */ + async getPublishStatus() { + const { ctx, service } = this; + if (!this._ensureAdmin()) return; + + try { + const { group } = ctx.query; + const groups = await service.admin.getScorePublishMap(); + const published = group + ? await service.admin.isScorePublished(group) + : await service.admin.isScorePublished(); + ctx.body = ctx.helper.success({ published, group: group || null, groups }); + } catch (err) { + ctx.logger.error('查询成绩发布状态失败:', err); + ctx.body = ctx.helper.error(500, '查询失败'); + } + } + + // ==================== 新闻管理 ==================== + + /** + * 创建新闻 + * POST /api/admin/news + * Body: { title, content, wx_link, publish_time, pinned, group_name } + */ + async createNews() { + const { ctx, service } = this; + if (!this._ensureAdmin()) return; + + try { + const rule = { + title: { type: 'string', required: true }, + content: { type: 'string', required: false }, + }; + try { + ctx.validate(rule); + } catch (validateErr) { + ctx.body = ctx.helper.error(400, '参数校验失败:' + validateErr.errors[0].message); + return; + } + + const news = await service.admin.createNews(ctx.request.body); + ctx.body = ctx.helper.success(news, '新闻创建成功'); + } catch (err) { + ctx.logger.error('创建新闻失败:', err); + ctx.body = ctx.helper.error(500, '创建新闻失败'); + } + } + + /** + * 更新新闻 + * PUT /api/admin/news/:id + */ + async updateNews() { + const { ctx, service } = this; + if (!this._ensureAdmin()) return; + + try { + const { id } = ctx.params; + const news = await service.admin.updateNews(id, ctx.request.body); + ctx.body = ctx.helper.success(news, '新闻更新成功'); + } catch (err) { + if (err.message === '新闻不存在') { + ctx.body = ctx.helper.error(404, '新闻不存在'); + return; + } + ctx.logger.error('更新新闻失败:', err); + ctx.body = ctx.helper.error(500, '更新新闻失败'); + } + } + + /** + * 删除新闻 + * DELETE /api/admin/news/:id + */ + async deleteNews() { + const { ctx, service } = this; + if (!this._ensureAdmin()) return; + + try { + const { id } = ctx.params; + await service.admin.deleteNews(id); + ctx.body = ctx.helper.success(null, '新闻删除成功'); + } catch (err) { + ctx.logger.error('删除新闻失败:', err); + ctx.body = ctx.helper.error(500, '删除新闻失败'); + } + } + + /** + * 更新新闻排序 + * PUT /api/admin/news/order + * Body: { orders: [{ id, sort_order }] } + */ + async updateNewsOrder() { + const { ctx, service } = this; + if (!this._ensureAdmin()) return; + + try { + const rule = { + orders: { type: 'array', required: true, min: 1 }, + }; + try { + ctx.validate(rule); + } catch (validateErr) { + ctx.body = ctx.helper.error(400, '参数校验失败:' + validateErr.errors[0].message); + return; + } + + const { orders } = ctx.request.body; + await service.news.updateSortOrder(orders); + ctx.body = ctx.helper.success(null, '新闻排序更新成功'); + } catch (err) { + ctx.logger.error('更新新闻排序失败:', err); + ctx.body = ctx.helper.error(500, '更新新闻排序失败'); + } + } + + /** + * 获取新闻列表(管理员视图) + * GET /api/admin/news + */ + async listNews() { + const { ctx, service } = this; + if (!this._ensureAdmin()) return; + + try { + const news = await service.admin.listNews(); + ctx.body = ctx.helper.success({ list: news }); + } catch (err) { + ctx.logger.error('获取新闻列表失败:', err); + ctx.body = ctx.helper.error(500, '获取新闻列表失败'); + } + } + + // ==================== 轮播图管理 ==================== + + /** + * 添加轮播图 + * POST /api/admin/carousel + * Body: { image_url, title, sort_order, link_url, enabled } + */ + async addCarousel() { + const { ctx, service } = this; + if (!this._ensureAdmin()) return; + + try { + // image_url 可为空(无底图时前端回退渐变);仅做类型校验,不强制必填 + const rule = { + image_url: { type: 'string', required: false, allowEmpty: true }, + }; + try { + ctx.validate(rule); + } catch (validateErr) { + ctx.body = ctx.helper.error(400, '参数校验失败:' + validateErr.errors[0].message); + return; + } + + const carousel = await service.admin.addCarousel(ctx.request.body); + ctx.body = ctx.helper.success(carousel, '轮播图添加成功'); + } catch (err) { + ctx.logger.error('添加轮播图失败:', err); + ctx.body = ctx.helper.error(500, '添加轮播图失败'); + } + } + + /** + * 更新轮播图 + * PUT /api/admin/carousel/:id + */ + async updateCarousel() { + const { ctx, service } = this; + if (!this._ensureAdmin()) return; + + try { + const { id } = ctx.params; + const carousel = await service.admin.updateCarousel(id, ctx.request.body); + ctx.body = ctx.helper.success(carousel, '轮播图更新成功'); + } catch (err) { + if (err.message === '轮播图不存在') { + ctx.body = ctx.helper.error(404, '轮播图不存在'); + return; + } + ctx.logger.error('更新轮播图失败:', err); + ctx.body = ctx.helper.error(500, '更新轮播图失败'); + } + } + + /** + * 删除轮播图 + * DELETE /api/admin/carousel/:id + */ + async deleteCarousel() { + const { ctx, service } = this; + if (!this._ensureAdmin()) return; + + try { + const { id } = ctx.params; + await service.admin.deleteCarousel(id); + ctx.body = ctx.helper.success(null, '轮播图删除成功'); + } catch (err) { + ctx.logger.error('删除轮播图失败:', err); + ctx.body = ctx.helper.error(500, '删除轮播图失败'); + } + } + + /** + * 获取轮播图列表 + * GET /api/admin/carousel + */ + async listCarousel() { + const { ctx, service } = this; + if (!this._ensureAdmin()) return; + + try { + const carousels = await service.admin.listCarousel(); + ctx.body = ctx.helper.success({ list: carousels }); + } catch (err) { + ctx.logger.error('获取轮播图列表失败:', err); + ctx.body = ctx.helper.error(500, '获取轮播图列表失败'); + } + } + + /** + * 上传图片(轮播图等) + * POST /api/admin/upload/image + */ + async uploadImage() { + const { ctx } = this; + if (!this._ensureAdmin()) return; + + try { + const stream = await ctx.getFileStream(); + if (!stream || !stream.filename) { + ctx.body = ctx.helper.error(400, '请选择图片文件'); + return; + } + + // 只允许图片格式 + const ext = path.extname(stream.filename).toLowerCase(); + if (!['.png', '.jpg', '.jpeg', '.gif', '.webp'].includes(ext)) { + ctx.body = ctx.helper.error(400, '仅支持 PNG/JPG/JPEG/GIF/WebP 格式'); + return; + } + + const fs = require('fs'); + const app = this.app; + const uploadDir = path.join(app.baseDir, 'app/public/carousel'); + if (!fs.existsSync(uploadDir)) { + fs.mkdirSync(uploadDir, { recursive: true }); + } + + const filename = `carousel_${Date.now()}${ext}`; + const destPath = path.join(uploadDir, filename); + + // 写入文件流,同时检查文件大小 + const MAX_SIZE = 5 * 1024 * 1024; // 5MB + let totalSize = 0; + const ws = fs.createWriteStream(destPath); + + stream.on('data', (chunk) => { + totalSize += chunk.length; + if (totalSize > MAX_SIZE) { + stream.destroy(); + ws.destroy(); + // 清理已写入的部分文件 + try { fs.unlinkSync(destPath); } catch (e) {} + return; + } + }); + + stream.pipe(ws); + await new Promise((resolve, reject) => { + ws.on('finish', resolve); + ws.on('error', reject); + }); + + // 检查最终大小 + if (totalSize > MAX_SIZE) { + try { fs.unlinkSync(destPath); } catch (e) {} + ctx.body = ctx.helper.error(400, '图片文件大小不能超过 5MB'); + return; + } + + const imageUrl = `/public/carousel/${filename}`; + ctx.body = ctx.helper.success({ imageUrl, filename }, '图片上传成功'); + } catch (err) { + ctx.logger.error('上传图片失败:', err); + ctx.body = ctx.helper.error(500, '上传图片失败'); + } + } + + async uploadNewsImage() { + const { ctx } = this; + if (!this._ensureAdmin()) return; + + try { + const stream = await ctx.getFileStream(); + if (!stream || !stream.filename) { + ctx.body = ctx.helper.error(400, '请选择图片文件'); + return; + } + + const ext = path.extname(stream.filename).toLowerCase(); + if (!['.png', '.jpg', '.jpeg', '.gif', '.webp'].includes(ext)) { + ctx.body = ctx.helper.error(400, '仅支持 PNG/JPG/JPEG/GIF/WebP 格式'); + return; + } + + const fs = require('fs'); + const app = this.app; + const uploadDir = path.join(app.baseDir, 'app/public/news'); + if (!fs.existsSync(uploadDir)) { + fs.mkdirSync(uploadDir, { recursive: true }); + } + + const filename = `news_${Date.now()}${ext}`; + const destPath = path.join(uploadDir, filename); + + const ws = fs.createWriteStream(destPath); + stream.pipe(ws); + await new Promise((resolve, reject) => { + ws.on('finish', resolve); + ws.on('error', reject); + }); + + const imageUrl = `/public/news/${filename}`; + ctx.body = ctx.helper.success({ imageUrl, filename }, '图片上传成功'); + } catch (err) { + ctx.logger.error('上传新闻图片失败:', err); + ctx.body = ctx.helper.error(500, '上传新闻图片失败'); + } + } + + /** + * 下载考生答案 Word 文件 + * GET /api/admin/submissions/:group/:id/word + */ + async downloadWord() { + const { ctx, service } = this; + if (!this._ensureAdmin()) return; + + try { + const { group, id } = ctx.params; + const fs = require('fs'); + const app = this.app; + const filePathCache = require('../lib/file_path_cache'); + + // 下载前 file_path 短缓存(进程内 30s,任务 #23):高并发下载同一 Word 时减少重复 DB 查询。 + // 命中缓存仍走 _sendFile 的磁盘存在性校验,文件已删则 404,不会吐错误内容。 + const wordCacheKey = `word:${group}:${id}`; + const wordCached = filePathCache.getFull(wordCacheKey); + if (wordCached) { + await this._sendFile(ctx, app, wordCached.filePath, wordCached.fileName || path.basename(wordCached.filePath), + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'); + return; + } + + // 中职/高职组:从 exam_submissions 表查询 + const isExamGroup = group === 'zhongzhi' || group === 'gaozhi'; + let wordFilePath, wordFileName; + + if (isExamGroup) { + const submission = await ctx.model.ExamSubmission.findOne({ + where: { contestant_id: id }, + attributes: ['word_file_path', 'word_file_name'], + }); + if (!submission || !submission.word_file_path) { + ctx.body = ctx.helper.error(404, '未找到该考生的 Word 答案文件'); + return; + } + wordFilePath = submission.word_file_path; + wordFileName = submission.word_file_name; + } else { + // 本科/教师组:Word 答案同样存于 exam_submissions, + // 与 saveWordFile 约定一致——以 aggregation_uid 作为 contestant_id 存储 + const submission = await ctx.model.ExamSubmission.findOne({ + where: { contestant_id: id }, + attributes: ['word_file_path', 'word_file_name'], + }); + if (!submission || !submission.word_file_path) { + ctx.body = ctx.helper.error(404, '未找到该考生的 Word 答案文件'); + return; + } + wordFilePath = submission.word_file_path; + wordFileName = submission.word_file_name; + } + + const filePath = path.join(app.baseDir, 'app/public', wordFilePath); + if (!fs.existsSync(filePath)) { + app.logger.error( + `[downloadWord] Word 文件缺失 group=${group} id=${id} ` + + `DB.word_file_path=${wordFilePath} 解析绝对路径=${filePath}` + ); + ctx.body = ctx.helper.error(404, `Word 文件不存在(期望路径: ${wordFilePath})`); + return; + } + + const fileName = wordFileName || path.basename(filePath); + filePathCache.setFull(wordCacheKey, wordFilePath, wordFileName); + // 🔴 X-Accel-Redirect 根治:生产经 nginx 内部重定向吐文件,开发回退流式。 + // 文件名编码与路径穿越防护、文件 IO 并发护栏统一在 _sendFile 内处理(见其注释)。 + await this._sendFile( + ctx, app, wordFilePath, fileName, + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' + ); + } catch (err) { + ctx.logger.error('下载 Word 文件失败:', err); + ctx.body = ctx.helper.error(500, '下载失败'); + } + } + + /** + * 下载本科/教师组作品文件(按 submission id) + * GET /api/admin/submission-file/:group/:id + */ + async downloadSubmissionFile() { + const { ctx, app } = this; + if (!this._ensureAdmin()) return; + + try { + const { group, id } = ctx.params; + if (!this._validateGroup(group, true)) return; + + const filePathCache = require('../lib/file_path_cache'); + // 下载前 file_path 短缓存(进程内 30s,任务 #23):命中缓存仍走 _sendFile 磁盘存在性校验。 + const subCacheKey = `sub:${group}:${id}`; + const subCached = filePathCache.getFull(subCacheKey); + if (subCached) { + await this._sendFile(ctx, app, subCached.filePath, subCached.fileName || path.basename(subCached.filePath), 'application/octet-stream'); + return; + } + + const SubmissionModel = group === 'benke' + ? ctx.model.BenkeSubmission + : ctx.model.JiaoshiSubmission; + + const submission = await SubmissionModel.findOne({ where: { id } }); + if (!submission || !submission.file_path) { + ctx.body = ctx.helper.error(404, '未找到该作品文件'); + return; + } + + const filePath = path.join(app.baseDir, 'app/public', submission.file_path); + if (!fs.existsSync(filePath)) { + // 🔴 关键诊断信息:把「按 DB 路径解析出的绝对路径」打进日志+响应, + // 生产排查时直接告诉我们文件究竟被期望放在哪里、实际缺失(多见于 + // app/public 目录缺失 / 重装后端清空了上传目录但 DB 仍记旧路径)。 + app.logger.error( + `[downloadSubmissionFile] 文件缺失 group=${group} id=${id} ` + + `DB.file_path=${submission.file_path} 解析绝对路径=${filePath}` + ); + ctx.body = ctx.helper.error(404, `文件不存在或已被清理(期望路径: ${submission.file_path})`); + return; + } + + const fileName = submission.file_name || path.basename(filePath); + filePathCache.setFull(subCacheKey, submission.file_path, submission.file_name); + // 🔴 X-Accel-Redirect 根治:生产经 nginx 内部重定向吐文件,开发回退流式。 + // 文件名编码与路径穿越防护、文件 IO 并发护栏统一在 _sendFile 内处理(见其注释)。 + await this._sendFile(ctx, app, submission.file_path, fileName, 'application/octet-stream'); + } catch (err) { + ctx.logger.error('下载作品文件失败:', err); + ctx.body = ctx.helper.error(500, '下载失败'); + } + } + + // ==================== Word 模板下载 ==================== + + /** + * 获取 Word 模板下载链接 + * GET /api/admin/exam/:group/word-template + */ + async getWordTemplate() { + const { ctx, app } = this; + if (!this._ensureAdmin()) return; + + try { + const { group } = ctx.params; + if (!this._validateGroup(group)) return; + + const time = await ctx.model.CompetitionTime.findOne({ + where: { group }, + attributes: ['word_template'], + }); + + if (!time || !time.word_template) { + ctx.body = ctx.helper.error(404, 'Word 模板不存在'); + return; + } + + const filePath = path.join(app.baseDir, 'app/public', time.word_template); + if (!fs.existsSync(filePath)) { + ctx.body = ctx.helper.error(404, 'Word 模板文件不存在'); + return; + } + + ctx.body = ctx.helper.success({ + templateUrl: ctx.helper.fileUrl(time.word_template), + }, '获取 Word 模板成功'); + } catch (err) { + ctx.logger.error('获取 Word 模板失败:', err); + ctx.body = ctx.helper.error(500, '获取 Word 模板失败'); + } + } + + // ==================== Word 模板删除 ==================== + + /** + * 删除 Word 模板 + * DELETE /api/admin/exam/:group/word-template + */ + async deleteWordTemplate() { + const { ctx, app } = this; + if (!this._ensureAdmin()) return; + + try { + const { group } = ctx.params; + if (!this._validateGroup(group)) return; + + // 查找并清空 word_template 字段 + const time = await ctx.model.CompetitionTime.findOne({ + where: { group }, + attributes: ['id', 'word_template'], + }); + + if (time && time.word_template) { + // 删除物理文件 + const filePath = path.join(app.baseDir, 'app/public', time.word_template); + try { + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + } + } catch (e) { + ctx.logger.warn('删除 Word 模板文件失败:', e.message); + } + + // 清空数据库记录 + await time.update({ word_template: null }); + } + + ctx.body = ctx.helper.success(null, 'Word 模板已删除'); + } catch (err) { + ctx.logger.error('删除 Word 模板失败:', err); + ctx.body = ctx.helper.error(500, '删除 Word 模板失败'); + } + } + + // ==================== 备赛资料管理 ==================== + + /** + * 获取备赛资料列表 + * GET /api/admin/materials/:group + */ + async getMaterials() { + const { ctx, app } = this; + if (!this._ensureAdmin()) return; + + try { + const { group } = ctx.params; + const validGroups = ['zhongzhi', 'gaozhi', 'benke', 'jiaoshi']; + if (!validGroups.includes(group)) { + ctx.body = ctx.helper.error(400, `无效的组别: ${group}`); + return; + } + + const materialsDir = path.join(app.baseDir, 'app/public', 'materials', group); + if (!fs.existsSync(materialsDir)) { + ctx.body = ctx.helper.success({ list: [] }, '获取备赛资料列表成功'); + return; + } + + // 逐文件 stat,单个文件 stat 失败(坏软链/权限/被占用)只跳过该文件并告警, + // 绝不能让一个坏文件把整份列表拖成空数组——否则「刚上传的文件也不显示」。 + const files = []; + for (const f of fs.readdirSync(materialsDir)) { + if (f.startsWith('.')) continue; + try { + const stat = fs.statSync(path.join(materialsDir, f)); + files.push({ + fileName: f, + fileSize: stat.size, + uploadTime: stat.mtime.toISOString(), + }); + } catch (e) { + app.logger.warn( + `[admin.getMaterials] 跳过无法 stat 的文件 group=${group} file=${f}:` + (e && e.message ? e.message : e) + ); + } + } + files.sort((a, b) => new Date(b.uploadTime) - new Date(a.uploadTime)); + + ctx.body = ctx.helper.success({ list: files }, '获取备赛资料列表成功'); + } catch (err) { + ctx.logger.error('获取备赛资料列表失败:', err); + // 纵深防御:磁盘异常(权限/EACCES、坏软链、个别文件 stat 失败)不应让整个 + // 管理页面 500。降级返回空列表,真实错误已落日志供运维排查(与 materials.js + // index() 的容错保持一致,避免读盘异常被 auth 中间件伪装成 401/500 踢人)。 + ctx.body = ctx.helper.success({ list: [] }, '获取备赛资料列表成功(目录异常,已降级为空)'); + } + } + + /** + * 上传备赛资料 + * POST /api/admin/materials/:group + */ + async uploadMaterial() { + const { ctx, app } = this; + if (!this._ensureAdmin()) return; + + try { + const { group } = ctx.params; + const validGroups = ['zhongzhi', 'gaozhi', 'benke', 'jiaoshi']; + if (!validGroups.includes(group)) { + ctx.body = ctx.helper.error(400, `无效的组别: ${group}`); + return; + } + + const stream = await ctx.getFileStream(); + if (!stream) { + ctx.body = ctx.helper.error(400, '未接收到文件'); + return; + } + + const materialsDir = path.join(app.baseDir, 'app/public', 'materials', group); + if (!fs.existsSync(materialsDir)) { + fs.mkdirSync(materialsDir, { recursive: true }); + } + + // 文件名归一化:解码百分号、NFC、去控制字符/路径符、折叠空白并 trim, + // 防止「前导空格 + 乱码」文件名导致后续读盘 stat ENOENT(2026-08-10 事故根因)。 + const rawName = stream.filename || 'material'; + let safeFileName = sanitizeUploadFileName(rawName, path.extname(rawName)); + // 清洗后为空(极端情况)回退到时间戳名,避免写出空名文件 + if (!safeFileName) { + safeFileName = `material_${Date.now()}${path.extname(rawName) || '.bin'}`; + } + const filePath = path.join(materialsDir, safeFileName); + + const writeStream = fs.createWriteStream(filePath); + await new Promise((resolve, reject) => { + stream.pipe(writeStream); + writeStream.on('finish', resolve); + writeStream.on('error', reject); + stream.on('error', reject); + }); + + ctx.body = ctx.helper.success({ + fileName: safeFileName, + fileSize: fs.statSync(filePath).size, + }, '备赛资料上传成功'); + } catch (err) { + ctx.logger.error('上传备赛资料失败:', err); + ctx.body = ctx.helper.error(500, '上传备赛资料失败'); + } + } + + /** + * 删除备赛资料 + * DELETE /api/admin/materials/:group/:fileName + */ + async deleteMaterial() { + const { ctx, app } = this; + if (!this._ensureAdmin()) return; + + try { + const { group, fileName } = ctx.params; + const decodedFileName = decodeURIComponent(fileName); + const validGroups = ['zhongzhi', 'gaozhi', 'benke', 'jiaoshi']; + if (!validGroups.includes(group)) { + ctx.body = ctx.helper.error(400, `无效的组别: ${group}`); + return; + } + + // 防止路径穿越攻击 + if (decodedFileName.includes('..') || decodedFileName.includes('/') || decodedFileName.includes('\\')) { + ctx.body = ctx.helper.error(400, '无效的文件名'); + return; + } + + const filePath = path.join(app.baseDir, 'app/public', 'materials', group, decodedFileName); + if (!fs.existsSync(filePath)) { + ctx.body = ctx.helper.error(404, '文件不存在'); + return; + } + + fs.unlinkSync(filePath); + ctx.body = ctx.helper.success(null, '备赛资料已删除'); + } catch (err) { + ctx.logger.error('删除备赛资料失败:', err); + ctx.body = ctx.helper.error(500, '删除备赛资料失败'); + } + } + + // ==================== 轮播图排序 ==================== + + /** + * 批量更新轮播图排序 + * PUT /api/admin/carousel/order + * Body: { orders: [{ id, sort_order }] } + */ + async updateCarouselOrder() { + const { ctx, service } = this; + if (!this._ensureAdmin()) return; + + try { + const rule = { + orders: { type: 'array', required: true, min: 1 }, + }; + try { + ctx.validate(rule); + } catch (validateErr) { + ctx.body = ctx.helper.error(400, '参数校验失败:' + validateErr.errors[0].message); + return; + } + + const { orders } = ctx.request.body; + + // 批量更新排序 + const updatePromises = orders.map(item => + ctx.model.Carousel.update( + { sort_order: item.sort_order }, + { where: { id: item.id } } + ) + ); + await Promise.all(updatePromises); + + ctx.body = ctx.helper.success(null, '轮播图排序更新成功'); + } catch (err) { + ctx.logger.error('更新轮播图排序失败:', err); + ctx.body = ctx.helper.error(500, '更新轮播图排序失败'); + } + } + + // ==================== 测试数据标记 ==================== + + /** + * 切换选手的测试数据标记 + * POST /api/admin/toggle-test-flag + * Body: { id, test_flag } + * + * 将选手标记为测试数据(test_flag=1)或生产数据(test_flag=0) + * 重置操作仅影响 test_flag=1 的选手及其提交记录 + */ + async toggleTestFlag() { + const { ctx } = this; + if (!this._ensureAdmin()) return; + + try { + const { id, test_flag } = ctx.request.body; + if (!id) { + ctx.body = ctx.helper.error(400, '缺少选手ID'); + return; + } + if (test_flag !== 0 && test_flag !== 1) { + ctx.body = ctx.helper.error(400, 'test_flag 必须为 0 或 1'); + return; + } + + const contestant = await ctx.model.Contestant.findByPk(id); + if (!contestant) { + ctx.body = ctx.helper.error(404, '选手不存在'); + return; + } + + await contestant.update({ test_flag }); + const label = test_flag === 1 ? '测试数据' : '生产数据'; + ctx.body = ctx.helper.success({ id, test_flag }, `已标记为${label}`); + } catch (err) { + ctx.logger.error('切换测试标记失败:', err); + ctx.body = ctx.helper.error(500, '切换测试标记失败'); + } + } + + // ==================== 测试数据重置 ==================== + + /** + * 重置测试数据(方便重复测试) + * POST /api/admin/reset-test-data + * + * 安全机制: + * - 生产环境(NODE_ENV=production)禁止重置 + * - 仅重置 test_flag=1 的选手及其提交记录 + * - 生产数据(test_flag=0)不受影响 + * + * 重置内容: + * - 选手:activated 设为 true(可直接以默认口令登录重测)、password 恢复为默认口令(bcrypt)、score 清空 + * - 提交记录:exam_submissions / work_submissions 删除 + * - 旧版提交记录:zhongzhi/gaozhi/benke/jiaoshi_submissions 删除 + */ + async resetTestData() { + const { ctx, service } = this; + if (!this._ensureAdmin()) return; + + // 生产环境禁止重置 + if (process.env.NODE_ENV === 'production') { + ctx.body = ctx.helper.error(403, '生产环境禁止重置测试数据'); + return; + } + + try { + const results = {}; + + // 获取所有测试选手的 ID + const testContestants = await ctx.model.Contestant.findAll({ + where: { test_flag: 1 }, + attributes: ['id'], + }); + const testIds = testContestants.map(c => c.id); + + results.testContestantsFound = testIds.length; + + if (testIds.length === 0) { + ctx.body = ctx.helper.success(results, '没有找到测试数据,无需重置(请先将选手标记为测试数据)'); + return; + } + + // 默认口令(bcrypt)。注意:重置同时把 activated 归零(见下方第 1 步,用户反馈 Req2), + // 因此测试人员需先重新走「短信激活」,激活后再以该口令登录重测——不是直连登录。 + // (此处曾误写「可直接以该口令登录重测」,与实际 activated:false 自相矛盾,已校正。 + // 权威断言:scripts/verify_feedback_3.py:70,74 与 scripts/verify_testdata_mgmt.py。) + const defaultHash = await ctx.helper.hashPassword(ctx.app.config.testDefaultPassword); + + // 事务包裹全部写操作:任意一步失败整体回滚,避免「部分重置」脏状态 + // (例如选手状态已清零、但提交记录未删,重测时仍显示旧作品 / 已提交态)。 + const tx = await ctx.model.transaction(); + let contestantCount = 0; + let examDeleted = 0; + let workDeleted = 0; + let zhongzhiDeleted = 0; + let gaozhiDeleted = 0; + let benkeDeleted = 0; + let jiaoshiDeleted = 0; + try { + // 1. 重置选手状态(仅测试数据选手):恢复为「未激活 + 默认口令 + 无成绩」, + // 便于从头回归「短信激活」与完整参赛流程;同时清空成绩,避免成绩管理残留旧分。 + [ contestantCount ] = await ctx.model.Contestant.update( + { activated: false, password: defaultHash, score: null, exam_score: 0, word_score: 0, ppt_score: 0 }, + { where: { id: testIds }, transaction: tx } + ); + // 同步清理关联 accounts 的成绩,避免数据随机变化 + const testIdNumbers = (await ctx.model.Contestant.findAll({ + where: { id: testIds }, + attributes: ['id_number'], + raw: true, + transaction: tx, + })).map(c => c.id_number); + if (testIdNumbers.length) { + await ctx.model.Account.update({ score: 0 }, { where: { uid: testIdNumbers }, transaction: tx }); + } + + // 1.1 重置成绩发布状态为「未发布」,避免上一轮已发布状态泄漏到本轮: + // 评分保存后仅管理后台可见,必须显式「发布成绩」考生端才能看到。 + // 发布状态已按组存储,必须四个分组键 + 遗留全局键一并重置, + // 只重置全局键会漏掉分组键,导致考生端仍处于「已发布」。 + // (与本次重置同处一个事务:若后续任一步失败,发布态复位一并回滚,绝不泄漏。) + await service.admin.publishScores(false, undefined, tx); + + // 2. 删除 exam_submissions(仅测试数据选手;客观题答案内嵌其中,一并清除) + examDeleted = await ctx.model.ExamSubmission.destroy({ + where: { contestant_id: testIds }, + transaction: tx, + }); + + // 3. 删除 work_submissions(仅测试数据选手) + workDeleted = await ctx.model.WorkSubmission.destroy({ + where: { contestant_id: testIds }, + transaction: tx, + }); + + // 4. 删除旧版组别提交记录(仅测试数据选手) + // 键位说明(易踩坑): + // - zhongzhi/gaozhi_submissions.aggregation_uid 实际存的是 contestant.id(历史实现); + // - benke/jiaoshi_submissions.aggregation_uid 存的是 aggregation.aggregation_uid, + // 须先经 aggregation 表 account_uid(=contestant.id) 解析,不能直接用 testIds, + // 否则会误删他人记录 / 漏删本人记录。 + zhongzhiDeleted = await ctx.model.ZhongzhiSubmission.destroy({ + where: { aggregation_uid: testIds }, + transaction: tx, + }); + + gaozhiDeleted = await ctx.model.GaozhiSubmission.destroy({ + where: { aggregation_uid: testIds }, + transaction: tx, + }); + + const testAggRows = await ctx.model.Aggregation.findAll({ + where: { account_uid: testIds }, + attributes: ['aggregation_uid'], + raw: true, + transaction: tx, + }); + const testAggUids = testAggRows.map(a => a.aggregation_uid); + + benkeDeleted = testAggUids.length ? await ctx.model.BenkeSubmission.destroy({ + where: { aggregation_uid: testAggUids }, + transaction: tx, + }) : 0; + + jiaoshiDeleted = testAggUids.length ? await ctx.model.JiaoshiSubmission.destroy({ + where: { aggregation_uid: testAggUids }, + transaction: tx, + }) : 0; + + await tx.commit(); + } catch (txErr) { + await tx.rollback(); + throw txErr; + } + + results.contestantsReset = contestantCount; + results.scorePublishReset = true; // 已在本事务内调用 publishScores(false),发布态已复位 + results.examSubmissionsDeleted = examDeleted; + results.workSubmissionsDeleted = workDeleted; + results.zhongzhiSubmissionsDeleted = zhongzhiDeleted; + results.gaozhiSubmissionsDeleted = gaozhiDeleted; + results.benkeSubmissionsDeleted = benkeDeleted; + results.jiaoshiSubmissionsDeleted = jiaoshiDeleted; + + ctx.logger.info('[测试数据重置]', results); + ctx.body = ctx.helper.success(results, `测试数据已重置:${contestantCount} 名选手,${examDeleted + workDeleted} 条提交记录。已恢复为「未激活 + 默认口令 ${ctx.app.config.testDefaultPassword}」,需先用默认口令完成短信激活即可登录重测`); + } catch (err) { + ctx.logger.error('重置测试数据失败:', err); + ctx.body = ctx.helper.error(500, '重置测试数据失败'); + } + } +} + +module.exports = AdminController; diff --git a/app/controller/auth.js b/app/controller/auth.js new file mode 100644 index 0000000..3f52c29 --- /dev/null +++ b/app/controller/auth.js @@ -0,0 +1,610 @@ +'use strict'; + +const Controller = require('egg').Controller; +const svgCaptcha = require('svg-captcha'); +const VALIDATION_RULES = require('../validate'); + +/** + * 认证控制器 - AuthController + * + * 负责选手和管理员的登录认证、账号激活、验证码、密码重置等功能。 + * 使用 Contestant 模型进行选手认证,Admin 模型进行管理员认证。 + * 密码采用 bcrypt 加盐哈希存储;登录时兼容存量 SHA256 旧哈希并在验证成功后透明升级为 bcrypt。 + * 统一响应格式:ctx.helper.success(data, msg) / ctx.helper.error(code, msg) + * + * 主要接口: + * - POST /api/auth/captcha : 获取图形验证码(SVG) + * - POST /api/auth/sms : 发送短信验证码(模拟) + * - POST /api/auth/activate : 激活账号(设置密码) + * - POST /api/auth/login : 选手登录 + * - POST /api/auth/admin-login : 管理员登录 + * - GET /api/auth/current-user : 获取当前用户信息(需认证) + * - POST /api/auth/forgot-password : 忘记密码 - 发送验证码 + * - POST /api/auth/reset-password : 重置密码 + * - POST /api/auth/logout : 退出登录(需认证) + */ +class AuthController extends Controller { + /** + * 交付短信验证码(统一入口,供 sms / forgotPassword 复用) + * + * 行为由运行模式(config.isProduction,来自环境变量 APP_ENV)驱动, + * 使测试版本与生产版本代码路径完全一致、仅差异于配置: + * - 生产环境 + 网关已开启(config.sms.realSend):通过阿里云真实下发, + * 验证码绝不写入 HTTP 响应体、不打印到控制台(防凭身份证号接管账号)。 + * - 测试环境(APP_ENV=test,或缺省):【不真实发送】验证码明文随响应返回 + * (字段 smsCode),由前端弹窗展示,便于测试人员直接获取,无需配置短信网关。 + * - 生产环境 + 网关未开启:安全起见不下发、不回传,仅记录告警(需运维配置 SMS_ENABLED=1)。 + * + * @param {object} contestant - 选手对象(含 phone) + * @param {string} smsCode - 6 位验证码 + * @param {string} sceneLabel - 场景标签(如 '账号激活' / '忘记密码') + * @return {boolean} 是否允许在响应体中回传验证码(仅测试环境为 true) + */ + async _deliverSmsCode(contestant, smsCode, sceneLabel) { + const { ctx, app } = this; + const sms = app.config.sms || {}; + const isProduction = app.config.isProduction; + + // 服务端审计日志(仅落日志文件,不含验证码明文回传前端) + ctx.logger.info( + `[短信验证码] 场景=${sceneLabel} 手机号=${contestant.phone} 环境=${app.config.runMode} realSend=${!!sms.realSend}` + ); + + // 生产环境 + 网关已开启 → 真实下发,绝不回传验证码明文 + if (isProduction && sms.realSend) { + try { + await ctx.service.sms.sendVerifyCode(contestant.phone, smsCode); + ctx.logger.info(`[短信验证码] 场景=${sceneLabel} 已通过网关下发至 ${contestant.phone}`); + } catch (e) { + // 下发失败:记录告警;验证码已存入 session,用户可重试获取,绝不在响应回传 + ctx.logger.error(`[短信验证码] 场景=${sceneLabel} 网关下发失败(${contestant.phone}):${e && e.message}`); + } + return false; + } + + if (isProduction) { + // 生产环境未开启网关:为避免账号接管风险,绝不回传或打印验证码,仅记录告警 + ctx.logger.warn('[短信验证码] 生产环境未启用短信网关(realSend=false),验证码未下发。请设置 SMS_ENABLED=1 并配置阿里云密钥。'); + return false; + } + + // 测试环境:不真实发送,回传验证码供前端弹窗显示(控制台同步打印便于联调) + console.log(''); + console.log('╔══════════════════════════════════════════════╗'); + console.log(`║ 📱 [测试环境] 模拟短信验证码 - ${sceneLabel}`); + console.log('╠══════════════════════════════════════════════╣'); + console.log(`║ 手机号 : ${contestant.phone}`); + console.log(`║ 验证码 : ${smsCode}`); + console.log('║ 有效期 : 5 分钟'); + console.log('╚══════════════════════════════════════════════╝'); + console.log(''); + return true; + } + + /** + * 获取点击式验证码(按顺序点击文字) + * + * 生成 4 个随机中文汉字,指定点击顺序,返回打乱后的文字和提示。 + * 用户需按照提示顺序依次点击文字,点击序列存入 session 用于校验。 + */ + async captcha() { + const { ctx } = this; + + // 字符池(常用中文字符,避免生僻字) + const charPool = ['天', '地', '人', '和', '山', '水', '风', '火', '日', '月', '星', '辰', '花', '鸟', '云', '雨']; + + // 随机选取 4 个不重复字符 + const pool = [...charPool]; + const selected = []; + for (let i = 0; i < 4; i++) { + const idx = Math.floor(Math.random() * pool.length); + selected.push(pool.splice(idx, 1)[0]); + } + + // 存入 session:点击顺序的字符串(如 "天地人和") + const clickOrder = selected.join(''); + ctx.session.captcha = clickOrder; + + // 打乱显示顺序 + const displayOrder = [...selected].sort(() => Math.random() - 0.5); + + ctx.body = ctx.helper.success({ + chars: displayOrder, + hint: '请依次点击:' + selected.join(' → '), + }, '验证码获取成功'); + } + + /** + * 发送短信验证码(模拟) + * + * 前端传入身份证号作为 phone 参数,后端根据身份证号查找选手的真实手机号。 + * + * 流程: + * 1. 校验图形验证码 + * 2. 通过身份证号查找参赛选手 + * 3. 生成 6 位随机数字验证码 + * 4. 存入 ctx.session.smsCode / smsPhone / smsIdNumber + * 5. 返回真实手机号、姓名、组别和模拟验证码 + */ + async sms() { + const { ctx, service } = this; + + const { phone, captcha } = ctx.request.body; + + // 统一输入校验(ctx.validate) + try { + ctx.validate(VALIDATION_RULES.sms, ctx.request.body); + } catch (validateErr) { + ctx.body = ctx.helper.error(400, '参数校验失败:' + validateErr.errors[0].message); + return; + } + + // 校验图形验证码 + if (!ctx.session.captcha || captcha !== ctx.session.captcha) { + ctx.body = ctx.helper.error(1002, '图形验证码错误'); + return; + } + + // 通过身份证号查找选手 + const contestant = await service.auth.findByAccount(phone); + if (!contestant) { + ctx.body = ctx.helper.error(1003, '该身份证号不在参赛名单中,请联系管理员'); + return; + } + + // 检查是否已激活 + if (contestant.activated) { + ctx.body = ctx.helper.error(1004, '该账号已激活,请直接登录'); + return; + } + + // 生成 6 位随机数字验证码 + const smsCode = ctx.helper.randomCode(6); + + // 存入 session(5 分钟有效期) + ctx.session.smsCode = smsCode; + ctx.session.smsPhone = contestant.phone; + ctx.session.smsIdNumber = phone; + ctx.session.smsCodeExpire = Date.now() + 5 * 60 * 1000; + + // 清除已使用的图形验证码 + ctx.session.captcha = null; + + // 交付验证码(生产环境不回传/不打印,仅走真实网关) + const exposeCode = await this._deliverSmsCode(contestant, smsCode, '账号激活'); + + const respData = { + phone: contestant.phone, + name: contestant.name, + group: contestant.group, + }; + if (exposeCode) respData.smsCode = smsCode; + + ctx.body = ctx.helper.success(respData, '短信验证码发送成功'); + } + + /** + * 激活账号 + * + * 流程: + * 1. 校验身份证号、手机号、短信验证码 + * 2. 通过身份证号查找选手 + * 3. 检查账号是否已激活 + * 4. SHA256 哈希密码,更新 activated=1 + * 5. 清除 session 中的短信验证码 + */ + async activate() { + const { ctx, service } = this; + + const { id_number, phone, smsCode, password } = ctx.request.body; + + // 统一输入校验(ctx.validate) + try { + ctx.validate(VALIDATION_RULES.activate, ctx.request.body); + } catch (validateErr) { + ctx.body = ctx.helper.error(400, '参数校验失败:' + validateErr.errors[0].message); + return; + } + + // 校验短信验证码 + if (!ctx.session.smsCode || smsCode !== ctx.session.smsCode) { + ctx.body = ctx.helper.error(2005, '短信验证码错误'); + return; + } + + // 校验短信验证码是否过期 + if (ctx.session.smsCodeExpire && Date.now() > ctx.session.smsCodeExpire) { + ctx.body = ctx.helper.error(2006, '短信验证码已过期'); + return; + } + + // 校验手机号一致性 + if (ctx.session.smsPhone !== phone && ctx.session.smsIdNumber !== id_number) { + ctx.body = ctx.helper.error(2007, '手机号与发送验证码时的手机号不一致'); + return; + } + + // 通过身份证号查找选手 + const contestant = await service.auth.findByAccount(id_number); + if (!contestant) { + ctx.body = ctx.helper.error(2008, '该身份证号不在参赛名单中,请联系管理员'); + return; + } + + // 检查是否已激活 + if (contestant.activated) { + ctx.body = ctx.helper.error(2009, '该账号已激活,请直接登录'); + return; + } + + // bcrypt 加盐哈希密码 + const hashedPassword = await ctx.helper.hashPassword(password); + + // 更新选手:激活状态 + 密码 + await service.auth.activateAccount(contestant.id, hashedPassword); + + // 清除 session 中的短信验证码 + ctx.session.smsCode = null; + ctx.session.smsPhone = null; + ctx.session.smsIdNumber = null; + ctx.session.smsCodeExpire = null; + + ctx.body = ctx.helper.success({ + id: contestant.id, + id_number: contestant.id_number, + name: contestant.name, + group: contestant.group, + }, '账号激活成功'); + } + + /** + * 选手登录 - 使用手机号 + * + * 流程: + * 1. 通过手机号查找选手 + * 2. 检查账号是否激活 + * 3. SHA256(输入密码) 与数据库密码比对 + * 4. 生成 JWT Token,返回用户信息和 Token + */ + async login() { + const { ctx, service } = this; + + const { phone, password } = ctx.request.body; + + // 统一输入校验(ctx.validate) + try { + ctx.validate(VALIDATION_RULES.login, ctx.request.body); + } catch (validateErr) { + ctx.body = ctx.helper.error(400, '参数校验失败:' + validateErr.errors[0].message); + return; + } + + // 查找选手 + const contestant = await service.auth.findByPhone(phone); + if (!contestant) { + ctx.body = ctx.helper.error(3003, '该手机号不在参赛名单中'); + return; + } + + // 检查是否激活 + if (!contestant.activated) { + ctx.body = ctx.helper.error(3004, '账号未激活,请先激活'); + return; + } + + // 验证密码(兼容 bcrypt 与存量 SHA256) + const isPasswordValid = await service.auth.verifyPassword(password, contestant.password); + if (!isPasswordValid) { + ctx.body = ctx.helper.error(3005, '密码错误'); + return; + } + + // 若为旧版 SHA256 哈希,登录成功后透明升级为 bcrypt + await service.auth.upgradePasswordIfLegacy(contestant, password); + + // 生成 JWT Token + const token = ctx.helper.generateToken({ + id: contestant.id, + phone: contestant.phone, + role: 'contestant', + }, this.app.config.jwt.secret, this.app.config.jwt.expiresIn); + + // 返回用户信息(排除密码) + const userData = contestant.toJSON(); + delete userData.password; + + ctx.body = ctx.helper.success({ + token, + user: userData, + }, '登录成功'); + } + + /** + * 管理员登录 + * + * 流程: + * 1. 通过用户名从 admins 表查找管理员 + * 2. 密码比对(SHA256) + * 3. 生成管理员 JWT Token + */ + async adminLogin() { + const { ctx, service } = this; + + const { username, password } = ctx.request.body; + + // 统一输入校验(ctx.validate) + try { + ctx.validate(VALIDATION_RULES.adminLogin, ctx.request.body); + } catch (validateErr) { + ctx.body = ctx.helper.error(400, '参数校验失败:' + validateErr.errors[0].message); + return; + } + + const admin = await service.auth.findAdmin(username); + if (!admin) { + ctx.body = ctx.helper.error(4003, '管理员不存在'); + return; + } + + // 管理员密码比对(兼容 bcrypt 与存量 SHA256) + const isValid = await service.auth.verifyPassword(password, admin.password); + if (!isValid) { + ctx.body = ctx.helper.error(4004, '密码错误'); + return; + } + + // P2-3 默认口令守卫:schema.sql 内置默认管理员(admin/admin123), + // 生产环境禁止使用默认口令登录,防止默认账号直接上线被接管; + // 非生产环境放行但记录告警,并在响应中提示尽快改密。 + const DEFAULT_ADMIN_PASSWORDS = ['admin123', 'admin123456']; + const usingDefaultPassword = DEFAULT_ADMIN_PASSWORDS.includes(password); + const isProd = this.app.config.isProduction; + if (usingDefaultPassword && isProd) { + ctx.logger.error(`[安全阻断] 管理员 ${admin.username} 在生产环境尝试使用默认密码登录,已拒绝`); + ctx.body = ctx.helper.error(4005, '生产环境禁止使用默认密码登录,请联系运维在数据库中重置为强密码后再试'); + return; + } + if (usingDefaultPassword) { + ctx.logger.warn(`[安全提醒] 管理员 ${admin.username} 正在使用默认密码,请尽快修改`); + } + + // 若为旧版 SHA256 哈希,登录成功后透明升级为 bcrypt + await service.auth.upgradePasswordIfLegacy(admin, password); + + const token = ctx.helper.generateToken({ + id: admin.id, + role: 'admin', + }, this.app.config.jwt.secret, this.app.config.jwt.expiresIn); + + ctx.body = ctx.helper.success({ + token, + user: { + id: admin.id, + username: admin.username, + role: 'admin', + }, + // 使用默认口令时向前端透出改密提醒标记 + mustChangePassword: usingDefaultPassword, + }, '管理员登录成功'); + } + + /** + * 获取当前登录用户信息(需认证) + */ + async currentUser() { + const { ctx } = this; + + // 从 JWT token 中获取用户信息 + const token = ctx.headers.authorization + ? ctx.headers.authorization.replace('Bearer ', '') + : null; + + if (!token) { + ctx.body = ctx.helper.error(5001, '未登录'); + return; + } + + let decoded; + try { + decoded = ctx.helper.verifyToken(token, this.app.config.jwt.secret); + } catch (e) { + ctx.body = ctx.helper.error(5002, 'Token 已过期,请重新登录'); + return; + } + + if (!decoded || !decoded.id) { + ctx.body = ctx.helper.error(5003, 'Token 无效'); + return; + } + + const contestant = await ctx.model.Contestant.findOne({ + where: { id: decoded.id }, + }); + + if (!contestant) { + ctx.body = ctx.helper.error(5001, '用户不存在'); + return; + } + + const userData = contestant.toJSON(); + delete userData.password; + + ctx.body = ctx.helper.success(userData, '获取用户信息成功'); + } + + /** + * 忘记密码 - 发送验证码 + */ + async forgotPassword() { + const { ctx, service } = this; + + const { id_number, captcha } = ctx.request.body; + + // 统一输入校验(ctx.validate) + try { + ctx.validate(VALIDATION_RULES.forgotPassword, ctx.request.body); + } catch (validateErr) { + ctx.body = ctx.helper.error(400, '参数校验失败:' + validateErr.errors[0].message); + return; + } + + if (!ctx.session.captcha || captcha !== ctx.session.captcha) { + ctx.body = ctx.helper.error(6002, '图形验证码错误'); + return; + } + + const contestant = await service.auth.findByAccount(id_number); + if (!contestant) { + ctx.body = ctx.helper.error(6003, '该身份证号不在参赛名单中'); + return; + } + + if (!contestant.activated) { + ctx.body = ctx.helper.error(6004, '该账号尚未激活,请先激活'); + return; + } + + const smsCode = ctx.helper.randomCode(6); + + ctx.session.smsCode = smsCode; + ctx.session.smsPhone = contestant.phone; + ctx.session.smsIdNumber = id_number; + ctx.session.smsCodeExpire = Date.now() + 5 * 60 * 1000; + + ctx.session.captcha = null; + + // 交付验证码(生产环境不回传/不打印,仅走真实网关) + const exposeCode = await this._deliverSmsCode(contestant, smsCode, '忘记密码'); + + const respData = { phone: contestant.phone }; + if (exposeCode) respData.smsCode = smsCode; + + ctx.body = ctx.helper.success(respData, '验证码已发送'); + } + + /** + * 重置密码 + */ + async resetPassword() { + const { ctx, service } = this; + + const { id_number, smsCode, password } = ctx.request.body; + + // 统一输入校验(ctx.validate) + try { + ctx.validate(VALIDATION_RULES.resetPassword, ctx.request.body); + } catch (validateErr) { + ctx.body = ctx.helper.error(400, '参数校验失败:' + validateErr.errors[0].message); + return; + } + + if (!ctx.session.smsCode || smsCode !== ctx.session.smsCode) { + ctx.body = ctx.helper.error(7004, '短信验证码错误'); + return; + } + + if (ctx.session.smsCodeExpire && Date.now() > ctx.session.smsCodeExpire) { + ctx.body = ctx.helper.error(7005, '短信验证码已过期'); + return; + } + + const contestant = await service.auth.findByAccount(id_number); + if (!contestant) { + ctx.body = ctx.helper.error(7006, '账号不存在'); + return; + } + + const hashedPassword = await ctx.helper.hashPassword(password); + await contestant.update({ password: hashedPassword }); + + ctx.session.smsCode = null; + ctx.session.smsPhone = null; + ctx.session.smsIdNumber = null; + ctx.session.smsCodeExpire = null; + + ctx.body = ctx.helper.success(null, '密码重置成功'); + } + + /** + * 退出登录 + */ + async logout() { + const { ctx } = this; + + ctx.body = ctx.helper.success(null, '退出登录成功'); + } + + /** + * 刷新 Token(静默续期,用户无感知) + * + * 设计目标:让活跃用户永远不会被"意外踢出登录"。 + * - 接受可能已过期的令牌(verifyToken 忽略过期)。 + * - 仅校验签名有效 + 用户仍真实存在且已激活。 + * - 在宽限期内(refreshGrace)允许刷新;超过宽限期则必须重新登录。 + * - 该接口刻意不挂 auth 中间件(因为携带的 token 可能已过期)。 + */ + async refresh() { + const { ctx } = this; + + const authorization = ctx.headers.authorization || ''; + const token = authorization.replace(/^Bearer\s+/i, '') || null; + if (!token) { + ctx.body = ctx.helper.error(5001, '未登录'); + return; + } + + let decoded; + try { + // 忽略过期,仅校验签名与载荷结构,用于静默续期 + decoded = ctx.helper.verifyToken(token, this.app.config.jwt.secret, { ignoreExpiration: true }); + } catch (e) { + ctx.body = ctx.helper.error(5002, 'Token 无效,请重新登录'); + return; + } + if (!decoded || !decoded.id) { + ctx.body = ctx.helper.error(5003, 'Token 无效,请重新登录'); + return; + } + + // 宽限期校验:仅允许在过期后 refreshGrace 秒内续期,防止会话永久不失效 + const nowSec = Math.floor(Date.now() / 1000); + const expiredAgo = decoded.exp ? nowSec - decoded.exp : 99999999; + const refreshGrace = this.app.config.jwt.refreshGrace || 7 * 24 * 60 * 60; + if (expiredAgo > refreshGrace) { + ctx.body = ctx.helper.error(5004, '登录已过期太久,请重新登录'); + return; + } + + // 校验用户仍真实存在(且选手需已激活) + let account = null; + if (decoded.role === 'admin') { + account = await ctx.model.Admin.findOne({ where: { id: decoded.id } }); + } else { + account = await ctx.model.Contestant.findOne({ where: { id: decoded.id } }); + if (account && !account.activated) { + ctx.body = ctx.helper.error(5001, '账号未激活,请先激活'); + return; + } + } + if (!account) { + ctx.body = ctx.helper.error(5001, '用户不存在,请重新登录'); + return; + } + + // 签发新令牌(沿用登录时的有效期配置) + const payload = decoded.role === 'admin' + ? { id: account.id, role: 'admin' } + : { id: account.id, phone: account.phone, role: 'contestant' }; + const newToken = ctx.helper.generateToken(payload, this.app.config.jwt.secret, this.app.config.jwt.expiresIn); + + const userData = account.toJSON(); + delete userData.password; + + ctx.body = ctx.helper.success({ + token: newToken, + user: userData, + }, '刷新成功'); + } +} + +module.exports = AuthController; diff --git a/app/controller/carousel.js b/app/controller/carousel.js new file mode 100644 index 0000000..05bb23a --- /dev/null +++ b/app/controller/carousel.js @@ -0,0 +1,35 @@ +'use strict'; + +const Controller = require('egg').Controller; + +/** + * 轮播图控制器 - CarouselController + * + * 负责首页轮播图的查询和展示。 + * + * 主要接口: + * - GET /api/carousel : 获取启用的轮播图列表(按 sort_order 升序排列) + */ +class CarouselController extends Controller { + /** + * 获取轮播图列表 + * + * 查询条件:enabled = 1(仅返回启用状态的轮播图) + * 排序规则:sort_order ASC(序号越小越靠前) + */ + async index() { + const { ctx } = this; + + const list = await ctx.model.Carousel.findAll({ + where: { enabled: true }, + order: [ + ['sort_order', 'ASC'], + ], + attributes: ['id', 'image_url', 'title', 'content', 'button_text', 'button_link', 'sort_order', 'link_url', 'enabled', 'created_at', 'updated_at'], + }); + + ctx.body = ctx.helper.success(list, '获取轮播图列表成功'); + } +} + +module.exports = CarouselController; \ No newline at end of file diff --git a/app/controller/exam.js b/app/controller/exam.js new file mode 100644 index 0000000..ea677fb --- /dev/null +++ b/app/controller/exam.js @@ -0,0 +1,813 @@ +'use strict'; + +const Controller = require('egg').Controller; +const path = require('path'); +const fs = require('fs'); +const crypto = require('crypto'); +const jwt = require('jsonwebtoken'); +const { acquire: acquireFileIo, release: releaseFileIo } = require('../lib/file_io_guard'); +// 🔴 同源 collapse(缺口9 修复):文件发送统一走 sendFile util +const { sendFile } = require('../utils/sendFile'); + +/** + * 团队考试锁的进程内互斥(单实例部署适用)。 + * 防止同一小组两名成员"同一瞬间"点击进入考试时,都越过 checkTeamLock + * 各自写入占位 submission 而导致两人同时进入正式考试的竞态(G1 修复)。 + * 进入临界区的胜者写入占位记录并提交;落后者进入临界区时复检到该记录, + * 返回 4005。多实例部署需替换为 Redis 分布式锁。 + */ +const teamLockChain = new Map(); +async function withTeamLock(teamId, fn) { + const prev = teamLockChain.get(teamId) || Promise.resolve(); + let release; + const next = new Promise((resolve) => { release = resolve; }); + teamLockChain.set(teamId, prev.then(() => next)); + try { + await prev; + return await fn(); + } finally { + teamLockChain.delete(teamId); + release(); + } +} + +/** + * 考试控制器 - ExamController + * + * 负责在线答题相关功能,包括获取题目、提交答案、上传 Word 文件、 + * 一体提交等。适用于中职组和高职组的在线考试模式。 + * + * 主要接口: + * - GET /api/exam/:group/status : 获取考试时间状态 + * - GET /api/exam/:group/questions : 获取随机题目列表 + * - GET /api/exam/:group/word-template: 下载 Word 模板 + * - POST /api/exam/:group/submit-answers : 提交答题答案 + * - POST /api/exam/:group/upload-word : 上传 Word 操作文件 + * - POST /api/exam/:group/submit-all : 一体提交(答案 + 文件) + */ +class ExamController extends Controller { + /** + * 校验组别参数是否有效 + * @param {string} group - 组别 + * @returns {boolean} 是否有效 + */ + _validateGroup(group) { + const validGroups = ['zhongzhi', 'gaozhi', 'benke', 'jiaoshi']; + if (!validGroups.includes(group)) { + this.ctx.body = this.ctx.helper.error(400, '无效的组别'); + return false; + } + return true; + } + + /** + * 获取用户的报名信息及组别 + * 直接从 Contestant 表获取(简化版数据库无 accounts/aggregation 表) + * @returns {Promise} + */ + async _getUserAggregation() { + const user = this.ctx.state.currentUser; + if (!user || !user.id) return null; + const contestant = await this.ctx.model.Contestant.findOne({ + where: { id: user.id }, + attributes: ['id', 'id_number', 'group', 'name', 'team_id'], + }); + if (!contestant) return null; + + // 团队归属解析(兼容两种来源): + // 1) 首选 contestant.team_id(导入流程填充的整数团队 uid,G1 锁主依据); + // 2) 回退到聚合报名表 aggregation.team_uid(避免真实种子仅建了聚合、未回填 contestant.team_id 时锁失效)。 + let teamId = contestant.team_id; + if (!teamId) { + const agg = await this.ctx.model.Aggregation.findOne({ + where: { account_uid: contestant.id }, + attributes: ['team_uid'], + }); + if (agg && agg.team_uid) teamId = agg.team_uid; + } + + return { + aggregation_uid: contestant.id, + team: { group_name: contestant.group, team_id: teamId }, + contestant, + }; + } + + /** + * 团队考试锁统一入口(原子:检查 + 占位):同一小组仅允许一人进入正式考试。 + * 用进程内按 team_id 互斥锁包裹「检查他人是否已进 + 为自己写占位记录」, + * 消除并发竞态与绕过 questions 直接提交的旁路(G1 修复)。 + * @returns {Promise<{allowed:boolean, lockedBy?:string}>} + */ + async _enterExamWithTeamLock(aggregation) { + if (!aggregation.team || !aggregation.team.team_id) { return { allowed: true }; } + const teamId = aggregation.team.team_id; + // 🔴 team_uid 仅组内唯一:互斥锁与队友判定均须带组别,否则不同组的同号队伍 + // 会被互相串行化 + 误判为队友(4005 误锁)。 + const group = aggregation.team.group_name; + return withTeamLock(`${group}:${teamId}`, async () => { + const lockInfo = await this.ctx.service.exam.checkTeamLock(teamId, aggregation.aggregation_uid, group); + if (lockInfo.locked) return { allowed: false, lockedBy: lockInfo.lockedBy }; + // 抢占成功:为自己写占位 submission(同组他人此后将被 4005 拦截) + const rec = await this.ctx.service.exam.setTeamLock(teamId, aggregation.aggregation_uid); + return { allowed: true }; + }); + } + + /** + * 获取考试时间状态 + * GET /api/exam/:group/status + * + * 返回比赛是否激活、开始/结束时间、剩余秒数 + */ + async status() { + const { ctx, service } = this; + + try { + const { group } = ctx.params; + if (!this._validateGroup(group)) return; + + // 获取考试时间状态 + const status = await service.exam.getExamStatus(group); + + // 获取用户的聚合信息,确认组别匹配 + const aggregation = await this._getUserAggregation(); + if (aggregation && aggregation.team) { + const userGroup = aggregation.team.group_name; + if (userGroup !== group) { + ctx.body = ctx.helper.error(403, '您不属于该组别'); + return; + } + } + + // 如果已提交过,附加已提交状态 + let submitted = false; + if (aggregation) { + try { + submitted = await service.exam.hasSubmitted(aggregation.aggregation_uid, group); + } catch (e) { + ctx.logger.warn('[Exam] 查询提交状态失败:', e.message); + } + } + + // 检查团队锁:同一小组只能有一人进入考试 + let teamLocked = false; + let teamLockedBy = null; + if (aggregation && aggregation.team && aggregation.team.team_id) { + try { + const lockInfo = await service.exam.checkTeamLock( + aggregation.team.team_id, + aggregation.aggregation_uid, + aggregation.team.group_name + ); + teamLocked = lockInfo.locked; + teamLockedBy = lockInfo.lockedBy || null; + } catch (e) { + ctx.logger.warn('[Exam] 查询团队锁状态失败:', e.message); + } + } + + ctx.body = ctx.helper.success({ + isActive: status.isActive, + startTime: status.startTime, + endTime: status.endTime, + countdown: status.countdown, + status: status.status, + submitted, + teamLocked, + teamLockedBy, + }, '获取考试状态成功'); + } catch (err) { + ctx.logger.error('获取考试状态失败:', err); + ctx.body = ctx.helper.error(500, '获取考试状态失败'); + } + } + + /** + * 获取随机题目列表 + * GET /api/exam/:group/questions + * + * 从题库中随机抽取指定数量的题目,排除答案字段保证安全 + */ + async questions() { + const { ctx, service } = this; + + try { + const { group } = ctx.params; + if (!this._validateGroup(group)) return; + + // 验证考试是否活跃 + const examStatus = await service.exam.getExamStatus(group); + if (!examStatus.isActive) { + ctx.body = ctx.helper.error(4001, examStatus.message || '考试未在进行中'); + return; + } + + // 获取用户聚合信息 + const aggregation = await this._getUserAggregation(); + if (!aggregation) { + ctx.body = ctx.helper.error(403, '未找到您的报名信息'); + return; + } + + // 校验组别 + if (aggregation.team && aggregation.team.group_name !== group) { + ctx.body = ctx.helper.error(403, '您不属于该组别'); + return; + } + + // 团队锁:同一小组只能一人进入正式考试(原子检查+占位,G1 修复) + const teamGate = await this._enterExamWithTeamLock(aggregation); + if (!teamGate.allowed) { + ctx.body = ctx.helper.error(4005, '您的小组已进入考试'); + return; + } + + // 抽题数量以 competition_times 配置为准(题目数量不足时 LIMIT 自动按题库实际封顶) + let choiceCount = 50; + let judgeCount = 20; + try { + const ct = await ctx.model.CompetitionTime.findOne({ + where: { group }, + attributes: ['choice_count', 'judge_count'], + raw: true, + }); + if (ct) { + choiceCount = ct.choice_count != null ? ct.choice_count : 50; + judgeCount = ct.judge_count != null ? ct.judge_count : 20; + } + } catch (e) { /* 配置缺失时使用默认 50/20 */ } + + // 取题:按考生固化题目顺序(首次进入随机生成并持久化,之后重进复用同一顺序, + // 避免「重进考试题目与顺序变化」缺陷)。顺序固化在 exam_submissions.question_order。 + const { questions } = await service.exam.getAssignedQuestions( + aggregation.aggregation_uid, + group, + { choiceCount, judgeCount } + ); + + // 题库实际数量:用于前端提示「配置抽取 N / 题库实际 M」,体现自适应 + const [availableChoice, availableJudge] = await Promise.all([ + ctx.model.ChoiceQuestion.count({ where: { group } }), + ctx.model.JudgeQuestion.count({ where: { group } }), + ]); + + ctx.body = ctx.helper.success({ + group, + count: questions.length, + questions, + configuredChoice: choiceCount, + configuredJudge: judgeCount, + availableChoice, + availableJudge, + }, '获取题目成功'); + } catch (err) { + ctx.logger.error('获取题目失败:', err); + ctx.body = ctx.helper.error(500, '获取题目失败'); + } + } + + /** + * 下载 Word 模板文件 + * GET /api/exam/:group/word-template + * + * 以流式方式下载该组别的 Word 操作模板 + */ + async wordTemplate() { + const { ctx, service, app } = this; + + try { + const { group } = ctx.params; + if (!this._validateGroup(group)) return; + + // 鉴权:Word 模板是文件下载(前端通过 fetch 触发,可能只在 URL 上带 ?token= 查询参数, + // 也可能带 Authorization 请求头)。这里自行校验 token,兼容两种方式, + // 避免被「只认 Authorization 头的 auth 中间件」误判为 401「未提供认证令牌」。 + const authHeader = ctx.get('Authorization'); + const tokenFromQuery = ctx.query.token; + const token = (authHeader && authHeader.startsWith('Bearer ')) + ? authHeader.slice(7) + : tokenFromQuery; + if (!token) { + ctx.status = 401; + ctx.body = ctx.helper.error(401, '未提供认证令牌,请先登录'); + return; + } + try { + const decoded = jwt.verify(token, app.config.jwt.secret); + ctx.state.currentUser = { + id: decoded.id || decoded.uid || 0, + uid: decoded.uid || decoded.id || 0, + id_number: decoded.id_number || '', + phone: decoded.phone || '', + role: decoded.role, + }; + } catch (e) { + ctx.status = 401; + ctx.body = ctx.helper.error(401, '认证令牌无效或已过期,请重新登录'); + return; + } + + // 获取模板路径 + const templatePath = await service.exam.getWordTemplatePath(group); + if (!templatePath) { + ctx.body = ctx.helper.error(4002, 'Word 模板尚未上传'); + return; + } + + // 构建完整文件路径(保留「模板未上传」早返回:service 可能返回不存在路径) + const fullPath = path.isAbsolute(templatePath) + ? templatePath + : path.join(app.baseDir, 'app/public', templatePath); + + // 检查文件是否存在(早返回,沿用原错误码 4003) + if (!fs.existsSync(fullPath)) { + ctx.body = ctx.helper.error(4003, '模板文件不存在'); + return; + } + + // 🔴 同源 collapse(缺口9 修复):统一走 sendFile(磁盘校验 + X-File-Size 哨兵 + X-Accel 卸载) + const fileName = path.basename(fullPath); + return sendFile(ctx, app, { + relativePath: path.isAbsolute(templatePath) ? undefined : templatePath, + absolutePath: path.isAbsolute(templatePath) ? fullPath : undefined, + fileName, + contentType: 'application/octet-stream', + }); + } catch (err) { + ctx.logger.error('下载模板失败:', err); + ctx.body = ctx.helper.error(500, '下载模板失败'); + } + } + + /** + * 提交答题答案 + * POST /api/exam/:group/submit-answers + * + * 接收答案数组,自动判分并保存记录 + * Body: { answers: [{ question_uid, answer_data }] } + */ + async submitAnswers() { + const { ctx, service } = this; + + try { + const { group } = ctx.params; + if (!this._validateGroup(group)) return; + + // 验证考试是否活跃 + const examStatus = await service.exam.getExamStatus(group); + if (!examStatus.isActive) { + ctx.body = ctx.helper.error(4001, examStatus.message || '考试未在进行中'); + return; + } + + // 获取用户聚合信息 + const aggregation = await this._getUserAggregation(); + if (!aggregation) { + ctx.body = ctx.helper.error(403, '未找到您的报名信息'); + return; + } + + // 团队锁:同一小组只能一人进入正式考试(G1 修复,防绕过 questions 直接提交) + const teamGate = await this._enterExamWithTeamLock(aggregation); + if (!teamGate.allowed) { + ctx.body = ctx.helper.error(4005, '您的小组已进入考试'); + return; + } + + // 参数校验 + const rule = { + answers: { type: 'array', required: true, min: 1 }, + }; + try { + ctx.validate(rule); + } catch (validateErr) { + ctx.body = ctx.helper.error(400, '参数校验失败:' + validateErr.errors[0].message); + return; + } + + const { answers } = ctx.request.body; + + // 验证每个答案的数据格式(需要 question_id 和 question_type) + // 容错:question_type 缺失时按 uid 前缀(choice_x/judge_x)推导, + // 避免旧版前端缓存逐题保存时被「答案格式错误」拦截。 + for (const ans of answers) { + if (!ans.question_id && !ans.question_uid) { + ctx.body = ctx.helper.error(400, '答案格式错误,需要 question_id 或 question_uid'); + return; + } + if (!ans.question_type) { + const uid = String(ans.question_uid || ans.question_id || ''); + if (uid.startsWith('choice')) ans.question_type = 'choice'; + else if (uid.startsWith('judge')) ans.question_type = 'judge'; + } + if (!ans.question_type) { + ctx.body = ctx.helper.error(400, '答案格式错误,需要 question_type(choice/judge/word)'); + return; + } + } + + // 自动判分 + const scoreResult = await service.exam.autoScore(answers); + + // 构建保存数据 + const saveData = scoreResult.details.map(d => ({ + question_uid: d.question_id, + question_type: d.question_type, + answer_data: d.answer_data, + is_correct: d.is_correct, + score: d.score, + })); + + // 保存答题记录 + const savedRecords = await service.exam.saveAnswers( + aggregation.aggregation_uid, + saveData, + group + ); + + // ⚠️ 此处【不】调用 ensureExamSubmission:逐题保存只落库客观题答案(saveAnswers), + // 绝不写 exam_submissions.submit_time。占位记录由 _enterExamWithTeamLock→setTeamLock + // 以 submit_time=null 创建。若在此写入 submit_time,选手答一题即被 hasSubmitted 判为 + // 「已提交」,导致意外退出后无法重新进入考试(考试组重进需求的直接根因)。 + + // 成绩门控:自动评分结果只落库供管理后台使用,绝不回传给考生。 + // 历史实现直接返回 autoScore/total/details,考生在浏览器网络面板即可看到客观题得分, + // 与「管理后台评分并发布后考生才能看到成绩」的业务约束冲突。 + ctx.body = ctx.helper.success({ + savedCount: savedRecords.length, + }, '答案提交成功'); + } catch (err) { + ctx.logger.error('提交答案失败:', err); + ctx.body = ctx.helper.error(500, '提交答案失败'); + } + } + + /** + * 上传 Word 操作文件 + * POST /api/exam/:group/upload-word + * + * 处理 Word 文件上传,保存到对应组别的目录中 + */ + async uploadWord() { + const { ctx, service, app } = this; + + try { + const { group } = ctx.params; + if (!this._validateGroup(group)) return; + + // 获取用户聚合信息 + const aggregation = await this._getUserAggregation(); + if (!aggregation) { + ctx.body = ctx.helper.error(403, '未找到您的报名信息'); + return; + } + + // 获取上传的文件流 + const stream = await ctx.getFileStream(); + if (!stream) { + ctx.body = ctx.helper.error(400, '未接收到文件'); + return; + } + + // 验证文件扩展名(仅允许 .doc / .docx) + const ext = path.extname(stream.filename).toLowerCase(); + if (!['.doc', '.docx'].includes(ext)) { + ctx.body = ctx.helper.error(400, '仅支持 .doc / .docx 格式的 Word 文件'); + return; + } + + // 验证文件大小(最大 50MB) + const maxSize = 50 * 1024 * 1024; // 50MB + // 从请求头获取文件大小 + const contentLength = parseInt(ctx.get('content-length')) || 0; + if (contentLength > maxSize) { + ctx.body = ctx.helper.error(400, '文件大小超过限制(最大 50MB)'); + return; + } + + // 🔴 文件 IO 并发护栏:与分片上传/合并共享配额,极端并发下统一限流, + // 超出返回 429 由前端退避重试,避免单 worker fd/事件循环耗尽。 + try { + await acquireFileIo(); + } catch (e) { + ctx.status = 429; + ctx.body = ctx.helper.error(429, '服务器繁忙,上传通道暂满,请稍后重试'); + return; + } + + try { + // 构建存储目录:app/public/answers/{group}/ + const uploadDir = path.join(app.baseDir, 'app/public/answers', group); + if (!fs.existsSync(uploadDir)) { + fs.mkdirSync(uploadDir, { recursive: true }); + } + + // 构建文件名:{aggregation_uid}_{timestamp}_{原始文件名} + const timestamp = Date.now(); + const safeFileName = `exam_${aggregation.aggregation_uid}_${timestamp}${ext}`; + const filePath = path.join(uploadDir, safeFileName); + + // 将文件流写入磁盘 + const writeStream = fs.createWriteStream(filePath); + await new Promise((resolve, reject) => { + stream.pipe(writeStream); + writeStream.on('finish', resolve); + writeStream.on('error', reject); + stream.on('error', reject); + }); + + // 计算文件 MD5 + const md5 = await service.exam.calculateFileMd5(filePath); + + // 构建相对路径(相对于 app/public/) + const relativePath = path.join('answers', group, safeFileName).replace(/\\/g, '/'); + + // 从 query 或 body 获取关联的题目 ID + const questionUid = ctx.query.question_uid + || (ctx.request.body && ctx.request.body.question_uid); + + // 保存文件信息到数据库 + const fileInfo = { + filePath: relativePath, + fileName: stream.filename, + md5, + }; + const record = await service.exam.saveWordFile( + aggregation.aggregation_uid, + questionUid || 0, + group, + fileInfo + ); + + ctx.body = ctx.helper.success({ + id: record.id, + fileName: stream.filename, + filePath: relativePath, + fileUrl: ctx.helper.fileUrl(relativePath), + fileSize: contentLength, + md5, + }, 'Word 文件上传成功'); + } finally { + releaseFileIo(); + } + } catch (err) { + ctx.logger.error('上传 Word 文件失败:', err); + ctx.body = ctx.helper.error(500, '上传 Word 文件失败'); + } + } + + /** + * 删除已上传的 Word 答题文件(中职/高职组) + * DELETE /api/exam/:group/word + * + * 仅允许删除当前登录用户本人的 Word 文件:清空 exam_submissions 中的 + * word_file_path / word_file_name,并尽力删除磁盘物理文件(文件不存在时静默忽略)。 + * 注意:删除 Word 文件不影响「正式提交」状态(submit_time 不变),仅移除附件。 + */ + async deleteWord() { + const { ctx, app } = this; + + try { + const { group } = ctx.params; + if (!this._validateGroup(group)) return; + if (!['zhongzhi', 'gaozhi'].includes(group)) { + ctx.body = this.ctx.helper.error(400, '仅中职组/高职组支持删除 Word 答题文件'); + return; + } + + const aggregation = await this._getUserAggregation(); + if (!aggregation) { + ctx.body = ctx.helper.error(403, '未找到您的报名信息'); + return; + } + + const record = await ctx.model.ExamSubmission.findOne({ + where: { contestant_id: aggregation.aggregation_uid }, + }); + + if (!record || !record.word_file_path) { + ctx.body = ctx.helper.error(404, '未找到已上传的 Word 文件'); + return; + } + + // 尽力删除磁盘物理文件(沙箱安全钩子可能拦截 unlink,文件不存在时忽略) + const absPath = path.join(app.baseDir, 'app/public', record.word_file_path); + try { + if (fs.existsSync(absPath)) fs.unlinkSync(absPath); + } catch (e) { + ctx.logger.warn('删除 Word 物理文件失败(忽略):', e.message); + } + + // 清空数据库中的文件信息 + await record.update({ word_file_path: null, word_file_name: null }); + + ctx.body = ctx.helper.success({ deleted: true }, 'Word 文件已删除'); + } catch (err) { + ctx.logger.error('删除 Word 文件失败:', err); + ctx.body = ctx.helper.error(500, '删除 Word 文件失败'); + } + } + + /** + * 一体提交:答案 + Word 文件 + * POST /api/exam/:group/submit-all + * + * 验证考试活跃状态,处理答案提交并确保 Word 文件已上传 + * Body: { answers: [{ question_uid, answer_data }] } + */ + async submitAll() { + const { ctx, service } = this; + + try { + const { group } = ctx.params; + if (!this._validateGroup(group)) return; + + // forceSubmit 仅由「倒计时结束的自动提交」传入:允许在考试窗口临界/已关闭、 + // 且未答满全部题目时仍记录提交(倒计时归零即合法交卷),确保 submit_time 落库, + // 否则会被下方两道闸门拦截 → submit_time 永不写入 → 管理后台回退显示进入考试时间。 + const forceSubmit = ctx.request.body && ctx.request.body.forceSubmit === true; + + // 验证考试是否活跃 + const examStatus = await service.exam.getExamStatus(group); + if (!examStatus.isActive && !forceSubmit) { + ctx.body = ctx.helper.error(4001, examStatus.message || '考试未在进行中'); + return; + } + + // 获取用户聚合信息 + const aggregation = await this._getUserAggregation(); + if (!aggregation) { + ctx.body = ctx.helper.error(403, '未找到您的报名信息'); + return; + } + + // 团队锁:同一小组只能一人进入正式考试(G1 修复,防绕过 questions 直接提交) + const teamGate = await this._enterExamWithTeamLock(aggregation); + if (!teamGate.allowed) { + ctx.body = ctx.helper.error(4005, '您的小组已进入考试'); + return; + } + + // 参数校验 + const rule = { + answers: { type: 'array', required: true, min: 1 }, + }; + try { + ctx.validate(rule); + } catch (validateErr) { + ctx.body = ctx.helper.error(400, '参数校验失败:' + validateErr.errors[0].message); + return; + } + + const { answers } = ctx.request.body; + + // 验证每个答案的数据格式(必须包含 question_type,否则 autoScore 无法判分) + // 容错:question_type 缺失时按 uid 前缀(choice_x/judge_x)推导, + // 避免恢复会话等场景 questions 未加载导致整卷被「答案格式错误」拦截。 + for (const ans of answers) { + if (!ans.question_uid || ans.answer_data === undefined) { + ctx.body = ctx.helper.error(400, '答案格式错误,需要 question_uid 和 answer_data'); + return; + } + if (!ans.question_type) { + const uid = String(ans.question_uid); + if (uid.startsWith('choice')) ans.question_type = 'choice'; + else if (uid.startsWith('judge')) ans.question_type = 'judge'; + } + if (!ans.question_type || !['choice', 'judge'].includes(ans.question_type)) { + ctx.body = ctx.helper.error(400, '答案格式错误,需要合法的 question_type(choice/judge)'); + return; + } + } + + // 校验是否所有题目都已作答:以【出题数量】为基准(前端回传 totalQuestions), + // 避免与随机抽题基准不一致。原先使用全库 999 比对,会导致只抽到 N 题却要求答满全库的悖论。 + const expectedCount = parseInt(ctx.request.body.totalQuestions) || answers.length; + if (answers.length < expectedCount && !forceSubmit) { + ctx.body = ctx.helper.error(4004, `还有 ${expectedCount - answers.length} 道题目未作答`); + return; + } + + // 检查是否已上传 Word 文件 + const existingSubmission = await service.exam.getUserAnswers( + aggregation.aggregation_uid, + group + ); + const hasWordFile = existingSubmission.some(s => s.word_file_path); + + // 自动判分 + const scoreResult = await service.exam.autoScore(answers); + + // 构建保存数据 + const saveData = scoreResult.details.map(d => ({ + question_uid: d.question_id, + question_type: d.question_type, + answer_data: d.answer_data, + is_correct: d.is_correct, + score: d.score, + })); + + // 保存答题记录 + const savedRecords = await service.exam.saveAnswers( + aggregation.aggregation_uid, + saveData, + group + ); + + // 服务端三重断言:事务提交 ≠ 一定落库(MariaDB 不支持 RETURNING / 死锁回滚等 + // 都会导致「内存返回成功实例、磁盘 0 行」的静默丢数据)。故提交后实查持久化条数, + // 与实际应保存条数比对,缺失即视为提交失败,绝不明文返回「提交成功」。 + const SubmissionModel = group === 'zhongzhi' + ? ctx.model.ZhongzhiSubmission + : ctx.model.GaozhiSubmission; + const persisted = await SubmissionModel.count({ + where: { aggregation_uid: aggregation.aggregation_uid }, + }); + if (!persisted || persisted < answers.length) { + ctx.logger.error(`[submitAll] 落库不完整 agg=${aggregation.aggregation_uid} persisted=${persisted} expected=${answers.length}`); + ctx.body = ctx.helper.error(500, `提交失败:答案保存不完整(已保存 ${persisted || 0}/${answers.length} 题),请重试`); + return; + } + + // 同步写入 exam_submissions 汇总行(answers JSON + submit_time), + // 供选手端 hasSubmitted / 管理后台「考生答题记录」提交状态 / 试卷回看使用。 + // 聚合键取 aggregation.aggregation_uid(中职/高职即 contestant.id),与 hasSubmitted 一致。 + await service.exam.ensureExamSubmission(aggregation.aggregation_uid, saveData); + + // 成绩门控:不回传 autoScore/total/details(自动评分仅落库供管理后台阅卷与合分使用)。 + // 考生端提交后只应得到「提交成功 + 已保存题数」的确认,成绩须待管理后台发布。 + ctx.body = ctx.helper.success({ + answeredCount: answers.length, + totalQuestions: expectedCount, + hasWordFile, + savedCount: persisted, + submitted: true, + submitTime: new Date().toISOString(), + }, '提交成功'); + } catch (err) { + ctx.logger.error('一体提交失败:', err); + ctx.body = ctx.helper.error(500, '提交失败'); + } + } + + /** + * 获取已有答案 + * GET /api/exam/:group/answers + * + * 获取用户已提交的答案,用于考试重新进入时恢复状态 + */ + async getAnswers() { + const { ctx, service } = this; + + try { + const { group } = ctx.params; + if (!this._validateGroup(group)) return; + + // 获取用户聚合信息 + const aggregation = await this._getUserAggregation(); + if (!aggregation) { + ctx.body = ctx.helper.error(403, '未找到您的报名信息'); + return; + } + + // 获取已有答案 + const answers = await service.exam.getUserAnswers( + aggregation.aggregation_uid, + group + ); + + // 成绩门控:未发布前,答案回看不得携带逐题得分与对错标记。 + // 否则考生把 score 累加即可得出客观题总分,完全绕过「管理后台发布后才可见」的约束。 + // 答案内容本身保留(前端用于恢复/回看作答)。 + const isPublished = await service.admin.isScorePublished(group); + const safeAnswers = answers.map(row => { + const item = typeof row.toJSON === 'function' ? row.toJSON() : { ...row }; + if (!isPublished) { + delete item.score; + delete item.is_correct; + } + // 还原 question_uid 为前端键格式(choice_x / judge_x):DB 中 zhongzhi/gaozhi_submissions + // .question_uid 经 _extractRawId 存为数字 id(int 列),而前端 answers 以 'choice_x' 字符串为键, + // 若直接回传数字 id,重进恢复答案时键对不上 → 已答题目全部丢失。补回题型前缀保证可恢复; + // 同时保留 raw_question_uid 供后台阅卷/回看按数字 id 定位题目。 + if (item.question_type && item.question_uid != null) { + const prefix = item.question_type === 'judge' ? 'judge_' : 'choice_'; + item.raw_question_uid = item.question_uid; + item.question_uid = prefix + item.question_uid; + } + return item; + }); + + ctx.body = ctx.helper.success({ + answers: safeAnswers, + scoreVisible: isPublished, + }, '获取已有答案成功'); + } catch (err) { + ctx.logger.error('获取已有答案失败:', err); + ctx.body = ctx.helper.error(500, '获取已有答案失败'); + } + } +} + +module.exports = ExamController; diff --git a/app/controller/health.js b/app/controller/health.js new file mode 100644 index 0000000..90115e8 --- /dev/null +++ b/app/controller/health.js @@ -0,0 +1,42 @@ +'use strict'; + +const Controller = require('egg').Controller; + +/** + * 健康检查控制器 - HealthController + * + * 提供容器探针 / 负载均衡探活使用的轻量级接口。 + * 接口:GET /api/health(公开,无需认证) + * 返回统一信封 { code: 0, data: { status, db, timestamp, uptime } }。 + * + * 设计说明: + * - 进程存活即返回 HTTP 200; + * - db 字段反映数据库连通性('ok' / 'down'),便于运维监控但不会导致探针因瞬时抖动而误重启。 + */ +class HealthController extends Controller { + async check() { + const { ctx, app } = this; + + let db = 'ok'; + try { + // egg-sequelize 将 Sequelize 实例直接挂载在 app.model 上(即 delegate) + await app.model.authenticate(); + } catch (e) { + db = 'down'; + ctx.logger.warn('[健康检查] 数据库连通性异常:', e.message); + } + + ctx.status = 200; + ctx.body = ctx.helper.success( + { + status: 'ok', + db, + timestamp: Date.now(), + uptime: Math.floor(process.uptime()), + }, + 'ok' + ); + } +} + +module.exports = HealthController; diff --git a/app/controller/materials.js b/app/controller/materials.js new file mode 100644 index 0000000..627ce22 --- /dev/null +++ b/app/controller/materials.js @@ -0,0 +1,119 @@ +'use strict'; + +const Controller = require('egg').Controller; +const fs = require('fs'); +const path = require('path'); +// 🔴 同源 collapse(缺口9 修复):文件发送统一走 sendFile util +const { sendFile } = require('../utils/sendFile'); + +/** + * 资料下载控制器 - MaterialsController + * + * 负责比赛相关资料的查询和文件下载。 + * 从 app/public/materials/{group}/ 目录读取管理员上传的备赛资料。 + * + * 主要接口: + * - GET /api/materials?group={group} : 获取资料列表 + * - GET /api/materials/:group/:fileName/download : 流式下载指定文件 + */ +class MaterialsController extends Controller { + /** + * 获取资料列表 + * + * 读取 app/public/materials/{group}/ 目录下的所有文件, + * 返回文件名、大小、上传时间等信息。 + */ + async index() { + const { ctx, app } = this; + const { group } = ctx.query; + + // 如果未指定组别,使用当前用户的组别 + const userGroup = group || (ctx.state.currentUser && ctx.state.currentUser.group); + + const validGroups = ['zhongzhi', 'gaozhi', 'benke', 'jiaoshi']; + const targetGroup = validGroups.includes(userGroup) ? userGroup : null; + + if (!targetGroup) { + ctx.body = ctx.helper.success([], '获取资料列表成功'); + return; + } + + const materialsDir = path.join(app.baseDir, 'app/public', 'materials', targetGroup); + + // 防御性读取:目录不存在或读取过程抛出异常(如生产环境目录权限/EACCES、 + // 坏软链 ENOENT 等)都优雅返回空列表,绝不向上抛出—— + // 否则异常会被 auth 中间件的 try/catch 兜底,被误报为 401「认证失败」, + // 导致前端误以为登录过期而将用户踢回登录页。 + try { + if (!fs.existsSync(materialsDir)) { + ctx.body = ctx.helper.success([], '获取资料列表成功'); + return; + } + + // 逐文件 stat:单个文件 stat 失败(坏软链/权限)只跳过并告警, + // 绝不让一个坏文件把整份列表拖空(否则「刚上传的文件也不显示」)。 + const files = []; + for (const f of fs.readdirSync(materialsDir)) { + if (f.startsWith('.')) continue; + try { + const stat = fs.statSync(path.join(materialsDir, f)); + const ext = path.extname(f).toLowerCase(); + files.push({ + id: encodeURIComponent(f), + fileName: f, + name: f, + size: stat.size, + type: ext.replace('.', ''), + uploadTime: stat.mtime.toISOString(), + }); + } catch (e) { + app.logger.warn( + `[materials.index] 跳过无法 stat 的文件 group=${targetGroup} file=${f}:` + (e && e.message ? e.message : e) + ); + } + } + files.sort((a, b) => new Date(b.uploadTime) - new Date(a.uploadTime)); + + ctx.body = ctx.helper.success(files, '获取资料列表成功'); + } catch (err) { + // 记录真实错误到日志,方便运维定位(权限/软链/磁盘问题) + app.logger.error( + `[materials] 读取备赛资料目录失败 group=${targetGroup} path=${materialsDir}:` + + (err && err.stack ? err.stack : err) + ); + // 不因磁盘异常影响整个页面,返回空列表(前端不再被误踢) + ctx.body = ctx.helper.success([], '获取资料列表成功'); + } + } + + /** + * 流式下载文件 + * + * 通过 URL 参数 :group 和 :fileName 定位文件, + * 读取并通过流式响应发送给客户端。 + */ + async download() { + const { ctx, app } = this; + + const { group, fileName } = ctx.params; + const decodedFileName = decodeURIComponent(fileName); + + // 防止路径穿越攻击 + if (decodedFileName.includes('..') || decodedFileName.includes('/') || decodedFileName.includes('\\')) { + ctx.body = ctx.helper.error(400, '无效的文件名'); + return; + } + + const validGroups = ['zhongzhi', 'gaozhi', 'benke', 'jiaoshi']; + if (!validGroups.includes(group)) { + ctx.body = ctx.helper.error(400, '无效的组别'); + return; + } + + // 🔴 同源 collapse(缺口9 修复):统一走 sendFile(磁盘校验 + X-File-Size 哨兵 + X-Accel 卸载) + const relativePath = `materials/${group}/${decodedFileName}`; + return sendFile(ctx, app, { relativePath, fileName: decodedFileName, contentType: 'application/octet-stream' }); + } +} + +module.exports = MaterialsController; diff --git a/app/controller/news.js b/app/controller/news.js new file mode 100644 index 0000000..04ae9c8 --- /dev/null +++ b/app/controller/news.js @@ -0,0 +1,70 @@ +'use strict'; + +const Controller = require('egg').Controller; + +/** + * 新闻控制器 - NewsController + * + * 负责新闻通知的查询和展示。 + * 支持按 group 组别过滤、按 pinned 置顶过滤、分页查询。 + * + * 主要接口: + * - GET /api/news : 获取新闻列表(支持 ?group= & ?pinned= & ?page= & ?pageSize= 参数) + * - GET /api/news/:id : 获取新闻详情 + */ +class NewsController extends Controller { + /** + * 获取新闻列表 + * + * Query 参数: + * - group: 组别筛选(如 zhongzhi/gaozhi/benke/jiaoshi),不传则返回全部 + * - pinned: 是否仅返回置顶新闻(true/false),不传则返回全部 + * - page: 页码,默认 1 + * - pageSize: 每页数量,默认 20 + * + * 排序规则:置顶优先(pinned DESC),然后按发布时间降序 + */ + async index() { + const { ctx, service } = this; + + const { group, pinned, page, pageSize } = ctx.query; + + const result = await service.news.list({ group, pinned, page, pageSize }); + + ctx.body = ctx.helper.success(result, '获取新闻列表成功'); + } + + /** + * 获取新闻详情 + * + * 通过 URL 参数 :id 获取单条新闻的完整信息。 + */ + async show() { + const { ctx, service } = this; + + const { id } = ctx.params; + + const news = await service.news.findById(id); + + if (!news) { + ctx.body = ctx.helper.error(4001, '新闻不存在'); + return; + } + + ctx.body = ctx.helper.success(news, '获取新闻详情成功'); + } + + /** + * 获取各分组新闻计数 + * GET /api/news/counts + * + * 返回各分组可见新闻数量,用于首页 Tab 显示 + */ + async counts() { + const { ctx, service } = this; + const result = await service.news.getGroupCounts(); + ctx.body = ctx.helper.success(result, '获取分组计数成功'); + } +} + +module.exports = NewsController; diff --git a/app/controller/score.js b/app/controller/score.js new file mode 100644 index 0000000..5874140 --- /dev/null +++ b/app/controller/score.js @@ -0,0 +1,230 @@ +'use strict'; + +const Controller = require('egg').Controller; + +/** + * 成绩查询控制器 - ScoreController + * + * 负责选手查询个人成绩,包括答题得分、作品评分和总分。 + * 成绩发布后才能查看。 + * + * 主要接口: + * - GET /api/score : 获取当前选手的(队伍)成绩详情 + * + * 设计目标:一人考试,全组(同队伍)都能查看该队伍成绩。 + * - 考试以「队伍」为单位,仅一名成员(考试操作员)实际进入并作答; + * 成绩(客观题/作品/演示阅卷分)落在操作员对应的记录上。 + * - 同队其他成员查看时,须解析到本队「考试操作员」的成绩并一致返回, + * 而非各自读取自己(未考试、得分恒为 0)的行。 + * - 队伍身份以 aggregation.team_uid 为准(与考试锁 _enterExamWithTeamLock 一致), + * 回退到 contestants.team_id(与 teams.id 关联)。 + */ +class ScoreController extends Controller { + /** + * 解析「队伍成绩来源」: + * 返回当前用户所在队伍中实际产生成绩的「考试操作员」信息, + * 包括其聚合键(用于客观题/提交记录归集)与分项分。 + * @returns {Promise} + * { group, isExam, operatorId, operator, aggKeys, viewerName } + */ + async _resolveTeamScoreSource() { + const { ctx } = this; + const user = ctx.state.currentUser; + if (!user || !user.id) return null; + + const me = await ctx.model.Contestant.findOne({ + where: { id: user.id }, + attributes: ['id', 'team_id', 'group', 'name', 'word_score', 'ppt_score', 'score', 'exam_score'], + }); + if (!me || !me.group) return null; + + const group = me.group; + const isExam = group === 'zhongzhi' || group === 'gaozhi'; + const Op = ctx.model.Sequelize.Op; + + // 1) 收集同队成员 contestant id(队伍身份 = aggregation.team_uid,回退 contestants.team_id) + // 必须按 me.group 限定:运行库中 team_uid 仅在本组内唯一(不同组可能复用同一数值, + // 例如 benke 与 zhongzhi 都可能有 team_uid=1 但分属不同队),不限定 group 会把他组 + // 成员错误并入本队,导致「一人考试全组看分」归集串组(见 verify_team_uid_consistency.py C2)。 + let memberIds = [me.id]; + const myAgg = await ctx.model.Aggregation.findOne({ + where: { account_uid: me.id }, + attributes: ['aggregation_uid', 'team_uid'], + }); + if (myAgg && myAgg.team_uid) { + const aggs = await ctx.model.Aggregation.findAll({ + where: { team_uid: myAgg.team_uid, group: me.group }, + attributes: ['account_uid'], + }); + const ids = aggs.map(a => a.account_uid).filter(Boolean); + if (ids.length) memberIds = ids; + } else if (me.team_id) { + const members = await ctx.model.Contestant.findAll({ + where: { team_id: me.team_id, group: me.group }, + attributes: ['id'], + }); + const ids = members.map(m => m.id); + if (ids.length) memberIds = ids; + } + + // 2) 找到「考试操作员」:同队中实际产生提交/成绩的选手 + let operatorId = me.id; + if (isExam) { + // 客观题考试以 exam_submissions.contestant_id 唯一标记操作员(UNIQUE 约束,仅一人) + const sub = await ctx.model.ExamSubmission.findOne({ + where: { contestant_id: memberIds }, + attributes: ['contestant_id'], + order: [['submit_time', 'DESC']], + }); + if (sub) operatorId = sub.contestant_id; + } else { + // 本科/教师:作品/演示阅卷分落在「考试操作员」的 contestants 行。 + // 操作员判定规则(用户决策 #1): + // 1) 优先以 benke_submissions / jiaoshi_submissions 的提交记录存在性定操作员; + // 2) 同队成员中无 / 多人有提交记录时,以 (word_score+ppt_score) 最高者兜底,保证确定性。 + // 提交表以 aggregation_uid 归集(≠ contestant_id),须先反查 account_uid -> aggregation_uid。 + const SubmissionModel = group === 'benke' + ? ctx.model.BenkeSubmission + : ctx.model.JiaoshiSubmission; + + // 取同队成员的 aggregation_uid 映射(按当前组过滤,避免跨组行干扰) + const memberAggs = await ctx.model.Aggregation.findAll({ + where: { account_uid: memberIds, group }, + attributes: ['account_uid', 'aggregation_uid'], + raw: true, + }); + const aggUidByAccount = {}; + for (const a of memberAggs) aggUidByAccount[a.account_uid] = a.aggregation_uid; + + const graded = await ctx.model.Contestant.findAll({ + where: { id: memberIds }, + attributes: ['id', 'word_score', 'ppt_score'], + }); + + // 统计「有提交记录」的成员(提交记录以 aggregation_uid 存在为准) + const submittedIds = []; + for (const c of graded) { + const aggUid = aggUidByAccount[c.id]; + if (!aggUid) continue; + const sub = await SubmissionModel.findOne({ + where: { aggregation_uid: aggUid }, + attributes: ['id'], + }); + if (sub) submittedIds.push(c.id); + } + + // 兜底:总分最高者(无 / 多人有提交记录时皆以此定操作员) + let best = { id: me.id, total: -1 }; + for (const c of graded) { + const t = (c.word_score || 0) + (c.ppt_score || 0); + if (t > best.total) best = { id: c.id, total: t }; + } + + if (submittedIds.length >= 1) { + // 有提交记录:唯一则取其;多人(异常)取总分最高者兜底 + operatorId = submittedIds.length === 1 ? submittedIds[0] : best.id; + } else { + operatorId = best.id; + } + } + + // 3) 取操作员的聚合键(用于客观题/提交记录归集)与分项分 + const opAgg = await ctx.model.Aggregation.findOne({ + where: { account_uid: operatorId }, + attributes: ['aggregation_uid'], + }); + const operatorAggUid = opAgg ? opAgg.aggregation_uid : null; + const operator = await ctx.model.Contestant.findOne({ + where: { id: operatorId }, + attributes: ['id', 'name', 'word_score', 'ppt_score', 'score', 'exam_score'], + }); + const keys = [String(operatorId)]; + if (operatorAggUid) keys.push(String(operatorAggUid)); + + return { group, isExam, operatorId, operator, aggKeys: keys, viewerName: me.name }; + } + + /** + * 获取当前选手的(队伍)成绩 + * GET /api/score + * + * 流程:先检查成绩是否已发布,再解析本队考试操作员并查询其得分。 + * 返回:总分、计分式说明、是否已发布。 + */ + async index() { + const { ctx, service } = this; + + try { + const user = ctx.state.currentUser; + + // 仅选手可查看成绩 + if (!user || user.role !== 'contestant') { + ctx.body = ctx.helper.error(403, '无权限查看成绩'); + return; + } + + // 解析队伍成绩来源(含同队归集:一人考试全组可看) + const src = await this._resolveTeamScoreSource(); + if (!src) { + ctx.body = ctx.helper.error(404, '未找到您的报名信息'); + return; + } + + const { group, isExam, operator, aggKeys, viewerName } = src; + + // 检查该组成绩是否已发布(按组门控:发布本科组不得让中职组考生看到成绩) + const isPublished = await service.admin.isScorePublished(group); + if (!isPublished) { + ctx.body = ctx.helper.success({ + score: null, + details: null, + published: false, + message: '成绩尚未发布,请耐心等待', + }, '获取成绩状态成功'); + return; + } + + // 按组别返回分项成绩(与后台评分公式一致) + let examScore = undefined; + if (isExam) { + // 客观题分数取逐题自动评分合计;聚合键用「操作员 contestant.id」与「操作员真实 aggregation_uid」双键归集。 + const SubmissionModel = group === 'zhongzhi' + ? ctx.model.ZhongzhiSubmission + : ctx.model.GaozhiSubmission; + const rows = await SubmissionModel.findAll({ + where: { aggregation_uid: aggKeys }, + attributes: ['score'], + raw: true, + }); + examScore = rows.reduce((sum, r) => sum + (r.score || 0), 0); + } + // 分项分从「操作员」行取(其实际产生成绩),而非查看者本人。 + const wordScore = operator.word_score || 0; + const pptScore = isExam ? undefined : (operator.ppt_score || 0); + // 总分 = 客观题 + Word 分(中职/高职);Word 分 + PPT 分(本科/教师)。 + const score = isExam + ? (examScore + wordScore) + : (wordScore + (pptScore || 0)); + const formula = isExam + ? '客观题系统评分 + Word 文档阅卷评分' + : 'Word 文档阅卷评分 + PPT 演示阅卷评分'; + + // 注意:考生端只回传「总分(score)」与计分式说明(formula), + // 不得回传分项分数(examScore/wordScore/pptScore)——分数分布仅管理后台可见。 + // nameCn 用查看者本人姓名(各成员看到自己的名字 + 本队共享总分)。 + ctx.body = ctx.helper.success({ + score, + nameCn: viewerName || '', + group, + published: true, + details: null, + formula, + }, '获取成绩成功'); + } catch (err) { + ctx.logger.error('获取成绩失败:', err); + ctx.body = ctx.helper.error(500, '获取成绩失败'); + } + } +} + +module.exports = ScoreController; diff --git a/app/controller/upload.js b/app/controller/upload.js new file mode 100644 index 0000000..cee3a12 --- /dev/null +++ b/app/controller/upload.js @@ -0,0 +1,478 @@ +'use strict'; + +const Controller = require('egg').Controller; +const path = require('path'); +const fs = require('fs'); +const { acquire: acquireFileIo, release: releaseFileIo } = require('../lib/file_io_guard'); + +/** + * 上传控制器 - UploadController + * + * 负责作品文件上传相关功能,包括普通上传、分片上传、分片查询、 + * 合并分片、提交状态查询等。适用于本科组和教师组。 + * + * 主要接口: + * - POST /api/upload/:group : 上传作品文件 + * - POST /api/upload/:group/chunk : 上传分片 + * - POST /api/upload/:group/merge : 合并分片 + * - GET /api/upload/:group/chunk-status : 查询分片状态 + * - GET /api/upload/:group/status : 查询提交状态 + * - GET /api/upload/:group/submissions : 查询提交历史 + */ +class UploadController extends Controller { + /** + * 校验组别参数 + * @param {string} group - 组别 + * @returns {boolean} 是否有效 + */ + _validateGroup(group) { + const validGroups = ['benke', 'jiaoshi']; + if (!validGroups.includes(group)) { + this.ctx.body = this.ctx.helper.error(400, '无效的组别,仅支持 benke / jiaoshi'); + return false; + } + return true; + } + + /** + * 获取用户的聚合报名信息 + * @returns {Promise} + */ + async _getUserAggregation() { + const user = this.ctx.state.currentUser; + if (!user || !user.uid) return null; + return await this.service.upload.getUserAggregation(user.uid); + } + + /** + * 上传作品文件 + * POST /api/upload/:group + * + * 支持 .pdf / .doc / .docx / .ppt / .pptx / .zip 格式,最大 50MB + */ + async upload() { + const { ctx, service } = this; + + try { + const { group } = ctx.params; + if (!this._validateGroup(group)) return; + + // 验证截止时间 + const deadlineCheck = await service.upload.validateDeadline(group); + if (!deadlineCheck.valid) { + ctx.body = ctx.helper.error(6001, deadlineCheck.message); + return; + } + + // 获取用户聚合信息 + const aggregation = await this._getUserAggregation(); + if (!aggregation) { + ctx.body = ctx.helper.error(403, '未找到您的报名信息'); + return; + } + + // 验证组别匹配 + if (aggregation.team && aggregation.team.group_name !== group) { + ctx.body = ctx.helper.error(403, '您不属于该组别'); + return; + } + + // 获取上传的文件流 + const stream = await ctx.getFileStream(); + if (!stream) { + ctx.body = ctx.helper.error(400, '未接收到文件'); + return; + } + + // 验证文件扩展名 + const allowedExts = ['.pdf', '.doc', '.docx', '.ppt', '.pptx', '.zip']; + const ext = path.extname(stream.filename).toLowerCase(); + if (!allowedExts.includes(ext)) { + ctx.body = ctx.helper.error(6002, '不支持的文件格式,仅支持 pdf/doc/docx/ppt/pptx/zip'); + return; + } + + // 验证文件大小:zip 最大 200MB,其余格式最大 100MB(从请求头判断) + const contentLength = parseInt(ctx.get('content-length')) || 0; + const isZip = ext === '.zip'; + const maxSize = isZip ? 200 * 1024 * 1024 : 100 * 1024 * 1024; + const limitLabel = isZip ? '200MB' : '100MB'; + if (contentLength > maxSize) { + ctx.body = ctx.helper.error(6003, `文件大小超过限制(最大 ${limitLabel})`); + return; + } + + // 保存文件 + const fileInfo = await service.upload.saveWorkFile(stream, stream.filename, group); + + // 获取作品名称(流式模式字段在 stream.fields 中) + const workName = (stream.fields && stream.fields.work_name) || stream.filename; + // 文件类型:word / ppt(前端上传时透传) + const fileType = (stream.fields && stream.fields.file_type) || 'word'; + + // 保存提交记录 + const record = await service.upload.saveSubmissionRecord( + aggregation.aggregation_uid, + group, + fileInfo, + workName, + fileType + ); + + ctx.body = ctx.helper.success({ + id: record.id, + workName: record.work_name, + filePath: fileInfo.filePath, + fileUrl: ctx.helper.fileUrl(fileInfo.filePath), + fileName: fileInfo.fileName, + fileSize: fileInfo.fileSize, + md5: fileInfo.md5, + uploadTime: record.upload_time || new Date().toISOString(), + }, '文件上传成功'); + } catch (err) { + ctx.logger.error('文件上传失败:', err); + const msg = (err && err.message) ? err.message : '未知错误'; + ctx.body = ctx.helper.error(500, msg); + } + } + + /** + * 上传分片 + * POST /api/upload/:group/chunk + * + * Body: multipart/form-data 包含 chunk(文件分片)、chunkIndex、totalChunks、fileId、fileName + */ + async uploadChunk() { + const { ctx, app } = this; + + try { + const { group } = ctx.params; + if (!this._validateGroup(group)) return; + + // 🔴 文件 IO 并发护栏:先于读取请求体获取配额,提供连接级背压; + // 极端并发(无带宽限制 + 单 worker)下超出上限则返回 429, + // 由前端 uploadWithRetry 指数退避重试,避免 fd/事件循环被压垮雪崩。 + try { + await acquireFileIo(); + } catch (e) { + ctx.status = 429; + ctx.body = ctx.helper.error(429, '服务器繁忙,上传通道暂满,请稍后重试'); + return; + } + + try { + // 获取上传的文件流(流式模式下,非文件字段需在消费流后从 ctx.request.body 读取) + const stream = await ctx.getFileStream(); + if (!stream) { + ctx.body = ctx.helper.error(400, '未接收到分片数据'); + return; + } + + // 流式模式下,非文件字段在 stream.fields 中(非 ctx.request.body) + const { chunkIndex, totalChunks, fileId, fileName } = stream.fields || {}; + if (chunkIndex === undefined || !totalChunks || !fileId || !fileName) { + ctx.body = ctx.helper.error(400, '缺少必要参数:chunkIndex, totalChunks, fileId, fileName'); + return; + } + + // 构建临时分片目录 + const tempDir = path.join(app.baseDir, 'app/public/tmp', group, fileId); + if (!fs.existsSync(tempDir)) { + fs.mkdirSync(tempDir, { recursive: true }); + } + + // 保存分片文件 + const chunkFilePath = path.join(tempDir, `chunk_${String(chunkIndex).padStart(6, '0')}`); + const writeStream = fs.createWriteStream(chunkFilePath); + + await new Promise((resolve, reject) => { + stream.pipe(writeStream); + writeStream.on('finish', resolve); + writeStream.on('error', reject); + stream.on('error', reject); + }); + + ctx.body = ctx.helper.success({ + received: parseInt(chunkIndex), + totalChunks: parseInt(totalChunks), + fileId, + }, `分片 ${chunkIndex} 上传成功`); + } finally { + releaseFileIo(); + } + } catch (err) { + ctx.logger.error('分片上传失败:', err); + ctx.body = ctx.helper.error(500, '分片上传失败'); + } + } + + /** + * 合并分片 + * POST /api/upload/:group/merge + * + * Body: { fileId, fileName, totalChunks, work_name } + */ + async mergeChunks() { + const { ctx, service, app } = this; + + try { + const { group } = ctx.params; + if (!this._validateGroup(group)) return; + + // 验证截止时间 + const deadlineCheck = await service.upload.validateDeadline(group); + if (!deadlineCheck.valid) { + ctx.body = ctx.helper.error(6001, deadlineCheck.message); + return; + } + + // 获取用户聚合信息 + const aggregation = await this._getUserAggregation(); + if (!aggregation) { + ctx.body = ctx.helper.error(403, '未找到您的报名信息'); + return; + } + + const { fileId, fileName, totalChunks, work_name, file_type } = ctx.request.body; + if (!fileId || !fileName) { + ctx.body = ctx.helper.error(400, '缺少必要参数:fileId, fileName'); + return; + } + + // 检查分片目录 + const tempDir = path.join(app.baseDir, 'app/public/tmp', group, fileId); + if (!fs.existsSync(tempDir)) { + ctx.body = ctx.helper.error(6004, '未找到分片文件'); + return; + } + + // 收集所有分片文件并按文件名排序 + const chunkFiles = fs.readdirSync(tempDir) + .filter(f => f.startsWith('chunk_')) + .sort() + .map(f => path.join(tempDir, f)); + + const expectedCount = parseInt(totalChunks) || 0; + if (expectedCount > 0 && chunkFiles.length < expectedCount) { + ctx.body = ctx.helper.error(6005, `分片未完成,已收到 ${chunkFiles.length}/${expectedCount}`); + return; + } + + // 构建目标文件路径 + const uploadDir = path.join(app.baseDir, 'app/public/works', group); + if (!fs.existsSync(uploadDir)) { + fs.mkdirSync(uploadDir, { recursive: true }); + } + + const ext = path.extname(fileName); + const timestamp = Date.now(); + const safeFileName = `work_${timestamp}_${Math.random().toString(36).substring(2, 8)}${ext}`; + const targetPath = path.join(uploadDir, safeFileName); + + // 🔴 文件 IO 并发护栏:分片合并是重 IO,与 uploadChunk 共享同一配额, + // 极端并发(无带宽限制 + 单 worker)下被统一限流,避免 fd/事件循环耗尽雪崩。 + try { + await acquireFileIo(); + } catch (e) { + ctx.status = 429; + ctx.body = ctx.helper.error(429, '服务器繁忙,上传通道暂满,请稍后重试'); + return; + } + + try { + // 合并分片 + const mergeResult = await service.upload.mergeFileChunks(chunkFiles, targetPath); + + // 🔴 删除临时目录:best-effort,**绝不能让清理失败把已成功的合并判成 500**。 + // 旧代码是裸 fs.rmdirSync,删除失败(杀软锁文件/句柄未释放/安全钩子拦截)会 + // 抛到外层 catch → 返回「分片合并失败」,而目标文件已落盘且 DB 记录还没写, + // 直接产出孤儿文件 + 数据不一致 + 用户重试再生孤儿。清理属副作用,不是业务结果。 + ctx.helper.safeCleanup(tempDir, { label: 'mergeChunks-tmp' }); + + // 构建相对路径 + const relativePath = path.join('works', group, safeFileName).replace(/\\/g, '/'); + + // 保存提交记录 + const fileInfo = { + filePath: relativePath, + fileName, + fileSize: mergeResult.fileSize, + md5: mergeResult.md5, + }; + const record = await service.upload.saveSubmissionRecord( + aggregation.aggregation_uid, + group, + fileInfo, + work_name || fileName, + file_type || 'word' + ); + + ctx.body = ctx.helper.success({ + id: record.id, + workName: record.work_name, + filePath: relativePath, + fileUrl: ctx.helper.fileUrl(relativePath), + fileName, + fileSize: mergeResult.fileSize, + md5: mergeResult.md5, + uploadTime: record.upload_time || new Date().toISOString(), + }, '分片合并成功'); + } finally { + releaseFileIo(); + } + } catch (err) { + ctx.logger.error('分片合并失败:', err); + ctx.body = ctx.helper.error(500, '分片合并失败'); + } + } + + /** + * 查询分片上传进度 + * GET /api/upload/:group/chunk-status?fileId=xxx + */ + async chunkStatus() { + const { ctx, app } = this; + + try { + const { group } = ctx.params; + if (!this._validateGroup(group)) return; + + const { fileId } = ctx.query; + if (!fileId) { + ctx.body = ctx.helper.error(400, '缺少 fileId 参数'); + return; + } + + // 检查临时目录 + const tempDir = path.join(app.baseDir, 'app/public/tmp', group, fileId); + if (!fs.existsSync(tempDir)) { + ctx.body = ctx.helper.success({ + received: 0, + chunks: [], + }, '尚未收到分片'); + return; + } + + const chunkFiles = fs.readdirSync(tempDir) + .filter(f => f.startsWith('chunk_')) + .sort(); + + ctx.body = ctx.helper.success({ + received: chunkFiles.length, + chunks: chunkFiles.map(f => f.replace('chunk_', '')), + }, '获取分片状态成功'); + } catch (err) { + ctx.logger.error('查询分片状态失败:', err); + ctx.body = ctx.helper.error(500, '查询分片状态失败'); + } + } + + /** + * 查询当前用户的提交状态 + * GET /api/upload/:group/status + */ + async status() { + const { ctx, service } = this; + + try { + const { group } = ctx.params; + if (!this._validateGroup(group)) return; + + // 获取用户聚合信息 + const aggregation = await this._getUserAggregation(); + if (!aggregation) { + ctx.body = ctx.helper.error(403, '未找到您的报名信息'); + return; + } + + // 检查提交状态 + const submitted = await service.upload.hasSubmitted(aggregation.aggregation_uid, group); + + // 检查截止时间 + const deadlineCheck = await service.upload.validateDeadline(group); + + ctx.body = ctx.helper.success({ + submitted, + deadline: deadlineCheck.deadline, + canSubmit: deadlineCheck.valid, + }, '获取提交状态成功'); + } catch (err) { + ctx.logger.error('查询提交状态失败:', err); + ctx.body = ctx.helper.error(500, '查询提交状态失败'); + } + } + + /** + * 删除已上传的作品文件 + * DELETE /api/upload/:group/:id + * + * 仅允许删除当前登录用户本人的提交记录,并同步删除物理文件。 + */ + async delete() { + const { ctx, service } = this; + + try { + const { group, id } = ctx.params; + if (!this._validateGroup(group)) return; + + const aggregation = await this._getUserAggregation(); + if (!aggregation) { + ctx.body = ctx.helper.error(403, '未找到您的报名信息'); + return; + } + + const numId = parseInt(id, 10); + if (!Number.isInteger(numId)) { + ctx.body = ctx.helper.error(400, '无效的文件 ID'); + return; + } + + const ok = await service.upload.deleteSubmission(aggregation.aggregation_uid, group, numId); + if (!ok) { + ctx.body = ctx.helper.error(404, '未找到该文件或无权删除'); + return; + } + + ctx.body = ctx.helper.success({ id: numId }, '文件已删除'); + } catch (err) { + ctx.logger.error('删除提交文件失败:', err); + ctx.body = ctx.helper.error(500, '删除失败'); + } + } + + /** + * 查询当前用户的提交历史 + * GET /api/upload/:group/submissions + */ + async submissions() { + const { ctx, service } = this; + + try { + const { group } = ctx.params; + if (!this._validateGroup(group)) return; + + // 获取用户聚合信息 + const aggregation = await this._getUserAggregation(); + if (!aggregation) { + ctx.body = ctx.helper.error(403, '未找到您的报名信息'); + return; + } + + // 获取提交历史 + const submissionList = await service.upload.getSubmissions( + aggregation.aggregation_uid, + group + ); + + ctx.body = ctx.helper.success({ + list: submissionList, + total: submissionList.length, + }, '获取提交历史成功'); + } catch (err) { + ctx.logger.error('查询提交历史失败:', err); + ctx.body = ctx.helper.error(500, '查询提交历史失败'); + } + } +} + +module.exports = UploadController; diff --git a/app/extend/helper.js b/app/extend/helper.js new file mode 100644 index 0000000..99e284e --- /dev/null +++ b/app/extend/helper.js @@ -0,0 +1,273 @@ +'use strict'; + +const crypto = require('crypto'); +const jwt = require('jsonwebtoken'); +const fs = require('fs'); +const bcrypt = require('bcryptjs'); + +// bcrypt 计算强度(10 轮,登录场景性能与安全的平衡点) +const BCRYPT_ROUNDS = 10; + +/** + * 应用扩展工具函数 - Helper + * + * 提供加密、JWT、文件哈希、统一响应等工具函数。 + * 在控制器中通过 ctx.helper.xxx() 调用。 + */ + +module.exports = { + /** + * SHA256 哈希计算(可选盐值) + * + * @param {string} password - 明文密码 + * @param {string} [salt=''] - 盐值(可选,不传则不加盐) + * @returns {string} SHA256 哈希后的密码字符串 + * + * 示例: + * const hashed = ctx.helper.sha256('123456', 'abc123salt'); + * const hashed2 = ctx.helper.sha256('123456'); // 无盐值 + */ + sha256(password, salt) { + const hash = crypto.createHash('sha256'); + hash.update(password + (salt || '')); + return hash.digest('hex'); + }, + + /** + * 生成随机盐值 + * + * @returns {string} 32 位随机十六进制字符串 + * + * 示例: + * const salt = ctx.helper.generateSalt(); + * // => 'a1b2c3d4e5f6...' (32位) + */ + generateSalt() { + return crypto.randomBytes(16).toString('hex'); + }, + + /** + * 判断给定哈希是否为 bcrypt 格式($2a$ / $2b$ / $2y$ 开头) + * + * @param {string} hash - 待判断的哈希字符串 + * @returns {boolean} 是否为 bcrypt 哈希 + */ + isBcryptHash(hash) { + return typeof hash === 'string' && /^\$2[aby]\$/.test(hash); + }, + + /** + * 使用 bcrypt 生成加盐密码哈希(新密码统一走此方法) + * + * @param {string} plain - 明文密码 + * @returns {Promise} bcrypt 哈希字符串 + */ + async hashPassword(plain) { + return bcrypt.hash(plain, BCRYPT_ROUNDS); + }, + + /** + * 校验明文密码与存储哈希是否匹配。 + * 兼容 bcrypt 哈希与存量 SHA256(无盐)哈希,实现平滑迁移。 + * + * @param {string} plain - 明文密码 + * @param {string} hash - 存储的哈希(bcrypt 或旧版 SHA256) + * @returns {Promise} 是否匹配 + */ + async comparePassword(plain, hash) { + if (!hash) return false; + if (this.isBcryptHash(hash)) { + return bcrypt.compare(plain, hash); + } + // 兼容存量 SHA256(无盐)密码 + return this.sha256(plain) === hash; + }, + + /** + * 生成 JWT Token + * + * @param {Object} payload - 要编码的载荷数据 + * @param {string} secret - JWT 签名密钥 + * @param {string} [expiresIn] - 有效期,默认使用配置文件中的 '24h' + * @returns {string} 生成的 JWT Token 字符串 + * + * 示例: + * const token = ctx.helper.generateToken({ uid: 1, role: 'contestant' }, secret); + */ + generateToken(payload, secret, expiresIn) { + const finalExpiresIn = expiresIn || this.config.jwt.expiresIn; + return jwt.sign(payload, secret, { + expiresIn: finalExpiresIn, + }); + }, + + /** + * 验证 JWT Token + * + * @param {string} token - JWT Token 字符串 + * @param {string} secret - JWT 签名密钥 + * @returns {Object} 解码后的载荷数据,验证失败抛出异常 + * + * 示例: + * const decoded = ctx.helper.verifyToken(token, secret); + */ + verifyToken(token, secret, options = {}) { + return jwt.verify(token, secret, options); + }, + + /** + * 统一成功响应 + * + * @param {*} data - 响应数据,默认 null + * @param {string} message - 响应消息,默认 'success' + * @returns {Object} 统一格式的成功响应 { code: 0, message, data } + * + * 示例: + * ctx.body = ctx.helper.success({ id: 1 }, '操作成功'); + * // => { code: 0, message: '操作成功', data: { id: 1 } } + */ + success(data = null, message = 'success') { + return { + code: 0, + message, + data, + }; + }, + + /** + * 统一失败/错误响应 + * + * @param {number} code - 错误码 + * @param {string} message - 错误消息 + * @returns {Object} 统一格式的错误响应 { code, message } + * + * 示例: + * ctx.body = ctx.helper.error(1001, '用户不存在'); + * // => { code: 1001, message: '用户不存在' } + */ + error(code, message) { + return { + code, + message, + }; + }, + + /** + * 生成随机数字验证码 + * + * @param {number} length - 验证码长度,默认 6 + * @returns {string} 指定长度的数字验证码字符串 + * + * 示例: + * ctx.helper.randomCode(6); + * // => '372819' + */ + randomCode(length = 6) { + let code = ''; + for (let i = 0; i < length; i++) { + code += Math.floor(Math.random() * 10); + } + return code; + }, + + /** + * 生成文件 MD5 哈希 + * + * @param {string} filePath - 文件完整路径 + * @returns {Promise} 文件的 MD5 哈希字符串 + * + * 示例: + * const md5 = await ctx.helper.generateFileMd5('/path/to/file.pdf'); + * // => 'd41d8cd98f00b204e9800998ecf8427e' + */ + async generateFileMd5(filePath) { + return new Promise((resolve, reject) => { + const hash = crypto.createHash('md5'); + const stream = fs.createReadStream(filePath); + + stream.on('data', (chunk) => { + hash.update(chunk); + }); + + stream.on('end', () => { + resolve(hash.digest('hex')); + }); + + stream.on('error', (err) => { + reject(err); + }); + }); + }, + + /** + * 构建文件访问 URL + * + * 将 app/public 下的相对路径转换为完整的 HTTP 访问 URL。 + * 前端通过此 URL 可直接访问上传的文件。 + * + * @param {string} relativePath - 相对路径(如 'carousel/banner.jpg' 或 'templates/zhongzhi/template.docx') + * @returns {string} 完整的外部访问 URL + * + * 示例: + * ctx.helper.fileUrl('carousel/banner.jpg'); + * // => 'http://127.0.0.1:7001/public/carousel/banner.jpg' + * + * ctx.helper.fileUrl('templates/zhongzhi/template.docx'); + * // => 'http://127.0.0.1:7001/public/templates/zhongzhi/template.docx' + */ + fileUrl(relativePath) { + const { app } = this.ctx; + // 支持通过环境变量配置域名,默认使用 localhost:端口 + const baseUrl = app.config.staticBaseUrl || `http://127.0.0.1:${app.config.cluster.listen.port}`; + // 确保路径以 /public/ 开头 + const normalizedPath = relativePath.replace(/\\/g, '/').replace(/^\/+/, ''); + return `${baseUrl}/public/${normalizedPath}`; + }, + + /** + * 🔴 best-effort 清理:删除临时文件/目录,**任何失败都不抛错**。 + * + * 【为什么必须存在】临时目录/半截文件的清理是**副作用**,不是业务结果。 + * 若把它写成裸 fs.rmdirSync / fs.unlinkSync 放在成功路径里,一旦删除失败 + * (杀毒软件锁文件、Windows 句柄未释放、只读挂载、沙箱安全钩子拦截……), + * 整个请求就会抛异常走到 catch → 返回 500,而此时: + * · 目标文件其实已经落盘 → 产生**孤儿文件** + * · DB 记录尚未写入 → **文件与数据不一致** + * · 用户看到"失败"后重试 → 再生成一个孤儿文件,越滚越多 + * 实证:controller/upload.js:mergeChunks 与 service/admin.js:createBatchZip + * 都曾因临时目录删除失败而整体 500(common-error.log 有完整堆栈)。 + * + * 【使用约定】 + * · 清理副作用 → 一律用本函数,永不裸调 fs 删除 API。 + * · "删除即业务动作"(如管理员删除备赛资料)→ 不要用本函数, + * 那种场景删除失败必须让用户知道,应当照常抛错/返回错误码。 + * + * @param {string} targetPath - 待删除的文件或目录绝对路径 + * @param {object} [opts] - { label:'用于日志的场景名' } + * @returns {'removed'|'missing'|'skipped'} 删除结果(不抛错) + */ + safeCleanup(targetPath, opts = {}) { + const label = opts.label || 'cleanup'; + const logger = (this.ctx && this.ctx.logger) || console; + if (!targetPath) return 'missing'; + try { + if (!fs.existsSync(targetPath)) return 'missing'; + const st = fs.statSync(targetPath); + if (st.isDirectory()) { + // fs.rm 优先(Node 14.14+ 官方推荐,force 语义更稳);老版本回退 rmdirSync + if (typeof fs.rmSync === 'function') { + fs.rmSync(targetPath, { recursive: true, force: true }); + } else { + fs.rmdirSync(targetPath, { recursive: true }); + } + } else { + fs.unlinkSync(targetPath); + } + return 'removed'; + } catch (e) { + // 只告警不抛错:清理失败绝不能污染主业务结果 + logger.warn(`[safeCleanup:${label}] 清理失败(已忽略,不影响主流程): ${targetPath} -> ${e && e.message}`); + return 'skipped'; + } + }, +}; diff --git a/app/lib/file_io_guard.js b/app/lib/file_io_guard.js new file mode 100644 index 0000000..8101d3a --- /dev/null +++ b/app/lib/file_io_guard.js @@ -0,0 +1,83 @@ +'use strict'; + +// ==================== 文件 IO 并发护栏 ==================== +// 背景(2026-08-11 压测结论): +// 场景「无带宽限制 + 单 worker + 极端并发」(大量考生持续上传大文件 + +// 管理后台同时持续下载大文件)下,单 worker 的 fd / 事件循环会被海量 +// 并发文件流(分片写入、分片合并、Word 写入)压垮,触发 +// MaxListenersExceededWarning 并最终雪崩。 +// +// 本护栏把「同时进行的文件 IO 操作数」限制在上限内: +// - 未超限:立即放行,计数 +1; +// - 已超限:进入有界队列等待(提供连接级背压,而非崩溃); +// - 队列满 / 等待超时:抛出 busy 错误,调用方返回 429,由前端 +// uploadWithRetry / 下载侧重试指数退避重试——保证后端存活。 +// +// 覆盖范围(同一全局配额,按进程单例共享): +// - 上传:分片写入、分片合并、Word 写入(controller/upload.js、exam.js) +// - 下载:管理后台作品/Word 下载(controller/admin.js 的 _sendFile) +// 生产经 X-Accel-Redirect 卸载给 nginx,Node 仅发头、瞬间释放配额; +// 开发回退 Node 流式时,配额随「流生命周期」(end/close/error)释放。 + +const LIMIT = parseInt(process.env.FILE_IO_CONCURRENCY || '20', 10); +const MAX_QUEUE = parseInt(process.env.FILE_IO_MAX_QUEUE || '300', 10); +const WAIT_TIMEOUT = parseInt(process.env.FILE_IO_WAIT_TIMEOUT || '8000', 10); + +let active = 0; +const queue = []; + +function remove(entry) { + const i = queue.indexOf(entry); + if (i !== -1) queue.splice(i, 1); +} + +/** + * 获取一个文件 IO 配额。 + * 成功(或排到队首被放行)resolve;队列满 / 超时则 reject({ busy: true })。 + * @returns {Promise} + */ +function acquire() { + return new Promise((resolve, reject) => { + if (active < LIMIT) { + active++; + return resolve(); + } + if (queue.length >= MAX_QUEUE) { + const err = new Error('FILE_IO_BUSY'); + err.busy = true; + return reject(err); + } + let timer = null; + const entry = { + run() { + if (timer) clearTimeout(timer); + active++; + resolve(); + }, + cancel() { + if (timer) clearTimeout(timer); + const err = new Error('FILE_IO_BUSY'); + err.busy = true; + reject(err); + }, + }; + timer = setTimeout(() => { + remove(entry); + entry.cancel(); + }, WAIT_TIMEOUT); + queue.push(entry); + }); +} + +/** + * 释放一个文件 IO 配额(须在 finally 中调用)。 + */ +function release() { + if (active > 0) active--; + if (active < LIMIT && queue.length) { + const entry = queue.shift(); + entry.run(); + } +} + +module.exports = { acquire, release, LIMIT, MAX_QUEUE, WAIT_TIMEOUT }; diff --git a/app/lib/file_path_cache.js b/app/lib/file_path_cache.js new file mode 100644 index 0000000..854469e --- /dev/null +++ b/app/lib/file_path_cache.js @@ -0,0 +1,47 @@ +'use strict'; + +/** + * 下载前 file_path 短缓存(进程内,默认 30s)。 + * + * 收益:高并发下载同一文件时,避免每次请求都回查 DB 取 file_path / file_name。 + * + * 取舍(任务 #23 已确认):文件替换 / 删除后,≤ TTL 内可能命中旧路径。 + * - 下游 _sendFile 仍按实际文件做 fs.existsSync 校验,旧路径不存在 → 404,不会吐错误内容; + * - 被替换文件的旧路径在 TTL 内至多吐出旧版本一小段时间(非安全敏感、非实时场景,可接受)。 + * - 因此本缓存为「只读穿透 + TTL 自愈」,不强制接入上传/删除失效, + * 以最小化改动面与回归风险;如需更强一致可后续在上传/删除成功处调用 invalidate()。 + * + * 键约定:调用方自行拼装唯一 key,如 `word:${group}:${id}` / `sub:${group}:${id}`。 + */ +const TTL_MS = 30 * 1000; +const store = new Map(); // key -> { filePath, fileName, expire } + +function _now() { + return Date.now(); +} + +function getFull(key) { + if (!key) return null; + const hit = store.get(key); + if (!hit) return null; + if (hit.expire < _now()) { + store.delete(key); + return null; + } + return { filePath: hit.filePath, fileName: hit.fileName }; +} + +function setFull(key, filePath, fileName) { + if (!key || filePath == null || filePath === '') return; + store.set(key, { + filePath: String(filePath), + fileName: fileName == null ? '' : String(fileName), + expire: _now() + TTL_MS, + }); +} + +function invalidate(key) { + if (key) store.delete(key); +} + +module.exports = { getFull, setFull, invalidate, TTL_MS }; diff --git a/app/middleware/auth.js b/app/middleware/auth.js new file mode 100644 index 0000000..bc85f80 --- /dev/null +++ b/app/middleware/auth.js @@ -0,0 +1,116 @@ +'use strict'; + +const jwt = require('jsonwebtoken'); + +/** + * JWT 认证中间件 + * + * 功能: + * 1. 从请求头 Authorization 中提取 Bearer Token + * 2. 验证 JWT 的有效性 + * 3. 将解码后的用户信息挂载到 ctx.state.currentUser + * 4. 支持管理员角色校验(admin 路由专用) + * 5. 验证失败返回统一错误响应 + * + * 使用方式: + * - 选手认证:app.middleware.auth() + * - 管理员认证:app.middleware.auth({ adminOnly: true }) + * + * ctx.state.currentUser 数据结构: + * { + * uid: 账号 uid, + * id_number: 身份证号, + * role: 'contestant' | 'admin' + * } + */ + +module.exports = (options = {}, app) => { + return async function authMiddleware(ctx, next) { + // 从 ctx.app.config 中获取 JWT 密钥 + const appConfig = app || ctx.app; + const { secret } = appConfig.config.jwt; + + // 获取 Authorization 请求头;同时支持 Query ?token= 回退(用于跨设备直链下载, + // 如 iOS Safari 不响应 blob + a.download 时改用浏览器原生导航下载的场景)。 + const authorization = ctx.get('Authorization') || + (ctx.query && ctx.query.token ? `Bearer ${ctx.query.token}` : ''); + + // 检查 Authorization 头是否存在 + if (!authorization) { + ctx.status = 401; + ctx.body = { + code: 401, + message: '未提供认证令牌,请先登录', + }; + return; + } + + // 提取 Bearer Token(格式:Bearer ) + const tokenParts = authorization.split(' '); + if (tokenParts.length !== 2 || tokenParts[0] !== 'Bearer') { + ctx.status = 401; + ctx.body = { + code: 401, + message: '认证令牌格式错误,应为 Bearer ', + }; + return; + } + + const token = tokenParts[1]; + + // 验证 JWT Token + try { + const decoded = jwt.verify(token, secret); + + // 将解码后的用户信息挂载到 ctx.state.currentUser + ctx.state.currentUser = { + id: decoded.id || decoded.uid || 0, + uid: decoded.uid || decoded.id || 0, + id_number: decoded.id_number || '', + phone: decoded.phone || '', + role: decoded.role, + }; + + // 如果启用了管理员模式,校验当前用户是否为管理员 + if (options.adminOnly) { + if (ctx.state.currentUser.role !== 'admin') { + ctx.status = 403; + ctx.body = { + code: 403, + message: '无权限访问,需要管理员权限', + }; + return; + } + } + + // 继续执行后续中间件和控制器 + await next(); + } catch (err) { + // 仅当错误确实是 JWT 校验问题(过期/签名无效)时,才返回 401「认证失败」。 + // 注意:try 内部包含 `await next()`,即后续中间件与控制器也在同一 try 中。 + // 若下游控制器/服务抛出异常(如文件读取失败、数据库异常等),绝不能被 + // 这里误吞并伪装成 401,否则前端会误判「登录过期」把用户踢回登录页。 + // 因此:非 JWT 类错误必须重新抛出,交由 Egg 全局错误处理(返回 500 并写日志)。 + if (err.name === 'TokenExpiredError') { + // Token 已过期 + ctx.status = 401; + ctx.body = { + code: 401, + message: '认证令牌已过期,请重新登录', + }; + return; + } + if (err.name === 'JsonWebTokenError') { + // Token 无效(签名错误、格式错误等) + ctx.status = 401; + ctx.body = { + code: 401, + message: '无效的认证令牌', + }; + return; + } + // 其它异常(下游业务逻辑抛出):原样抛出,避免被误报为 401 + throw err; + } + }; +}; diff --git a/app/middleware/concurrency_guard.js b/app/middleware/concurrency_guard.js new file mode 100644 index 0000000..9179eef --- /dev/null +++ b/app/middleware/concurrency_guard.js @@ -0,0 +1,53 @@ +'use strict'; + +// ==================== 请求级并发闸门(大文件上传/下载) ==================== +// 背景(2026-08-11 压测结论): +// 场景「无带宽限制 + 单 worker + 极端并发」(大量考生同时上传大文件 + +// 管理后台同时下载大文件)下,单 worker 即使有文件 IO 护栏(file_io_guard), +// 仍会在「被接受的并发重请求数」过高时被海量大文件流压垮(OOM 被系统杀掉, +// 无任何 Node 异常日志)。原因是 file_io_guard 只限制「同时进行的文件流」, +// 而 90 个并发请求的连接已先被 TCP 接受、请求体已在传输中。 +// +// 本闸门在「读取请求体之前」对重 IO 接口做全局并发上限: +// - 命中重路由且 active < MAX:放行(active++),请求结束(finally)后 active--; +// - 命中重路由且 active >= MAX:立即回 429(连接级背压),请求体不会被解析/缓冲, +// 由前端 uploadWithRetry / 下载侧重试指数退避重试 —— 保证后端存活而非雪崩。 +// - 非重路由:直接放行,不影响登录/统计等轻接口。 +// +// 与 file_io_guard 互补:本闸门限制「被接受的并发重请求数」(防连接/内存被打满), +// file_io_guard 限制「同时进行的文件流数(按流生命周期释放)」(防 fd/事件循环耗尽)。 +// 双保险,使单 worker 在极端并发下「降级(429)而非崩溃」。 + +const HEAVY_PATTERNS = [ + /^\/api\/upload\/[^/]+\/chunk$/i, + /^\/api\/upload\/[^/]+\/merge$/i, + /^\/api\/exam\/[^/]+\/word$/i, + /^\/api\/admin\/submission-file\//i, + /^\/api\/admin\/submissions\/[^/]+\/[^/]+\/word$/i, +]; + +module.exports = (options, app) => { + const MAX = parseInt(process.env.HEAVY_REQ_MAX || (options && options.max) || '20', 10); + + let active = 0; + + return async function concurrencyGuard(ctx, next) { + if (!HEAVY_PATTERNS.some((re) => re.test(ctx.path))) { + return next(); + } + + if (active >= MAX) { + // 🔴 连接级背压:直接拒绝,避免请求体被解析/缓冲占用内存 + ctx.status = 429; + ctx.body = ctx.helper.error(429, '服务器繁忙,请稍后重试'); + return; + } + + active++; + try { + await next(); + } finally { + active--; + } + }; +}; diff --git a/app/middleware/loginRateLimit.js b/app/middleware/loginRateLimit.js new file mode 100644 index 0000000..9fa2e4f --- /dev/null +++ b/app/middleware/loginRateLimit.js @@ -0,0 +1,73 @@ +'use strict'; + +/** + * 登录限流中间件 - loginRateLimit + * + * 面向 /api/auth/login 与 /api/auth/admin-login,防止暴力破解。 + * + * 策略: + * - 以「客户端 IP + 登录标识(phone/username)」为维度,在滑动窗口内统计「失败」次数。 + * - 达到阈值后,在窗口结束前拒绝该维度的后续登录尝试(返回业务错误码 4290)。 + * - 登录成功(响应 code === 0)立即清空该维度的失败计数。 + * + * 说明:采用进程内内存计数(Map),零外部依赖、不影响本地/无 Redis 环境。 + * 多进程(egg-cluster)下各 worker 各自计数,属于可接受的基线防护; + * 如需全局精确限流,可后续替换为基于 Redis 的实现。 + * + * @param {object} options - 配置项 + * @param {number} [options.windowMs=900000] - 统计窗口(毫秒),默认 15 分钟 + * @param {number} [options.maxAttempts=10] - 窗口内允许的最大失败次数 + * @return {Function} Egg 中间件 + */ +module.exports = (options = {}) => { + const windowMs = options.windowMs || 15 * 60 * 1000; + const maxAttempts = options.maxAttempts || 10; + + // key -> { count, first } + const store = new Map(); + + const cleanup = (now) => { + for (const [ k, v ] of store) { + if (now - v.first >= windowMs) store.delete(k); + } + }; + + return async function loginRateLimit(ctx, next) { + const body = ctx.request.body || {}; + const identifier = body.phone || body.username || 'unknown'; + const key = `${ctx.ip}|${identifier}`; + const now = Date.now(); + + let rec = store.get(key); + // 窗口已过期则视为无记录 + if (rec && now - rec.first >= windowMs) { + store.delete(key); + rec = undefined; + } + + // 已超过阈值:直接拒绝 + if (rec && rec.count >= maxAttempts) { + const remainMin = Math.ceil((windowMs - (now - rec.first)) / 60000); + ctx.logger.warn(`[登录限流] 拒绝来自 ${key} 的登录尝试(已失败 ${rec.count} 次)`); + ctx.body = ctx.helper.error(4290, `登录尝试过于频繁,请约 ${remainMin} 分钟后再试`); + return; + } + + await next(); + + // 依据响应判定成败:统一响应体 { code: 0 } 为成功 + const succeeded = ctx.body && ctx.body.code === 0; + if (succeeded) { + store.delete(key); + } else { + if (rec) { + rec.count += 1; + } else { + store.set(key, { count: 1, first: now }); + } + } + + // 惰性清理,避免 Map 无限增长 + if (store.size > 5000) cleanup(now); + }; +}; diff --git a/app/middleware/rateLimit.js b/app/middleware/rateLimit.js new file mode 100644 index 0000000..d84116d --- /dev/null +++ b/app/middleware/rateLimit.js @@ -0,0 +1,139 @@ +'use strict'; + +/** + * API 限流中间件 + * 基于 token bucket 的简单限流实现,适用于考试并发场景 + * 针对不同端点使用不同限流策略: + * - 通用 API: 100次/分钟 + * - 登录/激活: 10次/分钟(防暴力破解) + * - 考试提交: 30次/分钟(高峰期保护) + * - 管理后台: 200次/分钟 + */ + +// 请求计数器存储(内存实现,生产环境建议换 Redis) +const counters = new Map(); + +// 每60秒自动清理过期计数器 +setInterval(() => { + const now = Date.now(); + for (const [key, entry] of counters) { + if (now - entry.resetTime > 60000) { + counters.delete(key); + } + } +}, 60000); + +/** + * 限流配置预设 + */ +const LIMITS = { + default: { max: 100, window: 60 }, // 通用: 100次/分钟 + auth: { max: 10, window: 60 }, // 登录/激活: 10次/分钟 + exam: { max: 30, window: 60 }, // 考试提交: 30次/分钟 + upload: { max: 20, window: 60 }, // 文件上传: 20次/分钟 + admin: { max: 200, window: 60 }, // 管理后台: 200次/分钟 +}; + +/** + * 获取客户端标识(优先用用户ID,其次IP) + */ +function getClientId(ctx) { + if (ctx.state.currentUser && ctx.state.currentUser.uid) { + return `user:${ctx.state.currentUser.uid}`; + } + return `ip:${ctx.ip}`; +} + +/** + * 根据路由自动判断限流等级 + */ +function getLimitLevel(ctx) { + const path = ctx.path; + if (path.startsWith('/api/auth/login') || path.startsWith('/api/auth/activate') || + path.startsWith('/api/auth/forgot-password') || path.startsWith('/api/auth/admin-login')) { + return 'auth'; + } + if (path.startsWith('/api/exam')) { + return 'exam'; + } + if (path.startsWith('/api/upload')) { + return 'upload'; + } + if (path.startsWith('/api/admin')) { + return 'admin'; + } + return 'default'; +} + +module.exports = (options = {}) => { + const level = options.level || 'default'; + const limit = options.max || LIMITS[level]?.max || LIMITS.default.max; + const windowSec = options.window || LIMITS[level]?.window || LIMITS.default.window; + + return async function rateLimit(ctx, next) { + // 自动检测限流级别 + const effectiveLevel = options.level || getLimitLevel(ctx); + const effectiveLimit = options.max || LIMITS[effectiveLevel]?.max || LIMITS.default.max; + const effectiveWindow = options.window || LIMITS[effectiveLevel]?.window || LIMITS.default.window; + + const clientId = getClientId(ctx); + const routeKey = `${effectiveLevel}:${clientId}:${ctx.path}`; + const now = Date.now(); + + let entry = counters.get(routeKey); + + // 初始化或重置过期计数器 + if (!entry || now - entry.resetTime > effectiveWindow * 1000) { + entry = { count: 0, resetTime: now + effectiveWindow * 1000 }; + counters.set(routeKey, entry); + } + + entry.count++; + + // 设置响应头告知限流状态 + const remaining = Math.max(0, effectiveLimit - entry.count); + const resetTime = new Date(entry.resetTime).toISOString(); + ctx.set('X-RateLimit-Limit', effectiveLimit); + ctx.set('X-RateLimit-Remaining', remaining); + ctx.set('X-RateLimit-Reset', resetTime); + + // 超限拒绝 + if (entry.count > effectiveLimit) { + ctx.status = 429; + ctx.body = { + code: 429, + message: `请求过于频繁,请${effectiveWindow}秒后重试。限流级别: ${effectiveLevel},上限: ${effectiveLimit}次/${effectiveWindow}秒`, + data: { + retryAfter: effectiveWindow, + limit: effectiveLimit, + remaining: 0, + resetTime, + }, + }; + ctx.logger.warn(`[RateLimit] ${effectiveLevel} 限流触发: ${clientId} -> ${ctx.path} (${entry.count}/${effectiveLimit})`); + return; + } + + ctx.logger.debug(`[RateLimit] ${effectiveLevel}: ${clientId} -> ${ctx.path} (${entry.count}/${effectiveLimit})`); + + await next(); + }; +}; + +/** + * 获取当前限流统计(调试用) + */ +module.exports.getStats = () => { + const stats = {}; + for (const [key, entry] of counters) { + stats[key] = { count: entry.count, resetAt: new Date(entry.resetTime).toISOString() }; + } + return stats; +}; + +/** + * 清除所有计数器(测试/重置用) + */ +module.exports.reset = () => { + counters.clear(); +}; diff --git a/app/model/account.js b/app/model/account.js new file mode 100644 index 0000000..9c85607 --- /dev/null +++ b/app/model/account.js @@ -0,0 +1,167 @@ +'use strict'; + +/** + * 账号模型 - Account + * + * 存储参赛者账号的全部信息,包括个人资料、联系方式和密码等。 + * 表名:accounts + */ + +module.exports = app => { + const { STRING, INTEGER, BOOLEAN, DATE } = app.Sequelize; + + const Account = app.model.define('account', { + // 主键,自增 + uid: { + type: INTEGER, + primaryKey: true, + autoIncrement: true, + comment: '账号唯一标识', + }, + // 角色:contestant / admin + role: { + type: STRING(20), + allowNull: false, + defaultValue: 'contestant', + comment: '账号角色', + }, + // 中文姓名 + name_cn: { + type: STRING(50), + allowNull: false, + comment: '中文姓名', + }, + // 英文姓名 + name_en: { + type: STRING(100), + allowNull: true, + defaultValue: null, + comment: '英文姓名', + }, + // 性别:male / female + gender: { + type: STRING(10), + allowNull: true, + defaultValue: null, + comment: '性别', + }, + // 手机号码 + phone: { + type: STRING(20), + allowNull: true, + defaultValue: null, + comment: '手机号码', + }, + // 电子邮箱 + email: { + type: STRING(100), + allowNull: true, + defaultValue: null, + comment: '电子邮箱', + }, + // 身份证号,唯一 + id_number: { + type: STRING(18), + allowNull: false, + unique: true, + comment: '身份证号', + }, + // 登录密码(SHA256 哈希存储) + password: { + type: STRING(100), + allowNull: false, + comment: '登录密码', + }, + // 密码盐值 + salt: { + type: STRING(32), + allowNull: false, + comment: '密码盐值', + }, + // 职位 + position: { + type: STRING(100), + allowNull: true, + defaultValue: null, + comment: '职位', + }, + // 专业 + major: { + type: STRING(100), + allowNull: true, + defaultValue: null, + comment: '专业', + }, + // 国家 + country: { + type: STRING(50), + allowNull: true, + defaultValue: null, + comment: '国家', + }, + // 地区 + region: { + type: STRING(50), + allowNull: true, + defaultValue: null, + comment: '地区', + }, + // 机构/单位(中文) + organization_cn: { + type: STRING(200), + allowNull: true, + defaultValue: null, + comment: '机构/单位(中文)', + }, + // 机构/单位(英文) + organization_en: { + type: STRING(200), + allowNull: true, + defaultValue: null, + comment: '机构/单位(英文)', + }, + // 账号是否已激活 + activated: { + type: BOOLEAN, + allowNull: false, + defaultValue: false, + comment: '账号激活状态', + }, + // 总分 + score: { + type: INTEGER, + allowNull: true, + defaultValue: null, + comment: '账号总分', + }, + // 记录创建时间 + created_at: { + type: DATE, + allowNull: false, + defaultValue: app.Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '创建时间', + }, + // 记录更新时间 + updated_at: { + type: DATE, + allowNull: false, + defaultValue: app.Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '更新时间', + }, + }, { + tableName: 'accounts', + timestamps: false, + underscored: true, + indexes: [ + { + unique: true, + fields: ['id_number'], + }, + { + fields: ['role'], + }, + ], + }); + + return Account; +}; diff --git a/app/model/admin.js b/app/model/admin.js new file mode 100644 index 0000000..01b7de3 --- /dev/null +++ b/app/model/admin.js @@ -0,0 +1,57 @@ +'use strict'; + +/** + * 管理员模型 - Admin + * + * 存储后台管理员账号信息,管理员通过用户名和密码进行登录。 + */ + +module.exports = app => { + const { STRING, INTEGER, DATE } = app.Sequelize; + + const Admin = app.model.define('admin', { + // 主键 ID,自增 + id: { + type: INTEGER, + primaryKey: true, + autoIncrement: true, + comment: '管理员唯一标识', + }, + // 管理员用户名,唯一 + username: { + type: STRING(50), + allowNull: false, + unique: true, + comment: '管理员用户名', + }, + // 管理员密码(明文存储,生产环境应使用 bcrypt 加密) + password: { + type: STRING(100), + allowNull: false, + comment: '管理员密码', + }, + // 记录创建时间 + created_at: { + type: DATE, + allowNull: false, + defaultValue: app.Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '创建时间', + }, + // 记录更新时间 + updated_at: { + type: DATE, + allowNull: false, + defaultValue: app.Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '更新时间', + }, + }, { + // 表名 + tableName: 'admins', + // 时间戳字段 + timestamps: false, + // 下划线命名 + underscored: true, + }); + + return Admin; +}; \ No newline at end of file diff --git a/app/model/aggregation.js b/app/model/aggregation.js new file mode 100644 index 0000000..cf22d98 --- /dev/null +++ b/app/model/aggregation.js @@ -0,0 +1,135 @@ +'use strict'; + +/** + * 聚合报名模型 - Aggregation + * + * 存储参赛报名聚合信息,关联竞赛、团队和账号。 + * 表名:aggregation + */ + +module.exports = app => { + const { STRING, INTEGER, BOOLEAN, ENUM, DATE } = app.Sequelize; + + const Aggregation = app.model.define('aggregation', { + // 主键,自增 + aggregation_uid: { + type: INTEGER, + primaryKey: true, + autoIncrement: true, + comment: '聚合唯一标识', + }, + // 外键:竞赛 ID + competition_uid: { + type: INTEGER, + allowNull: false, + comment: '关联竞赛 ID', + }, + // 外键:团队 ID(允许为空:单人参赛/未分组的选手仍可建立聚合报名, + // 以便 benke/jiaoshi 作品上传等依赖 aggregation 的流程正常工作) + team_uid: { + type: INTEGER, + allowNull: true, + comment: '关联团队 ID', + }, + // 外键:账号 ID + account_uid: { + type: INTEGER, + allowNull: false, + comment: '关联账号 ID', + }, + // 身份:队长/队员/指导教师 + identity: { + type: STRING(20), + allowNull: false, + comment: '身份', + }, + // 是否为报名负责人 + is_registrant: { + type: BOOLEAN, + allowNull: false, + defaultValue: false, + comment: '是否为报名负责人', + }, + // 报名表文件路径 + registration_form: { + type: STRING(500), + allowNull: true, + defaultValue: null, + comment: '报名表文件路径', + }, + // 盖章报名表文件路径 + stamped_form: { + type: STRING(500), + allowNull: true, + defaultValue: null, + comment: '盖章报名表文件路径', + }, + // 审核状态:pending/approved/rejected + audit_status: { + type: STRING(20), + allowNull: false, + defaultValue: 'pending', + comment: '审核状态', + }, + // 驳回原因 + reject_reason: { + type: STRING(500), + allowNull: true, + defaultValue: null, + comment: '驳回原因', + }, + // 记录创建时间 + created_at: { + type: DATE, + allowNull: false, + defaultValue: app.Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '创建时间', + }, + // 记录更新时间 + updated_at: { + type: DATE, + allowNull: false, + defaultValue: app.Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '更新时间', + }, + }, { + tableName: 'aggregation', + timestamps: false, + underscored: true, + indexes: [ + { + fields: ['competition_uid'], + }, + { + fields: ['team_uid'], + }, + { + fields: ['account_uid'], + }, + { + fields: ['audit_status'], + }, + ], + }); + + // 关联定义 + Aggregation.associate = function() { + app.model.Aggregation.belongsTo(app.model.CompetitionInfo, { + foreignKey: 'competition_uid', + targetKey: 'competition_uid', + as: 'competition', + }); + app.model.Aggregation.belongsTo(app.model.Team, { + foreignKey: 'team_uid', + targetKey: 'team_uid', + as: 'team', + }); + app.model.Aggregation.belongsTo(app.model.Account, { + foreignKey: 'account_uid', + targetKey: 'uid', + as: 'account', + }); + }; + + return Aggregation; +}; diff --git a/app/model/benkeSubmission.js b/app/model/benkeSubmission.js new file mode 100644 index 0000000..55b70b3 --- /dev/null +++ b/app/model/benkeSubmission.js @@ -0,0 +1,115 @@ +'use strict'; + +/** + * 本科组作品提交模型 - BenkeSubmission + * + * 存储本科组参赛选手的作品提交记录,包括文件信息和评分。 + * 表名:benke_submissions + */ + +module.exports = app => { + const { STRING, INTEGER, DATE } = app.Sequelize; + + const BenkeSubmission = app.model.define('benke_submission', { + // 主键 ID,自增 + id: { + type: INTEGER, + primaryKey: true, + autoIncrement: true, + comment: '提交记录唯一标识', + }, + // 外键:关联聚合报名 ID + aggregation_uid: { + type: INTEGER, + allowNull: false, + comment: '关联聚合报名 ID', + }, + // 文件类型:word(文档)/ ppt(演示文稿) + file_type: { + type: STRING(20), + allowNull: true, + defaultValue: null, + comment: '文件类型 word/ppt', + }, + // 作品名称 + work_name: { + type: STRING(200), + allowNull: false, + comment: '作品名称', + }, + // 文件存储路径 + file_path: { + type: STRING(500), + allowNull: false, + comment: '文件存储路径', + }, + // 文件原始名称 + file_name: { + type: STRING(200), + allowNull: false, + comment: '文件原始名称', + }, + // 文件大小(字节) + file_size: { + type: INTEGER, + allowNull: true, + defaultValue: null, + comment: '文件大小(字节)', + }, + // 文件 MD5 + file_md5: { + type: STRING(32), + allowNull: true, + defaultValue: null, + comment: '文件 MD5', + }, + // 上传时间 + upload_time: { + type: DATE, + allowNull: false, + defaultValue: app.Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '上传时间', + }, + // 评分 + score: { + type: INTEGER, + allowNull: true, + defaultValue: null, + comment: '评分', + }, + // 记录创建时间 + created_at: { + type: DATE, + allowNull: false, + defaultValue: app.Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '创建时间', + }, + // 记录更新时间 + updated_at: { + type: DATE, + allowNull: false, + defaultValue: app.Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '更新时间', + }, + }, { + tableName: 'benke_submissions', + timestamps: false, + underscored: true, + indexes: [ + { + fields: ['aggregation_uid'], + }, + ], + }); + + // 关联定义 + BenkeSubmission.associate = function() { + app.model.BenkeSubmission.belongsTo(app.model.Aggregation, { + foreignKey: 'aggregation_uid', + targetKey: 'aggregation_uid', + as: 'aggregation', + }); + }; + + return BenkeSubmission; +}; diff --git a/app/model/carousel.js b/app/model/carousel.js new file mode 100644 index 0000000..6e2d394 --- /dev/null +++ b/app/model/carousel.js @@ -0,0 +1,105 @@ +'use strict'; + +/** + * 轮播图模型 - Carousel + * + * 存储首页轮播图信息,支持标题、排序和启用/禁用控制。 + * 表名:carousel + */ + +module.exports = app => { + const { STRING, INTEGER, BOOLEAN, DATE, TEXT } = app.Sequelize; + + const Carousel = app.model.define('carousel', { + // 主键 ID,自增 + id: { + type: INTEGER, + primaryKey: true, + autoIncrement: true, + comment: '轮播图唯一标识', + }, + // 图片 URL 地址(可为空时前端回退渐变背景) + image_url: { + type: STRING(500), + allowNull: true, + defaultValue: null, + comment: '图片 URL 地址(可为空回退渐变)', + }, + // 轮播图标题 + title: { + type: STRING(200), + allowNull: true, + comment: '轮播图标题', + }, + // 富文本正文(HTML,由 wangEditor 生成,前端 v-html 渲染) + content: { + type: TEXT, + allowNull: true, + defaultValue: null, + comment: '富文本正文(HTML)', + }, + // 按钮文字(为空则不显示按钮) + button_text: { + type: STRING(100), + allowNull: true, + defaultValue: null, + comment: '按钮文字(为空则不显示按钮)', + }, + // 按钮链接(内部路由如 /activate,或外部 http(s):// 链接) + button_link: { + type: STRING(500), + allowNull: true, + defaultValue: null, + comment: '按钮链接(内部路由/外部URL)', + }, + // 排序序号(越小越靠前) + sort_order: { + type: INTEGER, + allowNull: false, + defaultValue: 0, + comment: '排序序号', + }, + // 点击跳转链接 + link_url: { + type: STRING(500), + allowNull: true, + defaultValue: null, + comment: '点击跳转链接', + }, + // 是否启用 + enabled: { + type: BOOLEAN, + allowNull: false, + defaultValue: true, + comment: '是否启用', + }, + // 记录创建时间 + created_at: { + type: DATE, + allowNull: false, + defaultValue: app.Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '创建时间', + }, + // 记录更新时间 + updated_at: { + type: DATE, + allowNull: false, + defaultValue: app.Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '更新时间', + }, + }, { + tableName: 'carousel', + timestamps: false, + underscored: true, + indexes: [ + { + fields: ['sort_order'], + }, + { + fields: ['enabled'], + }, + ], + }); + + return Carousel; +}; \ No newline at end of file diff --git a/app/model/choiceQuestion.js b/app/model/choiceQuestion.js new file mode 100644 index 0000000..02ea628 --- /dev/null +++ b/app/model/choiceQuestion.js @@ -0,0 +1,67 @@ +'use strict'; + +/** + * 选择题模型 - ChoiceQuestion + * + * 对应 choice_questions 表,存储所有组别的选择题(单选 + 多选)。 + * 通过 group 字段区分中职组(zhongzhi)和高职组(gaozhi)。 + */ +module.exports = app => { + const { STRING, INTEGER, DATE, JSON: JSON_TYPE, TEXT } = app.Sequelize; + + return app.model.define('ChoiceQuestion', { + id: { + type: INTEGER, + primaryKey: true, + autoIncrement: true, + comment: '主键ID', + }, + group: { + type: STRING(20), + allowNull: false, + comment: '组别:zhongzhi/gaozhi', + }, + question: { + type: TEXT, + allowNull: false, + comment: '题目内容', + }, + options: { + type: JSON_TYPE, + allowNull: false, + comment: '选项列表 [{ key: "A", text: "..." }, ...]', + }, + answer: { + type: STRING(10), + allowNull: false, + comment: '正确答案(如 A / AB / ABCD)', + }, + score: { + type: INTEGER, + allowNull: false, + defaultValue: 5, + comment: '分值', + }, + sort_order: { + type: INTEGER, + allowNull: false, + defaultValue: 0, + comment: '排序', + }, + created_at: { + type: DATE, + defaultValue: app.Sequelize.NOW, + comment: '创建时间', + }, + updated_at: { + type: DATE, + defaultValue: app.Sequelize.NOW, + comment: '更新时间', + }, + }, { + tableName: 'choice_questions', + timestamps: true, + createdAt: 'created_at', + updatedAt: 'updated_at', + }); +}; \ No newline at end of file diff --git a/app/model/competitionInfo.js b/app/model/competitionInfo.js new file mode 100644 index 0000000..ecb47f6 --- /dev/null +++ b/app/model/competitionInfo.js @@ -0,0 +1,61 @@ +'use strict'; + +/** + * 竞赛信息模型 - CompetitionInfo + * + * 存储竞赛的基本信息,包括竞赛名称和代码。 + * 表名:competition_info + */ + +module.exports = app => { + const { STRING, INTEGER, DATE } = app.Sequelize; + + const CompetitionInfo = app.model.define('competition_info', { + // 主键,自增 + competition_uid: { + type: INTEGER, + primaryKey: true, + autoIncrement: true, + comment: '竞赛唯一标识', + }, + // 竞赛名称 + competition_name: { + type: STRING(200), + allowNull: false, + comment: '竞赛名称', + }, + // 竞赛代码(唯一标识码) + competition_code: { + type: STRING(50), + allowNull: false, + unique: true, + comment: '竞赛代码', + }, + // 记录创建时间 + created_at: { + type: DATE, + allowNull: false, + defaultValue: app.Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '创建时间', + }, + // 记录更新时间 + updated_at: { + type: DATE, + allowNull: false, + defaultValue: app.Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '更新时间', + }, + }, { + tableName: 'competition_info', + timestamps: false, + underscored: true, + indexes: [ + { + unique: true, + fields: ['competition_code'], + }, + ], + }); + + return CompetitionInfo; +}; diff --git a/app/model/competitionTime.js b/app/model/competitionTime.js new file mode 100644 index 0000000..7a99b79 --- /dev/null +++ b/app/model/competitionTime.js @@ -0,0 +1,93 @@ +'use strict'; + +/** + * 比赛时间配置模型 - CompetitionTime + * + * 存储各参赛组别的比赛时间配置,包括开始时间、结束时间、提交截止时间和Word模板路径。 + * 每个组别只有一条配置记录。 + * 表名:competition_times + */ + +module.exports = app => { + const { STRING, INTEGER, DATE } = app.Sequelize; + + const CompetitionTime = app.model.define('competition_time', { + // 主键 ID,自增 + id: { + type: INTEGER, + primaryKey: true, + autoIncrement: true, + comment: '时间配置唯一标识', + }, + // 组别:zhongzhi/gaozhi/benke/jiaoshi,唯一约束 + group: { + type: STRING(20), + allowNull: false, + unique: true, + comment: '组别', + }, + // 比赛开始时间 + start_time: { + type: DATE, + allowNull: true, + defaultValue: null, + comment: '比赛开始时间', + }, + // 比赛结束时间 + end_time: { + type: DATE, + allowNull: true, + defaultValue: null, + comment: '比赛结束时间', + }, + // 作品提交截止时间 + deadline: { + type: DATE, + allowNull: true, + defaultValue: null, + comment: '作品提交截止时间', + }, + // Word 操作题模板文件路径(相对于 app/public/ 的相对路径) + word_template: { + type: STRING(500), + allowNull: true, + defaultValue: null, + comment: 'Word 操作题模板文件路径', + }, + // 客观题(选择题)抽取数量:考试时从该组别题库随机抽取的数量上限; + // 若题库实际数量不足,则自动按实际数量封顶(题目序号自适应题目数量)。 + choice_count: { + type: INTEGER, + allowNull: false, + defaultValue: 50, + comment: '选择题抽取数量', + }, + // 客观题(判断题)抽取数量,语义同 choice_count + judge_count: { + type: INTEGER, + allowNull: false, + defaultValue: 20, + comment: '判断题抽取数量', + }, + // 记录创建时间 + created_at: { + type: DATE, + allowNull: false, + defaultValue: app.Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '创建时间', + }, + // 记录更新时间 + updated_at: { + type: DATE, + allowNull: false, + defaultValue: app.Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '更新时间', + }, + }, { + tableName: 'competition_times', + timestamps: false, + underscored: true, + }); + + return CompetitionTime; +}; diff --git a/app/model/contestant.js b/app/model/contestant.js new file mode 100644 index 0000000..5e061c0 --- /dev/null +++ b/app/model/contestant.js @@ -0,0 +1,161 @@ +'use strict'; + +/** + * 选手模型 - Contestant + * + * 存储参赛选手的基本信息,包括身份认证、组别、成绩等。 + * 选手通过身份证号和密码进行登录认证。 + */ + +module.exports = app => { + const { STRING, INTEGER, BOOLEAN, DATE } = app.Sequelize; + + const Contestant = app.model.define('contestant', { + // 主键 ID,自增 + id: { + type: INTEGER, + primaryKey: true, + autoIncrement: true, + comment: '选手唯一标识', + }, + // 身份证号,唯一,长度 18,用于选手登录 + id_number: { + type: STRING(18), + allowNull: false, + unique: true, + comment: '身份证号(登录账号)', + validate: { + len: [18, 18], // 严格校验 18 位身份证号 + }, + }, + // 选手姓名 + name: { + type: STRING(50), + allowNull: false, + comment: '选手姓名', + }, + // 手机号码 + phone: { + type: STRING(20), + allowNull: false, + comment: '手机号码', + }, + // 参赛组别:zhongzhi(中职) / gaozhi(高职) / benke(本科) / jiaoshi(教师) + group: { + type: STRING(20), + allowNull: false, + comment: '参赛组别', + validate: { + isIn: [['zhongzhi', 'gaozhi', 'benke', 'jiaoshi']], + }, + }, + // 登录密码(存储明文,生产环境应使用 bcrypt 加密存储) + password: { + type: STRING(100), + allowNull: false, + defaultValue: '', + comment: '登录密码', + }, + // 账号是否已激活(激活后才可以登录) + activated: { + type: BOOLEAN, + allowNull: false, + defaultValue: false, + comment: '账号激活状态', + }, + // 总分(管理员打分后更新,NULL 表示尚未评分) + score: { + type: INTEGER, + allowNull: true, + defaultValue: null, + comment: '选手总分(exam_score + word_score)', + }, + // 卷面分(选择题+判断题自动计算) + exam_score: { + type: INTEGER, + allowNull: true, + defaultValue: null, + comment: '卷面考试成绩', + }, + // Word题分数(管理员手动打分) + word_score: { + type: INTEGER, + allowNull: true, + defaultValue: null, + comment: 'Word题得分', + }, + // PPT分数(本科/教师组管理员手动打分) + ppt_score: { + type: INTEGER, + allowNull: true, + defaultValue: null, + comment: 'PPT得分(本科/教师组)', + }, + // 所属团队 ID(外键关联 teams 表) + team_id: { + type: INTEGER, + allowNull: true, + defaultValue: null, + comment: '所属团队ID', + }, + // 测试数据标记:0=生产数据,1=测试数据(重置时仅重置测试数据) + test_flag: { + type: INTEGER, + allowNull: false, + defaultValue: 0, + comment: '测试数据标记:0=生产数据,1=测试数据', + }, + // 记录创建时间 + created_at: { + type: DATE, + allowNull: false, + defaultValue: app.Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '创建时间', + }, + // 记录更新时间 + updated_at: { + type: DATE, + allowNull: false, + defaultValue: app.Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '更新时间', + }, + }, { + // 表名保持为 contestants(原样,不做复数变换) + tableName: 'contestants', + // 不自动生成 createdAt / updatedAt(我们手动定义 created_at / updated_at) + timestamps: false, + // 下划线命名 + underscored: true, + // 索引 + indexes: [ + { + unique: true, + fields: ['id_number'], + }, + { + fields: ['group'], + }, + { + fields: ['team_id'], + }, + ], + }); + + Contestant.associate = () => { + app.model.Contestant.belongsTo(app.model.Team, { + foreignKey: 'team_id', + targetKey: 'team_uid', + as: 'team', + }); + app.model.Contestant.hasMany(app.model.ExamSubmission, { + foreignKey: 'contestant_id', + as: 'examSubmissions', + }); + app.model.Contestant.hasMany(app.model.WorkSubmission, { + foreignKey: 'contestant_id', + as: 'workSubmissions', + }); + }; + + return Contestant; +}; \ No newline at end of file diff --git a/app/model/examQuestion.js b/app/model/examQuestion.js new file mode 100644 index 0000000..bc699d2 --- /dev/null +++ b/app/model/examQuestion.js @@ -0,0 +1,84 @@ +'use strict'; + +/** + * 试卷题目模型 - ExamQuestion + * + * 存储各参赛组别的试卷题目,按组别区分,支持排序。 + * 表名:exam_questions + */ + +module.exports = app => { + const { STRING, TEXT, INTEGER, DATE } = app.Sequelize; + + const ExamQuestion = app.model.define('exam_question', { + // 主键 ID,自增 + id: { + type: INTEGER, + primaryKey: true, + autoIncrement: true, + comment: '题目唯一标识', + }, + // 所属组别:zhongzhi/gaozhi/benke/jiaoshi + group: { + type: STRING(20), + allowNull: false, + comment: '题目所属组别', + validate: { + isIn: [['zhongzhi', 'gaozhi', 'benke', 'jiaoshi']], + }, + }, + // 题目标题 + title: { + type: STRING(200), + allowNull: false, + comment: '题目标题', + }, + // 题目详细内容/描述 + content: { + type: TEXT, + allowNull: false, + comment: '题目内容描述', + }, + // 题目分值 + score: { + type: INTEGER, + allowNull: false, + comment: '题目分值', + }, + // 排序序号(决定题目在试卷中的展示顺序) + sort_order: { + type: INTEGER, + allowNull: false, + defaultValue: 0, + comment: '排序序号', + }, + // 创建时间 + created_at: { + type: DATE, + allowNull: false, + defaultValue: app.Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '创建时间', + }, + // 更新时间 + updated_at: { + type: DATE, + allowNull: false, + defaultValue: app.Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '更新时间', + }, + }, { + tableName: 'exam_questions', + timestamps: false, + underscored: true, + indexes: [ + { + fields: ['group'], + }, + { + fields: ['group', 'sort_order'], + }, + ], + }); + + return ExamQuestion; +}; \ No newline at end of file diff --git a/app/model/examSubmission.js b/app/model/examSubmission.js new file mode 100644 index 0000000..5c44e02 --- /dev/null +++ b/app/model/examSubmission.js @@ -0,0 +1,110 @@ +'use strict'; + +/** + * 答题记录模型 - ExamSubmission + * + * 存储选手的试卷答题记录,包括答题内容、提交时间和得分。 + * 每个选手在其组别的比赛中只允许提交一次。 + * 表名:exam_submissions + */ + +module.exports = app => { + const { STRING, INTEGER, DATE, TEXT, JSON: JSON_TYPE } = app.Sequelize; + + const ExamSubmission = app.model.define('exam_submission', { + // 主键 ID,自增 + id: { + type: INTEGER, + primaryKey: true, + autoIncrement: true, + comment: '答题记录唯一标识', + }, + // 选手 ID,外键关联 contestants 表 + contestant_id: { + type: INTEGER, + allowNull: false, + comment: '选手 ID', + references: { + model: 'contestants', + key: 'id', + }, + }, + // 答题内容(JSON 格式存储,如 [{question_id: 1, answer: "..."}, ...]) + answers: { + type: JSON_TYPE, + allowNull: false, + comment: '答题内容(JSON)', + }, + // 该考生固化后的题目顺序(question_uid 数组 JSON 字符串):首次进入随机生成并持久化, + // 之后重进复用,避免每次进入都 RAND() 重新打乱顺序(重进题目顺序变乱 Bug 修复)。 + question_order: { + type: TEXT, + allowNull: true, + defaultValue: null, + comment: '该考生固化题目顺序(question_uid JSON 数组),重进复用', + }, + // Word 答案文件路径 + word_file_path: { + type: STRING(500), + allowNull: true, + defaultValue: null, + comment: 'Word答案文件路径', + }, + // Word 答案文件名 + word_file_name: { + type: STRING(255), + allowNull: true, + defaultValue: null, + comment: 'Word答案文件名', + }, + // 提交时间 + submit_time: { + type: DATE, + allowNull: true, + comment: '提交时间(占位/未提交为 NULL,正式提交后为具体时间)', + }, + // 得分(管理员评分后更新,NULL 表示尚未评分) + score: { + type: INTEGER, + allowNull: true, + defaultValue: null, + comment: '试卷得分', + }, + // 创建时间 + created_at: { + type: DATE, + allowNull: false, + defaultValue: app.Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '创建时间', + }, + // 更新时间 + updated_at: { + type: DATE, + allowNull: false, + defaultValue: app.Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '更新时间', + }, + }, { + tableName: 'exam_submissions', + timestamps: false, + underscored: true, + indexes: [ + { + // 唯一约束:每选手仅一条答题提交记录,并发创建时折叠为一条(B12)。 + unique: true, + name: 'uk_exam_submission_contestant', + fields: ['contestant_id'], + }, + ], + }); + + // 关联关系:一条答题记录属于一名选手 + ExamSubmission.associate = () => { + app.model.ExamSubmission.belongsTo(app.model.Contestant, { + foreignKey: 'contestant_id', + as: 'contestant', + }); + }; + + return ExamSubmission; +}; \ No newline at end of file diff --git a/app/model/gaozhiQuestion.js b/app/model/gaozhiQuestion.js new file mode 100644 index 0000000..139a8a5 --- /dev/null +++ b/app/model/gaozhiQuestion.js @@ -0,0 +1,89 @@ +'use strict'; + +/** + * 高职组题库模型 - GaozhiQuestion + * + * 存储高职组比赛题目,支持单选、多选、判断等多种题型。 + * 表名:gaozhi_questions + */ + +module.exports = app => { + const { STRING, INTEGER, JSON, DATE } = app.Sequelize; + + const GaozhiQuestion = app.model.define('gaozhi_question', { + // 主键,自增 + question_uid: { + type: INTEGER, + primaryKey: true, + autoIncrement: true, + comment: '题目唯一标识', + }, + // 题目内容 + question: { + type: STRING(1000), + allowNull: false, + comment: '题目内容', + }, + // 正确答案 + answer: { + type: STRING(500), + allowNull: false, + comment: '正确答案', + }, + // 题目类型:single(单选) / multiple(多选) / judge(判断) + question_type: { + type: STRING(20), + allowNull: false, + comment: '题目类型', + }, + // 选项(JSON 格式) + options: { + type: JSON, + allowNull: true, + defaultValue: null, + comment: '选项', + }, + // 题目分值 + score: { + type: INTEGER, + allowNull: false, + defaultValue: 0, + comment: '题目分值', + }, + // 排序序号 + sort_order: { + type: INTEGER, + allowNull: false, + defaultValue: 0, + comment: '排序序号', + }, + // 记录创建时间 + created_at: { + type: DATE, + allowNull: false, + defaultValue: app.Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '创建时间', + }, + // 记录更新时间 + updated_at: { + type: DATE, + allowNull: false, + defaultValue: app.Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '更新时间', + }, + }, { + tableName: 'gaozhi_questions', + timestamps: false, + underscored: true, + indexes: [ + { + fields: ['question_type'], + }, + { + fields: ['sort_order'], + }, + ], + }); + + return GaozhiQuestion; +}; diff --git a/app/model/gaozhiSubmission.js b/app/model/gaozhiSubmission.js new file mode 100644 index 0000000..d03a67c --- /dev/null +++ b/app/model/gaozhiSubmission.js @@ -0,0 +1,133 @@ +'use strict'; + +/** + * 高职组答题记录模型 - GaozhiSubmission + * + * 存储高职组参赛选手的答题记录,包括答题数据、判分和Word文件上传。 + * 表名:gaozhi_submissions + */ + +module.exports = app => { + const { STRING, INTEGER, TEXT, BOOLEAN, DATE } = app.Sequelize; + + const GaozhiSubmission = app.model.define('gaozhi_submission', { + // 主键 ID,自增 + id: { + type: INTEGER, + primaryKey: true, + autoIncrement: true, + comment: '提交记录唯一标识', + }, + // 外键:关联聚合报名 ID + aggregation_uid: { + type: INTEGER, + allowNull: false, + comment: '关联聚合报名 ID', + }, + // 外键:关联题目 ID + question_uid: { + type: INTEGER, + allowNull: false, + comment: '关联题目 ID', + }, + // 题型(choice / judge):与 question_uid 联合唯一标识一道题。 + // 选择题与判断题的 id 在不同表中可能数值重叠(如 choice#5 与 judge#5), + // 仅凭 question_uid 无法区分,必须结合 question_type 存储,否则会折叠丢失答案(见 B11)。 + question_type: { + type: STRING(16), + allowNull: false, + defaultValue: '', + comment: '题型:choice/judge,与 question_uid 联合区分题目', + }, + // 答题数据(选手提交的答案) + answer_data: { + type: TEXT, + allowNull: true, + defaultValue: null, + comment: '答题数据', + }, + // 是否判为正确 + is_correct: { + type: BOOLEAN, + allowNull: true, + defaultValue: null, + comment: '是否判为正确', + }, + // 得分 + score: { + type: INTEGER, + allowNull: true, + defaultValue: null, + comment: '得分', + }, + // Word 文件存储路径 + word_file_path: { + type: STRING(500), + allowNull: true, + defaultValue: null, + comment: 'Word 文件存储路径', + }, + // Word 文件原始名称 + word_file_name: { + type: STRING(200), + allowNull: true, + defaultValue: null, + comment: 'Word 文件原始名称', + }, + // Word 文件 MD5 + word_file_md5: { + type: STRING(32), + allowNull: true, + defaultValue: null, + comment: 'Word 文件 MD5', + }, + // 记录创建时间 + created_at: { + type: DATE, + allowNull: false, + defaultValue: app.Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '创建时间', + }, + // 记录更新时间 + updated_at: { + type: DATE, + allowNull: false, + defaultValue: app.Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '更新时间', + }, + }, { + tableName: 'gaozhi_submissions', + timestamps: false, + underscored: true, + indexes: [ + { + fields: ['aggregation_uid'], + }, + { + fields: ['question_uid'], + }, + { + // 唯一约束:同一选手同一题(题型+题号)只保留一行,杜绝并发作答重复落库(B12)。 + unique: true, + name: 'uk_gaozhi_submission', + fields: ['aggregation_uid', 'question_uid', 'question_type'], + }, + ], + }); + + // 关联定义 + GaozhiSubmission.associate = function() { + app.model.GaozhiSubmission.belongsTo(app.model.Aggregation, { + foreignKey: 'aggregation_uid', + targetKey: 'aggregation_uid', + as: 'aggregation', + }); + app.model.GaozhiSubmission.belongsTo(app.model.GaozhiQuestion, { + foreignKey: 'question_uid', + targetKey: 'question_uid', + as: 'gaozhiQuestion', + }); + }; + + return GaozhiSubmission; +}; diff --git a/app/model/jiaoshiSubmission.js b/app/model/jiaoshiSubmission.js new file mode 100644 index 0000000..5a1fc7b --- /dev/null +++ b/app/model/jiaoshiSubmission.js @@ -0,0 +1,115 @@ +'use strict'; + +/** + * 教师组作品提交模型 - JiaoshiSubmission + * + * 存储教师组参赛选手的作品提交记录,包括文件信息和评分。 + * 表名:jiaoshi_submissions + */ + +module.exports = app => { + const { STRING, INTEGER, DATE } = app.Sequelize; + + const JiaoshiSubmission = app.model.define('jiaoshi_submission', { + // 主键 ID,自增 + id: { + type: INTEGER, + primaryKey: true, + autoIncrement: true, + comment: '提交记录唯一标识', + }, + // 外键:关联聚合报名 ID + aggregation_uid: { + type: INTEGER, + allowNull: false, + comment: '关联聚合报名 ID', + }, + // 文件类型:word(文档)/ ppt(演示文稿) + file_type: { + type: STRING(20), + allowNull: true, + defaultValue: null, + comment: '文件类型 word/ppt', + }, + // 作品名称 + work_name: { + type: STRING(200), + allowNull: false, + comment: '作品名称', + }, + // 文件存储路径 + file_path: { + type: STRING(500), + allowNull: false, + comment: '文件存储路径', + }, + // 文件原始名称 + file_name: { + type: STRING(200), + allowNull: false, + comment: '文件原始名称', + }, + // 文件大小(字节) + file_size: { + type: INTEGER, + allowNull: true, + defaultValue: null, + comment: '文件大小(字节)', + }, + // 文件 MD5 + file_md5: { + type: STRING(32), + allowNull: true, + defaultValue: null, + comment: '文件 MD5', + }, + // 上传时间 + upload_time: { + type: DATE, + allowNull: false, + defaultValue: app.Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '上传时间', + }, + // 评分 + score: { + type: INTEGER, + allowNull: true, + defaultValue: null, + comment: '评分', + }, + // 记录创建时间 + created_at: { + type: DATE, + allowNull: false, + defaultValue: app.Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '创建时间', + }, + // 记录更新时间 + updated_at: { + type: DATE, + allowNull: false, + defaultValue: app.Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '更新时间', + }, + }, { + tableName: 'jiaoshi_submissions', + timestamps: false, + underscored: true, + indexes: [ + { + fields: ['aggregation_uid'], + }, + ], + }); + + // 关联定义 + JiaoshiSubmission.associate = function() { + app.model.JiaoshiSubmission.belongsTo(app.model.Aggregation, { + foreignKey: 'aggregation_uid', + targetKey: 'aggregation_uid', + as: 'aggregation', + }); + }; + + return JiaoshiSubmission; +}; diff --git a/app/model/judgeQuestion.js b/app/model/judgeQuestion.js new file mode 100644 index 0000000..d86f74b --- /dev/null +++ b/app/model/judgeQuestion.js @@ -0,0 +1,62 @@ +'use strict'; + +/** + * 判断题模型 - JudgeQuestion + * + * 对应 judge_questions 表,存储所有组别的判断题。 + * 通过 group 字段区分中职组(zhongzhi)和高职组(gaozhi)。 + */ +module.exports = app => { + const { STRING, INTEGER, DATE, TEXT } = app.Sequelize; + + return app.model.define('JudgeQuestion', { + id: { + type: INTEGER, + primaryKey: true, + autoIncrement: true, + comment: '主键ID', + }, + group: { + type: STRING(20), + allowNull: false, + comment: '组别:zhongzhi/gaozhi', + }, + question: { + type: TEXT, + allowNull: false, + comment: '题目内容', + }, + answer: { + type: STRING(10), + allowNull: false, + comment: '正确答案:true 或 false', + }, + score: { + type: INTEGER, + allowNull: false, + defaultValue: 5, + comment: '分值', + }, + sort_order: { + type: INTEGER, + allowNull: false, + defaultValue: 0, + comment: '排序', + }, + created_at: { + type: DATE, + defaultValue: app.Sequelize.NOW, + comment: '创建时间', + }, + updated_at: { + type: DATE, + defaultValue: app.Sequelize.NOW, + comment: '更新时间', + }, + }, { + tableName: 'judge_questions', + timestamps: true, + createdAt: 'created_at', + updatedAt: 'updated_at', + }); +}; \ No newline at end of file diff --git a/app/model/news.js b/app/model/news.js new file mode 100644 index 0000000..20069aa --- /dev/null +++ b/app/model/news.js @@ -0,0 +1,117 @@ +'use strict'; + +/** + * 新闻模型 - News + * + * 存储新闻通知信息,支持置顶、微信公众号链接和按组别过滤显示。 + * 表名:news + */ + +module.exports = app => { + const { STRING, TEXT, BOOLEAN, DATE, INTEGER } = app.Sequelize; + + const News = app.model.define('news', { + // 主键 ID,自增 + id: { + type: INTEGER, + primaryKey: true, + autoIncrement: true, + comment: '新闻唯一标识', + }, + // 新闻标题 + title: { + type: STRING(200), + allowNull: false, + comment: '新闻标题', + }, + // 新闻正文内容(支持 HTML) + content: { + type: TEXT, + allowNull: true, + defaultValue: null, + comment: '新闻正文(微信链接型新闻可为空)', + }, + // 微信公众号文章链接 + wx_link: { + type: STRING(500), + allowNull: true, + defaultValue: null, + comment: '微信公众号文章链接', + }, + // 新闻类型:content(内容型)/ wx_link(微信链接型) + news_type: { + type: STRING(20), + allowNull: false, + defaultValue: 'content', + comment: '新闻类型:content=内容型, wx_link=微信链接型', + }, + // 发布时间 + publish_time: { + type: DATE, + allowNull: false, + comment: '发布时间', + }, + // 是否置顶 + pinned: { + type: BOOLEAN, + allowNull: false, + defaultValue: false, + comment: '是否置顶', + }, + // 排序序号 + sort_order: { + type: INTEGER, + allowNull: false, + defaultValue: 0, + comment: '排序序号,越小越靠前', + }, + // 目标可见组别 + group: { + type: STRING(20), + allowNull: false, + defaultValue: 'all', + comment: '目标可见组别', + }, + // 标签(逗号分隔) + tags: { + type: STRING(500), + allowNull: true, + defaultValue: null, + comment: '标签,多个标签用逗号分隔', + }, + // 记录创建时间 + created_at: { + type: DATE, + allowNull: false, + defaultValue: app.Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '创建时间', + }, + // 记录更新时间 + updated_at: { + type: DATE, + allowNull: false, + defaultValue: app.Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '更新时间', + }, + }, { + tableName: 'news', + timestamps: false, + underscored: true, + indexes: [ + { + fields: ['publish_time'], + }, + { + fields: ['pinned', 'publish_time'], + }, + { + fields: ['group'], + }, + { + fields: ['sort_order'], + }, + ], + }); + + return News; +}; diff --git a/app/model/systemConfig.js b/app/model/systemConfig.js new file mode 100644 index 0000000..0f9ac20 --- /dev/null +++ b/app/model/systemConfig.js @@ -0,0 +1,55 @@ +'use strict'; + +/** + * 系统配置模型 - SystemConfig + * + * 存储系统级别的配置项,以键值对形式存储。 + * 表名:system_configs + */ + +module.exports = app => { + const { STRING, TEXT, INTEGER, DATE } = app.Sequelize; + + const SystemConfig = app.model.define('system_config', { + // 主键 ID,自增 + id: { + type: INTEGER, + primaryKey: true, + autoIncrement: true, + comment: '配置唯一标识', + }, + // 配置键名,唯一 + config_key: { + type: STRING(100), + allowNull: false, + unique: true, + comment: '配置键名', + }, + // 配置值(文本存储,可存储 JSON 字符串) + config_value: { + type: TEXT, + allowNull: false, + comment: '配置值', + }, + // 记录创建时间 + created_at: { + type: DATE, + allowNull: false, + defaultValue: app.Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '创建时间', + }, + // 记录更新时间 + updated_at: { + type: DATE, + allowNull: false, + defaultValue: app.Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '更新时间', + }, + }, { + tableName: 'system_configs', + timestamps: false, + underscored: true, + }); + + return SystemConfig; +}; diff --git a/app/model/team.js b/app/model/team.js new file mode 100644 index 0000000..593e178 --- /dev/null +++ b/app/model/team.js @@ -0,0 +1,78 @@ +'use strict'; + +/** + * 参赛团队模型 - Team + * + * 存储参赛团队的基本信息,包括组别和团队编号等。 + * 表名:teams + */ + +module.exports = app => { + const { STRING, INTEGER, DATE } = app.Sequelize; + + const Team = app.model.define('team', { + // 主键(业务唯一标识),由应用层显式赋值,不依赖数据库自增。 + // 说明:运行库中 teams 表真实自增主键为 id,team_uid 仅是普通 INT 列(非自增)。 + // 此处声明 autoIncrement:false,使模型与运行库一致——create 时必须显式传入 team_uid, + // 避免 Sequelize 误以为 DB 会生成 team_uid 而省去该列(旧 bug:写入 NULL 导致 G1 团队锁失效)。 + team_uid: { + type: INTEGER, + primaryKey: true, + comment: '团队唯一标识(由应用层显式生成,非 DB 自增)', + }, + // 组别名称:zhongzhi/gaozhi/benke/jiaoshi + group_name: { + type: STRING(20), + allowNull: false, + comment: '参赛组别', + }, + // 团队 ID(外部编号) + team_id: { + type: STRING(50), + allowNull: false, + comment: '团队编号', + }, + // 团队序号(人类可读队伍编号,如 BRICS2026-ST-027-91001,含字母与连字符,必须为字符串) + team_number: { + type: STRING(50), + allowNull: true, + defaultValue: null, + comment: '团队序号(人类可读队伍编号,如 BRICS2026-ST-027-91001)', + }, + // 团队地址/地区 + address: { + type: STRING(200), + allowNull: true, + defaultValue: null, + comment: '团队地址', + }, + // 记录创建时间 + created_at: { + type: DATE, + allowNull: false, + defaultValue: app.Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '创建时间', + }, + // 记录更新时间 + updated_at: { + type: DATE, + allowNull: false, + defaultValue: app.Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '更新时间', + }, + }, { + tableName: 'teams', + timestamps: false, + underscored: true, + indexes: [ + { + fields: ['group_name'], + }, + { + fields: ['team_id'], + }, + ], + }); + + return Team; +}; diff --git a/app/model/wordQuestion.js b/app/model/wordQuestion.js new file mode 100644 index 0000000..4f487f2 --- /dev/null +++ b/app/model/wordQuestion.js @@ -0,0 +1,67 @@ +'use strict'; + +/** + * Word操作题模型 - WordQuestion + * + * 对应 word_questions 表,存储所有组别的 Word 操作题。 + * 通过 group 字段区分中职组(zhongzhi)和高职组(gaozhi)。 + */ +module.exports = app => { + const { STRING, INTEGER, DATE, TEXT } = app.Sequelize; + + return app.model.define('WordQuestion', { + id: { + type: INTEGER, + primaryKey: true, + autoIncrement: true, + comment: '主键ID', + }, + group: { + type: STRING(20), + allowNull: false, + comment: '组别:zhongzhi/gaozhi', + }, + title: { + type: STRING(200), + allowNull: false, + comment: '题目标题', + }, + description: { + type: TEXT, + allowNull: true, + comment: '题目描述/操作要求', + }, + score: { + type: INTEGER, + allowNull: false, + defaultValue: 30, + comment: '分值', + }, + sort_order: { + type: INTEGER, + allowNull: false, + defaultValue: 0, + comment: '排序', + }, + template_path: { + type: STRING(500), + allowNull: true, + comment: 'Word 模板文件路径', + }, + created_at: { + type: DATE, + defaultValue: app.Sequelize.NOW, + comment: '创建时间', + }, + updated_at: { + type: DATE, + defaultValue: app.Sequelize.NOW, + comment: '更新时间', + }, + }, { + tableName: 'word_questions', + timestamps: true, + createdAt: 'created_at', + updatedAt: 'updated_at', + }); +}; \ No newline at end of file diff --git a/app/model/workSubmission.js b/app/model/workSubmission.js new file mode 100644 index 0000000..5091f0f --- /dev/null +++ b/app/model/workSubmission.js @@ -0,0 +1,109 @@ +'use strict'; + +/** + * 作品提交模型 - WorkSubmission + * + * 存储选手上传的作品文件信息,包括文件类型、名称、路径等。 + * 支持 plan(计划书)和 ppt(演示文稿)两种文件类型。 + * 表名:work_submissions + */ + +module.exports = app => { + const { STRING, INTEGER, DATE } = app.Sequelize; + + const WorkSubmission = app.model.define('work_submission', { + // 主键 ID,自增 + id: { + type: INTEGER, + primaryKey: true, + autoIncrement: true, + comment: '作品提交唯一标识', + }, + // 选手 ID,外键关联 contestants 表 + contestant_id: { + type: INTEGER, + allowNull: false, + comment: '选手 ID', + references: { + model: 'contestants', + key: 'id', + }, + }, + // 文件类型:plan(计划书)/ ppt(演示文稿) + file_type: { + type: STRING(20), + allowNull: false, + comment: '文件类型', + validate: { + isIn: [['plan', 'ppt']], + }, + }, + // 文件名(原始文件名) + file_name: { + type: STRING(200), + allowNull: false, + comment: '原始文件名', + }, + // 文件存储路径(服务器上的相对路径) + file_path: { + type: STRING(500), + allowNull: false, + comment: '文件存储路径', + }, + // 文件大小(字节) + file_size: { + type: INTEGER, + allowNull: false, + comment: '文件大小(字节)', + }, + // 提交时间 + submit_time: { + type: DATE, + allowNull: false, + comment: '提交时间', + }, + // 得分(管理员评分后更新,NULL 表示尚未评分) + score: { + type: INTEGER, + allowNull: true, + defaultValue: null, + comment: '作品得分', + }, + // 创建时间 + created_at: { + type: DATE, + allowNull: false, + defaultValue: app.Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '创建时间', + }, + // 更新时间 + updated_at: { + type: DATE, + allowNull: false, + defaultValue: app.Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '更新时间', + }, + }, { + tableName: 'work_submissions', + timestamps: false, + underscored: true, + indexes: [ + { + fields: ['contestant_id'], + }, + { + fields: ['file_type'], + }, + ], + }); + + // 关联关系:一条作品提交记录属于一名选手 + WorkSubmission.associate = () => { + app.model.WorkSubmission.belongsTo(app.model.Contestant, { + foreignKey: 'contestant_id', + as: 'contestant', + }); + }; + + return WorkSubmission; +}; \ No newline at end of file diff --git a/app/model/zhongzhiQuestion.js b/app/model/zhongzhiQuestion.js new file mode 100644 index 0000000..ae8cce4 --- /dev/null +++ b/app/model/zhongzhiQuestion.js @@ -0,0 +1,89 @@ +'use strict'; + +/** + * 中职组题库模型 - ZhongzhiQuestion + * + * 存储中职组比赛题目,支持单选、多选、判断等多种题型。 + * 表名:zhongzhi_questions + */ + +module.exports = app => { + const { STRING, INTEGER, JSON, DATE } = app.Sequelize; + + const ZhongzhiQuestion = app.model.define('zhongzhi_question', { + // 主键,自增 + question_uid: { + type: INTEGER, + primaryKey: true, + autoIncrement: true, + comment: '题目唯一标识', + }, + // 题目内容 + question: { + type: STRING(1000), + allowNull: false, + comment: '题目内容', + }, + // 正确答案 + answer: { + type: STRING(500), + allowNull: false, + comment: '正确答案', + }, + // 题目类型:single(单选) / multiple(多选) / judge(判断) + question_type: { + type: STRING(20), + allowNull: false, + comment: '题目类型', + }, + // 选项(JSON 格式,如 ["A. xxx", "B. xxx", "C. xxx", "D. xxx"]) + options: { + type: JSON, + allowNull: true, + defaultValue: null, + comment: '选项', + }, + // 题目分值 + score: { + type: INTEGER, + allowNull: false, + defaultValue: 0, + comment: '题目分值', + }, + // 排序序号 + sort_order: { + type: INTEGER, + allowNull: false, + defaultValue: 0, + comment: '排序序号', + }, + // 记录创建时间 + created_at: { + type: DATE, + allowNull: false, + defaultValue: app.Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '创建时间', + }, + // 记录更新时间 + updated_at: { + type: DATE, + allowNull: false, + defaultValue: app.Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '更新时间', + }, + }, { + tableName: 'zhongzhi_questions', + timestamps: false, + underscored: true, + indexes: [ + { + fields: ['question_type'], + }, + { + fields: ['sort_order'], + }, + ], + }); + + return ZhongzhiQuestion; +}; diff --git a/app/model/zhongzhiSubmission.js b/app/model/zhongzhiSubmission.js new file mode 100644 index 0000000..bcff88f --- /dev/null +++ b/app/model/zhongzhiSubmission.js @@ -0,0 +1,133 @@ +'use strict'; + +/** + * 中职组答题记录模型 - ZhongzhiSubmission + * + * 存储中职组参赛选手的答题记录,包括答题数据、判分和Word文件上传。 + * 表名:zhongzhi_submissions + */ + +module.exports = app => { + const { STRING, INTEGER, TEXT, BOOLEAN, DATE } = app.Sequelize; + + const ZhongzhiSubmission = app.model.define('zhongzhi_submission', { + // 主键 ID,自增 + id: { + type: INTEGER, + primaryKey: true, + autoIncrement: true, + comment: '提交记录唯一标识', + }, + // 外键:关联聚合报名 ID + aggregation_uid: { + type: INTEGER, + allowNull: false, + comment: '关联聚合报名 ID', + }, + // 外键:关联题目 ID + question_uid: { + type: INTEGER, + allowNull: false, + comment: '关联题目 ID', + }, + // 题型(choice / judge):与 question_uid 联合唯一标识一道题。 + // 选择题与判断题的 id 在不同表中可能数值重叠(如 choice#5 与 judge#5), + // 仅凭 question_uid 无法区分,必须结合 question_type 存储,否则会折叠丢失答案(见 B11)。 + question_type: { + type: STRING(16), + allowNull: false, + defaultValue: '', + comment: '题型:choice/judge,与 question_uid 联合区分题目', + }, + // 答题数据(选手提交的答案) + answer_data: { + type: TEXT, + allowNull: true, + defaultValue: null, + comment: '答题数据', + }, + // 是否判为正确 + is_correct: { + type: BOOLEAN, + allowNull: true, + defaultValue: null, + comment: '是否判为正确', + }, + // 得分 + score: { + type: INTEGER, + allowNull: true, + defaultValue: null, + comment: '得分', + }, + // Word 文件存储路径 + word_file_path: { + type: STRING(500), + allowNull: true, + defaultValue: null, + comment: 'Word 文件存储路径', + }, + // Word 文件原始名称 + word_file_name: { + type: STRING(200), + allowNull: true, + defaultValue: null, + comment: 'Word 文件原始名称', + }, + // Word 文件 MD5 + word_file_md5: { + type: STRING(32), + allowNull: true, + defaultValue: null, + comment: 'Word 文件 MD5', + }, + // 记录创建时间 + created_at: { + type: DATE, + allowNull: false, + defaultValue: app.Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '创建时间', + }, + // 记录更新时间 + updated_at: { + type: DATE, + allowNull: false, + defaultValue: app.Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '更新时间', + }, + }, { + tableName: 'zhongzhi_submissions', + timestamps: false, + underscored: true, + indexes: [ + { + fields: ['aggregation_uid'], + }, + { + fields: ['question_uid'], + }, + { + // 唯一约束:同一选手同一题(题型+题号)只保留一行,杜绝并发作答重复落库(B12)。 + unique: true, + name: 'uk_zhongzhi_submission', + fields: ['aggregation_uid', 'question_uid', 'question_type'], + }, + ], + }); + + // 关联定义 + ZhongzhiSubmission.associate = function() { + app.model.ZhongzhiSubmission.belongsTo(app.model.Aggregation, { + foreignKey: 'aggregation_uid', + targetKey: 'aggregation_uid', + as: 'aggregation', + }); + app.model.ZhongzhiSubmission.belongsTo(app.model.ZhongzhiQuestion, { + foreignKey: 'question_uid', + targetKey: 'question_uid', + as: 'zhongzhiQuestion', + }); + }; + + return ZhongzhiSubmission; +}; diff --git a/app/public/templates/.gitkeep b/app/public/templates/.gitkeep new file mode 100644 index 0000000..e02abfc --- /dev/null +++ b/app/public/templates/.gitkeep @@ -0,0 +1 @@ + diff --git a/app/public/templates/gaozhi/word_template_1785117066172.docx b/app/public/templates/gaozhi/word_template_1785117066172.docx new file mode 100644 index 0000000000000000000000000000000000000000..2604a97bad6d361bcf9437ee629b3328e40ad2fe GIT binary patch literal 921 zcmWIWW@Zs#U|`^2NcQ_3Q<-Oc{RNP>mXU!$7)VDu=jWBA=9R>UR2HNb$Ldw&=F|q9 z^gC=I@K-c@okrn<27#{2%%=q{rtnMLoP1=JdF7o;|7`uDs~lP%pNanZ>*_u0jf>Bi zv9WH?Wp8TgJJ#lT@cFN;l?(@2n;pfOH7x`VXT7-I?xM5GY(vw|DPJ^HU(d8Dk``|5 z>M^X=`Fduu&kA#kiF;n}s$Q9_A>tJ*#xL`EuwtI|2~>lbmd5l`1f^t736Y%ywvY6=D#0+5-_v>n!j2L3=mCVz;FZU z_@dODVtqXj*?Zoa?~nlx+k@3E_cEi{S^_TXajewVV&5QSzBWV7>(Juu>defpJU!d$ z>!-c`mYG(zTtGdYHN=&(XH7}grlK6%Ng%SW2! zh!`w;!av8|a?-&F3x6|zHgTS1F>GgTPibd8y1nVn#}AnYZl%l>60izod@b((|3;pK zny0&Zep}A3!bjQ@Q+Z}hYG~)~T3x>TgwBU$&sK}i7Dx3}SfxGV8lZ1p1F;|wm**Fy z=%?f-m*xWF92D8EeY-(^bFEiBPNs;ZFyC?44?#UvZrct)SPU_(vMK5kI-Iy{@Zi1^=Le?}t zzcUL1wPtee@e^e|FvnJ->TG52;wX(<0w*23ZoW@1UGaMHr&|kkPFdgFdL}@Cdx!ZG z<_(Wy{O^3)o$w*?8c&kCoA3OSTUXr?J=UA}`25m;w!ifQycwB97;vX$p#Q+20T_i? yQ#QH*=y49w&cM*XCsysM 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 literal 0 HcmV?d00001 diff --git a/app/public/templates/gaozhi/word_template_1785207663577.docx b/app/public/templates/gaozhi/word_template_1785207663577.docx new file mode 100644 index 0000000000000000000000000000000000000000..2bb8928d445c0feff63f23cbab762721afa71ecf GIT binary patch literal 17747 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 literal 0 HcmV?d00001 diff --git a/app/public/templates/zhongzhi/word_template_1785117066098.docx b/app/public/templates/zhongzhi/word_template_1785117066098.docx new file mode 100644 index 0000000000000000000000000000000000000000..0f105d7bfef46098e34f22a10f903f729af272e1 GIT binary patch literal 922 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 literal 0 HcmV?d00001 diff --git a/app/router.js b/app/router.js new file mode 100644 index 0000000..7015268 --- /dev/null +++ b/app/router.js @@ -0,0 +1,158 @@ +'use strict'; + +/** + * 路由配置 - Router + * + * 通信与导航应用技术创新赛 - 线上选拔赛网站后端路由定义 + * + * 路由分组说明: + * - /api/auth/* : 认证相关(登录、激活、验证码等) + * - /api/news/* : 新闻通知(公开访问) + * - /api/carousel : 轮播图(公开访问) + * - /api/materials/* : 资料下载(需认证) + * - /api/exam/* : 比赛答题(需认证) + * - /api/upload/* : 文件上传(需认证) + * - /api/admin/* : 管理后台(需管理员认证) + * - /api/score/* : 成绩查询(需认证) + */ + +module.exports = app => { + const { router, controller, middleware } = app; + + // 获取 JWT 认证中间件实例 + const auth = middleware.auth(); + // 管理员认证中间件(需要 admin 角色) + const adminAuth = middleware.auth({ adminOnly: true }); + // 登录限流中间件(防暴力破解):15 分钟内同一 IP+账号最多 10 次失败 + const loginRateLimit = middleware.loginRateLimit({ windowMs: 15 * 60 * 1000, maxAttempts: 10 }); + + // ==================== 公开接口(无需认证) ==================== + + // 健康检查(容器探针 / 负载均衡探活,无需认证) + router.get('/api/health', controller.health.check); + + // 认证相关 - 公开接口 + router.post('/api/auth/captcha', controller.auth.captcha); + router.post('/api/auth/sms', controller.auth.sms); + router.post('/api/auth/activate', controller.auth.activate); + router.post('/api/auth/login', loginRateLimit, controller.auth.login); + router.post('/api/auth/admin-login', loginRateLimit, controller.auth.adminLogin); + router.post('/api/auth/forgot-password', controller.auth.forgotPassword); + router.post('/api/auth/reset-password', controller.auth.resetPassword); + + // ==================== 选手认证接口(需要 JWT,不需要 admin 角色) ==================== + + // 认证相关 - 受保护接口 + router.get('/api/auth/current-user', auth, controller.auth.currentUser); + router.post('/api/auth/logout', auth, controller.auth.logout); + // 静默刷新令牌(不挂 auth 中间件,因为携带的 token 可能已过期) + router.post('/api/auth/refresh', controller.auth.refresh); + + // ==================== 公开内容接口 ==================== + + // 新闻通知 + router.get('/api/news', controller.news.index); + router.get('/api/news/counts', controller.news.counts); + router.get('/api/news/:id', controller.news.show); + + // 轮播图 + router.get('/api/carousel', controller.carousel.index); + + // ==================== 资料下载(需认证) ==================== + + router.get('/api/materials', auth, controller.materials.index); + router.get('/api/materials/:group/:fileName/download', auth, controller.materials.download); + + // ==================== 比赛答题(需认证) ==================== + + router.get('/api/exam/:group/status', auth, controller.exam.status); + router.get('/api/exam/:group/questions', auth, controller.exam.questions); + router.get('/api/exam/:group/answers', auth, controller.exam.getAnswers); + router.get('/api/exam/:group/word-template', controller.exam.wordTemplate); + router.post('/api/exam/:group/submit-answers', auth, controller.exam.submitAnswers); + router.post('/api/exam/:group/upload-word', auth, controller.exam.uploadWord); + router.delete('/api/exam/:group/word', auth, controller.exam.deleteWord); + router.post('/api/exam/:group/submit-all', auth, controller.exam.submitAll); + + // ==================== 文件上传(需认证) ==================== + + router.post('/api/upload/:group', auth, controller.upload.upload); + router.get('/api/upload/:group/status', auth, controller.upload.status); + router.get('/api/upload/:group/submissions', auth, controller.upload.submissions); + router.delete('/api/upload/:group/:id', auth, controller.upload.delete); + + // 分片上传(需认证) + router.post('/api/upload/:group/chunk', auth, controller.upload.uploadChunk); + router.post('/api/upload/:group/merge', auth, controller.upload.mergeChunks); + router.get('/api/upload/:group/chunk-status', auth, controller.upload.chunkStatus); + + // ==================== 管理后台(需管理员认证) ==================== + + // 数据统计 + router.get('/api/admin/stats', adminAuth, controller.admin.stats); + + // 账号管理 + router.get('/api/admin/accounts', adminAuth, controller.admin.accounts); + router.post('/api/admin/import-excel', adminAuth, controller.admin.importExcel); + + // 提交管理 + router.get('/api/admin/submissions/:group', adminAuth, controller.admin.submissions); + router.get('/api/admin/submission-file/:group/:id', adminAuth, controller.admin.downloadSubmissionFile); + router.get('/api/admin/export-csv/:group', adminAuth, controller.admin.exportCsv); + router.get('/api/admin/batch-download/:group', adminAuth, controller.admin.batchDownload); + router.get('/api/admin/works/:group', adminAuth, controller.admin.works); + + // 时间设置 + router.get('/api/admin/times', adminAuth, controller.admin.getTimes); + router.put('/api/admin/times', adminAuth, controller.admin.setTimes); + + // 分数管理 + router.get('/api/admin/scores/:group', adminAuth, controller.admin.getScores); + router.put('/api/admin/scores/:group', adminAuth, controller.admin.setScores); + + // 试题管理 + router.get('/api/admin/exam/:group/questions', adminAuth, controller.admin.getQuestions); + router.put('/api/admin/exam/:group/questions', adminAuth, controller.admin.setQuestions); + router.post('/api/admin/exam/:group/questions/import-excel', adminAuth, controller.admin.importQuestionsExcel); + router.get('/api/admin/exam/:group/questions/template', adminAuth, controller.admin.downloadQuestionTemplate); + router.put('/api/admin/exam/:group/word-template', adminAuth, controller.admin.uploadWordTemplate); + router.get('/api/admin/exam/:group/word-template', adminAuth, controller.admin.getWordTemplate); + router.delete('/api/admin/exam/:group/word-template', adminAuth, controller.admin.deleteWordTemplate); + + // 备赛资料管理 + router.get('/api/admin/materials/:group', adminAuth, controller.admin.getMaterials); + router.post('/api/admin/materials/:group', adminAuth, controller.admin.uploadMaterial); + router.delete('/api/admin/materials/:group/:fileName', adminAuth, controller.admin.deleteMaterial); + + // 成绩发布 + router.put('/api/admin/publish-scores', adminAuth, controller.admin.publishScores); + router.get('/api/admin/publish-scores', adminAuth, controller.admin.getPublishStatus); + + // 新闻管理 + router.get('/api/admin/news', adminAuth, controller.admin.listNews); + router.post('/api/admin/news', adminAuth, controller.admin.createNews); + router.put('/api/admin/news/order', adminAuth, controller.admin.updateNewsOrder); + router.put('/api/admin/news/:id', adminAuth, controller.admin.updateNews); + router.delete('/api/admin/news/:id', adminAuth, controller.admin.deleteNews); + + // 轮播图管理 + router.get('/api/admin/carousel', adminAuth, controller.admin.listCarousel); + router.post('/api/admin/carousel', adminAuth, controller.admin.addCarousel); + router.put('/api/admin/carousel/:id', adminAuth, controller.admin.updateCarousel); + router.delete('/api/admin/carousel/:id', adminAuth, controller.admin.deleteCarousel); + router.put('/api/admin/carousel/order', adminAuth, controller.admin.updateCarouselOrder); + // 图片上传(轮播图等) + router.post('/api/admin/upload/image', adminAuth, controller.admin.uploadImage); + router.post('/api/admin/upload/news-image', adminAuth, controller.admin.uploadNewsImage); + + // 下载考生答案 Word 文件 + router.get('/api/admin/submissions/:group/:id/word', adminAuth, controller.admin.downloadWord); + + // 测试数据标记与重置 + router.post('/api/admin/toggle-test-flag', adminAuth, controller.admin.toggleTestFlag); + router.post('/api/admin/reset-test-data', adminAuth, controller.admin.resetTestData); + + // ==================== 成绩查询(需认证) ==================== + + router.get('/api/score', auth, controller.score.index); +}; diff --git a/app/service/admin.js b/app/service/admin.js new file mode 100644 index 0000000..86ae9a6 --- /dev/null +++ b/app/service/admin.js @@ -0,0 +1,1912 @@ +'use strict'; + +const Service = require('egg').Service; +const path = require('path'); +const fs = require('fs'); +const XLSX = require('xlsx'); + +/** + * 管理后台服务 - AdminService + * + * 封装管理后台相关的数据库操作,包括数据统计、账号管理、Excel 导入、 + * 提交记录管理、CSV 导出、批量下载 ZIP、时间/评分/题目管理等。 + */ +class AdminService extends Service { + // ==================== 数据统计 ==================== + + /** + * 获取数据总览统计 + * 统计各组的账号数、提交数、总分、激活数 + * + * @returns {Promise} 综合统计数据 + */ + async getStats(options = {}) { + const { ctx } = this; + const groups = ['zhongzhi', 'gaozhi', 'benke', 'jiaoshi']; + + // excludeTest=true 时排除测试数据(test_flag=1),仅统计生产作用域 + const excludeTest = options && options.excludeTest === true; + const testWhere = excludeTest ? { test_flag: 0 } : {}; + + // 直接从 contestants 表统计 + const totalAccounts = await ctx.model.Contestant.count({ where: testWhere }); + const activatedCount = await ctx.model.Contestant.count({ + where: { ...testWhere, activated: true }, + }); + + const groupStats = {}; + for (const group of groups) { + const baseWhere = { ...testWhere, group }; + const totalCount = await ctx.model.Contestant.count({ where: baseWhere }); + const activatedGroupCount = await ctx.model.Contestant.count({ + where: { ...baseWhere, activated: true }, + }); + + // 提交数:中职/高职查 exam_submissions,本科/教师查 work_submissions + let submittedCount = 0; + let totalScore = 0; + const contestants = await ctx.model.Contestant.findAll({ + where: baseWhere, + attributes: ['id'], + }); + const contestantIds = contestants.map(c => c.id); + if (contestantIds.length > 0) { + const SubmissionModel = (group === 'zhongzhi' || group === 'gaozhi') + ? ctx.model.ExamSubmission + : ctx.model.WorkSubmission; + const submissions = await SubmissionModel.findAll({ + where: { contestant_id: contestantIds }, + attributes: ['contestant_id', 'score'], + raw: true, + }); + const uniqueIds = new Set(submissions.map(s => s.contestant_id)); + submittedCount = uniqueIds.size; + totalScore = submissions.reduce((sum, s) => sum + (s.score || 0), 0); + } + + groupStats[group] = { + totalAccounts: totalCount, + activatedAccounts: activatedGroupCount, + submittedCount, + totalScore, + submissionRate: totalCount > 0 + ? ((submittedCount / totalCount) * 100).toFixed(1) + : '0', + }; + } + + // 计算总提交数 + let totalSubmissions = 0; + for (const group of groups) { + totalSubmissions += groupStats[group].submittedCount; + } + + return { + totalAccounts, + activatedCount, + submissionsCount: totalSubmissions, + activationRate: totalAccounts > 0 + ? ((activatedCount / totalAccounts) * 100).toFixed(1) + : '0', + groups: groupStats, + testIncluded: !excludeTest, + }; + } + + // ==================== 账号管理 ==================== + + /** + * 获取分页账号列表 + * @param {Object} options - 查询选项 { group, keyword, page, pageSize, startDate, endDate } + * @returns {Promise} { list, total, page, pageSize } + */ + async getAccounts(options = {}) { + const { ctx } = this; + const { group, keyword, startDate, endDate } = options; + const page = parseInt(options.page, 10) || 1; + const pageSize = parseInt(options.pageSize, 10) || 20; + const offset = (page - 1) * pageSize; + + // 构建查询条件 + const where = {}; + if (group) { + where.group = group; + } + if (keyword) { + where[ctx.model.Sequelize.Op.or] = [ + { name: { [ctx.model.Sequelize.Op.like]: `%${keyword}%` } }, + { id_number: { [ctx.model.Sequelize.Op.like]: `%${keyword}%` } }, + { phone: { [ctx.model.Sequelize.Op.like]: `%${keyword}%` } }, + ]; + } + if (startDate && endDate) { + where.created_at = { + [ctx.model.Sequelize.Op.gte]: new Date(startDate + 'T00:00:00'), + [ctx.model.Sequelize.Op.lte]: new Date(endDate + 'T23:59:59'), + }; + } else if (startDate) { + where.created_at = { [ctx.model.Sequelize.Op.gte]: new Date(startDate + 'T00:00:00') }; + } else if (endDate) { + where.created_at = { [ctx.model.Sequelize.Op.lte]: new Date(endDate + 'T23:59:59') }; + } + + const { rows, count } = await ctx.model.Contestant.findAndCountAll({ + where, + attributes: { exclude: ['password'] }, + order: [['id', 'DESC']], + offset, + limit: pageSize, + }); + + // 转换为前端期望的字段格式 + const list = rows.map(row => ({ + uid: row.id, + name: row.name, + name_cn: row.name, + idNumber: row.id_number, + id_number: row.id_number, + phone: row.phone, + group: row.group, + activated: row.activated, + score: row.score, + test_flag: row.test_flag, + createdAt: row.created_at, + created_at: row.created_at, + })); + + return { + list, + total: count, + page: parseInt(page), + pageSize: parseInt(pageSize), + }; + } + + // ==================== Excel 导入 ==================== + + /** + * 从 Excel 文件导入账号数据 + * @param {string} filePath - Excel 文件路径 + * @returns {Promise} { imported, skipped, errors } + */ + async importFromExcel(filePath, options = {}) { + const { ctx } = this; + + // 组别归一化:兼容中文全称(中职组/高职组/本科组/教师组)与代码(zhongzhi 等), + // 避免用户直接用原始花名册(中文组别)导入时全部被 contestants.group 的 isIn 校验拒绝、 + // 静默回滚成 imported=0 却仍返回 HTTP 200「成功」的假成功。 + const GROUP_ALIAS = { + '中职': 'zhongzhi', '中职组': 'zhongzhi', '中職': 'zhongzhi', '中職組': 'zhongzhi', + '高职': 'gaozhi', '高职组': 'gaozhi', '高職': 'gaozhi', '高職組': 'gaozhi', + '本科': 'benke', '本科组': 'benke', '本科組': 'benke', + '教师': 'jiaoshi', '教师组': 'jiaoshi', '教師': 'jiaoshi', '教師組': 'jiaoshi', + }; + const normalizeGroup = (g) => { + if (!g) return ''; + const s = String(g).trim(); + return GROUP_ALIAS[s] || s; + }; + + // 是否将本批次标记为测试数据(test_flag=1)。 + // 优先使用显式入参;未传时回退到运行模式:测试环境导入默认归类为测试数据, + // 生产环境导入默认归类为生产数据(差异最小化,调用方无需区分环境)。 + const importAsTest = (options && typeof options.testFlag === 'boolean') + ? options.testFlag + : ctx.app.config.isTest; + // 测试数据统一使用「默认口令」的 bcrypt 哈希,便于重置/导入后直接登录重测。 + let defaultTestHash = null; + if (importAsTest) { + defaultTestHash = await ctx.helper.hashPassword(ctx.app.config.testDefaultPassword); + } + + // 读取 Excel 文件 + const workbook = XLSX.readFile(filePath); + const sheetName = workbook.SheetNames[0]; + const sheet = workbook.Sheets[sheetName]; + + // 转换为 JSON 数组 + const rows = XLSX.utils.sheet_to_json(sheet); + + const result = { + imported: 0, + skipped: 0, + teamsCreated: 0, + errors: [], + }; + + // 取一个可用的竞赛 ID 用于聚合报名记录(aggregation.competition_uid 非空约束) + let competitionUid = 1; + try { + const comp = await ctx.model.CompetitionInfo.findOne({ + attributes: ['competition_uid'], + order: [['competition_uid', 'DESC']], + }); + if (comp && comp.competition_uid) competitionUid = comp.competition_uid; + } catch (e) { + ctx.logger.warn('[import] 获取竞赛 ID 失败,使用默认 1:', e.message); + } + + // 第一遍:解析并预建/复用团队(在任意行事务之外,确保已提交、全局可见)。 + // 团队归属是整数 team_uid,是 G1 团队考试锁在生产数据下生效的前提; + // 必须在行事务开启前完成,否则 Egg 的 CLS 会把团队查/建绑到当前行事务快照, + // 导致同一导入的后续行读不到上一行已提交的团队,把同一队伍拆成多个团队。 + // 注意:teams.team_uid 在模型/迁移中已声明为「由应用层显式赋值、非 DB 自增」 + // (运行库真实自增主键为 id,team_uid 为普通 INT 列)。故此处显式生成唯一 team_uid, + // 不依赖数据库自增,保证 contestants.team_id 与 teams.team_uid 一致、G1 锁可靠。 + const teamUidCache = {}; + let maxTeamUid = 0; + try { + const row = await ctx.model.Team.findOne({ + attributes: [[ctx.model.Sequelize.fn('MAX', ctx.model.Sequelize.col('team_uid')), 'm']], + }); + if (row && row.get('m')) maxTeamUid = parseInt(row.get('m'), 10) || 0; + } catch (e) { /* ignore */ } + for (let i = 0; i < rows.length; i++) { + const row = rows[i]; + const teamIdRaw = String(row['队伍编号'] || row['team_id'] || row['team_number'] || '').trim(); + const group = normalizeGroup(row['组别'] || row['group']); + if (!teamIdRaw) continue; + const cacheKey = teamIdRaw + '|' + group; + if (teamUidCache[cacheKey] !== undefined) continue; + let team = await ctx.model.Team.findOne({ where: { team_id: teamIdRaw, group_name: group } }); + // 健壮性:命中 team_uid 为空的损坏占位行时,销毁并重建,避免脏数据污染 + if (team && !team.team_uid) { + try { await team.destroy(); } catch (e2) { /* ignore */ } + team = null; + } + if (!team) { + maxTeamUid += 1; + team = await ctx.model.Team.create({ + team_uid: maxTeamUid, + group_name: group, + team_id: teamIdRaw, + team_number: teamIdRaw, + }); + result.teamsCreated++; + } + teamUidCache[cacheKey] = maxTeamUid; + } + + for (let i = 0; i < rows.length; i++) { + const row = rows[i]; + const rowIndex = i + 2; + + // 整行事务:队伍 + 选手 + 聚合记录要么一起成功,要么整体回滚 + const t = await ctx.model.transaction(); + try { + const idNumber = String(row['身份证号'] || row['id_number'] || '').trim(); + const name = String(row['姓名'] || row['name'] || '').trim(); + const phone = String(row['手机号'] || row['phone'] || '').trim(); + const group = normalizeGroup(row['组别'] || row['group']); + // 可选:队伍编号(人类可读,如 BENKE-01)。不填 = 单人参赛(不建团队、不触发团队锁) + const teamIdRaw = String(row['队伍编号'] || row['team_id'] || row['team_number'] || '').trim(); + // 可选:身份(队长/队员),默认队员 + const identity = String(row['身份'] || row['identity'] || '').trim() || '队员'; + + if (!idNumber) { + result.errors.push({ row: rowIndex, message: '身份证号为空' }); + result.skipped++; + await t.rollback(); + continue; + } + if (!name) { + result.errors.push({ row: rowIndex, message: '姓名为空' }); + result.skipped++; + await t.rollback(); + continue; + } + + // 检查身份证号是否已存在 + const existing = await ctx.model.Contestant.findOne({ + where: { id_number: idNumber }, + transaction: t, + }); + if (existing) { + result.errors.push({ row: rowIndex, message: `身份证号 ${idNumber} 已存在` }); + result.skipped++; + await t.rollback(); + continue; + } + + // 解析团队归属(如有):取自第一遍预建的缓存,确保同队同 team_uid(G1 锁前提) + let teamUid = null; + if (teamIdRaw) { + teamUid = teamUidCache[teamIdRaw + '|' + group]; + } + + // 生成密码:测试数据统一使用「默认口令」的 bcrypt 哈希;生产数据沿用 + // 「身份证后6位 SHA256」派生(保持原行为,不暴露统一弱口令)。 + const password = importAsTest + ? defaultTestHash + : ctx.helper.sha256(idNumber.substring(idNumber.length - 6)); + + // 创建选手记录(team_id 为团队整数 uid;单人参赛则为 NULL) + const contestant = await ctx.model.Contestant.create({ + id_number: idNumber, + name, + phone: phone || null, + group: group || 'zhongzhi', + password, + activated: false, + score: 0, + team_id: teamUid, + test_flag: importAsTest ? 1 : 0, + }, { transaction: t }); + + // 建立聚合报名记录(与生产上传/作品流程对齐:benke/jiaoshi 作品上传依赖 aggregation)。 + // 始终为每位导入选手建立(team_uid 可空:单人参赛也能建聚合,避免作品上传 403)。 + const aggExists = await ctx.model.Aggregation.findOne({ + where: { account_uid: contestant.id }, + transaction: t, + }); + if (!aggExists) { + await ctx.model.Aggregation.create({ + competition_uid: competitionUid, + team_uid: teamUid, + account_uid: contestant.id, + identity, + is_registrant: true, + }, { transaction: t }); + } + + await t.commit(); + result.imported++; + } catch (err) { + await t.rollback(); + ctx.logger.error(`导入第 ${rowIndex} 行失败:`, err); + result.errors.push({ row: rowIndex, message: err.message }); + result.skipped++; + } + } + + return result; + } + + // ==================== 提交记录管理 ==================== + + /** + * 获取考试答题记录(查询组别对应的提交表) + * @param {string} group - 组别 + * @returns {Promise} 答题记录 + */ + async getExamSubmissions(group) { + const { ctx } = this; + + const SubmissionModel = group === 'zhongzhi' + ? ctx.model.ZhongzhiSubmission + : ctx.model.GaozhiSubmission; + + const contestants = await ctx.model.Contestant.findAll({ + where: { group }, + attributes: ['id', 'name', 'id_number', 'phone', 'group', 'score', 'exam_score', 'word_score'], + order: [['id', 'ASC']], + }); + + // 建立 contestant.id -> 真实 aggregation_uid 映射。 + // 旧代码曾把聚合报名 uid 直接写进 zhongzhi/gaozhi_submissions 的 aggregation_uid 列, + // 而当前 _getUserAggregation 返回 aggregation_uid = contestant.id;两种写法并存于历史数据。 + // 若仅按 contestant.id 归集,历史提交者的答题数/提交状态会因聚合键错位而恒为 0 / 未提交。 + // 故按「contestant.id」与「真实 aggregation_uid」两种 key 同时归集,消除错位。 + const aggRows = await ctx.model.Aggregation.findAll({ + where: { account_uid: contestants.map(c => c.id) }, + attributes: ['account_uid', 'aggregation_uid'], + raw: true, + }); + const realAggByContestant = {}; + aggRows.forEach(a => { realAggByContestant[a.account_uid] = a.aggregation_uid; }); + + // 获取所有提交记录 + const allSubmissions = await SubmissionModel.findAll({ + order: [['created_at', 'ASC']], + }); + + // 按 contestant.id 分组提交记录 + const submissionsByUser = {}; + allSubmissions.forEach(s => { + const uid = String(s.aggregation_uid); + if (!submissionsByUser[uid]) submissionsByUser[uid] = []; + submissionsByUser[uid].push(s); + }); + + // 中职/高职的 Word 操作文件由 uploadWord 写入 exam_submissions.word_file_path, + // 需单独查询以补全 hasWordFile / wordFilePath(逐题作答表 zhongzhi_submissions/ + // gaozhi_submissions 本身不存储 Word 文件信息)。 + const examSubs = await ctx.model.ExamSubmission.findAll({ + where: { contestant_id: contestants.map(c => c.id) }, + attributes: ['contestant_id', 'word_file_path', 'word_file_name', 'submit_time'], + raw: true, + }); + const wordMap = {}; + examSubs.forEach(w => { wordMap[String(w.contestant_id)] = w; }); + + return contestants.map(c => { + // 兼容新旧两种聚合键:同时归集 contestant.id 与真实 aggregation_uid 下的答题记录 + const candidateKeys = [String(c.id), String(realAggByContestant[c.id])].filter(Boolean); + const subs = candidateKeys.flatMap(k => submissionsByUser[k] || []); + const answeredCount = subs.length; + const autoScore = subs.reduce((sum, s) => sum + (s.score || 0), 0); + const wordSub = wordMap[String(c.id)]; + const hasWord = !!(wordSub && wordSub.word_file_path); + // 正式提交状态:优先以 exam_submissions.submit_time 为准(与选手端 hasSubmitted 同一判定源); + // 历史数据/旧流程未回填 submit_time,但已有逐题答题记录(answeredCount>0)同样视为已提交, + // 与 CSV 导出(answeredCount>0 -> 是)保持一致,避免「试卷管理恒显未提交」的错位。 + const formalSubmitTime = (wordSub && wordSub.submit_time) || null; + return { + contestantId: c.id, + aggregationUid: String(c.id), + userId: String(c.id), + userName: c.name || '', + idNumber: c.id_number, + phone: c.phone, + group: c.group, + answeredCount, + totalQuestions: answeredCount, + autoScore, + examScore: c.exam_score ?? 0, + wordScore: c.word_score ?? 0, + // 总分 = 客观题自动评分合计 + Word 阅卷分(与卷面分/Word 分口径一致); + // 不读 contestants.score(未人工评分时恒为 0,会导致"客观题有分但总分=0")。 + totalScore: autoScore + (c.word_score ?? 0), + hasWordFile: hasWord, + wordFilePath: (wordSub && wordSub.word_file_path) || null, + wordFileName: (wordSub && wordSub.word_file_name) || null, + submitted: !!formalSubmitTime || answeredCount > 0, + submitTime: formalSubmitTime || (subs.length > 0 ? subs[0].created_at : null), + // 卷面分(客观分)统一取客观题自动评分合计,与成绩管理表口径一致 + examScore: autoScore, + }; + }); + } + + /** + * 获取提交记录(作品提交等) + * @param {string} group - 组别 (zhongzhi/gaozhi/benke/jiaoshi) + * @param {Object} options - 查询选项 + */ + async getSubmissions(group, options = {}) { + const { ctx } = this; + const SubmissionModel = group === 'benke' + ? ctx.model.BenkeSubmission + : ctx.model.JiaoshiSubmission; + + // 获取该组所有选手(含成绩分项) + const contestants = await ctx.model.Contestant.findAll({ + where: { group }, + attributes: ['id', 'name', 'id_number', 'phone', 'group', 'score', 'exam_score', 'word_score', 'ppt_score'], + order: [['id', 'ASC']], + }); + + // 获取所有提交记录(benke_submissions.aggregation_uid 存的是"聚合报名ID", + // 与 contestants.id 并非同一序列,需经 aggregation 表 account_uid 映射) + const allSubmissions = await SubmissionModel.findAll({ + order: [['upload_time', 'DESC']], + }); + + const submissionMap = {}; + allSubmissions.forEach(s => { + const key = `${s.aggregation_uid}__${s.file_type || 'word'}`; + if (!submissionMap[key]) submissionMap[key] = s; + }); + + // 建立 contestant.id -> aggregation_uid 映射(修复"作品管理看不到上传"的错位) + const aggRows = await ctx.model.Aggregation.findAll({ + where: { account_uid: contestants.map(c => c.id) }, + attributes: ['account_uid', 'aggregation_uid'], + raw: true, + }); + const aggMap = {}; + aggRows.forEach(a => { aggMap[a.account_uid] = a.aggregation_uid; }); + + const toItem = (s) => s ? { + id: s.id, + file_name: s.file_name, + file_size: s.file_size, + file_path: s.file_path, + file_md5: s.file_md5, + upload_time: s.upload_time, + score: s.score, + file_type: s.file_type || 'word', + } : null; + + return contestants.map(c => { + const aggUid = aggMap[c.id] || c.id; + const wordSub = submissionMap[`${aggUid}__word`] || null; + const pptSub = submissionMap[`${aggUid}__ppt`] || null; + const sub = wordSub || pptSub; + return { + contestantId: c.id, + account: { + uid: c.id, + name_cn: c.name, + id_number: c.id_number, + phone: c.phone, + organization_cn: '', + score: c.score, + exam_score: c.exam_score, + word_score: c.word_score, + ppt_score: c.ppt_score, + }, + team: { team_id: '', team_number: '', group_name: c.group }, + identity: '选手', + submitted: !!sub, + wordSubmission: toItem(wordSub), + pptSubmission: toItem(pptSub), + workSubmissions: [toItem(wordSub), toItem(pptSub)].filter(Boolean), + }; + }); + } + + /** + * 获取作品管理统一列表(覆盖全部 4 个组别) + * - 本科/教师组:benke_submissions / jiaoshi_submissions 的 word + ppt 文件 + * - 中职/高职组:exam_submissions 中的 Word 答题答案文件 + * 返回归一化结构,便于前端统一渲染、在线预览、打分、批量下载。 + * @param {string} group + * @returns {Promise} [{ contestantId, name, idNumber, phone, group, files: [{ id, kind, fileName, filePath, fileSize, score, uploadedAt, scoreField }] }] + */ + async getWorks(group) { + const { ctx } = this; + const isExam = group === 'zhongzhi' || group === 'gaozhi'; + const contestants = await ctx.model.Contestant.findAll({ + where: { group }, + attributes: ['id', 'name', 'id_number', 'phone', 'group', 'word_score', 'ppt_score', 'exam_score'], + order: [['id', 'ASC']], + raw: true, + }); + + if (!isExam) { + const SubmissionModel = group === 'benke' ? ctx.model.BenkeSubmission : ctx.model.JiaoshiSubmission; + const rows = await SubmissionModel.findAll({ order: [['upload_time', 'DESC']], raw: true }); + // benke/jiaoshi_submissions.aggregation_uid 对应 aggregation.aggregation_uid(非 contestant.id), + // 需经 aggregation 表 account_uid(= contestant.id)映射回选手 + const aggRows = await ctx.model.Aggregation.findAll({ + where: { account_uid: contestants.map(c => c.id) }, + attributes: ['account_uid', 'aggregation_uid'], + raw: true, + }); + // 正向映射:contestant.id(=account_uid) → aggregation_uid, + // 再用 aggregation_uid 取该选手的提交记录(方向不可颠倒!) + const contestantToAgg = {}; + aggRows.forEach(a => { contestantToAgg[a.account_uid] = a.aggregation_uid; }); + const byAgg = {}; + rows.forEach(r => { (byAgg[r.aggregation_uid] = byAgg[r.aggregation_uid] || []).push(r); }); + return contestants.map(c => { + const subs = byAgg[contestantToAgg[c.id]] || []; + const files = subs.map(s => ({ + id: s.id, + kind: s.file_type || 'word', + fileName: s.file_name, + filePath: s.file_path, + fileSize: s.file_size, + score: s.score != null ? Number(s.score) : null, + uploadedAt: s.upload_time, + scoreField: s.file_type === 'ppt' ? 'ppt' : 'word', + })); + return { contestantId: c.id, name: c.name, idNumber: c.id_number, phone: c.phone, group: c.group, files }; + }); + } + + // 中职/高职组:Word 答题答案存于 exam_submissions + const examSubs = await ctx.model.ExamSubmission.findAll({ + where: { contestant_id: contestants.map(c => c.id) }, + attributes: ['id', 'contestant_id', 'word_file_path', 'word_file_name'], + raw: true, + }); + const wordMap = {}; + examSubs.forEach(w => { if (w.word_file_path) wordMap[w.contestant_id] = w; }); + return contestants.map(c => { + const w = wordMap[c.id]; + // ⚠️ kind==='answer' 的下载端点是 /api/admin/submissions/:group/:id/word, + // 后端按 contestant_id 查询,此处 id 必须传 contestant_id 而非 exam_submissions.id, + // 否则 ID 错位会下载到他人文件或 404 JSON(存成损坏 docx 的根因)。 + const files = w ? [{ + id: w.contestant_id, + kind: 'answer', + fileName: w.word_file_name, + filePath: w.word_file_path, + fileSize: null, + score: c.word_score != null ? Number(c.word_score) : null, + uploadedAt: null, + scoreField: 'word', + }] : []; + return { contestantId: c.id, name: c.name, idNumber: c.id_number, phone: c.phone, group: c.group, files }; + }); + } + + // ==================== CSV 导出 ==================== + + /** + * 导出某组提交记录为 CSV 字符串 + * @param {string} group - 组别 + * @returns {Promise} CSV 内容 + */ + async exportToCsv(group) { + const isExam = group === 'zhongzhi' || group === 'gaozhi'; + const submissions = isExam + ? await this.getExamSubmissions(group) + : await this.getSubmissions(group); + + let csvContent = ''; + + if (isExam) { + // 中职/高职组:客观题(系统) + Word(人工) + csvContent = '选手ID,身份证号,姓名,手机号,是否提交,客观题分,Word分,总分\n'; + for (const item of submissions) { + csvContent += [ + item.contestantId, + `"${item.idNumber || ''}"`, + `"${item.userName || ''}"`, + `"${item.phone || ''}"`, + item.answeredCount > 0 ? '是' : '否', + item.examScore ?? 0, + item.wordScore ?? 0, + item.totalScore ?? 0, + ].join(',') + '\n'; + } + } else { + // 本科/教师组:Word(人工) + PPT(人工) + csvContent = '选手ID,身份证号,姓名,手机号,是否提交,文档文件名,文档大小,文档分,PPT文件名,PPT大小,PPT分\n'; + for (const item of submissions) { + const account = item.account || {}; + const wordSub = item.wordSubmission; + const pptSub = item.pptSubmission; + csvContent += [ + item.contestantId, + `"${account.id_number || ''}"`, + `"${account.name_cn || ''}"`, + `"${account.phone || ''}"`, + item.submitted ? '是' : '否', + `"${wordSub ? wordSub.file_name : ''}"`, + wordSub ? wordSub.file_size : '', + wordSub && wordSub.score != null ? wordSub.score : '', + `"${pptSub ? pptSub.file_name : ''}"`, + pptSub ? pptSub.file_size : '', + pptSub && pptSub.score != null ? pptSub.score : '', + ].join(',') + '\n'; + } + } + + return csvContent; + } + + // ==================== 批量下载 ZIP ==================== + + /** + * 批量下载某组所有提交文件,打包为 ZIP + * 文件重命名为 "组号-学校-姓名-原文件名" 格式 + * + * @param {string} group - 组别 + * @returns {Promise} ZIP 文件路径 + */ + async batchDownloadZip(group) { + const { ctx, app } = this; + + // 获取该组的所有提交记录(考试组与作品组数据形状不同) + const isExam = group === 'zhongzhi' || group === 'gaozhi'; + const submissions = isExam + ? await this.getExamSubmissions(group) + : await this.getSubmissions(group); + + // 构建临时 ZIP 目录 + const zipDir = path.join(app.baseDir, 'app/public/tmp/zip', group, String(Date.now())); + if (!fs.existsSync(zipDir)) { + fs.mkdirSync(zipDir, { recursive: true }); + } + + // 收集所有需要打包的文件 + const filesToZip = []; + + // 本科/教师组需要 account.name_cn;考试组使用 getExamSubmissions 的 userName + for (const item of submissions) { + const account = item.account || {}; + const userName = (isExam ? (item.userName || '未知') : (account.name_cn || '未知')).replace(/[\\/:*?"<>|]/g, '_'); + + if (isExam) { + // 中职/高职组:收集 Word 答案文件 + if (item.wordFilePath) { + const fullPath = path.join(app.baseDir, 'app/public', item.wordFilePath); + if (fs.existsSync(fullPath)) { + const origName = item.wordFileName || path.basename(fullPath); + const targetName = `${userName}-${origName}`; + const targetPath = path.join(zipDir, targetName); + fs.copyFileSync(fullPath, targetPath); + filesToZip.push({ srcPath: fullPath, zipName: targetName }); + } + } + } else { + // 本科/教师组:收集 Word + PPT 作品文件 + for (const sub of (item.workSubmissions || [])) { + if (sub.file_path) { + const fullPath = path.join(app.baseDir, 'app/public', sub.file_path); + if (fs.existsSync(fullPath)) { + const origName = sub.file_name || path.basename(fullPath); + const targetName = `${userName}-${sub.file_type || 'file'}-${origName}`; + const targetPath = path.join(zipDir, targetName); + fs.copyFileSync(fullPath, targetPath); + filesToZip.push({ srcPath: fullPath, zipName: targetName }); + } + } + } + } + } + + // 创建 ZIP 文件 + const zipFileName = `${group}_submissions_${Date.now()}.zip`; + const zipFilePath = path.join(app.baseDir, 'app/public/tmp/zip', zipFileName); + + // 使用 archiver 将收集到的文件打包为 ZIP + // archiver v8 为 ESM,导出 ZipArchive 等类;用 new ZipArchive(...) 实例化 + const { ZipArchive } = require('archiver'); + const output = fs.createWriteStream(zipFilePath); + const archive = new ZipArchive({ zlib: { level: 9 } }); + + await new Promise((resolve, reject) => { + output.on('close', resolve); + archive.on('error', reject); + + archive.pipe(output); + + // 将重命名后的文件添加到 ZIP + const copiedFiles = fs.readdirSync(zipDir); + for (const file of copiedFiles) { + const filePath = path.join(zipDir, file); + archive.file(filePath, { name: file }); + } + + archive.finalize(); + }); + + // 🔴 清理临时目录:best-effort(同源缺陷族,与 controller/upload.js:mergeChunks 同修)。 + // ZIP 已 finalize 落盘,清理失败绝不能把成功的批量下载判成 500 + // (common-error.log 实证:/api/admin/batch-download/* 曾因此整体失败)。 + this.ctx.helper.safeCleanup(zipDir, { label: 'batchZip-tmp' }); + + return zipFilePath; + } + + // ==================== 评分管理 ==================== + + /** + * 批量更新评分 + * @param {Array} scores - 评分数组 [{ submissionId, score }] + * @param {string} group - 组别 + */ + async updateScores(scores, group) { + const { ctx } = this; + + for (const item of scores) { + const { contestantId, examScore, wordScore, pptScore } = item; + if (!contestantId) continue; + + const contestant = await ctx.model.Contestant.findByPk(contestantId); + if (!contestant) continue; + + // P2-2 事务补全:单个选手的评分涉及 contestants + accounts + 作品提交表 + // 多表写入,包裹在同一事务中,任一步失败整体回滚,避免"改了总分、 + // 没同步分项"的中间状态脏数据。 + await ctx.model.transaction(async (t) => { + if (group === 'zhongzhi' || group === 'gaozhi') { + // 中职/高职组:客观题(系统评分)+ Word(人工评分) + let examS; + if (examScore !== undefined && examScore !== null && examScore !== '') { + examS = Number(examScore); + } else { + // 未显式传客观题分则按逐题分数自动求和 + examS = await this._computeObjectiveScore(contestantId, group); + } + const wordS = (wordScore !== undefined && wordScore !== null && wordScore !== '') + ? Number(wordScore) + : (contestant.word_score || 0); + await contestant.update({ + exam_score: examS, + word_score: wordS, + score: examS + wordS, + }, { transaction: t }); + } else { + // 本科/教师组:Word(人工评分)+ PPT(人工评分) + const wordS = (wordScore !== undefined && wordScore !== null && wordScore !== '') + ? Number(wordScore) + : (contestant.word_score || 0); + const pptS = (pptScore !== undefined && pptScore !== null && pptScore !== '') + ? Number(pptScore) + : (contestant.ppt_score || 0); + await contestant.update({ + word_score: wordS, + ppt_score: pptS, + score: wordS + pptS, + }, { transaction: t }); + // 历史兼容:同步 accounts.score + const aggregation = await ctx.model.Aggregation.findOne({ + where: { account_uid: contestantId }, + transaction: t, + }); + if (aggregation && aggregation.account_uid) { + await ctx.model.Account.update( + { score: wordS + pptS }, + { where: { uid: aggregation.account_uid }, transaction: t } + ); + } + } + + // 将分项分数同步到作品提交记录(按文件查看分项) + await this._syncSubmissionScores(contestantId, group, { + wordS: contestant.word_score || 0, + pptS: contestant.ppt_score || 0, + }, t); + }); + } + } + + /** + * 计算选手客观题自动评分总和(中职/高职组) + * @param {number} contestantId - 选手 ID + * @param {string} group - 组别 + * @returns {Promise} + */ + async _computeObjectiveScore(contestantId, group) { + const { ctx } = this; + const SubmissionModel = group === 'zhongzhi' + ? ctx.model.ZhongzhiSubmission + : ctx.model.GaozhiSubmission; + const rows = await SubmissionModel.findAll({ + where: { aggregation_uid: contestantId }, + attributes: ['score'], + }); + return rows.reduce((sum, r) => sum + (r.score || 0), 0); + } + + /** + * 将分项分数同步到作品提交记录(Word / PPT 各自一行) + * @param {number} contestantId - 选手 ID + * @param {string} group - 组别 + * @param {Object} scores - { wordS, pptS } + * @param {Object} [transaction] - Sequelize 事务(P2-2:随 updateScores 事务一起提交/回滚) + */ + async _syncSubmissionScores(contestantId, group, { wordS, pptS }, transaction = null) { + const { ctx } = this; + // ⚠️ 仅本科/教师组有作品提交表(benke/jiaoshi_submissions)需要同步分项。 + // 中职/高职组分数存于 contestants + 逐题表,若误走 JiaoshiSubmission 会把 + // 教师组同 aggregation_uid 的作品分数改错(跨组数据污染,评分不同步 Bug 之一)。 + if (group !== 'benke' && group !== 'jiaoshi') return; + const SubmissionModel = group === 'benke' + ? ctx.model.BenkeSubmission + : ctx.model.JiaoshiSubmission; + + // benke_submissions.aggregation_uid 是聚合报名ID,需用 account_uid 解析 + const agg = await ctx.model.Aggregation.findOne({ + where: { account_uid: contestantId }, + transaction, + }); + const aggUid = agg ? agg.aggregation_uid : contestantId; + + const wordSub = await SubmissionModel.findOne({ + where: { aggregation_uid: aggUid, file_type: 'word' }, + transaction, + }); + if (wordSub) await wordSub.update({ score: wordS }, { transaction }); + + const pptSub = await SubmissionModel.findOne({ + where: { aggregation_uid: aggUid, file_type: 'ppt' }, + transaction, + }); + if (pptSub) await pptSub.update({ score: pptS }, { transaction }); + } + + /** + * 更新账号的总分(汇总所有提交记录的得分) + * @param {number} aggregationUid - 聚合报名 ID + * @param {string} group - 组别 + */ + async _updateAccountTotalScore(aggregationUid, group) { + const { ctx } = this; + + let totalScore = 0; + + if (group === 'zhongzhi' || group === 'gaozhi') { + // 中职/高职组:aggregation_uid 即考生 ID,直接从 contestants 表更新 + const SubmissionModel = group === 'zhongzhi' + ? ctx.model.ZhongzhiSubmission + : ctx.model.GaozhiSubmission; + + const submissions = await SubmissionModel.findAll({ + where: { aggregation_uid: aggregationUid }, + attributes: ['score'], + }); + totalScore = submissions.reduce((sum, s) => sum + (s.score || 0), 0); + + await ctx.model.Contestant.update( + { score: totalScore }, + { where: { id: aggregationUid } } + ); + } else { + // 本/教组:aggregation_uid 对应 aggregation 表主键 + const aggregation = await ctx.model.Aggregation.findByPk(aggregationUid); + if (!aggregation) return; + + const SubmissionModel = group === 'benke' + ? ctx.model.BenkeSubmission + : ctx.model.JiaoshiSubmission; + + const submissions = await SubmissionModel.findAll({ + where: { aggregation_uid: aggregationUid }, + attributes: ['score'], + }); + totalScore = submissions.reduce((sum, s) => sum + (s.score || 0), 0); + + await ctx.model.Account.update( + { score: totalScore }, + { where: { uid: aggregation.account_uid } } + ); + } + } + + // ==================== 题目管理 ==================== + + /** + * 根据题型获取对应的模型 + * @param {string} questionType - 题型:choice / judge / word + * @returns {Object} Sequelize 模型 + */ + _getQuestionModel(questionType) { + const { ctx } = this; + switch (questionType) { + case 'choice': return ctx.model.ChoiceQuestion; + case 'judge': return ctx.model.JudgeQuestion; + case 'word': return ctx.model.WordQuestion; + default: throw new Error(`未知题型: ${questionType}`); + } + } + + /** + * 管理考试题目(替换操作:先清空再创建) + * @param {Array} questions - 题目数组 + * @param {string} group - 组别 + * @param {string} questionType - 题型:choice / judge / word + */ + async manageQuestions(questions, group, questionType) { + const { ctx } = this; + const QuestionModel = this._getQuestionModel(questionType); + + const transaction = await ctx.model.transaction(); + + try { + // 清空该组该题型的所有题目 + await QuestionModel.destroy({ + where: { group }, + transaction, + truncate: false, + }); + + // 批量插入新题目 + const questionData = questions.map((q, index) => { + // 分值必须为大于 0 的合法数值;0 分题属非法业务数据,整批替换时直接拒绝 + const scoreNum = Number(q.score); + if (!isFinite(scoreNum) || scoreNum <= 0) { + throw new Error(`题目分值(score)必须为大于 0 的合法数值(第 ${index + 1} 题)`); + } + const base = { + group, + score: scoreNum, + sort_order: q.sort_order !== undefined ? q.sort_order : index + 1, + }; + if (questionType === 'choice') { + return { + ...base, + question: q.question || '', + options: q.options || [], + answer: q.answer || '', + }; + } else if (questionType === 'judge') { + return { + ...base, + question: q.question || '', + answer: q.answer || 'false', + }; + } else { + // word + return { + ...base, + title: q.title || q.question || '', + description: q.description || '', + template_path: q.template_path || null, + }; + } + }); + + if (questionData.length > 0) { + await QuestionModel.bulkCreate(questionData, { transaction }); + } + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + } + + /** + * 添加单道题目 + * @param {Object} questionData - 题目数据 + * @param {string} group - 组别 + * @param {string} questionType - 题型:choice / judge / word + * @returns {Promise} 创建的题目 + */ + async addQuestion(questionData, group, questionType) { + const QuestionModel = this._getQuestionModel(questionType); + + // 分值必须为大于 0 的合法数值;0 分题属非法业务数据 + // (抽中该题的考生答对也得 0 分,且不同人满分不一致)。缺失/非法分值直接拒绝。 + const scoreNum = Number(questionData.score); + if (!isFinite(scoreNum) || scoreNum <= 0) { + throw new Error('题目分值(score)必须为大于 0 的合法数值'); + } + + const data = { + group, + score: scoreNum, + sort_order: questionData.sort_order || 0, + }; + + if (questionType === 'choice') { + data.question = questionData.question || ''; + data.options = questionData.options || []; + data.answer = questionData.answer || ''; + } else if (questionType === 'judge') { + data.question = questionData.question || ''; + data.answer = questionData.answer || 'false'; + } else { + data.title = questionData.title || questionData.question || ''; + data.description = questionData.description || ''; + data.template_path = questionData.template_path || null; + } + + return await QuestionModel.create(data); + } + + /** + * 更新单道题目 + * @param {number} questionId - 题目 ID + * @param {Object} questionData - 更新数据 + * @param {string} questionType - 题型:choice / judge / word + */ + async updateQuestion(questionId, questionData, questionType) { + const QuestionModel = this._getQuestionModel(questionType); + + const question = await QuestionModel.findByPk(questionId); + if (!question) { + throw new Error('题目不存在'); + } + + const updateData = { + score: questionData.score !== undefined ? questionData.score : question.score, + sort_order: questionData.sort_order !== undefined ? questionData.sort_order : question.sort_order, + }; + + if (questionType === 'choice') { + updateData.question = questionData.question !== undefined ? questionData.question : question.question; + updateData.options = questionData.options !== undefined ? questionData.options : question.options; + updateData.answer = questionData.answer !== undefined ? questionData.answer : question.answer; + } else if (questionType === 'judge') { + updateData.question = questionData.question !== undefined ? questionData.question : question.question; + updateData.answer = questionData.answer !== undefined ? questionData.answer : question.answer; + } else { + updateData.title = questionData.title !== undefined ? questionData.title : question.title; + updateData.description = questionData.description !== undefined ? questionData.description : question.description; + updateData.template_path = questionData.template_path !== undefined ? questionData.template_path : question.template_path; + } + + await question.update(updateData); + return question; + } + + /** + * 删除单道题目 + * @param {number} questionId - 题目 ID + * @param {string} questionType - 题型:choice / judge / word + */ + async deleteQuestion(questionId, questionType) { + const QuestionModel = this._getQuestionModel(questionType); + await QuestionModel.destroy({ where: { id: questionId } }); + } + + /** + * 批量删除题目(按题型) + * @param {number[]} ids - 题目 ID 数组 + * @param {string} questionType - choice / judge / word + * @returns {Promise} 删除行数 + */ + async bulkDeleteQuestions(ids, questionType) { + const QuestionModel = this._getQuestionModel(questionType); + return await QuestionModel.destroy({ where: { id: ids } }); + } + + // ==================== 时间管理 ==================== + + /** + * 批量设置比赛时间 + * @param {Array} times - 时间配置数组 [{ group, startTime, endTime, deadline }] + */ + async setTimes(times) { + const { ctx } = this; + + for (const timeConfig of times) { + const { group, startTime, endTime, deadline } = timeConfig; + // 客观题抽取数量:缺省 50/20;数量不足时考试抽题会按题库实际数量自动封顶 + const choiceCount = timeConfig.choiceCount != null ? parseInt(timeConfig.choiceCount, 10) : 50; + const judgeCount = timeConfig.judgeCount != null ? parseInt(timeConfig.judgeCount, 10) : 20; + + // 时间线统一铁律:管理界面「考试结束时间/提交截止时间」均绑定 end_time; + // 本科/教师组的 deadline 列强制与 end_time 同步,防止旧 deadline 残值 + // 导致「状态显示进行中、上传却提示已截止」的不一致。 + const isUploadGroup = group === 'benke' || group === 'jiaoshi'; + const effectiveDeadline = isUploadGroup + ? (endTime ? new Date(endTime) : null) + : (deadline ? new Date(deadline) : null); + + await ctx.model.CompetitionTime.upsert({ + group: group, + start_time: startTime ? new Date(startTime) : null, + end_time: endTime ? new Date(endTime) : null, + deadline: effectiveDeadline, + choice_count: choiceCount, + judge_count: judgeCount, + }); + } + } + + // ==================== 时间查询 ==================== + + /** + * 获取所有组的比赛时间配置 + * @returns {Promise} 时间配置列表 + */ + async getTimes() { + const { ctx } = this; + const times = await ctx.model.CompetitionTime.findAll({ + attributes: ['group', 'start_time', 'end_time', 'deadline', 'word_template', 'choice_count', 'judge_count'], + order: [['group', 'ASC']], + raw: true, + }); + + // 附带各组题库实际题目数,便于后台在「配置抽取数量」时直观看到「题库实际 / 配置抽取」 + const fmtLocal = (d) => { + if (!d) return null; + const dt = new Date(d); + const p = (n) => String(n).padStart(2, '0'); + return `${dt.getFullYear()}-${p(dt.getMonth() + 1)}-${p(dt.getDate())}T${p(dt.getHours())}:${p(dt.getMinutes())}:${p(dt.getSeconds())}`; + }; + const result = []; + for (const t of times) { + const [availableChoice, availableJudge] = await Promise.all([ + ctx.model.ChoiceQuestion.count({ where: { group: t.group } }), + ctx.model.JudgeQuestion.count({ where: { group: t.group } }), + ]); + result.push({ + group: t.group, + startTime: fmtLocal(t.start_time), + endTime: fmtLocal(t.end_time), + deadline: fmtLocal(t.deadline), + wordTemplate: t.word_template || null, + choiceCount: t.choice_count != null ? t.choice_count : 50, + judgeCount: t.judge_count != null ? t.judge_count : 20, + availableChoice, + availableJudge, + }); + } + return result; + } + + // ==================== 评分查询 ==================== + + /** + * 获取某组评分列表 + * @param {string} group - 组别 + * @returns {Promise} 评分列表 + */ + async getScores(group) { + const { ctx } = this; + const isExam = group === 'zhongzhi' || group === 'gaozhi'; + const contestants = await ctx.model.Contestant.findAll({ + where: { group }, + attributes: ['id', 'name', 'id_number', 'group', 'score', 'exam_score', 'word_score', 'ppt_score'], + order: [['id', 'ASC']], + raw: true, + }); + + // contestant.id -> 真实 aggregation_uid 映射。 + // 历史数据曾把真实聚合 uid 写进 zhongzhi/gaozhi_submissions 的 aggregation_uid 列, + // 而当前 _getUserAggregation 返回 aggregation_uid = contestant.id;两种写法并存。 + // 若仅按 contestant.id 归集,纯历史数据者的答题数/卷面分会因聚合键错位而恒为 0。 + // 故按「contestant.id」与「真实 aggregation_uid」两种 key 同时归集,消除错位。 + const aggRows = await ctx.model.Aggregation.findAll({ + where: { account_uid: contestants.map(c => c.id) }, + attributes: ['account_uid', 'aggregation_uid'], + raw: true, + }); + const realAggByContestant = {}; + aggRows.forEach(a => { realAggByContestant[a.account_uid] = a.aggregation_uid; }); + + const list = contestants.map(c => ({ + uid: c.id, + name: c.name, + idNumber: c.id_number, + group: c.group, + score: c.score || 0, + examScore: c.exam_score ?? 0, + wordScore: c.word_score ?? 0, + pptScore: c.ppt_score ?? 0, + })); + + if (isExam) { + const SubmissionModel = group === 'zhongzhi' + ? ctx.model.ZhongzhiSubmission + : ctx.model.GaozhiSubmission; + const ans = await SubmissionModel.findAll({ + attributes: ['aggregation_uid', 'score'], + raw: true, + }); + // 按「真实聚合 uid」归集:count=答题数,score=客观题自动评分合计(卷面分) + const statMap = {}; + ans.forEach(s => { + const k = String(s.aggregation_uid); + if (!statMap[k]) statMap[k] = { count: 0, score: 0 }; + statMap[k].count += 1; + statMap[k].score += (s.score || 0); + }); + const examSubs = await ctx.model.ExamSubmission.findAll({ + where: { contestant_id: contestants.map(c => c.id) }, + attributes: ['contestant_id', 'word_file_path', 'submit_time', 'created_at'], + raw: true, + }); + const examMap = {}; + examSubs.forEach(w => { examMap[String(w.contestant_id)] = w; }); + list.forEach(item => { + const keys = [String(item.uid), String(realAggByContestant[item.uid])].filter(Boolean); + let answeredCount = 0; + let autoScore = 0; + keys.forEach(k => { + const m = statMap[k]; + if (m) { answeredCount += m.count; autoScore += m.score; } + }); + item.answeredCount = answeredCount; + // 卷面分 = 客观题自动评分合计(不依赖人工评分列 exam_score,避免未评分时恒为 0) + item.examScore = autoScore; + // 总分 = 卷面分 + Word 阅卷分(不读 contestants.score,避免未人工评分时恒为 0) + item.score = autoScore + item.wordScore; + const es = examMap[String(item.uid)]; + item.hasWordFile = !!(es && es.word_file_path); + item.submitted = !!(es && es.submit_time) || answeredCount > 0; + item.submitTime = (es && es.submit_time) + || (answeredCount > 0 ? (es && es.created_at) || null : null); + }); + } else { + // 本科/教师组:上传作品即视为已提交(与考生端 exam.status.submitted、作品管理口径一致)。 + // 🔴 聚合键铁律:benke/jiaoshi_submissions.aggregation_uid = aggregation 表真实聚合 uid + //(≠ contestant.id),须按「contestant.id + 真实 aggregation_uid」双向映射归集, + // 否则真实选手(如郑佳怡 cid=11/agg=1)恒显示未提交。 + const SubmissionModel = group === 'benke' + ? ctx.model.BenkeSubmission + : ctx.model.JiaoshiSubmission; + const rows = await SubmissionModel.findAll({ + attributes: ['aggregation_uid', 'upload_time'], + order: [['upload_time', 'ASC']], + raw: true, + }); + const subByAgg = {}; + rows.forEach(s => { + const k = String(s.aggregation_uid); + if (!subByAgg[k]) subByAgg[k] = s; + }); + list.forEach(item => { + const keys = [String(item.uid), String(realAggByContestant[item.uid])].filter(Boolean); + let sub = null; + keys.forEach(k => { if (!sub && subByAgg[k]) sub = subByAgg[k]; }); + item.answeredCount = 0; + item.hasWordFile = false; + item.submitted = !!sub; + item.submitTime = sub ? sub.upload_time : null; + // 总分 = Word 阅卷分 + PPT 阅卷分(与分项口径一致,不读 contestants.score) + item.score = item.wordScore + item.pptScore; + }); + } + + return list; + } + + // ==================== 题目查询 ==================== + + /** + * 获取某组考试题目列表(按题型分表返回) + * @param {string} group - 组别 + * @returns {Promise} { choice: [...], judge: [...], word: [...] } + */ + async getQuestions(group) { + const { ctx } = this; + const queryOptions = { where: { group }, order: [['sort_order', 'ASC']], raw: true }; + + const [choiceQuestions, judgeQuestions, wordQuestions] = await Promise.all([ + ctx.model.ChoiceQuestion.findAll(queryOptions), + ctx.model.JudgeQuestion.findAll(queryOptions), + ctx.model.WordQuestion.findAll(queryOptions), + ]); + + return { + choice: choiceQuestions.map(q => ({ + id: q.id, + question_type: 'choice', + question: q.question, + options: typeof q.options === 'string' ? JSON.parse(q.options) : q.options, + answer: q.answer, + score: q.score, + sort_order: q.sort_order, + })), + judge: judgeQuestions.map(q => ({ + id: q.id, + question_type: 'judge', + question: q.question, + answer: q.answer, + score: q.score, + sort_order: q.sort_order, + })), + word: wordQuestions.map(q => ({ + id: q.id, + question_type: 'word', + title: q.title, + description: q.description, + score: q.score, + sort_order: q.sort_order, + template_path: q.template_path, + })), + }; + } + + /** + * 批量按题型设置分值:将该组该题型全部题目统一设为指定分值 + * @param {string} group - 组别 + * @param {string} questionType - choice / judge / word + * @param {number} score - 目标分值 + * @returns {Promise} 受影响行数 + */ + async setTypeScore(group, questionType, score) { + const { ctx } = this; + const QuestionModel = this._getQuestionModel(questionType); + const [updated] = await QuestionModel.update( + { score }, + { where: { group } } + ); + return updated; + } + + /** + * 客观题 Excel 批量导入(单工作表:题型 / 题目 / 选项A~D / 正确答案 / 分值) + * @param {string} filePath - 临时 Excel 文件路径 + * @param {Object} options - { group } + * @returns {Promise<{imported:number, skipped:number, errors:Array}>} + */ + async importQuestionsFromExcel(filePath, options = {}) { + const { ctx } = this; + const group = options.group; + const workbook = XLSX.readFile(filePath); + const sheet = workbook.Sheets[workbook.SheetNames[0]]; + const rawRows = XLSX.utils.sheet_to_json(sheet); + + // 表头归一化(Excel 导入 0 成功 Bug 修复): + // 测试人员的 Excel 表头常带前后空格 / 全角空格(如「题型 」「 题目」), + // sheet_to_json 的键名严格匹配会导致 row['题型'] 全部 undefined → 整表被 + // 「题型无效」跳过。此处将每行键名 trim(含全角空格)后重建,同时兼容常见别名。 + const keyAlias = { + '题目类型': '题型', '类型': '题型', + '题干': '题目', '题目内容': '题目', + '答案': '正确答案', '标准答案': '正确答案', + '分数': '分值', '题目分值': '分值', + 'A': '选项A', 'B': '选项B', 'C': '选项C', 'D': '选项D', + }; + const rows = rawRows.map(raw => { + const clean = {}; + for (const k of Object.keys(raw)) { + let nk = String(k).replace(/[\s\u3000\uFEFF]+/g, ''); + if (keyAlias[nk]) nk = keyAlias[nk]; + clean[nk] = raw[k]; + } + return clean; + }); + + const result = { imported: 0, skipped: 0, errors: [] }; + + // 先按题型分桶,最后用 bulkCreate 一次性入库,减少逐条事务开销 + const buckets = { choice: [], judge: [] }; + const optionKeys = ['选项A', '选项B', '选项C', '选项D']; + + for (let i = 0; i < rows.length; i++) { + const row = rows[i]; + const rowIndex = i + 2; // Excel 行号(含表头) + + const rawType = String(row['题型'] || '').trim(); + let qType = null; + if (['选择题', 'choice'].includes(rawType)) qType = 'choice'; + else if (['判断题', 'judge'].includes(rawType)) qType = 'judge'; + if (!qType) { + result.errors.push({ row: rowIndex, message: `题型无效:${rawType || '(空)'}` }); + result.skipped++; + continue; + } + + const question = String(row['题目'] || '').trim(); + if (!question) { + result.errors.push({ row: rowIndex, message: '题目内容为空' }); + result.skipped++; + continue; + } + + let answer = String(row['正确答案'] || '').trim(); + let options = null; + if (qType === 'choice') { + options = optionKeys + .map((k, idx) => ({ key: String.fromCharCode(65 + idx), text: String(row[k] || '').trim() })) + .filter(o => o.text); + if (options.length < 2) { + result.errors.push({ row: rowIndex, message: '选择题至少需要 2 个非空选项' }); + result.skipped++; + continue; + } + if (!answer) { + result.errors.push({ row: rowIndex, message: '选择题正确答案(选项字母)为空' }); + result.skipped++; + continue; + } + // 校验答案字母均落在已填选项内(兼容多选如 AB) + const validKeys = options.map(o => o.key); + const ansKeys = answer.toUpperCase().split(''); + if (!ansKeys.every(k => validKeys.includes(k))) { + result.errors.push({ row: rowIndex, message: `正确答案 ${answer} 超出已填选项 ${validKeys.join('')}` }); + result.skipped++; + continue; + } + } else { + // 判断题答案归一化:对/正确/true/t/√ -> true;错/错误/false/f/× -> false + const a = answer.toLowerCase(); + if (['对', '正确', 'true', 't', '√', '是', 'y', 'yes'].includes(a)) answer = 'true'; + else if (['错', '错误', 'false', 'f', '×', '否', 'n', 'no'].includes(a)) answer = 'false'; + if (answer !== 'true' && answer !== 'false') { + result.errors.push({ row: rowIndex, message: `判断题正确答案无效:${answer || '(空)'}` }); + result.skipped++; + continue; + } + } + + const scoreRaw = row['分值']; + let score = parseInt(scoreRaw, 10); + if (isNaN(score) || score < 1) score = 5; + + const data = { group, question, answer, score, sort_order: 0 }; + if (qType === 'choice') data.options = options; + buckets[qType].push(data); + } + + // 一次性批量入库(各自在独立事务中,避免单条失败拖垮整批) + for (const qType of ['choice', 'judge']) { + const list = buckets[qType]; + if (!list.length) continue; + const QuestionModel = this._getQuestionModel(qType); + const created = await QuestionModel.bulkCreate(list); + result.imported += created.length; + } + + return result; + } + + /** + * 生成客观题 Excel 导入模板(含表头 + 1 行示例) + * @returns {Promise} xlsx 文件 Buffer + */ + async buildQuestionTemplate() { + const header = ['题型', '题目', '选项A', '选项B', '选项C', '选项D', '正确答案', '分值']; + const example = ['选择题', '北斗系统由多少颗卫星组成?', '24', '35', '55', '12', 'C', 5]; + const ws = XLSX.utils.aoa_to_sheet([header, example]); + // 判断题示例行(另起一行,便于参考) + XLSX.utils.sheet_add_aoa(ws, [['判断题', '北斗系统由我国自主研发。', '', '', '', '', '对', 5]], { origin: -1 }); + const wb = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet(wb, ws, '客观题'); + return XLSX.write(wb, { type: 'buffer', bookType: 'xlsx' }); + } + + // ==================== 新闻查询 ==================== + + /** + * 获取所有新闻列表(管理员视图,包含未发布) + * @returns {Promise} 新闻列表 + */ + async listNews() { + const { ctx } = this; + const news = await ctx.model.News.findAll({ + order: [['pinned', 'DESC'], ['sort_order', 'ASC'], ['publish_time', 'DESC']], + raw: true, + }); + + return news.map(n => ({ + id: n.id, + title: n.title, + content: n.content, + news_type: n.news_type, + wx_link: n.wx_link, + publish_time: n.publish_time, + pinned: n.pinned, + group: n.group, + tags: n.tags, + sort_order: n.sort_order, + created_at: n.created_at, + updated_at: n.updated_at, + })); + } + +// ==================== 轮播图查询 ==================== + + /** + * 获取所有轮播图列表 + * @returns {Promise} 轮播图列表 + */ + async listCarousel() { + const { ctx } = this; + const carousels = await ctx.model.Carousel.findAll({ + order: [['sort_order', 'ASC']], + raw: true, + }); + + return carousels.map(c => ({ + id: c.id, + image_url: c.image_url, + title: c.title || '', + content: c.content || '', + button_text: c.button_text || '', + button_link: c.button_link || '', + sort_order: c.sort_order, + link_url: c.link_url, + enabled: c.enabled, + created_at: c.created_at, + updated_at: c.updated_at, + })); + } + + // ==================== Word 模板管理 ==================== + + /** + * 上传 Word 模板文件并更新配置 + * @param {string} group - 组别 + * @param {Object} fileInfo - 文件信息 { filePath } + */ + async uploadWordTemplate(group, fileInfo) { + const { ctx } = this; + + await ctx.model.CompetitionTime.upsert({ + group: group, + word_template: fileInfo.filePath, + }); + } + + // ==================== 成绩发布管理 ==================== + + /** + * 成绩发布状态按组独立存储。 + * + * 历史实现只有一个全局键 `score_published`,而管理后台「发布成绩」按钮是按组呈现的 + * (scoresPublishedMap[group])——发布任意一组会让四个组的考生同时看到成绩, + * 这正是「中职/高职未发布却能看到成绩」的直接成因之一。 + * 现改为每组一个键 `score_published_`(仅新增 system_configs 行,无表结构改动)。 + * + * 注意:**不回退读取遗留全局键**。遗留库中该键自建库起即为 'true', + * 一旦作为兜底就会把缺陷重新引回(详见 setScorePublishKey 的还原说明)。 + */ + static get PUBLISH_GROUPS() { + return [ 'zhongzhi', 'gaozhi', 'benke', 'jiaoshi' ]; + } + + /** + * 组别 → 配置键名 + * @param {string} group - 组别 + * @returns {string} 配置键 + */ + _publishKey(group) { + return `score_published_${group}`; + } + + /** + * 写入单个配置键(不存在则创建) + * @param {string} key - 配置键 + * @param {string} value - 配置值 + */ + async _setConfig(key, value, transaction) { + const { ctx } = this; + const opts = transaction ? { transaction } : {}; + const [ config, created ] = await ctx.model.SystemConfig.findOrCreate({ + where: { config_key: key }, + defaults: { config_key: key, config_value: value }, + ...opts, + }); + if (!created && config.config_value !== value) { + await config.update({ config_value: value }, opts); + } + } + + /** + * 发布/撤销成绩 + * @param {boolean} publish - 是否发布 + * @param {string} [group] - 组别;缺省表示对全部组生效(兼容旧调用方) + */ + async publishScores(publish, group, transaction) { + const value = publish ? 'true' : 'false'; + const groups = group + ? [ group ] + : this.constructor.PUBLISH_GROUPS; + + for (const g of groups) { + await this._setConfig(this._publishKey(g), value, transaction); + } + + // 同步维护遗留全局键,仅用于兼容仍在读取它的外部工具; + // 业务判定一律以分组键为准。整体发布/撤销时才更新它。 + if (!group) { + await this._setConfig('score_published', value, transaction); + } + } + + /** + * 检查成绩是否已发布 + * @param {string} [group] - 组别;缺省时要求「全部组均已发布」才返回 true + * @returns {Promise} 严格布尔值 + */ + async isScorePublished(group) { + const { ctx } = this; + + if (group) { + const config = await ctx.model.SystemConfig.findOne({ + where: { config_key: this._publishKey(group) }, + }); + // 键缺失 = 未发布(安全默认)。必须返回严格布尔, + // 否则 `config && ...` 在无行时得到 null,前端 `=== true` 判定与断言都会失真。 + return !!(config && config.config_value === 'true'); + } + + const statusMap = await this.getScorePublishMap(); + return this.constructor.PUBLISH_GROUPS.every(g => statusMap[g] === true); + } + + /** + * 获取各组成绩发布状态 + * @returns {Promise} { zhongzhi: bool, gaozhi: bool, benke: bool, jiaoshi: bool } + */ + async getScorePublishMap() { + const { ctx } = this; + const groups = this.constructor.PUBLISH_GROUPS; + + const rows = await ctx.model.SystemConfig.findAll({ + where: { config_key: groups.map(g => this._publishKey(g)) }, + attributes: [ 'config_key', 'config_value' ], + raw: true, + }); + + const byKey = {}; + rows.forEach(r => { byKey[r.config_key] = r.config_value; }); + + const result = {}; + groups.forEach(g => { + result[g] = byKey[this._publishKey(g)] === 'true'; + }); + return result; + } + + // ==================== 新闻管理 ==================== + + /** + * 创建新闻 + * @param {Object} newsData - 新闻数据 + * @returns {Promise} + */ + async createNews(newsData) { + const { ctx } = this; + return await ctx.model.News.create({ + title: newsData.title, + content: newsData.news_type === 'wx_link' ? (newsData.content || null) : (newsData.content || ''), + wx_link: newsData.news_type === 'wx_link' ? (newsData.wx_link || null) : null, + news_type: newsData.news_type || 'content', + publish_time: newsData.publish_time || new Date(), + pinned: newsData.pinned || false, + group: newsData.group || newsData.group_name || 'all', + tags: newsData.tags || null, + }); + } + + /** + * 更新新闻 + * @param {number} id - 新闻 ID + * @param {Object} newsData - 更新数据 + */ + async updateNews(id, newsData) { + const { ctx } = this; + const news = await ctx.model.News.findByPk(id); + if (!news) throw new Error('新闻不存在'); + + await news.update({ + title: newsData.title !== undefined ? newsData.title : news.title, + content: newsData.content !== undefined ? newsData.content : news.content, + wx_link: newsData.wx_link !== undefined ? newsData.wx_link : news.wx_link, + news_type: newsData.news_type !== undefined ? newsData.news_type : news.news_type, + publish_time: newsData.publish_time !== undefined ? newsData.publish_time : news.publish_time, + pinned: newsData.pinned !== undefined ? newsData.pinned : news.pinned, + group: newsData.group !== undefined ? newsData.group : (newsData.group_name !== undefined ? newsData.group_name : news.group), + tags: newsData.tags !== undefined ? newsData.tags : news.tags, + }); + + return news; + } + + /** + * 删除新闻 + * @param {number} id - 新闻 ID + */ + async deleteNews(id) { + const { ctx } = this; + await ctx.model.News.destroy({ where: { id } }); + } + + /** + * 获取新闻列表(管理端) + * @param {Object} query - 查询参数 + * @returns {Promise} + */ + async listNews(query) { + const { ctx } = this; + const { page = 1, pageSize = 20, group, pinned } = query || {}; + const where = {}; + + if (group && group !== 'all') { + where.group = group; + } + if (pinned !== undefined) { + where.pinned = pinned; + } + + const { count, rows } = await ctx.model.News.findAndCountAll({ + where, + order: [['pinned', 'DESC'], ['publish_time', 'DESC']], + offset: (page - 1) * pageSize, + limit: parseInt(pageSize), + raw: true, + }); + + return { list: rows, total: count, page: parseInt(page), pageSize: parseInt(pageSize) }; + } + + /** + * 获取新闻详情(管理端) + * @param {number} id - 新闻 ID + * @returns {Promise} + */ + async getNewsDetail(id) { + const { ctx } = this; + const news = await ctx.model.News.findByPk(id); + if (!news) throw new Error('新闻不存在'); + return news; + } + + /** + * 添加轮播图 + * @param {Object} carouselData - 轮播图数据 + * @returns {Promise} + */ + async addCarousel(carouselData) { + const { ctx } = this; + return await ctx.model.Carousel.create({ + image_url: carouselData.image_url || null, + title: carouselData.title || null, + content: carouselData.content || null, + button_text: carouselData.button_text || null, + button_link: carouselData.button_link || null, + sort_order: carouselData.sort_order || 0, + link_url: carouselData.link_url || null, + enabled: carouselData.enabled !== false, + }); + } + + /** + * 更新轮播图 + * @param {number} id - 轮播图 ID + * @param {Object} carouselData - 更新数据 + */ + async updateCarousel(id, carouselData) { + const { ctx } = this; + const carousel = await ctx.model.Carousel.findByPk(id); + if (!carousel) throw new Error('轮播图不存在'); + + await carousel.update({ + image_url: carouselData.image_url !== undefined ? carouselData.image_url : carousel.image_url, + title: carouselData.title !== undefined ? carouselData.title : carousel.title, + content: carouselData.content !== undefined ? carouselData.content : carousel.content, + button_text: carouselData.button_text !== undefined ? carouselData.button_text : carousel.button_text, + button_link: carouselData.button_link !== undefined ? carouselData.button_link : carousel.button_link, + sort_order: carouselData.sort_order !== undefined ? carouselData.sort_order : carousel.sort_order, + link_url: carouselData.link_url !== undefined ? carouselData.link_url : carousel.link_url, + enabled: carouselData.enabled !== undefined ? carouselData.enabled : carousel.enabled, + }); + + return carousel; + } + + /** + * 删除轮播图(同时删除关联的图片文件) + * @param {number} id - 轮播图 ID + */ + async deleteCarousel(id) { + const { ctx } = this; + const carousel = await ctx.model.Carousel.findByPk(id); + if (!carousel) throw new Error('轮播图不存在'); + + // 删除关联的图片文件 + if (carousel.image_url) { + const path = require('path'); + const fs = require('fs'); + const app = this.app; + const filename = carousel.image_url.replace(/^\/public\/carousel\//, ''); + if (filename) { + const filePath = path.join(app.baseDir, 'app/public/carousel', filename); + try { + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + } + } catch (fileErr) { + ctx.logger.warn('删除轮播图图片文件失败:', fileErr.message); + } + } + } + + await ctx.model.Carousel.destroy({ where: { id } }); + } + + // ==================== 成绩查询 ==================== + // ==================== 成绩查询 ==================== + + /** + * 获取选手个人成绩详情 + * @param {number} aggregationUid - 聚合报名 ID + * @param {string} group - 组别 + * @returns {Promise} 成绩详情 + */ + async getContestantScore(aggregationUid, group) { + const { ctx } = this; + const result = {}; + + if (group === 'zhongzhi' || group === 'gaozhi') { + const SubmissionModel = group === 'zhongzhi' + ? ctx.model.ZhongzhiSubmission + : ctx.model.GaozhiSubmission; + + const submissions = await SubmissionModel.findAll({ + where: { aggregation_uid: aggregationUid }, + attributes: ['id', 'question_uid', 'answer_data', 'is_correct', 'score', 'word_file_path'], + }); + + const totalScore = submissions.reduce((sum, s) => sum + (s.score || 0), 0); + result.score = totalScore; + result.totalQuestions = submissions.length; + result.correctCount = submissions.filter(s => s.is_correct).length; + result.details = submissions; + result.hasWordFile = submissions.some(s => s.word_file_path); + } else { + const SubmissionModel = group === 'benke' + ? ctx.model.BenkeSubmission + : ctx.model.JiaoshiSubmission; + + const submissions = await SubmissionModel.findAll({ + where: { aggregation_uid: aggregationUid }, + attributes: ['id', 'work_name', 'file_name', 'file_path', 'file_size', 'upload_time', 'score'], + order: [['upload_time', 'DESC']], + }); + + result.score = submissions.reduce((sum, s) => sum + (s.score || 0), 0); + result.submissions = submissions; + } + + return result; + } +} + +module.exports = AdminService; diff --git a/app/service/auth.js b/app/service/auth.js new file mode 100644 index 0000000..5135cde --- /dev/null +++ b/app/service/auth.js @@ -0,0 +1,116 @@ +'use strict'; + +const Service = require('egg').Service; + +/** + * 认证服务 - AuthService + * + * 封装认证相关的数据库操作和业务逻辑。 + * 使用 Contestant 模型进行选手认证,Admin 模型进行管理员认证。 + * 密码采用 bcrypt 加盐哈希存储;存量 SHA256(无盐)密码在下次成功登录时透明升级为 bcrypt。 + */ +class AuthService extends Service { + /** + * 根据手机号查找选手 + * @param {string} phone - 手机号 + * @returns {object|null} 选手信息 + */ + async findByPhone(phone) { + const { ctx } = this; + const contestant = await ctx.model.Contestant.findOne({ + where: { phone }, + attributes: ['id', 'id_number', 'name', 'phone', 'group', 'password', 'activated', 'score'], + }); + return contestant; + } + + /** + * 根据身份证号查找选手(用于激活和忘记密码) + * @param {string} idNumber - 身份证号 + * @returns {object|null} 选手信息 + */ + async findByAccount(idNumber) { + const { ctx } = this; + + return await ctx.model.Contestant.findOne({ + where: { id_number: idNumber }, + }); + } + + /** + * 根据用户名查找管理员 + * + * @param {string} username - 管理员用户名 + * @returns {Promise} Admin 模型对象或 null + */ + async findAdmin(username) { + const { ctx } = this; + + return await ctx.model.Admin.findOne({ + where: { username }, + }); + } + + /** + * 验证密码 + * + * 兼容 bcrypt 哈希与存量 SHA256(无盐)哈希,便于平滑迁移。 + * + * @param {string} inputPassword - 用户输入的明文密码 + * @param {string} hashedPassword - 数据库中存储的哈希密码 + * @returns {Promise} 密码是否匹配 + */ + async verifyPassword(inputPassword, hashedPassword) { + const { ctx } = this; + return ctx.helper.comparePassword(inputPassword, hashedPassword); + } + + /** + * 透明升级:若账号仍使用旧版 SHA256 哈希,在验证通过后重写为 bcrypt。 + * 存量账号会在下一次成功登录时自动完成迁移,无需批量刷库。 + * + * @param {Model} account - 含 password 字段的模型实例(contestant 或 admin) + * @param {string} plainPassword - 明文密码 + * @returns {Promise} + */ + async upgradePasswordIfLegacy(account, plainPassword) { + const { ctx } = this; + if (account && account.password && !ctx.helper.isBcryptHash(account.password)) { + try { + const newHash = await ctx.helper.hashPassword(plainPassword); + await account.update({ password: newHash }); + } catch (e) { + // 升级失败不影响本次登录,仅记录日志,下次登录再尝试 + ctx.logger.warn('[密码迁移] bcrypt 升级失败:', e.message); + } + } + } + + /** + * 激活账号 + * + * 将指定 id 的选手设置为已激活状态,并更新密码。 + * + * @param {number} id - 选手 id + * @param {string} password - SHA256 哈希后的密码 + * @returns {Promise} 更新后的 Contestant 模型对象 + */ + async activateAccount(id, password) { + const { ctx } = this; + + const contestant = await ctx.model.Contestant.findOne({ + where: { id }, + }); + + if (!contestant) { + return null; + } + + return await contestant.update({ + activated: 1, + password, + }); + } +} + +module.exports = AuthService; diff --git a/app/service/exam.js b/app/service/exam.js new file mode 100644 index 0000000..dfc57c8 --- /dev/null +++ b/app/service/exam.js @@ -0,0 +1,745 @@ +'use strict'; + +const Service = require('egg').Service; +const path = require('path'); +const fs = require('fs'); +const crypto = require('crypto'); + +/** + * 考试服务 - ExamService + * + * 封装考试相关业务逻辑,包括题目随机获取、答案校验、自动判分、 + * 比赛状态检查、答题记录保存等功能。 + */ +class ExamService extends Service { + /** + * 根据用户的 id_number 获取聚合报名信息 + * @param {string} idNumber - 身份证号 + * @returns {Promise} 聚合报名记录 + */ + async getUserAggregation(idNumber) { + const { ctx } = this; + // 先通过 id_number 找到 Account,再通过 Account.uid 找到 Aggregation + const account = await ctx.model.Account.findOne({ + where: { id_number: idNumber }, + attributes: ['uid'], + }); + if (!account) return null; + + return await ctx.model.Aggregation.findOne({ + where: { account_uid: account.uid }, + include: [ + { model: ctx.model.Team, as: 'team', attributes: ['team_uid', 'group_name', 'team_id', 'team_number'] }, + ], + }); + } + + /** + * 根据题型获取对应的模型 + * @param {string} questionType - 题型:choice / judge / word + * @returns {Object} Sequelize 模型 + */ + _getQuestionModel(questionType) { + const { ctx } = this; + switch (questionType) { + case 'choice': return ctx.model.ChoiceQuestion; + case 'judge': return ctx.model.JudgeQuestion; + case 'word': return ctx.model.WordQuestion; + default: throw new Error(`未知题型: ${questionType}`); + } + } + + /** + * 从可能带题型前缀的 question_uid 中提取原始自增 id。 + * 兼容三种形态: + * - 'choice_12' / 'judge_3'(新:题型前缀,保证跨题型全局唯一) + * - '12'(旧:纯数字字符串) + * - 12(number) + * @param {string|number} uid + * @returns {string|number} 原始自增 id + */ + _extractRawId(uid) { + if (uid === undefined || uid === null) return uid; + const s = String(uid); + const idx = s.lastIndexOf('_'); + if (idx >= 0) { + const tail = s.slice(idx + 1); + return /^\d+$/.test(tail) ? tail : s; + } + return s; + } + + /** + * 随机获取指定组别的题目(选择题 + 判断题,按各自数量抽取) + * @param {string} group - 组别:zhongzhi / gaozhi + * @param {Object} options - 抽题数量配置 + * @param {number} options.choiceCount - 选择题数量,默认 50 + * @param {number} options.judgeCount - 判断题数量,默认 20 + * @returns {Promise} 题目列表(不含 answer 字段),顺序:选择题在前、判断题在后 + */ + async getRandomQuestions(group, options = {}) { + const { ctx } = this; + + // 严格按题型数量分别抽取,确保分布符合「50 道选择题 + 20 道判断题」的赛制要求, + // 而不是把单个总数对半切(否则会变成 35+35 等错误分布)。 + const choiceCount = options.choiceCount != null ? options.choiceCount : 50; + const judgeCount = options.judgeCount != null ? options.judgeCount : 20; + + const [choiceQuestions, judgeQuestions] = await Promise.all([ + ctx.model.ChoiceQuestion.findAll({ + where: { group }, + order: ctx.model.Sequelize.literal('RAND()'), + limit: choiceCount, + attributes: { exclude: ['answer'] }, + raw: true, + }), + ctx.model.JudgeQuestion.findAll({ + where: { group }, + order: ctx.model.Sequelize.literal('RAND()'), + limit: judgeCount, + attributes: { exclude: ['answer'] }, + raw: true, + }), + ]); + + // 合并并标记题型,并规范化输出字段 + // 顺序约定:选择题全部在前、判断题全部在后(各自内部仍由 RAND() 随机), + // 前端按全局索引编号,选择题 1~N、判断题 N+1~M,Word 综合题接续 M+1,题号严格连续。 + const questions = [ + ...choiceQuestions.map(q => normalizeQuestion(q, 'choice')), + ...judgeQuestions.map(q => normalizeQuestion(q, 'judge')), + ]; + + return questions; + } + + /** + * 按指定的题目 UID 顺序取题(不随机),用于「重进复用同一套题目与顺序」。 + * @param {string} group - 组别 + * @param {Array} questionUids - 固化顺序的 question_uid 数组,如 ['choice_12','judge_3',...] + * @returns {Promise} 严格按 questionUids 顺序的题目列表(缺失题目自动跳过) + */ + async getQuestionsByFixedOrder(group, questionUids) { + const { ctx } = this; + const buckets = { choice: [], judge: [] }; + for (const uid of (questionUids || [])) { + const s = String(uid); + const idx = s.lastIndexOf('_'); + if (idx < 0) continue; + const type = s.slice(0, idx); + const rawId = s.slice(idx + 1); + if ((type === 'choice' || type === 'judge') && /^\d+$/.test(rawId)) { + buckets[type].push(parseInt(rawId, 10)); + } + } + const out = []; + for (const type of ['choice', 'judge']) { + const ids = buckets[type]; + if (!ids.length) continue; + const Model = type === 'choice' ? ctx.model.ChoiceQuestion : ctx.model.JudgeQuestion; + const rows = await Model.findAll({ + where: { id: { [ctx.model.Sequelize.Op.in]: ids }, group }, + attributes: { exclude: ['answer'] }, + raw: true, + }); + const map = new Map(rows.map(r => [r.id, r])); + for (const id of ids) { + const r = map.get(id); + if (r) out.push(normalizeQuestion(r, type)); + } + } + return out; + } + + /** + * 获取/固化该考生的题目顺序:首次进入随机生成并持久化到 exam_submissions.question_order; + * 之后重进(或已提交后重进)一律复用同一顺序,彻底消除「重进题目顺序变乱」缺陷。 + * 兼容历史已提交但无 question_order 的记录:从其 answers 推导顺序(答案顺序即作答顺序)。 + * @param {number} aggregationUid - 聚合报名 ID(exam_submissions 以它为 contestant_id 存储) + * @param {string} group - 组别 + * @param {Object} options - 抽题数量配置 { choiceCount, judgeCount } + * @returns {Promise<{questions: Array, questionUids: Array}>} + */ + async getAssignedQuestions(aggregationUid, group, options = {}) { + const { ctx } = this; + // 确保该考生有一条记录(占位,submit_time 保持 null,不影响 hasSubmitted 判定)。 + const row = await this._ensureExamSubmissionSafe(aggregationUid, { + contestant_id: aggregationUid, + answers: [], + submit_time: null, + }); + + // 1) 已固化顺序 → 直接复用 + const saved = this._readQuestionOrder(row); + if (saved && saved.length) { + return { + questions: await this.getQuestionsByFixedOrder(group, saved), + questionUids: saved, + }; + } + + // 2) 历史已提交但无固化顺序:从 answers 推导(答案顺序即作答顺序) + const savedAnswers = row.answers; + if (Array.isArray(savedAnswers) && savedAnswers.length) { + const derived = savedAnswers + .map(a => a.question_uid || (a.question_type && a.question_id ? `${a.question_type}_${a.question_id}` : null)) + .filter(Boolean); + if (derived.length) { + await row.update({ question_order: JSON.stringify(derived) }); + return { + questions: await this.getQuestionsByFixedOrder(group, derived), + questionUids: derived, + }; + } + } + + // 3) 首次进入:随机生成并持久化(同一考生只发生一次) + const questions = await this.getRandomQuestions(group, options); + const uids = questions.map(q => q.question_uid); + await row.update({ question_order: JSON.stringify(uids) }); + return { questions, questionUids: uids }; + } + + /** + * 从 exam_submissions 记录中安全读取 question_order(JSON 字符串/对象 → 数组) + * @param {Object} row - 记录实例或原始对象 + * @returns {Array|null} + */ + _readQuestionOrder(row) { + if (!row) return null; + const v = row.question_order; + if (!v) return null; + try { + const a = typeof v === 'string' ? JSON.parse(v) : v; + return Array.isArray(a) ? a : null; + } catch (e) { + return null; + } + } + + /** + * 核对单道题目的答案 + * @param {number} questionId - 题目 ID + * @param {string} questionType - 题型:choice / judge / word + * @param {string} answerData - 选手提交的答案 + * @returns {Promise} { isCorrect, score, question } + */ + async checkAnswer(questionId, questionType, answerData) { + const QuestionModel = this._getQuestionModel(questionType); + + // question_uid 现在是「题型_自增id」的全局唯一形式(choice_12 / judge_3), + // 判分时需还原为原始自增 id 再 findByPk;同时兼容历史纯数字形态。 + const rawId = this._extractRawId(questionId); + const question = await QuestionModel.findByPk(rawId); + if (!question) { + return { isCorrect: false, score: 0, question: null, error: '题目不存在' }; + } + + // 比对答案(去除首尾空格,统一大小写) + const correctAnswer = String(question.answer).trim().toLowerCase(); + const userAnswer = String(answerData).trim().toLowerCase(); + const isCorrect = correctAnswer === userAnswer; + + return { + isCorrect, + score: isCorrect ? question.score : 0, + question: { + id: question.id, + question: question.question, + question_type: questionType, + correctAnswer: question.answer, + maxScore: question.score, + }, + }; + } + + /** + * 批量自动判分 + * @param {Array} answers - 答案数组 [{ question_id, question_type, answer_data }] + * @returns {Promise} { autoScore, total, details } + */ + async autoScore(answers) { + const details = []; + let autoScore = 0; + let total = 0; + + for (const item of answers) { + const result = await this.checkAnswer( + item.question_id || item.question_uid, + item.question_type, + item.answer_data + ); + if (result.question) { + total += result.question.maxScore; + autoScore += result.score; + details.push({ + question_id: item.question_id || item.question_uid, + question_type: item.question_type, + answer_data: item.answer_data, + is_correct: result.isCorrect, + score: result.score, + max_score: result.question.maxScore, + }); + } + } + + return { autoScore, total, details }; + } + + /** + * 查询比赛时间状态 + * @param {string} group - 组别 + * @returns {Promise} { isActive, startTime, endTime, countdown, status } + */ + async getExamStatus(group) { + const { ctx } = this; + + const timeConfig = await ctx.model.CompetitionTime.findOne({ + where: { group: group }, + }); + + // 未配置时间 + if (!timeConfig || !timeConfig.start_time || !timeConfig.end_time) { + return { + isActive: false, + startTime: null, + endTime: null, + countdown: 0, + status: 'not_set', + message: '考试时间尚未配置', + }; + } + + const now = new Date(); + const startTime = parseLocal(timeConfig.start_time); + const endTime = parseLocal(timeConfig.end_time); + + // 考试未开始 + if (now < startTime) { + return { + isActive: false, + startTime: startTime.toISOString(), + endTime: endTime.toISOString(), + countdown: 0, + status: 'not_started', + message: '考试尚未开始', + }; + } + + // 考试进行中 + if (now >= startTime && now <= endTime) { + const countdown = Math.max(0, Math.floor((endTime - now) / 1000)); + return { + isActive: true, + startTime: startTime.toISOString(), + endTime: endTime.toISOString(), + countdown, + status: 'in_progress', + message: '考试正在进行中', + }; + } + + // 考试已结束 + return { + isActive: false, + startTime: startTime.toISOString(), + endTime: endTime.toISOString(), + countdown: 0, + status: 'ended', + message: '考试已结束', + }; + } + + /** + * 获取 Word 模板文件路径 + * @param {string} group - 组别 + * @returns {Promise} 模板文件路径 + */ + async getWordTemplatePath(group) { + const { ctx } = this; + const timeConfig = await ctx.model.CompetitionTime.findOne({ + where: { group: group }, + attributes: ['id', 'word_template'], + }); + return timeConfig ? timeConfig.word_template : null; + } + + /** + * 保存答题记录(逐题存储) + * @param {number} aggregationUid - 聚合报名 ID + * @param {Array} answers - 答案数组 [{ question_uid, answer_data, is_correct, score }] + * @param {string} group - 组别 + * @returns {Promise} 保存后的记录列表 + */ + async saveAnswers(aggregationUid, answers, group) { + const { ctx } = this; + + // 根据组别选择对应的提交模型 + const SubmissionModel = group === 'zhongzhi' + ? ctx.model.ZhongzhiSubmission + : ctx.model.GaozhiSubmission; + + // 死锁判定:并发 upsert 抢同一唯一索引(uk_zhongzhi_submission)时会瞬时死锁, + // 属于可重试的瞬时冲突,整体重试事务即可(而非丢数据)。 + const isDeadlock = (e) => { + if (!e) return false; + if (e.code === 'ER_LOCK_DEADLOCK') return true; + if (e.parent && e.parent.code === 'ER_LOCK_DEADLOCK') return true; + return /ER_LOCK_DEADLOCK/.test(String(e.message || '')); + }; + + // 死锁重试(最多 3 次,指数退避)。事务整体提交/回滚是原子的,要么全落要么全不落。 + const MAX_ATTEMPTS = 3; + let lastErr = null; + for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) { + try { + await ctx.model.transaction(async (t) => { + for (const answer of answers) { + // 原子 upsert:依靠 (aggregation_uid, question_uid, question_type) 唯一约束, + // 并发作答时 MySQL 的 INSERT ... ON DUPLICATE KEY UPDATE 会将竞争请求 + // 折叠为对同一行的更新,彻底杜绝重复落库(B12 修复,覆盖 B11 的联合键)。 + // + // ⚠️ question_uid 列是 INT:必须先把前缀形态('choice_683')还原成原始数字 id, + // 否则 MySQL 会把字符串强转为 0,导致整卷 70 条答案坍缩成 choice/judge 各一行 + // 互相覆盖(B13 修复:uid 全局唯一化引入的落库回归)。 + // + // ⚠️ 注意:MariaDB/MySQL 不支持 RETURNING 子句,带 returning:true 时 Sequelize + // 会生成不被支持的 SQL,导致 upsert 静默不落库却返回「成功实例」。故此处去掉 + // returning,仅依赖事务提交 + 调用方 COUNT 复核(见 submitAll)。 + const rawQuestionId = this._extractRawId(answer.question_uid); + await SubmissionModel.upsert({ + aggregation_uid: aggregationUid, + question_uid: rawQuestionId, + question_type: answer.question_type, + answer_data: answer.answer_data, + is_correct: answer.is_correct, + score: answer.score, + }, { + conflictFields: ['aggregation_uid', 'question_uid', 'question_type'], + transaction: t, + }); + } + }); + return answers.length; + } catch (err) { + lastErr = err; + ctx.logger.error(`[saveAnswers] attempt ${attempt}/${MAX_ATTEMPTS} failed agg=${aggregationUid}:`, err && err.message); + if (isDeadlock(err) && attempt < MAX_ATTEMPTS) { + await new Promise(r => setTimeout(r, 50 * attempt)); + continue; + } + throw err; + } + } + throw lastErr; + } + + /** + * 并发安全地获取/创建 exam_submissions 提交记录。 + * 依赖 contestant_id 唯一约束:并发下若另一请求已插入,findOrCreate 的 create 会触发 + * ER_DUP_ENTRY,此时直接取回已有记录,避免产生重复行(B12 修复)。 + * @param {number} contestantId - 选手 ID + * @param {Object} defaults - 新建时的默认值 + * @returns {Promise} 提交记录实例 + */ + async _ensureExamSubmissionSafe(contestantId, defaults) { + const { ctx } = this; + try { + const [rec] = await ctx.model.ExamSubmission.findOrCreate({ + where: { contestant_id: contestantId }, + defaults, + }); + return rec; + } catch (e) { + if (e.name === 'SequelizeUniqueConstraintError' || + /ER_DUP_ENTRY/.test(String(e.message))) { + return await ctx.model.ExamSubmission.findOne({ where: { contestant_id: contestantId } }); + } + throw e; + } + } + + /** + * 获取/创建当前选手的 exam_submissions 提交记录(并发安全) + * @param {number} contestantId - 选手 ID + * @param {Array} answers - 答题内容 + * @returns {Promise} 提交记录实例 + */ + async ensureExamSubmission(contestantId, answers) { + const rec = await this._ensureExamSubmissionSafe(contestantId, { + contestant_id: contestantId, + answers: answers || [], + submit_time: new Date(), + }); + // 记录可能是团队锁占位 / Word 上传时创建的(submit_time 为 null,表示未正式提交)。 + // findOrCreate 命中已有行时不会更新字段,须显式补写 submit_time —— 否则 + // 正式提交后 hasSubmitted / 管理后台仍显示「未提交」(提交状态不一致修复)。 + if (rec && !rec.submit_time) { + await rec.update({ submit_time: new Date(), answers: answers || [] }); + } + return rec; + } + + /** + * 检查用户是否已正式提交全部考试 + * 仅当 exam_submissions 表中有 submit_time 记录时才判定为已提交 + * @param {number} aggregationUid - 聚合报名 ID + * @param {string} group - 组别 + * @returns {Promise} 是否已正式提交 + */ + async hasSubmitted(aggregationUid, group) { + const { ctx } = this; + // 提交组(本科/教师)的提交记录落在 benke_submissions / jiaoshi_submissions, + // 与考试组(中职/高职)的 exam_submissions 不同表。统一在此按组别判定, + // 使 GET /api/exam/:group/status 返回的 submitted 对全部组别一致生效 + // (前端 Dashboard「已提交」标签 + 进入拦截方可对提交组正确工作)。无新表列。 + if (group === 'benke' || group === 'jiaoshi') { + const SubmissionModel = group === 'benke' + ? ctx.model.BenkeSubmission + : ctx.model.JiaoshiSubmission; + // 🔴 聚合键铁律:上传链路(service.upload.getUserAggregation)写入的 + // benke/jiaoshi_submissions.aggregation_uid = aggregation 表的 aggregation_uid, + // 而本方法收到的 aggregationUid 来自 exam 控制器 = contestant.id,真实选手 + // 两者不相等(如郑佳怡 contestant.id=11 / aggregation_uid=1)。须按 + // account_uid → aggregation_uid 双向映射后再查(与 getScores 口径一致), + // 否则真实选手提交后 submitted 恒 false → 标签仍「进行中」+ 可重复进入。 + const keys = [ aggregationUid ]; + const agg = await ctx.model.Aggregation.findOne({ + where: { account_uid: aggregationUid }, + attributes: [ 'aggregation_uid' ], + }); + if (agg && agg.aggregation_uid && !keys.includes(agg.aggregation_uid)) { + keys.push(agg.aggregation_uid); + } + const count = await SubmissionModel.count({ + where: { aggregation_uid: keys }, + }); + return count > 0; + } + const record = await ctx.model.ExamSubmission.findOne({ + where: { contestant_id: aggregationUid }, + attributes: ['submit_time'], + }); + // 统一返回布尔(原为 record && ...,无记录时为 null),与提交组分支保持一致, + // 避免调用方对 submitted 字段类型判断不一致。 + return !!(record && record.submit_time !== null); + } + + /** + * 检查团队考试锁:同一小组是否已有人进入考试 + * + * 🔴 跨组 team_uid 复用防护(组内一人参赛锁的核心正确性前提): + * team_uid / team_id 仅在「组内唯一」,不同组别可复用同一个整数团队号 + * (实测:team_uid=1 同时存在于 zhongzhi 与 benke)。若不按组别限定队友, + * benke 组选手的 exam_submissions 行会把 zhongzhi 组选手误判为「队友已进入考试」, + * 导致 questions / submit-all 无故返回 4005「您的小组已进入考试」。 + * 因此队友候选集必须用 contestants.group 复筛为同组(选手组别的权威来源与 + * controller._getUserAggregation 的 team.group_name 一致,均取 contestant.group)。 + * + * @param {number} teamId - 团队 ID + * @param {number} excludeContestantId - 排除的选手 ID(当前用户) + * @param {string} [group] - 当前选手组别;传入时按组别限定队友(防跨组误锁) + * @returns {Promise} { locked: boolean, lockedBy: string|null } + */ + async checkTeamLock(teamId, excludeContestantId, group) { + const { ctx } = this; + if (!teamId) return { locked: false, lockedBy: null }; + + // 队友识别兼容两种团队来源: + // - contestants.team_id = teamId(导入流程填充) + // - aggregation.team_uid = teamId(聚合报名表填充,兼容旧数据) + const contestantWhere = { team_id: teamId }; + if (group) contestantWhere.group = group; + const contestants = await ctx.model.Contestant.findAll({ + where: contestantWhere, + attributes: ['id', 'name'], + }); + const contestantIds = contestants.map(c => c.id); + + // aggregation 表无组别列声明(Sequelize 模型未定义 group),故此来源先取全量 + // 候选,再由下方 contestants.group 复筛统一过滤跨组污染。 + const aggRows = await ctx.model.Aggregation.findAll({ + where: { team_uid: teamId }, + attributes: ['account_uid'], + }); + const aggIds = aggRows.map(a => a.account_uid); + + let allIds = Array.from(new Set([...contestantIds, ...aggIds])) + .filter(id => id !== excludeContestantId); + + if (allIds.length === 0) return { locked: false, lockedBy: null }; + + // 🔴 同组复筛:剔除跨组复用同一 team_uid 的他组选手(见方法头注释) + if (group) { + const sameGroup = await ctx.model.Contestant.findAll({ + where: { id: allIds, group }, + attributes: ['id'], + }); + allIds = sameGroup.map(r => r.id); + if (allIds.length === 0) return { locked: false, lockedBy: null }; + } + + const existingSubmission = await ctx.model.ExamSubmission.findOne({ + where: { + contestant_id: allIds, + }, + include: [{ + model: ctx.model.Contestant, + as: 'contestant', + attributes: ['name'], + }], + order: [['id', 'ASC']], + }); + + if (existingSubmission) { + return { + locked: true, + lockedBy: existingSubmission.contestant ? existingSubmission.contestant.name : '队友', + }; + } + + return { locked: false, lockedBy: null }; + } + + /** + * 设置团队考试锁(创建 exam_submission 占位记录) + * @param {number} teamId - 团队 ID + * @param {number} contestantId - 选手 ID + * @returns {Promise} + */ + async setTeamLock(teamId, contestantId) { + const { ctx } = this; + + // 使用并发安全方式获取/创建(唯一约束下避免重复行,B12 修复)。 + // ⚠️ 占位记录 submit_time 必须为 null:占位 ≠ 正式提交,否则选手一进考试 + // 就被 hasSubmitted 判为「已提交」(提交状态不一致 Bug 根源之一)。 + const rec = await this._ensureExamSubmissionSafe(contestantId, { + contestant_id: contestantId, + answers: [], + submit_time: null, + }); + return rec; + } + + /** + * 保存 Word 文件上传记录 + * @param {number} aggregationUid - 聚合报名 ID + * @param {number} questionUid - 题目 ID + * @param {string} group - 组别 + * @param {Object} fileInfo - 文件信息 { filePath, fileName, md5 } + * @returns {Promise} 保存后的记录 + */ + async saveWordFile(aggregationUid, questionUid, group, fileInfo) { + const { ctx } = this; + + // 查找该选手的答题记录 + let record = await ctx.model.ExamSubmission.findOne({ + where: { contestant_id: aggregationUid }, + }); + + // 不存在则并发安全地创建(唯一约束下避免重复行,B12 修复)。 + // ⚠️ 仅上传 Word ≠ 正式提交,submit_time 保持 null,正式提交由 + // ensureExamSubmission 统一补写(提交状态不一致修复)。 + if (!record) { + record = await this._ensureExamSubmissionSafe(aggregationUid, { + contestant_id: aggregationUid, + answers: [], + submit_time: null, + }); + } + + // 更新 Word 文件信息 + await record.update({ + word_file_path: fileInfo.filePath, + word_file_name: fileInfo.fileName, + }); + + return record; + } + + /** + * 获取某用户的所有答题记录 + * @param {number} aggregationUid - 聚合报名 ID + * @param {string} group - 组别 + * @returns {Promise} 答题记录列表 + */ + async getUserAnswers(aggregationUid, group) { + const { ctx } = this; + const SubmissionModel = group === 'zhongzhi' + ? ctx.model.ZhongzhiSubmission + : ctx.model.GaozhiSubmission; + + return await SubmissionModel.findAll({ + where: { aggregation_uid: aggregationUid }, + }); + } + + /** + * 计算 Word 文件 MD5 + * @param {string} filePath - 文件路径 + * @returns {Promise} MD5 值 + */ + async calculateFileMd5(filePath) { + return new Promise((resolve, reject) => { + const hash = crypto.createHash('md5'); + const stream = fs.createReadStream(filePath); + stream.on('data', chunk => hash.update(chunk)); + stream.on('end', () => resolve(hash.digest('hex'))); + stream.on('error', reject); + }); + } +} + +/** + * 将数据库中的时间字符串(如 '2026-07-22 09:00:00',无时区标记)按本地时区解析。 + * 注意:new Date('2026-07-22 09:00:00') 在 V8 中会按 UTC 解析,导致与业务库 + * 本地时间产生约 8 小时的偏移;将空格替换为 'T' 后按本地时区解析可修正该偏差。 + * @param {string|Date} ts - 时间字符串或 Date 对象 + * @returns {Date} + */ +function parseLocal(ts) { + if (!ts) return new Date(NaN); + if (ts instanceof Date) return ts; + const s = String(ts).trim().replace(' ', 'T'); + return new Date(s); +} + +/** + * 规范化题目输出字段 + * - 补齐 question_uid(铁律2:前端统一以 question_uid 作为题目稳定标识, + * 库表主键为自增 id,question_uid 以其字符串形式暴露) + * - options 在库中以 JSON 字符串存储,规范化为数组返回(judge 题型无 options) + * @param {Object} q - 原始题目(raw:true 结果) + * @param {string} questionType - choice / judge + * @returns {Object} 规范化后的题目 + */ +function normalizeQuestion(q, questionType) { + const out = { ...q }; + out.question_type = questionType; + if (out.question_uid === undefined || out.question_uid === null) { + // 题号全局唯一(关键修复):选择题与判断题各自的自增 id 都从 1 开始、会重复, + // 若仅用 id 作为 question_uid,choice#1 与 judge#1 会碰撞,导致: + // ① 前端 findIndex 命中错误位置 → 序号重复(如判断题里出现「1」)、真实序号丢失(如「55」不见); + // ② answers 以 question_uid 为键 → 两题相互覆盖 → 答完仍提示「未作答」/题目丢失。 + // 因此以「题型_自增id」作为跨题型全局唯一的稳定标识。 + out.question_uid = questionType + '_' + out.id; + } + if (questionType === 'choice') { + if (typeof out.options === 'string') { + try { + const parsed = JSON.parse(out.options); + out.options = Array.isArray(parsed) ? parsed : []; + } catch (e) { + out.options = []; + } + } else if (!Array.isArray(out.options)) { + out.options = []; + } + } else { + // 判断题无选项 + out.options = []; + } + return out; +} + +module.exports = ExamService; diff --git a/app/service/news.js b/app/service/news.js new file mode 100644 index 0000000..9e95dda --- /dev/null +++ b/app/service/news.js @@ -0,0 +1,132 @@ +'use strict'; + +const Service = require('egg').Service; + +/** + * 新闻服务 - NewsService + * + * 封装新闻相关的数据库查询操作。 + * 支持按组别过滤、按置顶过滤、分页查询。 + */ +class NewsService extends Service { + /** + * 查询新闻列表(支持分页和过滤) + * + * 过滤参数: + * - group: 组别筛选(如 zhongzhi, gaozhi, benke, jiaoshi),同时包含 group='all' 的通用新闻 + * - pinned: 是否仅查询置顶新闻(true/false) + * + * 排序规则:pinned DESC, publish_time DESC + * + * @param {Object} params - 查询参数 + * @param {string} [params.group] - 目标组别 + * @param {string|boolean} [params.pinned] - 是否只查置顶 + * @param {number} [params.page=1] - 页码 + * @param {number} [params.pageSize=20] - 每页数量 + * @returns {Promise<{list: Array, total: number, page: number, pageSize: number}>} 分页结果 + */ + async list(params = {}) { + const { ctx } = this; + const { Sequelize } = ctx.app; + + const { group, pinned, page, pageSize } = params; + + // 分页参数,默认第 1 页,每页 20 条 + const currentPage = parseInt(page) || 1; + const currentPageSize = parseInt(pageSize) || 20; + const offset = (currentPage - 1) * currentPageSize; + + // 构建查询条件 + const where = {}; + + // 组别过滤:同时包含指定组别和通用(all)新闻 + if (group) { + where.group = { + [Sequelize.Op.in]: [group, 'all'], + }; + } + + // 置顶过滤 + if (pinned !== undefined && pinned !== null && pinned !== '') { + where.pinned = pinned === 'true' || pinned === true; + } + + // 构建查询 + const queryOptions = { + where, + order: [ + ['pinned', 'DESC'], + ['sort_order', 'ASC'], + ['publish_time', 'DESC'], + ], + attributes: ['id', 'title', 'content', 'publish_time', 'pinned', 'group', 'tags', 'sort_order', 'created_at', 'updated_at'], + offset, + limit: currentPageSize, + }; + + // 查询新闻列表和总数 + const [list, total] = await Promise.all([ + ctx.model.News.findAll(queryOptions), + ctx.model.News.count({ where }), + ]); + + return { + list, + total, + page: currentPage, + pageSize: currentPageSize, + }; + } + + /** + * 获取各分组新闻计数(用于首页 Tab 显示数量) + * @returns {Promise} { all: number, zhongzhi: number, gaozhi: number, benke: number, jiaoshi: number } + */ + async getGroupCounts() { + const { ctx } = this; + const { Sequelize } = ctx.app; + const groups = ['all', 'zhongzhi', 'gaozhi', 'benke', 'jiaoshi']; + const counts = { all: 0, zhongzhi: 0, gaozhi: 0, benke: 0, jiaoshi: 0 }; + + // 并行查询各分组计数 + await Promise.all(groups.map(async group => { + const where = {}; + // 对于具体组别,同时包含该组别和 all 通用新闻 + if (group !== 'all') { + where.group = { [Sequelize.Op.in]: [group, 'all'] }; + } + counts[group] = await ctx.model.News.count({ where }); + })); + + return counts; + } + + /** + * 根据 ID 查询新闻详情 + * + * @param {number} id - 新闻 ID + * @returns {Promise} 新闻模型对象或 null + */ + async findById(id) { + const { ctx } = this; + + return await ctx.model.News.findOne({ + where: { id }, + attributes: ['id', 'title', 'content', 'publish_time', 'pinned', 'group', 'tags', 'sort_order', 'created_at', 'updated_at'], + }); + } + + /** + * 批量更新新闻排序 + * @param {Array<{id: number, sort_order: number}>} orders - 排序信息数组 + * @returns {Promise} + */ + async updateSortOrder(orders) { + const { ctx } = this; + await Promise.all(orders.map(async ({ id, sort_order }) => { + await ctx.model.News.update({ sort_order }, { where: { id } }); + })); + } +} + +module.exports = NewsService; diff --git a/app/service/sms.js b/app/service/sms.js new file mode 100644 index 0000000..c880167 --- /dev/null +++ b/app/service/sms.js @@ -0,0 +1,78 @@ +'use strict'; + +const Service = require('egg').Service; + +/** + * 短信网关服务(阿里云 Dysmsapi) + * + * 仅负责把"服务端已生成的验证码"下发给手机号,不参与验证码的存证与校验 + * (校验仍由 ctx.session 在 auth 控制器中完成,契约见 verify_sms_and_import_lock 测试)。 + * + * 依赖 SDK(生产环境需 npm install): + * @alicloud/openapi-client + * @alicloud/dysmsapi20170525 + * @alicloud/tea-util + */ +class SmsService extends Service { + /** + * 发送短信验证码 + * @param {string} phone - 接收短信的手机号 + * @param {string} code - 6 位验证码(由调用方生成并存入 session) + * @returns {Promise<{ok:boolean, message:string}>} + */ + async sendVerifyCode(phone, code) { + const { ctx, app } = this; + const cfg = app.config.sms && app.config.sms.aliyun; + if (!cfg || !cfg.accessKeyId || !cfg.accessKeySecret) { + ctx.logger.error('[短信网关] 未配置阿里云密钥,无法发送'); + return { ok: false, message: '短信网关未配置' }; + } + + let OpenApi; + let Dysmsapi; + let Util; + try { + OpenApi = require('@alicloud/openapi-client'); + Dysmsapi = require('@alicloud/dysmsapi20170525'); + Util = require('@alicloud/tea-util'); + } catch (e) { + ctx.logger.error('[短信网关] 未安装阿里云 SDK:' + e.message); + return { ok: false, message: '短信网关 SDK 未安装' }; + } + + try { + const config = new OpenApi.Config({ + accessKeyId: cfg.accessKeyId, + accessKeySecret: cfg.accessKeySecret, + endpoint: cfg.endpoint, + }); + const client = new Dysmsapi.default(config); + + const request = new Dysmsapi.SendSmsRequest({ + phoneNumbers: String(phone), + signName: cfg.signName, + templateCode: cfg.templateCode, + templateParam: JSON.stringify({ code: String(code) }), + }); + + const runtime = new Util.RuntimeOptions({}); + runtime.readTimeout = 5000; + runtime.connectTimeout = 5000; + + const response = await client.sendSmsWithOptions(request, runtime); + const body = response && response.body; + if (body && body.code === 'OK') { + ctx.logger.info(`[短信网关] 验证码已下发至 ${phone}`); + return { ok: true, message: '验证码发送成功' }; + } + const reason = (body && (body.message || body.code)) || '未知错误'; + ctx.logger.error(`[短信网关] 下发失败:${reason}`); + return { ok: false, message: reason }; + } catch (err) { + ctx.logger.error('[短信网关] 调用异常:' + (err && err.message)); + return { ok: false, message: (err && err.message) || '短信网关调用异常' }; + } + } +} + +module.exports = SmsService; diff --git a/app/service/upload.js b/app/service/upload.js new file mode 100644 index 0000000..3d3be0a --- /dev/null +++ b/app/service/upload.js @@ -0,0 +1,368 @@ +'use strict'; + +const Service = require('egg').Service; +const path = require('path'); +const fs = require('fs'); +const crypto = require('crypto'); + +/** + * 上传服务 - UploadService + * + * 封装作品文件上传相关业务逻辑,包括截止时间校验、文件保存、 + * 分片上传与合并等功能。适用于本科组和教师组的作品提交。 + */ +class UploadService extends Service { + /** + * 获取用户的聚合报名信息 + * @param {number} accountUid - 账号 UID + * @returns {Promise} 聚合报名记录 + */ + async getUserAggregation(accountUid) { + const { ctx } = this; + return await ctx.model.Aggregation.findOne({ + where: { account_uid: accountUid }, + include: [ + { + model: ctx.model.Team, + as: 'team', + attributes: ['team_uid', 'group_name', 'team_id', 'team_number'], + }, + ], + }); + } + + /** + * 验证提交截止时间 + * @param {string} group - 组别:benke / jiaoshi + * @returns {Promise} { valid, message, deadline } + */ + async validateDeadline(group) { + const { ctx } = this; + + const timeConfig = await ctx.model.CompetitionTime.findOne({ + where: { group }, + }); + + // 权威截止时间 = end_time(管理后台「提交截止时间」输入框绑定的正是该列)。 + // 历史遗留的 deadline 列仅作后备,避免出现「状态标签(按 end_time)显示进行中、 + // 上传却按旧 deadline 判定已截止」的两条时间线不一致问题。 + const effective = (timeConfig && (timeConfig.end_time || timeConfig.deadline)) || null; + if (!effective) { + return { + valid: false, + message: '提交截止时间尚未设置', + deadline: null, + }; + } + + const now = new Date(); + const deadline = new Date(effective); + + if (now > deadline) { + return { + valid: false, + message: '提交已截止', + deadline: deadline.toISOString(), + }; + } + + return { + valid: true, + message: '可以提交', + deadline: deadline.toISOString(), + }; + } + + /** + * 保存作品文件 + * @param {ReadableStream} stream - 文件流 + * @param {string} filename - 原始文件名 + * @param {string} group - 组别 + * @returns {Promise} { filePath, fileName, fileSize, md5 } + */ + async saveWorkFile(stream, filename, group) { + const { app } = this; + + // 确保上传目录存在(无条件递归创建,避免父目录偶发缺失 / TOCTOU 导致 ENOENT) + const uploadDir = path.join(app.baseDir, 'app/public/works', group); + try { + fs.mkdirSync(uploadDir, { recursive: true }); + } catch (mkErr) { + this.app.logger.error('[saveWorkFile] mkdir failed', { uploadDir, message: mkErr.message }); + throw mkErr; + } + + // 构建安全文件名 + const ext = path.extname(filename); + const timestamp = Date.now(); + const safeFileName = `work_${timestamp}_${Math.random().toString(36).substring(2, 8)}${ext}`; + const filePath = path.join(uploadDir, safeFileName); + + // 将文件流写入磁盘(用 pipe 自动处理背压,避免大文件内存暴涨 / 写入被截断) + const writeStream = fs.createWriteStream(filePath); + let fileSize = 0; + + await new Promise((resolve, reject) => { + stream.pipe(writeStream); + stream.on('data', chunk => { fileSize += chunk.length; }); + writeStream.on('finish', resolve); + stream.on('error', reject); + writeStream.on('error', reject); + }); + + // 🔴 落盘完整性校验(F17-B 防「假成功」): + // 文件必须真实存在且字节数与流读数一致,否则视为上传失败, + // 绝不允许把「写了一半 / 没写出来的文件」记成成功——那会导致管理后台下载时 404。 + const stat = fs.statSync(filePath); + if (stat.size !== fileSize) { + // 🔴 清理半截文件用 best-effort(同源缺陷族第三处): + // 裸 unlinkSync 一旦失败会抛出「删除失败」,把下面那条真正有诊断价值的 + // 「文件写入不完整:期望 X 实际 Y」**顶掉**,排障时被引向错误方向。 + // 清理成不成功都不影响结论——写入不完整这件事必须原样抛出。 + this.ctx.helper.safeCleanup(filePath, { label: 'partialFile' }); + throw new Error(`文件写入不完整:期望 ${fileSize} 字节,实际 ${stat.size} 字节`); + } + + // 计算文件 MD5 + const md5 = await this._calculateMd5(filePath); + + // 构建相对路径(相对于 app/public/) + const relativePath = path.join('works', group, safeFileName).replace(/\\/g, '/'); + + return { + filePath: relativePath, + fileName: filename, + fileSize, + md5, + }; + } + + /** + * 合并文件分片 + * @param {Array} chunks - 分片文件路径数组(按索引排序) + * @param {string} targetPath - 目标文件路径 + * @returns {Promise} { filePath, fileName, fileSize, md5 } + */ + async mergeFileChunks(chunks, targetPath) { + // 确保目标目录存在(无条件递归创建,避免父目录偶发缺失 / TOCTOU 导致 ENOENT) + const targetDir = path.dirname(targetPath); + try { + fs.mkdirSync(targetDir, { recursive: true }); + } catch (mkErr) { + this.app.logger.error('[mergeFileChunks] mkdir failed', { targetDir, message: mkErr.message }); + throw mkErr; + } + + // 按顺序合并所有分片。 + // 🔴 并发要点(2026-08-10 修):这里过去用 fs.readFileSync 把整个分片同步读进内存, + // 再 writeStream.write(buf) 且不等 drain。后果有二: + // ① 同步读盘会「冻结 event loop」——一个 50MB 文件合并期间,同一 worker 上 + // 其它考生的上传/答题请求全部排队,几十人同时交卷时表现为集体卡死; + // ② 不理会背压,N 个分片 Buffer 全堆在写队列里,单次合并瞬时吃掉整份文件的内存。 + // 现改为逐分片流式 pipe({ end: false } 续写),全程异步 + 自动背压, + // 内存占用恒定在一个 highWaterMark(64KB),event loop 不再被阻塞。 + // + // 🔴 监听器修复(2026-08-11):原实现在「循环内」对每个分片都执行 + // `writeStream.on('error', reject)`,单一 writeStream 会被挂上 ≈分片数 个 + // error 监听(10 分片 → 11 个),极端并发下触发 MaxListenersExceededWarning + // 并拖垮单 worker。现改为 writeStream.error 只挂【一次】,并用 settled 防重入。 + let totalSize = 0; + await new Promise((resolve, reject) => { + const writeStream = fs.createWriteStream(targetPath); + let settled = false; + const fail = (err) => { + if (settled) return; + settled = true; + writeStream.destroy(); + reject(err); + }; + // 仅挂一次:writeStream 自身的错误 + writeStream.on('error', fail); + + (async () => { + try { + for (const chunkPath of chunks) { + await new Promise((res, rej) => { + const readStream = fs.createReadStream(chunkPath); + // 每个 readStream 是独立 emitter,挂一个 error 监听是合理的 + readStream.on('error', rej); + readStream.on('data', buf => { totalSize += buf.length; }); + readStream.on('end', res); + readStream.pipe(writeStream, { end: false }); + }); + // 删除已合并的分片文件(异步,避免同步 unlink 累积阻塞) + await fs.promises.unlink(chunkPath).catch(() => {}); + } + writeStream.end(); + writeStream.on('finish', () => { + if (settled) return; + settled = true; + resolve(); + }); + } catch (e) { + fail(e); + } + })(); + }).catch((mergeErr) => { + // 清理半截目标文件,绝不把不完整文件记为成功(F17-B 防假成功) + return fs.promises.unlink(targetPath).catch(() => {}).then(() => { throw mergeErr; }); + }); + + // 落盘完整性校验:合并后字节数必须与读入总量一致 + const mergedStat = await fs.promises.stat(targetPath); + if (mergedStat.size !== totalSize) { + await fs.promises.unlink(targetPath).catch(() => {}); + throw new Error(`分片合并不完整:期望 ${totalSize} 字节,实际 ${mergedStat.size} 字节`); + } + + // 计算合并后文件的 MD5 + const md5 = await this._calculateMd5(targetPath); + + const fileName = path.basename(targetPath); + + return { + filePath: targetPath, + fileName, + fileSize: totalSize, + md5, + }; + } + + /** + * 计算文件 MD5 值 + * @param {string} filePath - 文件路径 + * @returns {Promise} MD5 哈希值 + */ + async _calculateMd5(filePath) { + return new Promise((resolve, reject) => { + const hash = crypto.createHash('md5'); + const stream = fs.createReadStream(filePath); + stream.on('data', chunk => hash.update(chunk)); + stream.on('end', () => resolve(hash.digest('hex'))); + stream.on('error', reject); + }); + } + + /** + * 保存作品提交记录到数据库 + * @param {number} aggregationUid - 聚合报名 ID + * @param {string} group - 组别 + * @param {Object} fileInfo - 文件信息 { filePath, fileName, fileSize, md5 } + * @param {string} workName - 作品名称(可选) + * @returns {Promise} 保存的记录 + */ + async saveSubmissionRecord(aggregationUid, group, fileInfo, workName = '', fileType = 'word') { + const { ctx } = this; + + const SubmissionModel = group === 'benke' + ? ctx.model.BenkeSubmission + : ctx.model.JiaoshiSubmission; + + // 按 (aggregation_uid, file_type) 定位记录:Word / PPT 各自独立存储, + // 避免后上传的文件覆盖先上传的文件(修复 PPT 顶掉 Word 的问题)。 + const existing = await SubmissionModel.findOne({ + where: { aggregation_uid: aggregationUid, file_type: fileType }, + }); + + const payload = { + work_name: workName || fileInfo.fileName, + file_path: fileInfo.filePath, + file_name: fileInfo.fileName, + file_size: fileInfo.fileSize, + file_md5: fileInfo.md5, + upload_time: new Date(), + file_type: fileType, + }; + + if (existing) { + await existing.update({ + ...payload, + score: null, // 重新上传后清除评分 + }); + return existing; + } + + return await SubmissionModel.create({ + aggregation_uid: aggregationUid, + ...payload, + }); + } + + /** + * 查询用户的提交状态 + * @param {number} aggregationUid - 聚合报名 ID + * @param {string} group - 组别 + * @returns {Promise} 是否已提交 + */ + async hasSubmitted(aggregationUid, group) { + const { ctx } = this; + const SubmissionModel = group === 'benke' + ? ctx.model.BenkeSubmission + : ctx.model.JiaoshiSubmission; + + const count = await SubmissionModel.count({ + where: { aggregation_uid: aggregationUid }, + }); + return count > 0; + } + + /** + * 获取用户的提交历史列表 + * @param {number} aggregationUid - 聚合报名 ID + * @param {string} group - 组别 + * @returns {Promise} 提交记录列表 + */ + async getSubmissions(aggregationUid, group) { + const { ctx } = this; + const SubmissionModel = group === 'benke' + ? ctx.model.BenkeSubmission + : ctx.model.JiaoshiSubmission; + + return await SubmissionModel.findAll({ + where: { aggregation_uid: aggregationUid }, + order: [['upload_time', 'DESC']], + include: [ + { + model: ctx.model.Aggregation, + as: 'aggregation', + attributes: ['aggregation_uid', 'team_uid', 'account_uid', 'identity'], + }, + ], + }); + } + + /** + * 删除用户的提交记录及其物理文件 + * @param {number} aggregationUid - 聚合报名 ID + * @param {string} group - 组别 + * @param {number} id - 提交记录 ID + * @returns {Promise} 删除成功返回 true,未找到(或无权限)返回 false + */ + async deleteSubmission(aggregationUid, group, id) { + const { ctx, app } = this; + const SubmissionModel = group === 'benke' + ? ctx.model.BenkeSubmission + : ctx.model.JiaoshiSubmission; + + // 仅允许删除本人名下的记录(聚合键强约束,防越权) + const record = await SubmissionModel.findOne({ + where: { id, aggregation_uid: aggregationUid }, + }); + if (!record) return false; + + // 删除物理文件(file_path 为相对 app/public 的路径,如 works/benke/xxx.docx) + if (record.file_path) { + const absPath = path.join(app.baseDir, 'app/public', record.file_path); + try { + if (fs.existsSync(absPath)) fs.unlinkSync(absPath); + } catch (e) { + ctx.logger.error('[deleteSubmission] 删除物理文件失败:', e.message); + } + } + + await record.destroy(); + return true; + } +} + +module.exports = UploadService; diff --git a/app/utils/filename.js b/app/utils/filename.js new file mode 100644 index 0000000..f1c2a3b --- /dev/null +++ b/app/utils/filename.js @@ -0,0 +1,62 @@ +'use strict' + +/** + * 上传文件名归一化。 + * + * 背景(生产事故复盘 2026-08-10): + * 备赛资料上传曾出现「列表读取 ENOENT」——磁盘上的文件名是乱码(GBK 字节被按 + * UTF-8 误解码)+ 前导空格,导致 admin.getMaterials 在 readdirSync().map(statSync) + * 时 stat 不到该文件而整列抛错。根因是 uploadMaterial 直接拿 stream.filename 当 + * 磁盘名、未做编码/空白归一化。 + * + * 本函数对传入的「客户端原始文件名」做最小但充分的清洗,保证落盘名: + * - 是合法 UTF-8(NFC 归一化,消除组合字符/变体差异) + * - 不含控制字符与路径分隔符(防目录穿越 + 防 stat 失败) + * - 不含前导/多余空白(折叠为单空格并 trim —— 直接修复「前导空格 + 乱码」类 ENOENT) + * - 长度受限(保留扩展名) + * 若清洗后为空(极端情况),调用方应回退到时间戳名,避免写出空名文件。 + * + * 注意:本函数不负责「还原」客户端用错误编码(如 GBK)发来的文件名——那是客户端 + * 规范问题;它的职责是「让落盘名干净、可往返、不会让读盘崩溃」。已落盘的旧乱码文件 + * 需运维侧手动 rm(见 diag_orphan + 人工清理)。 + */ + +function sanitizeUploadFileName(rawName, fallbackExt = '') { + if (!rawName || typeof rawName !== 'string') return '' + + let name = rawName + + // 1) 客户端可能预编码(如 %E5%89%8D...),先尝试解码 + if (name.includes('%') && /%[0-9A-Fa-f]{2}/.test(name)) { + try { name = decodeURIComponent(name) } catch (e) { /* 解码失败保留原串 */ } + } + + // 2) Unicode 归一化(NFC),消除同一字符的不同编码形态 + try { + if (typeof name.normalize === 'function') name = name.normalize('NFC') + } catch (e) { /* 兜底保留 */ } + + // 3) 去除控制字符(C0: 0x00-0x1F,DEL/0x7F)与路径分隔符/危险字符 + // 控制字符在正则里用 \x00-\x1f 与 \x7f 表示,避免源码里写裸控制字节 + name = name.replace(/[\x00-\x1f\x7f]/g, '') + name = name.replace(/[\/\\:*?"<>|]/g, '_') + + // 4) 折叠所有空白(含全角/不间断空格等 \s 覆盖的字符)为单空格并 trim + name = name.replace(/\s+/g, ' ').trim() + + // 5) 去除首尾点(避免 ".name" / "name." / ".." 类) + name = name.replace(/^[. ]+/, '').replace(/[. ]+$/, '') + + // 6) 长度限制(保留扩展名) + const MAX = 120 + if (name.length > MAX) { + const dot = name.lastIndexOf('.') + const ext = dot > 0 ? name.slice(dot) : '' + const safeExt = ext || fallbackExt || '' + name = name.slice(0, MAX - safeExt.length) + safeExt + } + + return name +} + +module.exports = { sanitizeUploadFileName } diff --git a/app/utils/sendFile.js b/app/utils/sendFile.js new file mode 100644 index 0000000..b59264a --- /dev/null +++ b/app/utils/sendFile.js @@ -0,0 +1,109 @@ +'use strict'; + +const path = require('path'); +const fs = require('fs'); +const { acquire: acquireFileIo, release: releaseFileIo } = require('../lib/file_io_guard'); + +/** + * 统一文件发送出口(X-Accel-Redirect 根治方案 + 完整性哨兵)。 + * + * 所有「把磁盘文件交给调用方」的通道都必须经此函数,杜绝同源漏网。 + * + * 2026-08-12 同源扩大化测试审计「缺口9」修复:materials.js(备赛资料) / exam.js(Word 模板) + * / admin.js(batchDownload ZIP 导出) 此前各自直写 fs.createReadStream,缺 X-File-Size 哨兵头, + * 导致前端 assertRealFile 的大小完整性校验收不到哨兵而失效(truncate / nginx fallback 替换 + * 无法被前端侦测)。重构进本函数后,未来任意通道的逻辑升级都会自动同步。 + * + * @param {object} ctx Egg 上下文 + * @param {object} app Egg app(读 config.xAccelRedirect / baseDir) + * @param {object} opts + * - relativePath {string} 相对 app/public 的路径(如 works/benke/x.docx);X-Accel 用此拼内部路径 + * - absolutePath {string} 绝对路径(如临时 zip);提供则忽略 relativePath,且强制 Node 流 + * - fileName {string} 原始文件名(Content-Disposition) + * - contentType {string} MIME 类型 + * - useXAccel {boolean} 是否允许 X-Accel 卸载(默认跟随 cfg.enabled;临时文件务必 false) + */ +async function sendFile(ctx, app, opts) { + const { absolutePath, relativePath, fileName, contentType, useXAccel } = opts; + const cfg = (app.config && app.config.xAccelRedirect) || { enabled: false, internalPrefix: '/internal-downloads/' }; + + // 路径解析 + let absPath; + if (absolutePath) { + absPath = absolutePath; + } else { + // 🔴 路径穿越防护:只允许 app/public 下的安全相对路径(字母/数字/_./- 且不含 ..) + if (!relativePath || !/^[A-Za-z0-9_.\/-]+$/.test(relativePath) || relativePath.includes('..')) { + ctx.status = 400; + ctx.body = ctx.helper.error(400, '非法文件路径'); + return; + } + absPath = path.join(app.baseDir, 'app/public', relativePath); + } + + // 🔴🔴 磁盘存在性校验(F17-B 防「假成功」· 2026-08-12 生产事故根因) + // 发头之前先确认磁盘上真有这个文件,缺失一律回干净 404(业务错误信封), + // 绝不把「下载」交给一个不存在的路径去碰运气。必须在 acquireFileIo 之前,避免 404 还配额。 + let fileStat; + try { + fileStat = fs.statSync(absPath); + } catch (e) { + ctx.logger.error(`[sendFile] 文件缺失,拒绝下载 abs=${absPath}:${e && e.message}`); + ctx.status = 200; // 业务错误统一 200 + 信封,前端 safeDownload / fetchBlobGuarded 会识别并弹错 + ctx.body = ctx.helper.error(404, `文件不存在(期望路径: ${relativePath || absolutePath})`); + return; + } + if (!fileStat.isFile()) { + ctx.body = ctx.helper.error(400, '目标不是一个有效文件'); + return; + } + + // 🔴 文件 IO 并发护栏(与上传通道共享同一全局配额):把「同时进行的文件流」限制在上限, + // 防止单 worker 在极端并发下被海量文件流压垮。配额必须在「打开/持有流」之前获取。 + let released = false; + const rel = () => { if (!released) { released = true; releaseFileIo(); } }; + try { + await acquireFileIo(); + } catch (e) { + ctx.status = 429; + ctx.body = ctx.helper.error(429, '服务器繁忙,下载通道暂满,请稍后重试'); + return; + } + + // X-Accel 决策:absolutePath(临时文件)强制 Node 流,避免「发头即 unlink」竞态损坏; + // 即使调用方误传 useXAccel:true + absolutePath,也因无 relativePath 拼内部路径而降级为 Node 流。 + const xAccel = (useXAccel === undefined ? cfg.enabled : useXAccel) && !!relativePath; + if (xAccel) { + // 🔴 X-Accel-Redirect:把文件交给 nginx 内部 location 直接从磁盘流式吐出, + // Node 只发响应头、不再持有文件流 → 极端并发下几乎零占用 worker。 + const internalPath = cfg.internalPrefix.replace(/\/+$/, '/') + relativePath.replace(/^\/+/, ''); + ctx.set('Content-Type', contentType); + // 🔴 下载完整性哨兵:把「服务端认定的真实字节数」透出给前端。 + // X-Accel 分支下真正吐字节的是 nginx,Node 无从得知 nginx 是否吐对了东西 + // (内部 location 缺失时会吐前端 index.html)。前端会比对 blob.size 与本头,不一致即拒落盘。 + // nginx 的 X-Accel-Redirect 会保留上游自定义响应头,故此头可穿透到浏览器。 + ctx.set('X-File-Size', String(fileStat.size)); + // 中文名按 RFC 5987 编码(filename*=UTF-8''),浏览器自动解码; + // 切勿再包 encodeURIComponent 造成双重编码。 + ctx.set('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(fileName)}`); + ctx.set('X-Accel-Redirect', internalPath); + ctx.set('X-Accel-Buffering', 'no'); // 大文件边收边发,避免 nginx 缓冲撑爆内存 + ctx.status = 200; + ctx.body = null; + rel(); // X-Accel 路径无 Node 文件流,立即释放配额 + } else { + // 开发回退 / 临时文件:Node 直接流式(与改造前行为一致)。配额须随流「生命周期」释放, + // 不能放在方法返回时的 finally —— 流的读取发生在控制器返回之后。 + ctx.attachment(fileName); + ctx.set('Content-Type', contentType); + // 与 X-Accel 分支保持一致,前端可统一按 X-File-Size 校验完整性 + ctx.set('X-File-Size', String(fileStat.size)); + const stream = fs.createReadStream(absPath); + stream.on('end', rel); + stream.on('close', rel); + stream.on('error', rel); + ctx.body = stream; + } +} + +module.exports = { sendFile }; diff --git a/app/validate/index.js b/app/validate/index.js new file mode 100644 index 0000000..1aa85a7 --- /dev/null +++ b/app/validate/index.js @@ -0,0 +1,86 @@ +'use strict'; + +/** + * 统一输入校验规则集(基于 egg-validate / parameter) + * + * 对应各接口请求体的字段级校验。校验失败由控制器以 + * `ctx.helper.error(400, '参数校验失败:' + validateErr.errors[0].message)` + * 统一封装为应用错误信封,与 admin 控制器既有模式保持一致。 + * + * 规则说明: + * - type: 'string' + required: true + allowEmpty: false:字段必填且不可为空串/纯空白 + * - trim: true:校验前去除首尾空白 + * - min: 6:字符串最小长度(用于密码强度底线) + * - message:为每个校验项提供中文错误文案,parameter 会在 errors[] 中携带对应 message + */ + +const required = (msg) => ({ + type: 'string', + required: true, + allowEmpty: false, + trim: true, + message: { required: msg, empty: msg }, +}); + +module.exports = { + // 选手登录:手机号 + 密码 + login: { + phone: required('请输入手机号'), + password: required('请输入密码'), + }, + + // 管理员登录:用户名 + 密码 + adminLogin: { + username: required('请输入管理员用户名'), + password: required('请输入密码'), + }, + + // 发送短信验证码:身份证号 + 图形验证码 + sms: { + phone: required('请输入身份证号'), + captcha: required('请输入图形验证码'), + }, + + // 账号激活:身份证号 + 手机号 + 短信验证码 + 密码(>=6) + activate: { + id_number: required('请输入身份证号'), + phone: required('请输入手机号'), + smsCode: required('请输入短信验证码'), + password: { + type: 'string', + required: true, + allowEmpty: false, + trim: true, + min: 6, + message: { + required: '请输入密码', + empty: '请输入密码', + min: '密码长度不能少于 6 位', + }, + }, + }, + + // 忘记密码-发码:身份证号 + 图形验证码 + forgotPassword: { + id_number: required('请输入身份证号'), + captcha: required('请输入图形验证码'), + }, + + // 重置密码:身份证号 + 短信验证码 + 密码(>=6) + resetPassword: { + id_number: required('请输入身份证号'), + smsCode: required('请输入短信验证码'), + password: { + type: 'string', + required: true, + allowEmpty: false, + trim: true, + min: 6, + message: { + required: '请输入密码', + empty: '请输入密码', + min: '密码长度不能少于 6 位', + }, + }, + }, +}; diff --git a/auto-patch-egg.js b/auto-patch-egg.js new file mode 100644 index 0000000..efc4e99 --- /dev/null +++ b/auto-patch-egg.js @@ -0,0 +1,65 @@ +// auto-patch-egg.js (v3) +const fs = require('fs'); +const path = require('path'); +const BASE = path.resolve(__dirname, 'node_modules'); +const TARGET_DIRS = ['egg', 'egg-cluster', 'cluster-client', 'egg-sequelize', 'egg-logger', 'egg-cors', 'egg-security', 'egg-onerror', 'egg-session', 'egg-view', 'egg-validate', 'egg-jsonp']; + +const BUILTINS = new Set([ + 'fs', 'path', 'os', 'util', 'events', 'stream', 'crypto', 'url', 'http', 'https', + 'net', 'tls', 'buffer', 'assert', 'process', 'v8', 'vm', 'zlib', 'querystring', + 'child_process', 'cluster', 'dns', 'dgram', 'readline', 'repl', 'string_decoder', + 'timers', 'tty', 'domain', 'module', 'console', 'perf_hooks', 'async_hooks', + 'worker_threads', 'inspector', 'trace_events', 'wasi', +]); + +function findRequires(filePath) { + try { + const content = fs.readFileSync(filePath, 'utf8'); + const re = /require\(['"]([^./][^'"]*)['"]\)/g; + const modules = new Set(); let match; + while ((match = re.exec(content)) !== null) { + const name = match[1].split('/')[0]; + if (!BUILTINS.has(name)) modules.add(name); + } + return modules; + } catch { return new Set(); } +} + +function moduleExists(name) { + try { require.resolve(name, { paths: [BASE] }); return true; } catch { return false; } +} + +function walkDir(dir) { + const results = []; + try { + for (const e of fs.readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, e.name); + if (e.isDirectory() && e.name !== 'node_modules') results.push(...walkDir(full)); + else if (e.isFile() && /\.(js|cjs)$/.test(e.name) && !e.name.includes('.test.')) results.push(full); + } + } catch {} + return results; +} + +function createShim(name) { + const dir = path.join(BASE, name); + if (fs.existsSync(dir)) return false; + fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ name, version: '0.0.1-shim', main: 'index.js' }, null, 2)); + fs.writeFileSync(path.join(dir, 'index.js'), `'use strict';module.exports={};`); + return true; +} + +console.log('=== 扫描依赖树 ==='); +const allRequires = new Set(); +for (const target of TARGET_DIRS) { + const targetDir = path.join(BASE, target); + if (!fs.existsSync(targetDir)) continue; + const files = walkDir(targetDir); + for (const f of files) { const reqs = findRequires(f); reqs.forEach(r => allRequires.add(r)); } +} +let count = 0; +for (const mod of allRequires) { + if (!moduleExists(mod)) { if (createShim(mod)) count++; } +} +console.log(`=== 创建 ${count} 个 shim ===`); diff --git a/config/config.default.js b/config/config.default.js new file mode 100644 index 0000000..6ec86cd --- /dev/null +++ b/config/config.default.js @@ -0,0 +1,409 @@ +'use strict'; + +// --------------------------------------------------------------------------- +// 部署环境变量自加载(兜底) +// 背景:Egg.js 默认不会读取任何 .env 文件,生产密钥(JWT_SECRET / APP_KEYS 等)必须 +// 靠 systemd EnvironmentFile= 或 shell export 注入。当注入环节出错(文件解析失败、 +// 路径不匹配、或根本不是用 systemd 启动)时,process.env 拿不到值,会触发下方 config +// 里的生产安全守卫误报「未配置的密钥」,且极难排查。 +// 这里在配置加载前主动从磁盘读取 deploy/.env.production(沿 __dirname / process.cwd() +// 向上查找,并支持 DEPLOY_ENV_FILE 显式指定),填充 process.env;仅当变量尚未设置时 +// 生效(外部注入优先),保证幂等。 +// 注意:APP_ENV / NODE_ENV 不从此处加载,避免本地开发误判为生产(真实生产由启动器注入)。 +// --------------------------------------------------------------------------- +(function loadDeployEnv() { + try { + const fs = require('fs'); + const path = require('path'); + const SKIP_KEYS = new Set(['APP_ENV', 'NODE_ENV']); + const targets = []; + + // 1) 显式指定(最高优先级) + if (process.env.DEPLOY_ENV_FILE) targets.push(process.env.DEPLOY_ENV_FILE); + + // 2) 已知裸机部署绝对路径 + targets.push('/home/root/production/agent-code/deploy/competition-website/.env.production'); + + // 3) 从 __dirname 与 process.cwd() 向上查找 + const starts = [__dirname, process.cwd()]; + for (const start of starts) { + let dir = start; + while (dir && dir !== path.dirname(dir)) { + targets.push( + path.join(dir, 'deploy', 'competition-website', '.env.production'), + path.join(dir, 'deploy', '.env.production'), + path.join(dir, '.env.production'), + ); + dir = path.dirname(dir); + } + } + + let loadedFrom = null; + for (const file of targets) { + let text; + try { + text = fs.readFileSync(file, 'utf8'); + } catch (_) { + continue; // 该候选不存在,尝试下一个 + } + for (const raw of text.split('\n')) { + const line = raw.trim(); + if (!line || line.startsWith('#') || !line.includes('=')) continue; + const eq = line.indexOf('='); + const key = line.slice(0, eq).trim(); + let val = line.slice(eq + 1).trim(); + if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) { + val = val.slice(1, -1); + } + if (key && !SKIP_KEYS.has(key) && process.env[key] === undefined) { + process.env[key] = val; + } + } + loadedFrom = file; + break; // 第一个存在的文件即为权威来源 + } + + if (loadedFrom) { + console.log('[deploy-env] 已从 ' + loadedFrom + ' 加载部署环境变量'); + } else { + console.warn('[deploy-env] 未找到 .env.production,将完全依赖进程环境变量注入(缺失生产密钥会触发安全守卫)'); + } + } catch (e) { + console.warn('[deploy-env] 加载部署环境变量失败:' + (e && e.message)); + } +})(); + +module.exports = (appInfo) => { + const config = (exports = {}); + + const isProd = process.env.NODE_ENV === 'production' || appInfo.env === 'prod'; + + // ----------------------------------------------------------------- + // 运行模式(测试 / 生产)—— 单一环境变量 APP_ENV 控制,用于最小化 + // 测试版本与生产版本的行为差异:两个版本使用同一套代码,仅通过 APP_ENV + // 与环境变量切换。 + // - 生产部署:APP_ENV=production + // - 测试 / 本地部署:APP_ENV=test(缺省值,未设置且非 egg prod 环境时默认 test) + // 作用:① 驱动生产安全守卫;② 驱动短信行为(test=不真实发送+前端弹窗, + // production=真实下发且不回传验证码)。 + // ----------------------------------------------------------------- + const runMode = process.env.APP_ENV + || (appInfo.env === 'prod' ? 'production' : 'test'); + config.runMode = runMode; + config.isProduction = runMode === 'production'; + config.isTest = runMode !== 'production'; + + // 测试数据默认口令:重置测试数据 / 标记为测试数据导入 时,统一把密码设为该口令的 + // bcrypt 哈希,方便测试人员重置后、或用 Excel「标记为测试数据」导入后,直接以该口令登录重测 + // (无需再走激活流程)。仅作用于 test_flag=1 的测试数据,生产数据(test_flag=0)不受影响。 + // 【测试模拟值 '123456';如需变更可设环境变量 TEST_DEFAULT_PASSWORD】 + config.testDefaultPassword = process.env.TEST_DEFAULT_PASSWORD || '123456'; + + // Cookie 签名密钥:优先读取环境变量 APP_KEYS,缺省时回退到内置值(仅供本地开发) + // 【测试模拟值,生产请通过环境变量 APP_KEYS 替换为随机字符串,否则启动被守卫阻断】 + const DEFAULT_APP_KEYS = appInfo.name + '_competition_website_secret_key_2026'; + config.keys = process.env.APP_KEYS || DEFAULT_APP_KEYS; + + // add your config here + config.middleware = ['concurrencyGuard']; + + // 🔴 请求级并发闸门(大文件上传/下载):在读取请求体之前限制「被接受的并发重请求数」, + // 极端并发下超出上限立即回 429(连接级背压),防止单 worker 被海量请求体缓冲压垮 OOM。 + // 与 app/lib/file_io_guard 互补(后者按文件流生命周期限流)。可按压测/生产调参: + // 单 worker 极端并发场景建议保持较低值(20);生产多 worker 下每 worker 独立计数, + // 总上限 = worker 数 × HEAVY_REQ_MAX,可适当提高。 + config.concurrencyGuard = { + max: parseInt(process.env.HEAVY_REQ_MAX, 10) || 20, + }; + + + // ----------------------------------------------------------------- + // sequelize - MariaDB/MySQL 数据库配置 + // ----------------------------------------------------------------- + config.sequelize = { + dialect: 'mysql', + database: process.env.DB_NAME || 'competition_db', + host: process.env.DB_HOST || 'localhost', + port: parseInt(process.env.DB_PORT, 10) || 3306, + // 兼容 docker-compose/.env.example 中的 DB_USER 与代码历史使用的 DB_USERNAME + username: process.env.DB_USERNAME || process.env.DB_USER || 'root', + // 【测试模拟值 '74191221',仅供本地/测试部署;生产请通过 DB_PASSWORD 环境变量注入真实密码】 + password: process.env.DB_PASSWORD || '74191221', + timezone: '+08:00', + define: { + underscored: true, + freezeTableName: true, + timestamps: true, + createdAt: 'created_at', + updatedAt: 'updated_at', + charset: 'utf8mb4', + collate: 'utf8mb4_unicode_ci', + }, + dialectOptions: { + // 注:mysql2 连接级只接受 charset,不接受 collate;collate 是表/模型级选项, + // 由上方 define.collate 负责。此处传 collate 会触发 + // "Ignoring invalid configuration option passed to Connection: collate" 警告。 + charset: 'utf8mb4', + }, + pool: { + // 🔴 压测调优(2026-08-11):单 worker 默认 10 连接,在「无带宽限制 + 极端并发」 + // 下分片合并/写入会短时占用大量连接。提高到 20(可用 DB_POOL_MAX 覆盖), + // 并放宽 acquire 到 45s,避免瞬时峰值触发 "connection acquire timeout"。 + // 注意:这是「每 worker」上限;生产 egg-cluster 按 os.cpus() 起多 worker, + // 总连接数 ≈ worker 数 × max,需确保 ≤ MySQL max_connections(默认 151, + // 启动期由 app.js didReady 自举断言兜底,超限只 WARN 不阻断)。 + max: parseInt(process.env.DB_POOL_MAX, 10) || 20, + min: 2, + acquire: 45000, + idle: 10000, + // 🔴 连接回收(2026-08-13 推进 #16):maxUses 限制单连接累计查询次数, + // 归还池时物理关闭并新建,避免 MySQL wait_timeout(默认 8h)静默杀掉长期空闲 + // 连接后被复用 → 生产偶发 "Connection lost / ECONNRESET / Cannot enqueue Query + // after fatal error"。5000 次 ≈ 中小赛事体量下数小时寿命,远小于 wait_timeout。 + // 可用 DB_POOL_MAX_USES 覆盖。 + maxUses: parseInt(process.env.DB_POOL_MAX_USES, 10) || 5000, + }, + // 仅在开发环境开启 SQL 日志,生产环境自动关闭 + logging: process.env.NODE_ENV === 'production' ? false : console.log, + }; + + // ----------------------------------------------------------------- + // X-Accel-Redirect 内部下载开关(根治管理后台大文件下载压垮 Node) + // ----------------------------------------------------------------- + // 生产(NODE_ENV=production)默认开启:管理端作品/Word 下载改由后端只回一个 + // X-Accel-Redirect 内部重定向头,nginx 直接从磁盘流式吐文件,Node 不再持有文件流, + // 极端并发下几乎零占用 worker fd/事件循环。 + // 开发环境默认关闭(回退为 Node 直接流式,便于本地调试);可用 X_ACCEL_REDIRECT=1 + // 临时开启以验证重定向头是否正确。 + config.xAccelRedirect = { + enabled: process.env.NODE_ENV === 'production' || process.env.X_ACCEL_REDIRECT === '1', + // nginx 内部 location 前缀(须与 deploy/competition_website.conf 的一致) + internalPrefix: '/internal-downloads/', + }; + + // ----------------------------------------------------------------- + // Redis 缓存配置 + // ----------------------------------------------------------------- + config.redis = { + client: { + port: parseInt(process.env.REDIS_PORT, 10) || 6379, + host: process.env.REDIS_HOST || '127.0.0.1', + password: process.env.REDIS_PASSWORD || '74191221', + db: 0, + }, + }; + + // ----------------------------------------------------------------- + // JWT 鉴权配置 + // ----------------------------------------------------------------- + const DEFAULT_JWT_SECRET = 'competition_jwt_secret_key_2026_br!cs'; + config.jwt = { + // JWT 签名密钥:优先读取环境变量 JWT_SECRET,缺省时回退到内置值(仅供本地开发) + // 【测试模拟值,生产请通过环境变量 JWT_SECRET 替换为 64 位以上随机串,否则启动被守卫阻断】 + secret: process.env.JWT_SECRET || DEFAULT_JWT_SECRET, + enable: true, + // Token 有效期(秒),默认 7 天 + expiresIn: 7 * 24 * 60 * 60, + // 静默刷新宽限期(秒):token 过期后在此窗口内仍可被 /api/auth/refresh 续期, + // 用于实现"活跃用户永不被意外踢出"的滑动会话;超过则必须重新登录。 + refreshGrace: 7 * 24 * 60 * 60, + // 匹配需要鉴权的路由规则(排除登录/注册等公开接口) + match: (ctx) => { + const publicPaths = [ + '/api/auth/login', + '/api/auth/register', + '/api/public/', + ]; + return !publicPaths.some((p) => ctx.path.startsWith(p)); + }, + }; + + // ----------------------------------------------------------------- + // multipart 文件上传配置 + // ----------------------------------------------------------------- + config.multipart = { + mode: 'stream', + fileSize: '100mb', + whitelist: [ + // 文档类 + '.pdf', + '.doc', + '.docx', + '.ppt', + '.pptx', + '.xls', + '.xlsx', + '.zip', + // 图片类 + '.png', + '.jpg', + '.jpeg', + '.gif', + '.webp', + ], + // 自定义文件名正则,允许中文、字母、数字、空格、连字符、下划线、括号 + // Egg 默认不允许 hyphen in Chinese filename, fix this + filenameRegex: /^[\u4e00-\u9fa5a-zA-Z0-9\s\-_\(\)]+\.[a-z0-9]+$/i, + // 上传文件临时存放目录 + tmpdir: require('path').join(appInfo.baseDir, 'app/public/tmp'), + // 清理临时文件的间隔时间 + cleanSchedule: { + cron: '0 30 4 * * *', + }, + }; + + // ----------------------------------------------------------------- + // CORS 跨域配置(白名单收敛) + // ----------------------------------------------------------------- + // 通过环境变量 CORS_ORIGINS 配置允许的来源(逗号分隔),例如: + // CORS_ORIGINS=https://competition.example.com,https://admin.example.com + // 规则: + // - 配置了白名单:仅反射白名单内的 origin; + // - 未配置白名单:开发环境反射请求来源(便于本地联调); + // 生产环境不反射任意源(生产走 nginx 同源代理 /api,无需跨域)。 + const corsOrigins = (process.env.CORS_ORIGINS || '') + .split(',') + .map((s) => s.trim()) + .filter(Boolean); + config.cors = { + origin: (ctx) => { + const reqOrigin = ctx.get('origin'); + if (corsOrigins.length > 0) { + return corsOrigins.includes(reqOrigin) ? reqOrigin : ''; + } + return isProd ? '' : reqOrigin; + }, + allowMethods: 'GET,HEAD,PUT,POST,DELETE,PATCH', + credentials: true, + }; + + // ----------------------------------------------------------------- + // Security 安全配置 + // ----------------------------------------------------------------- + // CSRF 说明:本项目前后端分离,鉴权全部通过 Authorization: Bearer + // 请求头携带(不依赖 Cookie 会话),浏览器跨站请求无法自动附带该头, + // 因此传统 CSRF 攻击面不成立,保持关闭;若未来引入 Cookie 会话需重新启用。 + config.security = { + csrf: { + enable: false, + }, + // 跳转/资源域名白名单与 CORS 白名单保持一致(未配置时为空 = 仅同源) + domainWhiteList: corsOrigins, + }; + + // ----------------------------------------------------------------- + // 静态资源服务配置 + // ----------------------------------------------------------------- + config.static = { + prefix: '/public/', + dir: require('path').join(appInfo.baseDir, 'app/public'), + maxAge: 31536000, + }; + + // 静态资源外部访问基础 URL(用于 helper.fileUrl() 拼接完整 URL) + // 生产环境通过环境变量 STATIC_BASE_URL 配置为实际域名,如 'https://competition.example.com' + // 未配置时回退为 null,helper.fileUrl() 会退化到 http://127.0.0.1:{port} + config.staticBaseUrl = process.env.STATIC_BASE_URL || null; + + // ----------------------------------------------------------------- + // bodyParser 配置 + // ----------------------------------------------------------------- + config.bodyParser = { + jsonLimit: '10mb', + formLimit: '10mb', + }; + + // ----------------------------------------------------------------- + // 日志配置 + // ----------------------------------------------------------------- + config.logger = { + dir: require('path').join(appInfo.baseDir, 'logs'), + appLogName: 'competition-web.log', + coreLogName: 'egg-web.log', + agentLogName: 'egg-agent.log', + errorLogName: 'common-error.log', + level: 'INFO', + }; + + // ----------------------------------------------------------------- + // 日志轮转配置(egg-logrotator 内置插件) + // - 按体积轮转:单文件超过 maxFileSize 即滚动 + // - maxDays:保留最近 N 天日志,到期后自动清理(设为 0 则永久保留) + // 说明:默认已按天轮转;此处显式收口为"按体积 + 保留 14 天", + // 避免长期运行后日志无限增长占满磁盘。 + // ----------------------------------------------------------------- + config.rotator = { + filesRotateBySize: [ + 'competition-web.log', + 'common-error.log', + 'egg-web.log', + 'egg-agent.log', + ], + maxFileSize: 50 * 1024 * 1024, // 50MB + maxDays: 14, + }; + + // ----------------------------------------------------------------- + // 生产环境安全守卫:禁止使用内置默认密钥上线 + // (由 APP_ENV=production 触发;测试环境允许使用模拟密钥) + // ----------------------------------------------------------------- + if (config.isProduction) { + const insecure = []; + if (config.jwt.secret === DEFAULT_JWT_SECRET) insecure.push('JWT_SECRET'); + if (config.keys === DEFAULT_APP_KEYS) insecure.push('APP_KEYS'); + if (insecure.length > 0) { + // 生产环境仍在使用硬编码默认密钥属于严重安全隐患,直接阻断启动 + throw new Error( + `[安全阻断] 生产环境检测到未配置的密钥: ${insecure.join(', ')}。` + + '请通过环境变量设置这些值后再启动(参见 deploy/.env.example)。' + ); + } + } + + // ----------------------------------------------------------------- + // 短信网关配置(阿里云 Dysmsapi) + // ----------------------------------------------------------------- + // 设计要点(测试 / 生产差异最小化): + // - 验证码的校验仍走 ctx.session(与现有激活/重置流程一致,已被 26 项边界测试覆盖), + // 短信网关只负责"把服务端已生成的 6 位验证码下发给手机号",不改动校验契约。 + // - 所有网关参数均通过环境变量注入(SMS_ENABLED / ALIYUN_SMS_*)。 + // - 阿里云 accessKeyId / accessKeySecret 为敏感凭证,源码不再保留任何硬编码值, + // 必须且只能通过环境变量注入;生产开启短信时若缺失将触发安全守卫阻断启动。 + // - endpoint / signName / templateCode 为非敏感配置,保留合理默认值以便本地/测试快速起服。 + // - realSend(真实下发开关)= 生产环境 且 SMS_ENABLED=1。 + // · 测试环境(APP_ENV=test):即使配置了网关也【不真实发送】,后端随响应回传 + // 验证码(字段 smsCode),由前端弹窗展示,便于测试人员直接获取。 + // · 生产环境且 realSend=true:真实下发,绝不回传/打印验证码明文。 + // · 生产环境且 realSend=false:安全起见不下发、不回传,仅记录告警。 + const smsEnabled = process.env.SMS_ENABLED === '1' || process.env.SMS_ENABLED === 'true'; + config.sms = { + enabled: smsEnabled, + // 真实下发:仅生产环境 + 网关开启时成立;测试环境恒为 false(不真发) + realSend: config.isProduction && smsEnabled, + provider: 'aliyun', + aliyun: { + // 敏感凭证:仅从环境变量读取,源码零硬编码(已轮换,杜绝泄露) + accessKeyId: process.env.ALIYUN_SMS_ACCESS_KEY_ID, + accessKeySecret: process.env.ALIYUN_SMS_ACCESS_KEY_SECRET, + endpoint: process.env.ALIYUN_SMS_ENDPOINT || 'dysmsapi.aliyuncs.com', + // 非敏感配置,保留合理默认值 + signName: process.env.ALIYUN_SMS_SIGN_NAME || '华云智联', + templateCode: process.env.ALIYUN_SMS_TEMPLATE_CODE || 'SMS_487315108', + }, + }; + + // 生产环境 + 开启短信时,阿里云密钥必须来自环境变量(源码已无硬编码凭证) + if (config.isProduction && smsEnabled) { + const missing = []; + if (!config.sms.aliyun.accessKeyId) missing.push('ALIYUN_SMS_ACCESS_KEY_ID'); + if (!config.sms.aliyun.accessKeySecret) missing.push('ALIYUN_SMS_ACCESS_KEY_SECRET'); + if (missing.length > 0) { + throw new Error( + `[安全阻断] 生产环境开启短信但缺少阿里云密钥: ${missing.join(', ')}。` + + '请通过环境变量注入真实密钥后再启动(参见 deploy/.env.production)。' + ); + } + } + + return config; +}; diff --git a/config/plugin.js b/config/plugin.js new file mode 100644 index 0000000..23dd7d5 --- /dev/null +++ b/config/plugin.js @@ -0,0 +1,32 @@ +'use strict'; + +/** @type Egg.EggPlugin */ +module.exports = { + // 启用 Sequelize ORM 插件(MariaDB/MySQL) + sequelize: { + enable: true, + package: 'egg-sequelize', + }, + + // 启用 CORS 跨域插件 + cors: { + enable: true, + package: 'egg-cors', + }, + + // 启用参数校验插件 + validate: { + enable: true, + package: 'egg-validate', + }, + + // 禁用开发环境热重载(Node 22 兼容性) + development: { + enable: false, + }, + + // 禁用文件监听(Node 22 兼容性) + watcher: { + enable: false, + }, +}; diff --git a/database/006-seed-50-20-30.sql b/database/006-seed-50-20-30.sql new file mode 100644 index 0000000..c57fbe1 --- /dev/null +++ b/database/006-seed-50-20-30.sql @@ -0,0 +1,207 @@ +-- ===================================================================== +-- 种子数据:选择题(50题×1分) + 判断题(20题×1分) + 综合性任务(1题×30分) +-- 总分:100分,符合"卫星通信与导航应用技术创新赛线上选拔赛"题型结构 +-- ===================================================================== + +-- 清空现有数据 +DELETE FROM choice_questions; +DELETE FROM judge_questions; +DELETE FROM word_questions; +ALTER TABLE choice_questions AUTO_INCREMENT = 1; +ALTER TABLE judge_questions AUTO_INCREMENT = 1; +ALTER TABLE word_questions AUTO_INCREMENT = 1; + +-- ===================================================================== +-- 第一篇:卫星通信技术(第1-6章)——选择题 (16题) +-- ===================================================================== + +INSERT INTO choice_questions (`group`, question, options, answer, score, sort_order, created_at, updated_at) VALUES +('zhongzhi', '卫星通信定义为?', '[{"key":"A","text":"利用卫星在地面站之间进行的通信"},{"key":"B","text":"利用卫星转发无线电信号实现多个地球站之间的通信"},{"key":"C","text":"卫星与飞机之间的通信"},{"key":"D","text":"卫星之间的宇宙通信"}]', 'B', 1, 1, NOW(), NOW()), +('zhongzhi', '卫星通信中,地球站向卫星发射信号的链路称为?', '[{"key":"A","text":"下行链路"},{"key":"B","text":"上行链路"},{"key":"C","text":"前向链路"},{"key":"D","text":"反向链路"}]', 'B', 1, 2, NOW(), NOW()), +('zhongzhi', '卫星通信的主要优点不包括以下哪项?', '[{"key":"A","text":"覆盖范围大"},{"key":"B","text":"通信容量大"},{"key":"C","text":"建站成本低"},{"key":"D","text":"不受地理条件限制"}]', 'C', 1, 3, NOW(), NOW()), +('zhongzhi', '通信系统中,将原始信号转换为适合信道传输的信号的过程称为?', '[{"key":"A","text":"编码"},{"key":"B","text":"调制"},{"key":"C","text":"解码"},{"key":"D","text":"解调"}]', 'B', 1, 4, NOW(), NOW()), +('zhongzhi', 'GEO卫星的轨道高度约为?', '[{"key":"A","text":"500-1500 km"},{"key":"B","text":"5000-12000 km"},{"key":"C","text":"20200 km"},{"key":"D","text":"35786 km"}]', 'D', 1, 5, NOW(), NOW()), +('zhongzhi', 'MEO卫星的轨道高度范围是?', '[{"key":"A","text":"低于500 km"},{"key":"B","text":"500-2000 km"},{"key":"C","text":"5000-20000 km"},{"key":"D","text":"35786 km"}]', 'C', 1, 6, NOW(), NOW()), +('zhongzhi', 'LEO卫星的轨道倾角通常为?', '[{"key":"A","text":"0度"},{"key":"B","text":"近极地轨道"},{"key":"C","text":"赤道轨道"},{"key":"D","text":"地球同步轨道"}]', 'B', 1, 7, NOW(), NOW()), +('zhongzhi', '卫星通信中,频率复用技术的主要作用是?', '[{"key":"A","text":"提高频率利用率"},{"key":"B","text":"降低发射功率"},{"key":"C","text":"减少卫星数量"},{"key":"D","text":"提高信号质量"}]', 'A', 1, 8, NOW(), NOW()), +('zhongzhi', '以下哪种不是卫星通信的多址方式?', '[{"key":"A","text":"FDMA"},{"key":"B","text":"TDMA"},{"key":"C","text":"CDMA"},{"key":"D","text":"ADMA"}]', 'D', 1, 9, NOW(), NOW()), +('zhongzhi', '卫星物联网中,星上处理的主要目的是?', '[{"key":"A","text":"增加卫星重量"},{"key":"B","text":"提高传输效率"},{"key":"C","text":"降低功率"},{"key":"D","text":"增加轨道高度"}]', 'B', 1, 10, NOW(), NOW()), +('zhongzhi', '低轨卫星星座的特点是?', '[{"key":"A","text":"单星覆盖大,但需要星上处理"},{"key":"B","text":"全球覆盖,大容量低时延"},{"key":"C","text":"只有一颗卫星"},{"key":"D","text":"不需要切换"}]', 'B', 1, 11, NOW(), NOW()), +('zhongzhi', '卫星通信系统的转发器作用是?', '[{"key":"A","text":"存储数据"},{"key":"B","text":"放大变频转发信号"},{"key":"C","text":"计算信号"},{"key":"D","text":"控制卫星姿态"}]', 'B', 1, 12, NOW(), NOW()), +('zhongzhi', '卫星天线的指向精度影响?', '[{"key":"A","text":"信号接收功率"},{"key":"B","text":"卫星重量"},{"key":"C","text":"轨道高度"},{"key":"D","text":"卫星寿命"}]', 'A', 1, 13, NOW(), NOW()), +('zhongzhi', '卫星电源系统主要采用?', '[{"key":"A","text":"燃料电池"},{"key":"B","text":"太阳能电池阵+蓄电池组"},{"key":"C","text":"核能发电"},{"key":"D","text":"化学电池"}]', 'B', 1, 14, NOW(), NOW()), +('zhongzhi', '以下哪个频段常用作卫星通信上行?', '[{"key":"A","text":"UHF"},{"key":"B","text":"C-band"},{"key":"C","text":"Ku-band"},{"key":"D","text":"Ka-band"}]', 'D', 1, 15, NOW(), NOW()), +('zhongzhi', '卫星姿态控制的目的是?', '[{"key":"A","text":"改变轨道高度"},{"key":"B","text":"保持天线指向目标区域"},{"key":"C","text":"改变速度"},{"key":"D","text":"减少功耗"}]', 'B', 1, 16, NOW(), NOW()); + +-- ===================================================================== +-- 第二篇:卫星导航技术(第1-8章)——选择题 (34题) +-- ===================================================================== + +INSERT INTO choice_questions (`group`, question, options, answer, score, sort_order, created_at, updated_at) VALUES +('zhongzhi', 'GNSS的全称是?', '[{"key":"A","text":"Global Navigation Satellite System"},{"key":"B","text":"Global Network Service System"},{"key":"C","text":"General Navigation Support System"},{"key":"D","text":"Geo Navigation Satellite System"}]', 'A', 1, 17, NOW(), NOW()), +('zhongzhi', '以下哪个不属于四大全球导航卫星星座?', '[{"key":"A","text":"GPS"},{"key":"B","text":"北斗"},{"key":"C","text":"GLONASS"},{"key":"D","text":"WAAS"}]', 'D', 1, 18, NOW(), NOW()), +('zhongzhi', '北斗三号全球系统提供多少颗卫星?', '[{"key":"A","text":"24"},{"key":"B","text":"30"},{"key":"C","text":"35"},{"key":"D","text":"40"}]', 'C', 1, 19, NOW(), NOW()), +('zhongzhi', 'GPS卫星导航系统至少需要几颗卫星才能实现三维定位?', '[{"key":"A","text":"2颗"},{"key":"B","text":"3颗"},{"key":"C","text":"4颗"},{"key":"D","text":"5颗"}]', 'C', 1, 20, NOW(), NOW()), +('zhongzhi', '伪距测量测量的是?', '[{"key":"A","text":"卫星到接收机的几何距离"},{"key":"B","text":"卫星信号传播时间乘以光速得到的距离"},{"key":"C","text":"卫星到地心的距离"},{"key":"D","text":"接收机到地面的距离"}]', 'B', 1, 21, NOW(), NOW()), +('zhongzhi', '卫星定位误差中,属于系统误差的是?', '[{"key":"A","text":"电离层延迟误差"},{"key":"B","text":"接收机噪声"},{"key":"C","text":"多径效应随机误差"},{"key":"D","text":"测量噪声"}]', 'A', 1, 22, NOW(), NOW()), +('zhongzhi', '差分定位技术中,RTK的全称是?', '[{"key":"A","text":"Real-Time Kinematic"},{"key":"B","text":"Rapid Tracking Kinematic"},{"key":"C","text":"Real-Time Kalman"},{"key":"D","text":"Radio Transmission Kinematic"}]', 'A', 1, 23, NOW(), NOW()), +('zhongzhi', 'NMEA-0183协议中,GGA语句主要提供什么信息?', '[{"key":"A","text":"卫星星历"},{"key":"B","text":"定位数据"},{"key":"C","text":"卫星状态"},{"key":"D","text":"速度信息"}]', 'B', 1, 24, NOW(), NOW()), +('zhongzhi', '北斗系统独具特色的服务是?', '[{"key":"A","text":"仅定位"},{"key":"B","text":"短报文通信"},{"key":"C","text":"仅授时"},{"key":"D","text":"仅测速"}]', 'B', 1, 25, NOW(), NOW()), +('zhongzhi', '高精度差分定位中,NTRIP协议用于?', '[{"key":"A","text":"卫星轨道计算"},{"key":"B","text":"通过互联网传输差分改正数据"},{"key":"C","text":"信号调制"},{"key":"D","text":"误差计算"}]', 'B', 1, 26, NOW(), NOW()), +('zhongzhi', '卫星导航接收机的灵敏度主要影响?', '[{"key":"A","text":"捕获卫星信号的能力"},{"key":"B","text":"发射功率"},{"key":"C","text":"卫星轨道高度"},{"key":"D","text":"计算速度"}]', 'A', 1, 27, NOW(), NOW()), +('zhongzhi', '组合导航GNSS+IMU中,IMU的作用是?', '[{"key":"A","text":"提供加速度和角速度"},{"key":"B","text":"提供定位结果"},{"key":"C","text":"提供时间信息"},{"key":"D","text":"提供卫星星历"}]', 'A', 1, 28, NOW(), NOW()), +('zhongzhi', '多路径效应产生的原因是?', '[{"key":"A","text":"卫星信号经过反射后与直达信号干涉"},{"key":"B","text":"多颗卫星信号叠加"},{"key":"C","text":"多个接收机同时工作"},{"key":"D","text":"电离层反射信号"}]', 'A', 1, 29, NOW(), NOW()), +('zhongzhi', 'RTCM标准主要用于?', '[{"key":"A","text":"卫星导航电文标准"},{"key":"B","text":"差分改正数据标准"},{"key":"C","text":"NMEA输出标准"},{"key":"D","text":"接口通信标准"}]', 'B', 1, 30, NOW(), NOW()), +('zhongzhi', 'PDOP值越大表示?', '[{"key":"A","text":"定位精度越好"},{"key":"B","text":"定位几何误差越大"},{"key":"C","text":"卫星越多"},{"key":"D","text":"温度越高"}]', 'B', 1, 31, NOW(), NOW()), +('zhongzhi', '北斗卫星导航系统的服务区覆盖?', '[{"key":"A","text":"仅中国"},{"key":"B","text":"亚太地区"},{"key":"C","text":"全球"},{"key":"D","text":"仅北半球"}]', 'C', 1, 32, NOW(), NOW()), +('zhongzhi', 'GPS卫星星历更新周期通常为?', '[{"key":"A","text":"30秒"},{"key":"B","text":"2小时"},{"key":"C","text":"1天"},{"key":"D","text":"1周"}]', 'B', 1, 33, NOW(), NOW()), +('zhongzhi', '静态相对定位主要用于?', '[{"key":"A","text":"需要高精度大地测量"},{"key":"B","text":"车辆导航"},{"key":"C","text":"船舶导航"},{"key":"D","text":"飞机导航"}]', 'A', 1, 34, NOW(), NOW()), +('zhongzhi', '载波相位测量相比伪距测量?', '[{"key":"A","text":"精度更高"},{"key":"B","text":"精度更低"},{"key":"C","text":"精度相同"},{"key":"D","text":"无法比较"}]', 'A', 1, 35, NOW(), NOW()), +('zhongzhi', '电离层延迟对伪距测量的影响?', '[{"key":"A","text":"夜间比白天大"},{"key":"B","text":"白天比夜间大"},{"key":"C","text":"没有影响"},{"key":"D","text":"只在冬季有影响"}]', 'B', 1, 36, NOW(), NOW()), +('zhongzhi', '周跳发生在?', '[{"key":"A","text":"载波相位测量中"},{"key":"B","text":"伪距测量中"},{"key":"C","text":"信号发射时"},{"key":"D","text":"卫星星历计算时"}]', 'A', 1, 37, NOW(), NOW()), +('zhongzhi', 'GPS系统由多少颗卫星组成?', '[{"key":"A","text":"24"},{"key":"B","text":"31"},{"key":"C","text":"35"},{"key":"D","text":"40"}]', 'A', 1, 38, NOW(), NOW()), +('zhongzhi', '以下哪种不属于北斗增强系统?', '[{"key":"A","text":"SBAS"},{"key":"B","text":"GBAS"},{"key":"C","text":"BDS-IS"},{"key":"D","text":"GLONASS"}]', 'D', 1, 39, NOW(), NOW()), +('zhongzhi', '单点定位精度通常为?', '[{"key":"A","text":"厘米级"},{"key":"B","text":"米级"},{"key":"C","text":"分米级"},{"key":"D","text":"毫米级"}]', 'B', 1, 40, NOW(), NOW()), +('zhongzhi', 'RTK定位精度通常达到?', '[{"key":"A","text":"厘米级"},{"key":"B","text":"米级"},{"key":"C","text":"千米级"},{"key":"D","text":"十米级"}]', 'A', 1, 41, NOW(), NOW()), +('zhongzhi', 'NMEA-0183语句的结束标志是?', '[{"key":"A","text":"换行"},{"key":"B","text":"校验和"},{"key":"C","text":"星号"},{"key":"D","text":"回车"}]', 'B', 1, 42, NOW(), NOW()), +('zhongzhi', '以下哪个不是北斗卫星导航系统的特点?', '[{"key":"A","text":"有源定位"},{"key":"B","text":"短报文通信"},{"key":"C","text":"区域服务不能全球覆盖"},{"key":"D","text":"三频信号"}]', 'C', 1, 43, NOW(), NOW()), +('zhongzhi', '卫星导航接收机捕获阶段的主要作用是?', '[{"key":"A","text":"搜索卫星信号并确定频率码相位"},{"key":"B","text":"跟踪信号"},{"key":"C","text":"计算位置"},{"key":"D","text":"输出速度"}]', 'A', 1, 44, NOW(), NOW()), +('zhongzhi', '对流层延迟改正模型中,通常使用?', '[{"key":"A","text":"Hopfield模型"},{"key":"B","text":"Klobuchar模型"},{"key":"C","text":"Black模型"},{"key":"D","text":"高斯模型"}]', 'A', 1, 45, NOW(), NOW()), +('zhongzhi', '全球定位系统中,UTC表示?', '[{"key":"A","text":"世界时"},{"key":"B","text":"协调世界时"},{"key":"C","text":"原子时"},{"key":"D","text":"恒星时"}]', 'B', 1, 46, NOW(), NOW()), +('zhongzhi', '整周模糊度解算属于?', '[{"key":"A","text":"伪距测量问题"},{"key":"B","text":"载波相位测量问题"},{"key":"C","text":"卫星轨道问题"},{"key":"D","text":"电离层问题"}]', 'B', 1, 47, NOW(), NOW()), +('zhongzhi', '以下哪个频率是北斗B1I信号频率?', '[{"key":"A","text":"1561.098 MHz"},{"key":"B","text":"1268.52 MHz"},{"key":"C","text":"1176.45 MHz"},{"key":"D","text":"1575.42 MHz"}]', 'A', 1, 48, NOW(), NOW()), +('zhongzhi', 'GPS选择的PRN码特点是?', '[{"key":"A","text":"自相关特性好,互相关特性弱"},{"key":"B","text":"自相关特性弱,互相关特性强"},{"key":"C","text":"长度固定不变"},{"key":"D","text":"只有一个码"}]', 'A', 1, 49, NOW(), NOW()), +('zhongzhi', '下列哪项不属于卫星导航的应用?', '[{"key":"A","text":"车辆导航"},{"key":"B","text":"工程测量"},{"key":"C","text":"火力发电"},{"key":"D","text":"时间同步"}]', 'C', 1, 50, NOW(), NOW()); + +-- ===================================================================== +-- 高职组补充选择题(同样50题) +-- ===================================================================== + +INSERT INTO choice_questions (`group`, question, options, answer, score, sort_order, created_at, updated_at) VALUES +('gaozhi', '卫星通信中,FDMA的含义是?', '[{"key":"A","text":"频分多址"},{"key":"B","text":"时分多址"},{"key":"C","text":"码分多址"},{"key":"D","text":"空分多址"}]', 'A', 1, 1, NOW(), NOW()), +('gaozhi', 'GEO卫星的星下点位置相对于地面观察者?', '[{"key":"A","text":"不断高速移动"},{"key":"B","text":"基本静止"},{"key":"C","text":"每天环绕一次"},{"key":"D","text":"每月环绕一次"}]', 'B', 1, 2, NOW(), NOW()), +('gaozhi', 'OFDM技术中,子载波之间保持正交性的目的是?', '[{"key":"A","text":"提高发射功率"},{"key":"B","text":"避免子载波间干扰"},{"key":"C","text":"增加带宽"},{"key":"D","text":"降低频率"}]', 'B', 1, 3, NOW(), NOW()), +('gaozhi', '北斗三号全球卫星导航系统具备以下哪种特色服务?', '[{"key":"A","text":"仅定位服务"},{"key":"B","text":"短报文通信"},{"key":"C","text":"仅授时服务"},{"key":"D","text":"仅测速服务"}]', 'B', 1, 4, NOW(), NOW()), +('gaozhi', '5G网络中,Massive MIMO技术的主要优势是?', '[{"key":"A","text":"降低功耗"},{"key":"B","text":"提升频谱效率和容量"},{"key":"C","text":"减少天线数量"},{"key":"D","text":"简化网络架构"}]', 'B', 1, 5, NOW(), NOW()), +('gaozhi', '网络切片技术属于5G的哪个核心特性?', '[{"key":"A","text":"eMBB"},{"key":"B","text":"URLLC"},{"key":"C","text":"mMTC"},{"key":"D","text":"网络能力开放"}]', 'D', 1, 6, NOW(), NOW()), +('gaozhi', '在通信安全中,以下哪种攻击属于被动攻击?', '[{"key":"A","text":"篡改"},{"key":"B","text":"窃听"},{"key":"C","text":"拒绝服务"},{"key":"D","text":"伪装"}]', 'B', 1, 7, NOW(), NOW()), +('gaozhi', 'NTRIP协议主要用于什么场景?', '[{"key":"A","text":"卫星遥测"},{"key":"B","text":"差分数据网络传输"},{"key":"C","text":"卫星轨道控制"},{"key":"D","text":"信号加密传输"}]', 'B', 1, 8, NOW(), NOW()), +('gaozhi', '组合导航(GNSS+IMU)的核心优势是什么?', '[{"key":"A","text":"降低成本"},{"key":"B","text":"无缝切换与误差互补"},{"key":"C","text":"减少卫星数量"},{"key":"D","text":"提高发射功率"}]', 'B', 1, 9, NOW(), NOW()), +('gaozhi', '5G网络中,URLLC场景的主要特征是?', '[{"key":"A","text":"高速率大带宽"},{"key":"B","text":"低时延高可靠"},{"key":"C","text":"海量连接"},{"key":"D","text":"广域覆盖"}]', 'B', 1, 10, NOW(), NOW()), +('gaozhi', '卫星定位中,伪距测量的误差来源不包括?', '[{"key":"A","text":"电离层延迟"},{"key":"B","text":"对流层延迟"},{"key":"C","text":"卫星轨道误差"},{"key":"D","text":"用户设备重量"}]', 'D', 1, 11, NOW(), NOW()), +('gaozhi', '低轨卫星物联网相比GEO卫星的主要优势是?', '[{"key":"A","text":"覆盖范围更大"},{"key":"B","text":"传输时延更低"},{"key":"C","text":"单星容量更大"},{"key":"D","text":"使用寿命更长"}]', 'B', 1, 12, NOW(), NOW()), +('gaozhi', 'QPSK调制属于?', '[{"key":"A","text":"模拟调制"},{"key":"B","text":"数字相位调制"},{"key":"C","text":"幅度调制"},{"key":"D","text":"频率调制"}]', 'B', 1, 13, NOW(), NOW()), +('gaozhi', '信道编码的主要目的是?', '[{"key":"A","text":"提高传输速率"},{"key":"B","text":"对抗传输错误"},{"key":"C","text":"增加功率"},{"key":"D","text":"压缩数据"}]', 'B', 1, 14, NOW(), NOW()), +('gaozhi', 'CDMA技术的特点是?', '[{"key":"A","text":"所有用户同时占用相同频段"},{"key":"B","text":"每个用户占用不同频率"},{"key":"C","text":"每个用户占用不同时隙"},{"key":"D","text":"用户按空间分隔"}]', 'A', 1, 15, NOW(), NOW()), +('gaozhi', 'M-QAM调制中,M越大?', '[{"key":"A","text":"频谱效率越高,误码性能越好"},{"key":"B","text":"频谱效率越高,误码性能越差"},{"key":"C","text":"频谱效率越低,误码性能越好"},{"key":"D","text":"频谱效率越低,误码性能越差"}]', 'B', 1, 16, NOW(), NOW()), +('gaozhi', '香农公式给出了?', '[{"key":"A","text":"信道容量与带宽信噪比的关系"},{"key":"B","text":"信号功率与频率的关系"},{"key":"C","text":"噪声功率与温度的关系"},{"key":"D","text":"带宽与频率的关系"}]', 'A', 1, 17, NOW(), NOW()), +('gaozhi', '均衡技术主要用于补偿?', '[{"key":"A","text":"信道衰落引起的码间干扰"},{"key":"B","text":"噪声干扰"},{"key":"C","text":"频率偏移"},{"key":"D","text":"相位噪声"}]', 'A', 1, 18, NOW(), NOW()), +('gaozhi', '编码效率等于?', '[{"key":"A","text":"信息位长度/总码长"},{"key":"B","text":"总码长/信息位长度"},{"key":"C","text":"监督位长度/总码长"},{"key":"D","text":"信息位长度/监督位长度"}]', 'A', 1, 19, NOW(), NOW()), +('gaozhi', 'RS编码属于?', '[{"key":"A","text":"分组码"},{"key":"B","text":"卷积码"},{"key":"C","text":"循环码"},{"key":"D","text":"RS不是编码"}]', 'A', 1, 20, NOW(), NOW()), +('gaozhi', 'TCM编码是?', '[{"key":"A","text":"网格编码调制"},{"key":"B","text":" Turbo编码"},{"key":"C","text":"卷积编码"},{"key":"D","text":"里德所罗门编码"}]', 'A', 1, 21, NOW(), NOW()), +('gaozhi', '扩频通信的主要优点不包括?', '[{"key":"A","text":"抗干扰能力强"},{"key":"B","text":"隐蔽性好"},{"key":"C","text":"功率大"},{"key":"D","text":"多址能力强"}]', 'C', 1, 22, NOW(), NOW()), +('gaozhi', 'DSSS表示?', '[{"key":"A","text":"跳频扩频"},{"key":"B","text":"直接序列扩频"},{"key":"C","text":"跳时扩频"},{"key":"D","text":"线性扩频"}]', 'B', 1, 23, NOW(), NOW()), +('gaozhi', 'AOS协议主要用于?', '[{"key":"A","text":"地面互联网传输"},{"key":"B","text":"星地链路 packet 传输"},{"key":"C","text":"卫星轨道测量"},{"key":"D","text":"信号调制"}]', 'B', 1, 24, NOW(), NOW()), +('gaozhi', 'CCSDS标准是?', '[{"key":"A","text":"空间数据系统咨询委员会标准"},{"key":"B","text":"通信系统标准"},{"key":"C","text":"卫星导航标准"},{"key":"D","text":"互联网标准"}]', 'A', 1, 25, NOW(), NOW()), +('gaozhi', 'LDPC编码属于?', '[{"key":"A","text":"低密度奇偶校验码"},{"key":"B","text":"低密度分组码"},{"key":"C","text":"循环码"},{"key":"D","text":"卷积码"}]', 'A', 1, 26, NOW(), NOW()), +('gaozhi', 'MIMO技术可以提高?', '[{"key":"A","text":"信道容量"},{"key":"B","text":"发射功率"},{"key":"C","text":"噪声功率"},{"key":"D","text":"传播距离"}]', 'A', 1, 27, NOW(), NOW()), +('gaozhi', 'OFDM循环前缀的作用是?', '[{"key":"A","text":"增加功率"},{"key":"B","text":"消除符号间干扰"},{"key":"C","text":"提高频率"},{"key":"D","text":"降低速度"}]', 'B', 1, 28, NOW(), NOW()), +('gaozhi', 'Turbo码的主要优点是?', '[{"key":"A","text":"接近香农极限的性能"},{"key":"B","text":"编码简单"},{"key":"C","text":"译码简单"},{"key":"D","text":"速率很高"}]', 'A', 1, 29, NOW(), NOW()), +('gaozhi', 'ARQ协议的作用是?', '[{"key":"A","text":"自动重传请求"},{"key":"B","text":"自动编码"},{"key":"C","text":"自动应答"},{"key":"D","text":"自动加密"}]', 'A', 1, 30, NOW(), NOW()), +('gaozhi', 'TCP/IP协议中,TCP提供?', '[{"key":"A","text":"面向连接可靠服务"},{"key":"B","text":"无连接不可靠服务"},{"key":"C","text":"广播服务"},{"key":"D","text":"组播服务"}]', 'A', 1, 31, NOW(), NOW()), +('gaozhi', 'UDP协议提供?', '[{"key":"A","text":"面向连接可靠服务"},{"key":"B","text":"无连接不可靠服务"},{"key":"C","text":"广播服务"},{"key":"D","text":"组播服务"}]', 'B', 1, 32, NOW(), NOW()), +('gaozhi', 'HTTPS协议使用什么端口?', '[{"key":"A","text":"80"},{"key":"B","text":"443"},{"key":"C","text":"8080"},{"key":"D","text":"22"}]', 'B', 1, 33, NOW(), NOW()), +('gaozhi', 'IPv6地址长度为?', '[{"key":"A","text":"32位"},{"key":"B","text":"64位"},{"key":"C","text":"128位"},{"key":"D","text":"256位"}]', 'C', 1, 34, NOW(), NOW()), +('gaozhi', 'OSI模型有几层?', '[{"key":"A","text":"5"},{"key":"B","text":"6"},{"key":"C","text":"7"},{"key":"D","text":"8"}]', 'C', 1, 35, NOW(), NOW()), +('gaozhi', '调制的主要作用是?', '[{"key":"A","text":"将基带信号搬移到高频载波"},{"key":"B","text":"将高频信号搬移到基带"},{"key":"C","text":"放大信号"},{"key":"D","text":"滤波信号"}]', 'A', 1, 36, NOW(), NOW()), +('gaozhi', '解调的主要作用是?', '[{"key":"A","text":"从高频载波提取基带信号"},{"key":"B","text":"放大信号"},{"key":"C","text":"滤波信号"},{"key":"D","text":"变频信号"}]', 'A', 1, 37, NOW(), NOW()), +('gaozhi', '信源编码的主要目的是?', '[{"key":"A","text":"提高传输可靠性"},{"key":"B","text":"压缩数据率"},{"key":"C","text":"增加冗余"},{"key":"D","text":"提高功率"}]', 'B', 1, 38, NOW(), NOW()), +('gaozhi', '信道编码的主要目的是?', '[{"key":"A","text":"压缩数据"},{"key":"B","text":"差错控制"},{"key":"C","text":"提高速率"},{"key":"D","text":"降低功率"}]', 'B', 1, 39, NOW(), NOW()), +('gaozhi', 'AM调制中,调幅指数m满足?', '[{"key":"A","text":"0 ≤ m ≤ 1"},{"key":"B","text":"m > 1"},{"key":"C","text":"m < 0"},{"key":"D","text":"m = 0"}]', 'A', 1, 40, NOW(), NOW()), +('gaozhi', 'FM调制相比AM调制?', '[{"key":"A","text":"抗噪声性能更好"},{"key":"B","text":"功率更小"},{"key":"C","text":"带宽更小"},{"key":"D","text":"实现更简单"}]', 'A', 1, 41, NOW(), NOW()), +('gaozhi', 'PCM编码三个步骤依次是?', '[{"key":"A","text":"抽样、量化、编码"},{"key":"B","text":"量化、抽样、编码"},{"key":"C","text":"编码、抽样、量化"},{"key":"D","text":"抽样、编码、量化"}]', 'A', 1, 42, NOW(), NOW()), +('gaozhi', '奈奎斯特第一准则解决什么问题?', '[{"key":"A","text":"无码间干扰的传输特性"},{"key":"B","text":"信噪比计算"},{"key":"C","text":"信道容量计算"},{"key":"D","text":"功率分配"}]', 'A', 1, 43, NOW(), NOW()), +('gaozhi', '匹配滤波器输出信噪比在什么时候最大?', '[{"key":"A","text":"抽样时刻"},{"key":"B","text":"发送时刻"},{"key":"C","text":"接收开始"},{"key":"D","text":"接收结束"}]', 'A', 1, 44, NOW(), NOW()), +('gaozhi', '升余弦滚降滤波器的作用是?', '[{"key":"A","text":"降低码间干扰"},{"key":"B","text":"增加码间干扰"},{"key":"C","text":"提高功率"},{"key":"D","text":"改变频率"}]', 'A', 1, 45, NOW(), NOW()), +('gaozhi', '希尔伯特变换主要用于?', '[{"key":"A","text":"产生解析信号"},{"key":"B","text":"滤波信号"},{"key":"C","text":"放大信号"},{"key":"D","text":"变频信号"}]', 'A', 1, 46, NOW(), NOW()), +('gaozhi', '随机过程的均值反映了?', '[{"key":"A","text":"信号的平均强度"},{"key":"B","text":"信号的起伏程度"},{"key":"C","text":"信号的相关性"},{"key":"D","text":"信号的频率"}]', 'A', 1, 47, NOW(), NOW()), +('gaozhi', '傅里叶变换将信号变换到?', '[{"key":"A","text":"时域"},{"key":"B","text":"频域"},{"key":"C","text":"空域"},{"key":"D","text":"码域"}]', 'B', 1, 48, NOW(), NOW()), +('gaozhi', '信噪比等于?', '[{"key":"A","text":"信号功率除以噪声功率"},{"key":"B","text":"噪声功率除以信号功率"},{"key":"C","text":"信号电压除以噪声电压"},{"key":"D","text":"信号电流除以噪声电流"}]', 'A', 1, 49, NOW(), NOW()), +('gaozhi', '纠错编码的主要作用是?', '[{"key":"A","text":"降低误码率"},{"key":"B","text":"提高误码率"},{"key":"C","text":"增加传输速率"},{"key":"D","text":"降低带宽占用"}]', 'A', 1, 50, NOW(), NOW()); + +-- ===================================================================== +-- 判断题 (20题 × 中职组 + 20题 × 高职组 = 40题) +-- ===================================================================== + +INSERT INTO judge_questions (`group`, question, answer, score, sort_order, created_at, updated_at) VALUES +-- 中职组 第1-20题 +('zhongzhi', '调制的目的是将低频信号搬移到高频载波上,以便于天线辐射和信道传输。', 'true', 1, 1, NOW(), NOW()), +('zhongzhi', 'GPS接收机只需要接收到3颗卫星的信号即可实现三维定位。', 'false', 1, 2, NOW(), NOW()), +('zhongzhi', 'AM调制的抗干扰能力优于FM调制。', 'false', 1, 3, NOW(), NOW()), +('zhongzhi', '卫星通信中的上行频率通常高于下行频率。', 'true', 1, 4, NOW(), NOW()), +('zhongzhi', 'NMEA-0183协议中每条语句都以"$"开头,以校验和结尾。', 'true', 1, 5, NOW(), NOW()), +('zhongzhi', '北斗卫星导航系统由空间段、地面段、用户段三部分组成。', 'true', 1, 6, NOW(), NOW()), +('zhongzhi', 'RTK定位可以达到厘米级精度。', 'true', 1, 7, NOW(), NOW()), +('zhongzhi', '电离层延迟误差与太阳活动无关。', 'false', 1, 8, NOW(), NOW()), +('zhongzhi', '差分定位可以提高定位精度。', 'true', 1, 9, NOW(), NOW()), +('zhongzhi', 'GPS卫星本身就是原子钟,提供精确的时间标准。', 'true', 1, 10, NOW(), NOW()), +('zhongzhi', '编码与调制是一回事,都是将信号变换为适合传输的形式。', 'false', 1, 11, NOW(), NOW()), +('zhongzhi', 'GEO卫星的星下点始终在赤道上空一个固定点的正上方。', 'true', 1, 12, NOW(), NOW()), +('zhongzhi', 'FDMA中,每个用户使用相同的频率。', 'false', 1, 13, NOW(), NOW()), +('zhongzhi', '电磁波在自由空间中的传播损耗与距离的平方成正比。', 'true', 1, 14, NOW(), NOW()), +('zhongzhi', 'NTRIP协议用于实时传输差分改正数据。', 'true', 1, 15, NOW(), NOW()), +('zhongzhi', '多路径效应可以通过提高接收机灵敏度来消除。', 'false', 1, 16, NOW(), NOW()), +('zhongzhi', '卫星导航中,PDOP值越小定位精度越好。', 'true', 1, 17, NOW(), NOW()), +('zhongzhi', 'GNSS接收机输出符合NMEA-0183标准的数据。', 'true', 1, 18, NOW(), NOW()), +('zhongzhi', 'GPS卫星信号包含两个频率L1和L2。', 'true', 1, 19, NOW(), NOW()), +('zhongzhi', '卫星通信时延与卫星高度无关。', 'false', 1, 20, NOW(), NOW()), + +-- 高职组 第21-40题 +('gaozhi', 'OFDM系统中,循环前缀的长度必须大于信道的最大时延扩展。', 'true', 1, 21, NOW(), NOW()), +('gaozhi', '北斗系统只能提供区域导航定位服务,不能提供全球服务。', 'false', 1, 22, NOW(), NOW()), +('gaozhi', '边缘计算可以将计算任务从云端下沉到网络边缘,降低时延。', 'true', 1, 23, NOW(), NOW()), +('gaozhi', '数字签名技术可以防止通信内容被窃听。', 'false', 1, 24, NOW(), NOW()), +('gaozhi', 'Massive MIMO技术通过增加天线数量来提升系统容量。', 'true', 1, 25, NOW(), NOW()), +('gaozhi', '香农公式说明信道容量与带宽和信噪比都正相关。', 'true', 1, 26, NOW(), NOW()), +('gaozhi', 'LDPC码是一种高性能的信道编码。', 'true', 1, 27, NOW(), NOW()), +('gaozhi', 'TCP协议提供面向连接的可靠服务。', 'true', 1, 28, NOW(), NOW()), +('gaozhi', 'UDP协议提供流量控制和拥塞控制。', 'false', 1, 29, NOW(), NOW()), +('gaozhi', '奈奎斯特第一准则告诉我们理想低通信道的最高码元传输速率等于2B。', 'true', 1, 30, NOW(), NOW()), +('gaozhi', 'CRC校验可以检测出所有的传输错误。', 'false', 1, 31, NOW(), NOW()), +('gaozhi', 'BPSK的误码性能优于QPSK。', 'false', 1, 32, NOW(), NOW()), +('gaozhi', '协方差函数反映了两个随机变量之间的相关程度。', 'true', 1, 33, NOW(), NOW()), +('gaozhi', '瑞利分布常用于描述没有直射径的多径衰落信道。', 'true', 1, 34, NOW(), NOW()), +('gaozhi', 'NMEA-0183的GGA语句包含卫星数量和HDOP信息。', 'true', 1, 35, NOW(), NOW()), +('gaozhi', '卡尔曼滤波可以用于组合导航中的状态估计。', 'true', 1, 36, NOW(), NOW()), +('gaozhi', '对流层延迟对L1和L2频段信号的影响相同。', 'false', 1, 37, NOW(), NOW()), +('gaozhi', 'AOS协议是CCSDS标准中用于空间通信的链路层协议。', 'true', 1, 38, NOW(), NOW()), +('gaozhi', '扩频通信中,扩频码速率越高,抗干扰能力越强。', 'true', 1, 39, NOW(), NOW()), +('gaozhi', '卷积码的译码通常使用维特比算法。', 'true', 1, 40, NOW(), NOW()); + +-- ===================================================================== +-- 综合性任务 (1题 × 中职组 + 1题 × 高职组) +-- ===================================================================== + +INSERT INTO word_questions (`group`, title, description, score, sort_order, created_at, updated_at) VALUES +('zhongzhi', '卫星通信系统基本原理', '请结合第一篇卫星通信技术,简述卫星通信系统的基本组成,包括空间段、地面段、用户段三大部分,并说明各部分的主要功能。分析GEO/MEO/LEO三种轨道的特点及其适用场景,举例说明典型应用。', 30, 1, NOW(), NOW()), +('gaozhi', '卫星导航融合定位方案设计', '请结合第二篇卫星导航技术,设计一个GNSS/INS组合导航定位方案。要求:(1) 分析组合导航的必要性;(2) 说明GNSS和INS各自优缺点;(3) 给出卡尔曼滤波融合框架;(4) 分析预期性能改进。', 30, 1, NOW(), NOW()); + +-- ===================================================================== +-- 统计报告 +-- ===================================================================== +-- 每组:选择题 50 × 1 = 50, 判断题 20 × 1 = 20, 综合性任务 1 × 30 = 30 → 总分 100 +-- ===================================================================== + +SELECT + (SELECT COUNT(*) FROM choice_questions WHERE `group`='zhongzhi') AS 'zhongzhi_choice', + (SELECT COUNT(*) FROM judge_questions WHERE `group`='zhongzhi') AS 'zhongzhi_judge', + (SELECT COUNT(*) FROM word_questions WHERE `group`='zhongzhi') AS 'zhongzhi_word', + (SELECT SUM(score) FROM choice_questions WHERE `group`='zhongzhi') + + (SELECT SUM(score) FROM judge_questions WHERE `group`='zhongzhi') + + (SELECT SUM(score) FROM word_questions WHERE `group`='zhongzhi') AS 'zhongzhi_total', + (SELECT COUNT(*) FROM choice_questions WHERE `group`='gaozhi') AS 'gaozhi_choice', + (SELECT COUNT(*) FROM judge_questions WHERE `group`='gaozhi') AS 'gaozhi_judge', + (SELECT COUNT(*) FROM word_questions WHERE `group`='gaozhi') AS 'gaozhi_word', + (SELECT SUM(score) FROM choice_questions WHERE `group`='gaozhi') + + (SELECT SUM(score) FROM judge_questions WHERE `group`='gaozhi') + + (SELECT SUM(score) FROM word_questions WHERE `group`='gaozhi') AS 'gaozhi_total'; diff --git a/database/config.json b/database/config.json new file mode 100644 index 0000000..f895899 --- /dev/null +++ b/database/config.json @@ -0,0 +1,71 @@ +{ + "development": { + "username": "root", + "password": "74191221", + "database": "competition_db", + "host": "localhost", + "port": 3306, + "dialect": "mysql", + "dialectOptions": { + "charset": "utf8mb4", + "collate": "utf8mb4_unicode_ci" + }, + "define": { + "underscored": true, + "freezeTableName": true, + "timestamps": true, + "createdAt": "created_at", + "updatedAt": "updated_at", + "charset": "utf8mb4", + "collate": "utf8mb4_unicode_ci" + }, + "timezone": "+08:00", + "seederStorage": "sequelize" + }, + "test": { + "username": "root", + "password": "74191221", + "database": "competition_db_test", + "host": "localhost", + "port": 3306, + "dialect": "mysql", + "dialectOptions": { + "charset": "utf8mb4", + "collate": "utf8mb4_unicode_ci" + }, + "define": { + "underscored": true, + "freezeTableName": true, + "timestamps": true, + "createdAt": "created_at", + "updatedAt": "updated_at", + "charset": "utf8mb4", + "collate": "utf8mb4_unicode_ci" + }, + "timezone": "+08:00", + "seederStorage": "sequelize" + }, + "production": { + "username": "root", + "password": null, + "database": "competition_db", + "host": "localhost", + "port": 3306, + "dialect": "mysql", + "dialectOptions": { + "charset": "utf8mb4", + "collate": "utf8mb4_unicode_ci" + }, + "define": { + "underscored": true, + "freezeTableName": true, + "timestamps": true, + "createdAt": "created_at", + "updatedAt": "updated_at", + "charset": "utf8mb4", + "collate": "utf8mb4_unicode_ci" + }, + "timezone": "+08:00", + "seederStorage": "sequelize" + } +} diff --git a/database/migrations/001-create-accounts.js b/database/migrations/001-create-accounts.js new file mode 100644 index 0000000..b8c0e6b --- /dev/null +++ b/database/migrations/001-create-accounts.js @@ -0,0 +1,125 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('accounts', { + uid: { + type: Sequelize.INTEGER, + primaryKey: true, + autoIncrement: true, + allowNull: false, + comment: '用户唯一标识', + }, + role: { + type: Sequelize.STRING(32), + allowNull: false, + defaultValue: 'student', + comment: '角色:student/teacher/judge/admin', + }, + name_cn: { + type: Sequelize.STRING(64), + allowNull: false, + comment: '中文姓名', + }, + name_en: { + type: Sequelize.STRING(128), + allowNull: true, + comment: '英文姓名/拼音', + }, + gender: { + type: Sequelize.STRING(8), + allowNull: true, + comment: '性别:男/女', + }, + phone: { + type: Sequelize.STRING(32), + allowNull: true, + comment: '手机号码', + }, + email: { + type: Sequelize.STRING(128), + allowNull: true, + comment: '电子邮箱', + }, + id_number: { + type: Sequelize.STRING(32), + allowNull: false, + unique: true, + comment: '身份证号(唯一)', + }, + password: { + type: Sequelize.STRING(128), + allowNull: false, + comment: '密码(加密存储)', + }, + salt: { + type: Sequelize.STRING(64), + allowNull: false, + comment: '密码盐值', + }, + position: { + type: Sequelize.STRING(64), + allowNull: true, + comment: '职务/职称', + }, + major: { + type: Sequelize.STRING(128), + allowNull: true, + comment: '专业', + }, + country: { + type: Sequelize.STRING(64), + allowNull: true, + comment: '国家', + }, + region: { + type: Sequelize.STRING(64), + allowNull: true, + comment: '地区/省份', + }, + organization_cn: { + type: Sequelize.STRING(255), + allowNull: true, + comment: '所属单位(中文)', + }, + organization_en: { + type: Sequelize.STRING(255), + allowNull: true, + comment: '所属单位(英文)', + }, + activated: { + type: Sequelize.TINYINT, + allowNull: false, + defaultValue: 0, + comment: '激活状态:0未激活 1已激活', + }, + score: { + type: Sequelize.INTEGER, + allowNull: true, + comment: '总分/积分', + }, + created_at: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '创建时间', + }, + updated_at: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'), + comment: '更新时间', + }, + }).then(() => { + return Promise.all([ + queryInterface.addIndex('accounts', ['role'], { name: 'idx_role' }), + queryInterface.addIndex('accounts', ['phone'], { name: 'idx_phone' }), + queryInterface.addIndex('accounts', ['name_cn'], { name: 'idx_name_cn' }), + ]); + }); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('accounts'); + }, +}; diff --git a/database/migrations/001-create-contestants.js b/database/migrations/001-create-contestants.js new file mode 100644 index 0000000..ece5366 --- /dev/null +++ b/database/migrations/001-create-contestants.js @@ -0,0 +1,105 @@ +'use strict'; + +/** + * 迁移文件:创建选手表(contestants) + * 包含选手基本信息、身份认证、组别和成绩字段 + */ + +module.exports = { + /** + * 执行迁移:创建 contestants 表 + * @param {import('sequelize').QueryInterface} queryInterface + * @param {import('sequelize').Sequelize} Sequelize + */ + up: async (queryInterface, Sequelize) => { + const { INTEGER, STRING, BOOLEAN, DATE } = Sequelize; + + await queryInterface.createTable('contestants', { + // 主键 ID,自增 + id: { + type: INTEGER, + primaryKey: true, + autoIncrement: true, + allowNull: false, + comment: '选手唯一标识', + }, + // 身份证号,唯一,18位 + id_number: { + type: STRING(18), + allowNull: false, + unique: true, + comment: '身份证号', + }, + // 选手姓名 + name: { + type: STRING(50), + allowNull: false, + comment: '选手姓名', + }, + // 手机号码 + phone: { + type: STRING(20), + allowNull: false, + comment: '手机号码', + }, + // 参赛组别 + group: { + type: STRING(20), + allowNull: false, + comment: '参赛组别(zhongzhi/gaozhi/benke/jiaoshi)', + }, + // 登录密码 + password: { + type: STRING(100), + allowNull: false, + defaultValue: '', + comment: '登录密码', + }, + // 账号激活状态 + activated: { + type: BOOLEAN, + allowNull: false, + defaultValue: false, + comment: '账号激活状态', + }, + // 总分 + score: { + type: INTEGER, + allowNull: true, + defaultValue: null, + comment: '选手总分', + }, + // 创建时间 + created_at: { + type: DATE, + allowNull: false, + comment: '创建时间', + }, + // 更新时间 + updated_at: { + type: DATE, + allowNull: false, + comment: '更新时间', + }, + }); + + // 为 id_number 创建唯一索引 + await queryInterface.addIndex('contestants', ['id_number'], { + unique: true, + name: 'idx_contestants_id_number', + }); + + // 为 group 创建索引 + await queryInterface.addIndex('contestants', ['group'], { + name: 'idx_contestants_group', + }); + }, + + /** + * 回滚迁移:删除 contestants 表 + * @param {import('sequelize').QueryInterface} queryInterface + */ + down: async (queryInterface) => { + await queryInterface.dropTable('contestants'); + }, +}; \ No newline at end of file diff --git a/database/migrations/002-create-admins.js b/database/migrations/002-create-admins.js new file mode 100644 index 0000000..e9454a1 --- /dev/null +++ b/database/migrations/002-create-admins.js @@ -0,0 +1,61 @@ +'use strict'; + +/** + * 迁移文件:创建管理员表(admins) + * 存储后台管理员账号信息 + */ + +module.exports = { + /** + * 执行迁移:创建 admins 表 + * @param {import('sequelize').QueryInterface} queryInterface + * @param {import('sequelize').Sequelize} Sequelize + */ + up: async (queryInterface, Sequelize) => { + const { INTEGER, STRING, DATE } = Sequelize; + + await queryInterface.createTable('admins', { + id: { + type: INTEGER, + primaryKey: true, + autoIncrement: true, + allowNull: false, + comment: '管理员唯一标识', + }, + username: { + type: STRING(50), + allowNull: false, + unique: true, + comment: '管理员用户名', + }, + password: { + type: STRING(100), + allowNull: false, + comment: '管理员密码', + }, + created_at: { + type: DATE, + allowNull: false, + comment: '创建时间', + }, + updated_at: { + type: DATE, + allowNull: false, + comment: '更新时间', + }, + }); + + await queryInterface.addIndex('admins', ['username'], { + unique: true, + name: 'idx_admins_username', + }); + }, + + /** + * 回滚迁移:删除 admins 表 + * @param {import('sequelize').QueryInterface} queryInterface + */ + down: async (queryInterface) => { + await queryInterface.dropTable('admins'); + }, +}; \ No newline at end of file diff --git a/database/migrations/002-create-teams.js b/database/migrations/002-create-teams.js new file mode 100644 index 0000000..60c91b0 --- /dev/null +++ b/database/migrations/002-create-teams.js @@ -0,0 +1,56 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('teams', { + team_uid: { + type: Sequelize.INTEGER, + primaryKey: true, + // 注意:运行库中 teams 表真实自增主键为 id,team_uid 为非自增普通列, + // 由应用层(importFromExcel 显式 MAX+1)赋值。故此处不声明 autoIncrement, + // 使迁移与运行库一致,避免新建库时 team_uid 被设为自增而与运行库行为背离。 + allowNull: false, + comment: '团队唯一标识(由应用层显式生成,非 DB 自增)', + }, + group_name: { + type: Sequelize.STRING(64), + allowNull: false, + comment: '组别名称', + }, + team_id: { + type: Sequelize.STRING(64), + allowNull: false, + unique: true, + comment: '团队编号(唯一)', + }, + team_number: { + type: Sequelize.STRING(32), + allowNull: true, + comment: '团队序号', + }, + address: { + type: Sequelize.STRING(255), + allowNull: true, + comment: '地址', + }, + created_at: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '创建时间', + }, + updated_at: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'), + comment: '更新时间', + }, + }).then(() => { + return queryInterface.addIndex('teams', ['group_name'], { name: 'idx_group_name' }); + }); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('teams'); + }, +}; diff --git a/database/migrations/003-create-competition-info.js b/database/migrations/003-create-competition-info.js new file mode 100644 index 0000000..18d4807 --- /dev/null +++ b/database/migrations/003-create-competition-info.js @@ -0,0 +1,42 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('competition_info', { + competition_uid: { + type: Sequelize.INTEGER, + primaryKey: true, + autoIncrement: true, + allowNull: false, + comment: '赛事唯一标识', + }, + competition_name: { + type: Sequelize.STRING(128), + allowNull: false, + comment: '赛事名称', + }, + competition_code: { + type: Sequelize.STRING(64), + allowNull: false, + unique: true, + comment: '赛事编码(唯一)', + }, + created_at: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '创建时间', + }, + updated_at: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'), + comment: '更新时间', + }, + }); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('competition_info'); + }, +}; diff --git a/database/migrations/004-create-aggregation.js b/database/migrations/004-create-aggregation.js new file mode 100644 index 0000000..9ff287f --- /dev/null +++ b/database/migrations/004-create-aggregation.js @@ -0,0 +1,94 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('aggregation', { + aggregation_uid: { + type: Sequelize.INTEGER, + primaryKey: true, + autoIncrement: true, + allowNull: false, + comment: '报名记录唯一标识', + }, + competition_uid: { + type: Sequelize.INTEGER, + allowNull: false, + comment: '赛事ID(关联competition_info)', + references: { model: 'competition_info', key: 'competition_uid' }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + }, + team_uid: { + type: Sequelize.INTEGER, + allowNull: false, + comment: '团队ID(关联teams)', + references: { model: 'teams', key: 'team_uid' }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + }, + account_uid: { + type: Sequelize.INTEGER, + allowNull: false, + comment: '用户ID(关联accounts)', + references: { model: 'accounts', key: 'uid' }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + }, + identity: { + type: Sequelize.STRING(32), + allowNull: true, + comment: '身份标识', + }, + is_registrant: { + type: Sequelize.TINYINT, + allowNull: false, + defaultValue: 0, + comment: '是否为主要报名人:0否 1是', + }, + registration_form: { + type: Sequelize.STRING(512), + allowNull: true, + comment: '报名表文件路径', + }, + stamped_form: { + type: Sequelize.STRING(512), + allowNull: true, + comment: '盖章报名表文件路径', + }, + audit_status: { + type: Sequelize.STRING(32), + allowNull: false, + defaultValue: 'pending', + comment: '审核状态:pending/approved/rejected', + }, + reject_reason: { + type: Sequelize.STRING(512), + allowNull: true, + comment: '驳回原因', + }, + created_at: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '创建时间', + }, + updated_at: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'), + comment: '更新时间', + }, + }).then(() => { + return Promise.all([ + queryInterface.addIndex('aggregation', ['competition_uid'], { name: 'idx_competition_uid' }), + queryInterface.addIndex('aggregation', ['team_uid'], { name: 'idx_team_uid' }), + queryInterface.addIndex('aggregation', ['account_uid'], { name: 'idx_account_uid' }), + queryInterface.addIndex('aggregation', ['audit_status'], { name: 'idx_audit_status' }), + ]); + }); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('aggregation'); + }, +}; diff --git a/database/migrations/004-create-exam-questions.js b/database/migrations/004-create-exam-questions.js new file mode 100644 index 0000000..0f8cfd8 --- /dev/null +++ b/database/migrations/004-create-exam-questions.js @@ -0,0 +1,69 @@ +'use strict'; + +/** + * 迁移文件:创建试卷题目表(exam_questions) + * 存储各组别的试卷题目 + */ + +module.exports = { + up: async (queryInterface, Sequelize) => { + const { INTEGER, STRING, TEXT, DATE } = Sequelize; + + await queryInterface.createTable('exam_questions', { + id: { + type: INTEGER, + primaryKey: true, + autoIncrement: true, + allowNull: false, + comment: '题目唯一标识', + }, + group: { + type: STRING(20), + allowNull: false, + comment: '题目所属组别', + }, + title: { + type: STRING(200), + allowNull: false, + comment: '题目标题', + }, + content: { + type: TEXT, + allowNull: false, + comment: '题目内容描述', + }, + score: { + type: INTEGER, + allowNull: false, + comment: '题目分值', + }, + sort_order: { + type: INTEGER, + allowNull: false, + defaultValue: 0, + comment: '排序序号', + }, + created_at: { + type: DATE, + allowNull: false, + comment: '创建时间', + }, + updated_at: { + type: DATE, + allowNull: false, + comment: '更新时间', + }, + }); + + await queryInterface.addIndex('exam_questions', ['group'], { + name: 'idx_exam_questions_group', + }); + await queryInterface.addIndex('exam_questions', ['group', 'sort_order'], { + name: 'idx_exam_questions_group_sort', + }); + }, + + down: async (queryInterface) => { + await queryInterface.dropTable('exam_questions'); + }, +}; \ No newline at end of file diff --git a/database/migrations/005-create-exam-submissions.js b/database/migrations/005-create-exam-submissions.js new file mode 100644 index 0000000..997664d --- /dev/null +++ b/database/migrations/005-create-exam-submissions.js @@ -0,0 +1,67 @@ +'use strict'; + +/** + * 迁移文件:创建答题记录表(exam_submissions) + * 存储选手的试卷答题记录 + */ + +module.exports = { + up: async (queryInterface, Sequelize) => { + const { INTEGER, TEXT, DATE } = Sequelize; + + await queryInterface.createTable('exam_submissions', { + id: { + type: INTEGER, + primaryKey: true, + autoIncrement: true, + allowNull: false, + comment: '答题记录唯一标识', + }, + contestant_id: { + type: INTEGER, + allowNull: false, + comment: '选手 ID', + references: { + model: 'contestants', + key: 'id', + }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + }, + answers: { + type: TEXT, + allowNull: false, + comment: '答题内容(JSON)', + }, + submit_time: { + type: DATE, + allowNull: false, + comment: '提交时间', + }, + score: { + type: INTEGER, + allowNull: true, + defaultValue: null, + comment: '试卷得分', + }, + created_at: { + type: DATE, + allowNull: false, + comment: '创建时间', + }, + updated_at: { + type: DATE, + allowNull: false, + comment: '更新时间', + }, + }); + + await queryInterface.addIndex('exam_submissions', ['contestant_id'], { + name: 'idx_exam_submissions_contestant_id', + }); + }, + + down: async (queryInterface) => { + await queryInterface.dropTable('exam_submissions'); + }, +}; \ No newline at end of file diff --git a/database/migrations/005-create-news.js b/database/migrations/005-create-news.js new file mode 100644 index 0000000..2825a78 --- /dev/null +++ b/database/migrations/005-create-news.js @@ -0,0 +1,68 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('news', { + id: { + type: Sequelize.INTEGER, + primaryKey: true, + autoIncrement: true, + allowNull: false, + comment: '新闻ID', + }, + title: { + type: Sequelize.STRING(255), + allowNull: false, + comment: '新闻标题', + }, + content: { + type: Sequelize.TEXT('long'), + allowNull: true, + comment: '新闻内容(富文本)', + }, + wx_link: { + type: Sequelize.STRING(512), + allowNull: true, + comment: '微信链接', + }, + publish_time: { + type: Sequelize.DATE, + allowNull: true, + comment: '发布时间', + }, + pinned: { + type: Sequelize.TINYINT, + allowNull: false, + defaultValue: 0, + comment: '是否置顶:0否 1是', + }, + group_name: { + type: Sequelize.STRING(64), + allowNull: true, + comment: '目标组别', + }, + created_at: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '创建时间', + }, + updated_at: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'), + comment: '更新时间', + }, + }).then(() => { + return Promise.all([ + queryInterface.addIndex('news', ['publish_time'], { name: 'idx_publish_time' }), + queryInterface.addIndex('news', ['pinned', 'publish_time'], { name: 'idx_pinned_publish_time' }), + queryInterface.addIndex('news', ['group_name'], { name: 'idx_group_name' }), + ]); + }); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('news'); + }, +}; diff --git a/database/migrations/006-create-carousel.js b/database/migrations/006-create-carousel.js new file mode 100644 index 0000000..6961d52 --- /dev/null +++ b/database/migrations/006-create-carousel.js @@ -0,0 +1,58 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('carousel', { + id: { + type: Sequelize.INTEGER, + primaryKey: true, + autoIncrement: true, + allowNull: false, + comment: '轮播图ID', + }, + image_url: { + type: Sequelize.STRING(512), + allowNull: false, + comment: '图片URL', + }, + sort_order: { + type: Sequelize.INTEGER, + allowNull: false, + defaultValue: 0, + comment: '排序序号', + }, + link_url: { + type: Sequelize.STRING(512), + allowNull: true, + comment: '点击跳转链接', + }, + enabled: { + type: Sequelize.TINYINT, + allowNull: false, + defaultValue: 1, + comment: '是否启用:0禁用 1启用', + }, + created_at: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '创建时间', + }, + updated_at: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'), + comment: '更新时间', + }, + }).then(() => { + return Promise.all([ + queryInterface.addIndex('carousel', ['sort_order'], { name: 'idx_sort_order' }), + queryInterface.addIndex('carousel', ['enabled'], { name: 'idx_enabled' }), + ]); + }); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('carousel'); + }, +}; diff --git a/database/migrations/006-create-work-submissions.js b/database/migrations/006-create-work-submissions.js new file mode 100644 index 0000000..c70f1d7 --- /dev/null +++ b/database/migrations/006-create-work-submissions.js @@ -0,0 +1,85 @@ +'use strict'; + +/** + * 迁移文件:创建作品提交表(work_submissions) + * 存储选手上传的作品文件信息 + */ + +module.exports = { + up: async (queryInterface, Sequelize) => { + const { INTEGER, STRING, DATE } = Sequelize; + + await queryInterface.createTable('work_submissions', { + id: { + type: INTEGER, + primaryKey: true, + autoIncrement: true, + allowNull: false, + comment: '作品提交唯一标识', + }, + contestant_id: { + type: INTEGER, + allowNull: false, + comment: '选手 ID', + references: { + model: 'contestants', + key: 'id', + }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + }, + file_type: { + type: STRING(20), + allowNull: false, + comment: '文件类型(plan/ppt)', + }, + file_name: { + type: STRING(200), + allowNull: false, + comment: '原始文件名', + }, + file_path: { + type: STRING(500), + allowNull: false, + comment: '文件存储路径', + }, + file_size: { + type: INTEGER, + allowNull: false, + comment: '文件大小(字节)', + }, + submit_time: { + type: DATE, + allowNull: false, + comment: '提交时间', + }, + score: { + type: INTEGER, + allowNull: true, + defaultValue: null, + comment: '作品得分', + }, + created_at: { + type: DATE, + allowNull: false, + comment: '创建时间', + }, + updated_at: { + type: DATE, + allowNull: false, + comment: '更新时间', + }, + }); + + await queryInterface.addIndex('work_submissions', ['contestant_id'], { + name: 'idx_work_submissions_contestant_id', + }); + await queryInterface.addIndex('work_submissions', ['file_type'], { + name: 'idx_work_submissions_file_type', + }); + }, + + down: async (queryInterface) => { + await queryInterface.dropTable('work_submissions'); + }, +}; \ No newline at end of file diff --git a/database/migrations/007-create-zhongzhi-questions.js b/database/migrations/007-create-zhongzhi-questions.js new file mode 100644 index 0000000..c3bf6f0 --- /dev/null +++ b/database/migrations/007-create-zhongzhi-questions.js @@ -0,0 +1,69 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('zhongzhi_questions', { + question_uid: { + type: Sequelize.INTEGER, + primaryKey: true, + autoIncrement: true, + allowNull: false, + comment: '题目唯一标识', + }, + question: { + type: Sequelize.TEXT, + allowNull: false, + comment: '题目内容', + }, + answer: { + type: Sequelize.STRING(32), + allowNull: false, + comment: '正确答案', + }, + question_type: { + type: Sequelize.STRING(16), + allowNull: false, + defaultValue: 'choice', + comment: '题目类型:choice选择题 judge判断题', + }, + options: { + type: Sequelize.JSON, + allowNull: true, + comment: '选项(JSON格式)', + }, + score: { + type: Sequelize.INTEGER, + allowNull: false, + defaultValue: 0, + comment: '题目分值', + }, + sort_order: { + type: Sequelize.INTEGER, + allowNull: false, + defaultValue: 0, + comment: '排序序号', + }, + created_at: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '创建时间', + }, + updated_at: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'), + comment: '更新时间', + }, + }).then(() => { + return Promise.all([ + queryInterface.addIndex('zhongzhi_questions', ['question_type'], { name: 'idx_question_type' }), + queryInterface.addIndex('zhongzhi_questions', ['sort_order'], { name: 'idx_sort_order' }), + ]); + }); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('zhongzhi_questions'); + }, +}; diff --git a/database/migrations/008-create-gaozhi-questions.js b/database/migrations/008-create-gaozhi-questions.js new file mode 100644 index 0000000..08e74f6 --- /dev/null +++ b/database/migrations/008-create-gaozhi-questions.js @@ -0,0 +1,69 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('gaozhi_questions', { + question_uid: { + type: Sequelize.INTEGER, + primaryKey: true, + autoIncrement: true, + allowNull: false, + comment: '题目唯一标识', + }, + question: { + type: Sequelize.TEXT, + allowNull: false, + comment: '题目内容', + }, + answer: { + type: Sequelize.STRING(32), + allowNull: false, + comment: '正确答案', + }, + question_type: { + type: Sequelize.STRING(16), + allowNull: false, + defaultValue: 'choice', + comment: '题目类型:choice选择题 judge判断题', + }, + options: { + type: Sequelize.JSON, + allowNull: true, + comment: '选项(JSON格式)', + }, + score: { + type: Sequelize.INTEGER, + allowNull: false, + defaultValue: 0, + comment: '题目分值', + }, + sort_order: { + type: Sequelize.INTEGER, + allowNull: false, + defaultValue: 0, + comment: '排序序号', + }, + created_at: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '创建时间', + }, + updated_at: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'), + comment: '更新时间', + }, + }).then(() => { + return Promise.all([ + queryInterface.addIndex('gaozhi_questions', ['question_type'], { name: 'idx_question_type' }), + queryInterface.addIndex('gaozhi_questions', ['sort_order'], { name: 'idx_sort_order' }), + ]); + }); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('gaozhi_questions'); + }, +}; diff --git a/database/migrations/009-create-zhongzhi-submissions.js b/database/migrations/009-create-zhongzhi-submissions.js new file mode 100644 index 0000000..1393a55 --- /dev/null +++ b/database/migrations/009-create-zhongzhi-submissions.js @@ -0,0 +1,82 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('zhongzhi_submissions', { + id: { + type: Sequelize.INTEGER, + primaryKey: true, + autoIncrement: true, + allowNull: false, + comment: '提交记录ID', + }, + aggregation_uid: { + type: Sequelize.INTEGER, + allowNull: false, + comment: '报名记录ID(关联aggregation)', + references: { model: 'aggregation', key: 'aggregation_uid' }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + }, + question_uid: { + type: Sequelize.INTEGER, + allowNull: false, + comment: '题目ID(关联zhongzhi_questions)', + references: { model: 'zhongzhi_questions', key: 'question_uid' }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + }, + answer_data: { + type: Sequelize.STRING(255), + allowNull: true, + comment: '提交的答案', + }, + is_correct: { + type: Sequelize.TINYINT, + allowNull: true, + comment: '是否正确:0错误 1正确', + }, + score: { + type: Sequelize.INTEGER, + allowNull: true, + comment: '得分', + }, + word_file_path: { + type: Sequelize.STRING(512), + allowNull: true, + comment: 'Word文件路径', + }, + word_file_name: { + type: Sequelize.STRING(255), + allowNull: true, + comment: 'Word文件原始名称', + }, + word_file_md5: { + type: Sequelize.STRING(64), + allowNull: true, + comment: 'Word文件MD5哈希值', + }, + created_at: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '创建时间', + }, + updated_at: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'), + comment: '更新时间', + }, + }).then(() => { + return Promise.all([ + queryInterface.addIndex('zhongzhi_submissions', ['aggregation_uid'], { name: 'idx_aggregation_uid' }), + queryInterface.addIndex('zhongzhi_submissions', ['question_uid'], { name: 'idx_question_uid' }), + ]); + }); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('zhongzhi_submissions'); + }, +}; diff --git a/database/migrations/010-create-gaozhi-submissions.js b/database/migrations/010-create-gaozhi-submissions.js new file mode 100644 index 0000000..f38ccc4 --- /dev/null +++ b/database/migrations/010-create-gaozhi-submissions.js @@ -0,0 +1,82 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('gaozhi_submissions', { + id: { + type: Sequelize.INTEGER, + primaryKey: true, + autoIncrement: true, + allowNull: false, + comment: '提交记录ID', + }, + aggregation_uid: { + type: Sequelize.INTEGER, + allowNull: false, + comment: '报名记录ID(关联aggregation)', + references: { model: 'aggregation', key: 'aggregation_uid' }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + }, + question_uid: { + type: Sequelize.INTEGER, + allowNull: false, + comment: '题目ID(关联gaozhi_questions)', + references: { model: 'gaozhi_questions', key: 'question_uid' }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + }, + answer_data: { + type: Sequelize.STRING(255), + allowNull: true, + comment: '提交的答案', + }, + is_correct: { + type: Sequelize.TINYINT, + allowNull: true, + comment: '是否正确:0错误 1正确', + }, + score: { + type: Sequelize.INTEGER, + allowNull: true, + comment: '得分', + }, + word_file_path: { + type: Sequelize.STRING(512), + allowNull: true, + comment: 'Word文件路径', + }, + word_file_name: { + type: Sequelize.STRING(255), + allowNull: true, + comment: 'Word文件原始名称', + }, + word_file_md5: { + type: Sequelize.STRING(64), + allowNull: true, + comment: 'Word文件MD5哈希值', + }, + created_at: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '创建时间', + }, + updated_at: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'), + comment: '更新时间', + }, + }).then(() => { + return Promise.all([ + queryInterface.addIndex('gaozhi_submissions', ['aggregation_uid'], { name: 'idx_aggregation_uid' }), + queryInterface.addIndex('gaozhi_submissions', ['question_uid'], { name: 'idx_question_uid' }), + ]); + }); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('gaozhi_submissions'); + }, +}; diff --git a/database/migrations/011-create-benke-submissions.js b/database/migrations/011-create-benke-submissions.js new file mode 100644 index 0000000..3b909e8 --- /dev/null +++ b/database/migrations/011-create-benke-submissions.js @@ -0,0 +1,77 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('benke_submissions', { + id: { + type: Sequelize.INTEGER, + primaryKey: true, + autoIncrement: true, + allowNull: false, + comment: '提交记录ID', + }, + aggregation_uid: { + type: Sequelize.INTEGER, + allowNull: false, + comment: '报名记录ID(关联aggregation)', + references: { model: 'aggregation', key: 'aggregation_uid' }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + }, + work_name: { + type: Sequelize.STRING(255), + allowNull: false, + comment: '作品名称', + }, + file_path: { + type: Sequelize.STRING(512), + allowNull: false, + comment: '文件存储路径', + }, + file_name: { + type: Sequelize.STRING(255), + allowNull: false, + comment: '文件原始名称', + }, + file_size: { + type: Sequelize.INTEGER.UNSIGNED, + allowNull: false, + defaultValue: 0, + comment: '文件大小(字节)', + }, + file_md5: { + type: Sequelize.STRING(64), + allowNull: true, + comment: '文件MD5哈希值', + }, + upload_time: { + type: Sequelize.DATE, + allowNull: true, + comment: '上传时间', + }, + score: { + type: Sequelize.INTEGER, + allowNull: true, + comment: '评分', + }, + created_at: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '创建时间', + }, + updated_at: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'), + comment: '更新时间', + }, + }).then(() => { + return queryInterface.addIndex('benke_submissions', ['aggregation_uid'], { name: 'idx_aggregation_uid' }); + }); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('benke_submissions'); + }, +}; diff --git a/database/migrations/012-create-jiaoshi-submissions.js b/database/migrations/012-create-jiaoshi-submissions.js new file mode 100644 index 0000000..a280031 --- /dev/null +++ b/database/migrations/012-create-jiaoshi-submissions.js @@ -0,0 +1,77 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('jiaoshi_submissions', { + id: { + type: Sequelize.INTEGER, + primaryKey: true, + autoIncrement: true, + allowNull: false, + comment: '提交记录ID', + }, + aggregation_uid: { + type: Sequelize.INTEGER, + allowNull: false, + comment: '报名记录ID(关联aggregation)', + references: { model: 'aggregation', key: 'aggregation_uid' }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + }, + work_name: { + type: Sequelize.STRING(255), + allowNull: false, + comment: '作品名称', + }, + file_path: { + type: Sequelize.STRING(512), + allowNull: false, + comment: '文件存储路径', + }, + file_name: { + type: Sequelize.STRING(255), + allowNull: false, + comment: '文件原始名称', + }, + file_size: { + type: Sequelize.INTEGER.UNSIGNED, + allowNull: false, + defaultValue: 0, + comment: '文件大小(字节)', + }, + file_md5: { + type: Sequelize.STRING(64), + allowNull: true, + comment: '文件MD5哈希值', + }, + upload_time: { + type: Sequelize.DATE, + allowNull: true, + comment: '上传时间', + }, + score: { + type: Sequelize.INTEGER, + allowNull: true, + comment: '评分', + }, + created_at: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '创建时间', + }, + updated_at: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'), + comment: '更新时间', + }, + }).then(() => { + return queryInterface.addIndex('jiaoshi_submissions', ['aggregation_uid'], { name: 'idx_aggregation_uid' }); + }); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('jiaoshi_submissions'); + }, +}; diff --git a/database/migrations/013-create-competition-times.js b/database/migrations/013-create-competition-times.js new file mode 100644 index 0000000..ee784bf --- /dev/null +++ b/database/migrations/013-create-competition-times.js @@ -0,0 +1,57 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('competition_times', { + id: { + type: Sequelize.INTEGER, + primaryKey: true, + autoIncrement: true, + allowNull: false, + comment: '配置ID', + }, + group_name: { + type: Sequelize.STRING(64), + allowNull: false, + unique: true, + comment: '组别名称(唯一)', + }, + start_time: { + type: Sequelize.DATE, + allowNull: true, + comment: '比赛开始时间', + }, + end_time: { + type: Sequelize.DATE, + allowNull: true, + comment: '比赛结束时间', + }, + deadline: { + type: Sequelize.DATE, + allowNull: true, + comment: '报名/提交截止时间', + }, + word_template: { + type: Sequelize.STRING(512), + allowNull: true, + comment: 'Word模板文件路径', + }, + created_at: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '创建时间', + }, + updated_at: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'), + comment: '更新时间', + }, + }); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('competition_times'); + }, +}; diff --git a/database/migrations/014-create-system-configs.js b/database/migrations/014-create-system-configs.js new file mode 100644 index 0000000..d7c85ff --- /dev/null +++ b/database/migrations/014-create-system-configs.js @@ -0,0 +1,42 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('system_configs', { + id: { + type: Sequelize.INTEGER, + primaryKey: true, + autoIncrement: true, + allowNull: false, + comment: '配置ID', + }, + config_key: { + type: Sequelize.STRING(128), + allowNull: false, + unique: true, + comment: '配置键名(唯一)', + }, + config_value: { + type: Sequelize.TEXT, + allowNull: true, + comment: '配置值', + }, + created_at: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '创建时间', + }, + updated_at: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'), + comment: '更新时间', + }, + }); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('system_configs'); + }, +}; diff --git a/database/migrations/015-create-choice-questions.js b/database/migrations/015-create-choice-questions.js new file mode 100644 index 0000000..14203ff --- /dev/null +++ b/database/migrations/015-create-choice-questions.js @@ -0,0 +1,68 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('choice_questions', { + id: { + type: Sequelize.INTEGER, + primaryKey: true, + autoIncrement: true, + allowNull: false, + comment: '题目ID', + }, + group: { + type: Sequelize.STRING(20), + allowNull: false, + comment: '组别:zhongzhi/gaozhi', + }, + question: { + type: Sequelize.TEXT, + allowNull: false, + comment: '题目内容', + }, + options: { + type: Sequelize.JSON, + allowNull: false, + comment: '选项列表 [{ key: "A", text: "..." }, ...]', + }, + answer: { + type: Sequelize.STRING(10), + allowNull: false, + comment: '正确答案(如 A / AB / ABCD)', + }, + score: { + type: Sequelize.INTEGER, + allowNull: false, + defaultValue: 5, + comment: '分值', + }, + sort_order: { + type: Sequelize.INTEGER, + allowNull: false, + defaultValue: 0, + comment: '排序', + }, + created_at: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '创建时间', + }, + updated_at: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'), + comment: '更新时间', + }, + }).then(() => { + return Promise.all([ + queryInterface.addIndex('choice_questions', ['group'], { name: 'idx_choice_group' }), + queryInterface.addIndex('choice_questions', ['sort_order'], { name: 'idx_choice_sort' }), + ]); + }); + }, + + down: async (queryInterface) => { + await queryInterface.dropTable('choice_questions'); + }, +}; \ No newline at end of file diff --git a/database/migrations/016-create-judge-questions.js b/database/migrations/016-create-judge-questions.js new file mode 100644 index 0000000..9f27919 --- /dev/null +++ b/database/migrations/016-create-judge-questions.js @@ -0,0 +1,63 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('judge_questions', { + id: { + type: Sequelize.INTEGER, + primaryKey: true, + autoIncrement: true, + allowNull: false, + comment: '题目ID', + }, + group: { + type: Sequelize.STRING(20), + allowNull: false, + comment: '组别:zhongzhi/gaozhi', + }, + question: { + type: Sequelize.TEXT, + allowNull: false, + comment: '题目内容', + }, + answer: { + type: Sequelize.STRING(10), + allowNull: false, + comment: '正确答案:true 或 false', + }, + score: { + type: Sequelize.INTEGER, + allowNull: false, + defaultValue: 5, + comment: '分值', + }, + sort_order: { + type: Sequelize.INTEGER, + allowNull: false, + defaultValue: 0, + comment: '排序', + }, + created_at: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '创建时间', + }, + updated_at: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'), + comment: '更新时间', + }, + }).then(() => { + return Promise.all([ + queryInterface.addIndex('judge_questions', ['group'], { name: 'idx_judge_group' }), + queryInterface.addIndex('judge_questions', ['sort_order'], { name: 'idx_judge_sort' }), + ]); + }); + }, + + down: async (queryInterface) => { + await queryInterface.dropTable('judge_questions'); + }, +}; \ No newline at end of file diff --git a/database/migrations/017-create-word-questions.js b/database/migrations/017-create-word-questions.js new file mode 100644 index 0000000..7f0fde7 --- /dev/null +++ b/database/migrations/017-create-word-questions.js @@ -0,0 +1,68 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('word_questions', { + id: { + type: Sequelize.INTEGER, + primaryKey: true, + autoIncrement: true, + allowNull: false, + comment: '题目ID', + }, + group: { + type: Sequelize.STRING(20), + allowNull: false, + comment: '组别:zhongzhi/gaozhi', + }, + title: { + type: Sequelize.STRING(200), + allowNull: false, + comment: '题目标题', + }, + description: { + type: Sequelize.TEXT, + allowNull: true, + comment: '题目描述/操作要求', + }, + score: { + type: Sequelize.INTEGER, + allowNull: false, + defaultValue: 30, + comment: '分值', + }, + sort_order: { + type: Sequelize.INTEGER, + allowNull: false, + defaultValue: 0, + comment: '排序', + }, + template_path: { + type: Sequelize.STRING(500), + allowNull: true, + comment: 'Word 模板文件路径', + }, + created_at: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '创建时间', + }, + updated_at: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'), + comment: '更新时间', + }, + }).then(() => { + return Promise.all([ + queryInterface.addIndex('word_questions', ['group'], { name: 'idx_word_group' }), + queryInterface.addIndex('word_questions', ['sort_order'], { name: 'idx_word_sort' }), + ]); + }); + }, + + down: async (queryInterface) => { + await queryInterface.dropTable('word_questions'); + }, +}; \ No newline at end of file diff --git a/database/migrations/018-add-test-flag-to-contestants.js b/database/migrations/018-add-test-flag-to-contestants.js new file mode 100644 index 0000000..a1a41e1 --- /dev/null +++ b/database/migrations/018-add-test-flag-to-contestants.js @@ -0,0 +1,23 @@ +'use strict'; + +/** + * 迁移文件:为 contestants 表添加 test_flag 字段 + * 用于区分测试数据和生产数据,防止重置操作误删生产数据 + */ + +module.exports = { + up: async (queryInterface, Sequelize) => { + const { INTEGER } = Sequelize; + + await queryInterface.addColumn('contestants', 'test_flag', { + type: INTEGER, + allowNull: false, + defaultValue: 0, + comment: '测试数据标记:0=生产数据,1=测试数据', + }); + }, + + down: async (queryInterface) => { + await queryInterface.removeColumn('contestants', 'test_flag'); + }, +}; \ No newline at end of file diff --git a/database/migrations/019-add-title-to-carousel.js b/database/migrations/019-add-title-to-carousel.js new file mode 100644 index 0000000..4f25af0 --- /dev/null +++ b/database/migrations/019-add-title-to-carousel.js @@ -0,0 +1,17 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.addColumn('carousel', 'title', { + type: Sequelize.STRING(200), + allowNull: true, + defaultValue: null, + comment: '轮播图标题', + after: 'image_url', + }); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.removeColumn('carousel', 'title'); + }, +}; \ No newline at end of file diff --git a/database/migrations/020-add-question-type-to-submissions.js b/database/migrations/020-add-question-type-to-submissions.js new file mode 100644 index 0000000..d0a7ae9 --- /dev/null +++ b/database/migrations/020-add-question-type-to-submissions.js @@ -0,0 +1,40 @@ +'use strict'; + +/** + * 迁移 020:为 zhongzhi_submissions / gaozhi_submissions 增加 question_type 列 + * + * 背景(Bug #11 / B11):选择题(choice_questions)与判断题(judge_questions)的 id + * 在不同表中数值重叠(如 choice#5 与 judge#5 均为 5),原 saveAnswers 仅按 + * (aggregation_uid, question_uid) 查找并 upsert,导致同 id 的选择题与判断题被 + * 折叠成一条记录,考生作答数少于抽题数、部分答案静默丢失。 + * + * 修复:新增 question_type 列(choice/judge),与 question_uid 联合唯一标识一道题; + * service.saveAnswers 的查找/写入键改为 (aggregation_uid, question_uid, question_type); + * autoScore 判分不受影响(其本就按 question_type 选模型 + 用原始 id 作主键)。 + */ +module.exports = { + up: async (queryInterface, Sequelize) => { + const addCol = async (table) => { + await queryInterface.addColumn(table, 'question_type', { + type: Sequelize.STRING(16), + allowNull: false, + defaultValue: '', + comment: '题型:choice/judge,与 question_uid 联合区分题目', + }); + await queryInterface.addIndex( + table, + ['aggregation_uid', 'question_uid', 'question_type'], + { name: `idx_${table}_agg_qtype` } + ); + }; + await addCol('zhongzhi_submissions'); + await addCol('gaozhi_submissions'); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.removeIndex('zhongzhi_submissions', 'idx_zhongzhi_submissions_agg_qtype'); + await queryInterface.removeColumn('zhongzhi_submissions', 'question_type'); + await queryInterface.removeIndex('gaozhi_submissions', 'idx_gaozhi_submissions_agg_qtype'); + await queryInterface.removeColumn('gaozhi_submissions', 'question_type'); + }, +}; diff --git a/database/migrations/021-unique-submission-indexes.js b/database/migrations/021-unique-submission-indexes.js new file mode 100644 index 0000000..4d4267d --- /dev/null +++ b/database/migrations/021-unique-submission-indexes.js @@ -0,0 +1,99 @@ +'use strict'; + +/** + * 迁移 021:为答题提交表增加唯一约束,杜绝并发作答重复落库(Bug #12 / B12) + * + * 背景(B12):原 saveAnswers 使用「先 findOne 再 create」的非原子写法,且 + * (aggregation_uid, question_uid, question_type) 仅为普通索引。同一用户并发提交 + * 时,多个请求都 find 不到记录,于是各自 create,导致同一题被插入多行 + * (实测:3 路并发产生 57 行 / 20 题,exam_submissions 出现 3 条同选手记录)。 + * 重复行会污染 answeredCount、判分明细与成绩恢复,属数据一致性缺陷。 + * + * 修复: + * 1) zhongzhi_submissions / gaozhi_submissions 的复合索引升级为唯一索引 + * uk_{table}_submission (aggregation_uid, question_uid, question_type); + * service.saveAnswers 改用 upsert(conflictFields=[...]),由 MySQL 的 + * INSERT ... ON DUPLICATE KEY UPDATE 在并发下原子折叠为单行更新。 + * 2) exam_submissions 的 contestant_id 索引升级为唯一索引 + * uk_exam_submission_contestant,配合 service 中并发安全的 + * _ensureExamSubmissionSafe,保证每选手仅一条提交记录。 + * + * 注意:up/down 均先去重(保留最小 id)再加减索引,确保幂等且不会因历史脏数据失败。 + */ +module.exports = { + up: async (queryInterface) => { + // ---- 去重:同 (aggregation_uid, question_uid, question_type) 仅留最小 id ---- + await queryInterface.sequelize.query(` + DELETE t FROM zhongzhi_submissions t + WHERE t.id NOT IN ( + SELECT min_id FROM ( + SELECT MIN(id) AS min_id FROM zhongzhi_submissions + GROUP BY aggregation_uid, question_uid, question_type + ) AS keep + ) + `); + await queryInterface.sequelize.query(` + DELETE t FROM gaozhi_submissions t + WHERE t.id NOT IN ( + SELECT min_id FROM ( + SELECT MIN(id) AS min_id FROM gaozhi_submissions + GROUP BY aggregation_uid, question_uid, question_type + ) AS keep + ) + `); + // exam_submissions:每 contestant_id 仅留最小 id + await queryInterface.sequelize.query(` + DELETE t FROM exam_submissions t + WHERE t.id NOT IN ( + SELECT min_id FROM ( + SELECT MIN(id) AS min_id FROM exam_submissions + GROUP BY contestant_id + ) AS keep + ) + `); + + // ---- zhongzhi ---- + await queryInterface.removeIndex('zhongzhi_submissions', 'idx_zhongzhi_submissions_agg_qtype').catch(() => {}); + await queryInterface.addIndex('zhongzhi_submissions', { + name: 'uk_zhongzhi_submission', + unique: true, + fields: ['aggregation_uid', 'question_uid', 'question_type'], + }).catch(() => {}); + + // ---- gaozhi ---- + await queryInterface.removeIndex('gaozhi_submissions', 'idx_gaozhi_submissions_agg_qtype').catch(() => {}); + await queryInterface.addIndex('gaozhi_submissions', { + name: 'uk_gaozhi_submission', + unique: true, + fields: ['aggregation_uid', 'question_uid', 'question_type'], + }).catch(() => {}); + + // ---- exam_submissions ---- + await queryInterface.removeIndex('exam_submissions', 'idx_exam_submissions_contestant_id').catch(() => {}); + await queryInterface.addIndex('exam_submissions', { + name: 'uk_exam_submission_contestant', + unique: true, + fields: ['contestant_id'], + }).catch(() => {}); + }, + + down: async (queryInterface) => { + await queryInterface.removeIndex('zhongzhi_submissions', 'uk_zhongzhi_submission').catch(() => {}); + await queryInterface.addIndex('zhongzhi_submissions', { + name: 'idx_zhongzhi_submissions_agg_qtype', + fields: ['aggregation_uid', 'question_uid', 'question_type'], + }); + + await queryInterface.removeIndex('gaozhi_submissions', 'uk_gaozhi_submission').catch(() => {}); + await queryInterface.addIndex('gaozhi_submissions', { + name: 'idx_gaozhi_submissions_agg_qtype', + fields: ['aggregation_uid', 'question_uid', 'question_type'], + }); + + await queryInterface.removeIndex('exam_submissions', 'uk_exam_submission_contestant').catch(() => {}); + await queryInterface.addIndex('exam_submissions', { + name: 'idx_exam_submissions_contestant_id', + fields: ['contestant_id'], + }); + }, +}; diff --git a/database/migrations/022-add-content-button-to-carousel.js b/database/migrations/022-add-content-button-to-carousel.js new file mode 100644 index 0000000..1b13c92 --- /dev/null +++ b/database/migrations/022-add-content-button-to-carousel.js @@ -0,0 +1,56 @@ +'use strict'; + +/** + * 为 carousel 表新增富文本正文与可配置按钮字段,并放宽 image_url 允许为空 + * (支持"渐变底图回退"的固定页迁移)。 + * + * 新增: + * - content TEXT 富文本正文(HTML,由 wangEditor 生成,前端 v-html 渲染) + * - button_text VARCHAR(100) 按钮文字(为空则不显示按钮) + * - button_link VARCHAR(500) 按钮链接(内部路由如 /activate,或外部 http(s):// 链接) + * 变更: + * - image_url 由 NOT NULL 放宽为 允许 NULL(无底图时前端回退 CSS 渐变) + */ +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.addColumn('carousel', 'content', { + type: Sequelize.TEXT, + allowNull: true, + defaultValue: null, + comment: '富文本正文(HTML)', + after: 'title', + }); + await queryInterface.addColumn('carousel', 'button_text', { + type: Sequelize.STRING(100), + allowNull: true, + defaultValue: null, + comment: '按钮文字(为空则不显示按钮)', + after: 'content', + }); + await queryInterface.addColumn('carousel', 'button_link', { + type: Sequelize.STRING(500), + allowNull: true, + defaultValue: null, + comment: '按钮链接(内部路由/外部URL)', + after: 'button_text', + }); + // 放宽 image_url 允许为空,支持渐变底图回退 + await queryInterface.changeColumn('carousel', 'image_url', { + type: Sequelize.STRING(500), + allowNull: true, + defaultValue: null, + comment: '图片 URL 地址(可为空时前端回退渐变)', + }); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.removeColumn('carousel', 'content'); + await queryInterface.removeColumn('carousel', 'button_text'); + await queryInterface.removeColumn('carousel', 'button_link'); + await queryInterface.changeColumn('carousel', 'image_url', { + type: Sequelize.STRING(500), + allowNull: false, + comment: '图片 URL 地址', + }); + }, +}; diff --git a/database/seeders/001-seed-competition-info.js b/database/seeders/001-seed-competition-info.js new file mode 100644 index 0000000..91177b5 --- /dev/null +++ b/database/seeders/001-seed-competition-info.js @@ -0,0 +1,21 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + const now = new Date(); + await queryInterface.bulkInsert('competition_info', [ + { + competition_name: '金砖国家职业技能大赛-卫星导航技术应用赛项', + competition_code: 'BRICS2026-ST-027', + created_at: now, + updated_at: now, + }, + ], {}); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.bulkDelete('competition_info', { + competition_code: 'BRICS2026-ST-027', + }, {}); + }, +}; diff --git a/database/seeders/001-seed-contestants.js b/database/seeders/001-seed-contestants.js new file mode 100644 index 0000000..7a1efc0 --- /dev/null +++ b/database/seeders/001-seed-contestants.js @@ -0,0 +1,50 @@ +'use strict'; + +/** + * 种子数据:初始化选手数据 + * 包含 20 名选手,中职/高职/本科/教师各 5 名 + * 账号未激活,密码为空 + */ + +module.exports = { + up: async (queryInterface) => { + const contestants = [ + // ==================== 中职组(5名) ==================== + { id_number: '110101200501010101', name: '张明远', phone: '13800010001', group: 'zhongzhi', password: '', activated: false, created_at: new Date(), updated_at: new Date() }, + { id_number: '110101200502020102', name: '李雨桐', phone: '13800010002', group: 'zhongzhi', password: '', activated: false, created_at: new Date(), updated_at: new Date() }, + { id_number: '110101200503030103', name: '王浩然', phone: '13800010003', group: 'zhongzhi', password: '', activated: false, created_at: new Date(), updated_at: new Date() }, + { id_number: '110101200504040104', name: '赵思琪', phone: '13800010004', group: 'zhongzhi', password: '', activated: false, created_at: new Date(), updated_at: new Date() }, + { id_number: '110101200505050105', name: '陈子涵', phone: '13800010005', group: 'zhongzhi', password: '', activated: false, created_at: new Date(), updated_at: new Date() }, + + // ==================== 高职组(5名) ==================== + { id_number: '110101200301010201', name: '刘博文', phone: '13800020001', group: 'gaozhi', password: '', activated: false, created_at: new Date(), updated_at: new Date() }, + { id_number: '110101200302020202', name: '杨紫萱', phone: '13800020002', group: 'gaozhi', password: '', activated: false, created_at: new Date(), updated_at: new Date() }, + { id_number: '110101200303030203', name: '黄俊杰', phone: '13800020003', group: 'gaozhi', password: '', activated: false, created_at: new Date(), updated_at: new Date() }, + { id_number: '110101200304040204', name: '周雅琪', phone: '13800020004', group: 'gaozhi', password: '', activated: false, created_at: new Date(), updated_at: new Date() }, + { id_number: '110101200305050205', name: '吴天宇', phone: '13800020005', group: 'gaozhi', password: '', activated: false, created_at: new Date(), updated_at: new Date() }, + + // ==================== 本科组(5名) ==================== + { id_number: '110101200101010301', name: '郑佳怡', phone: '13800030001', group: 'benke', password: '', activated: false, created_at: new Date(), updated_at: new Date() }, + { id_number: '110101200102020302', name: '冯子辰', phone: '13800030002', group: 'benke', password: '', activated: false, created_at: new Date(), updated_at: new Date() }, + { id_number: '110101200103030303', name: '褚文博', phone: '13800030003', group: 'benke', password: '', activated: false, created_at: new Date(), updated_at: new Date() }, + { id_number: '110101200104040304', name: '林晓萱', phone: '13800030004', group: 'benke', password: '', activated: false, created_at: new Date(), updated_at: new Date() }, + { id_number: '110101200105050305', name: '何志远', phone: '13800030005', group: 'benke', password: '', activated: false, created_at: new Date(), updated_at: new Date() }, + + // ==================== 教师组(5名) ==================== + { id_number: '110101198501010401', name: '韩建国', phone: '13800040001', group: 'jiaoshi', password: '', activated: false, created_at: new Date(), updated_at: new Date() }, + { id_number: '110101198602020402', name: '唐慧芳', phone: '13800040002', group: 'jiaoshi', password: '', activated: false, created_at: new Date(), updated_at: new Date() }, + { id_number: '110101198703030403', name: '沈志强', phone: '13800040003', group: 'jiaoshi', password: '', activated: false, created_at: new Date(), updated_at: new Date() }, + { id_number: '110101198804040404', name: '许美玲', phone: '13800040004', group: 'jiaoshi', password: '', activated: false, created_at: new Date(), updated_at: new Date() }, + { id_number: '110101198905050405', name: '曹永康', phone: '13800040005', group: 'jiaoshi', password: '', activated: false, created_at: new Date(), updated_at: new Date() }, + ]; + + await queryInterface.bulkInsert('contestants', contestants, {}); + }, + + /** + * 回滚种子数据:删除所有选手 + */ + down: async (queryInterface) => { + await queryInterface.bulkDelete('contestants', null, {}); + }, +}; \ No newline at end of file diff --git a/database/seeders/002-seed-admin.js b/database/seeders/002-seed-admin.js new file mode 100644 index 0000000..9235333 --- /dev/null +++ b/database/seeders/002-seed-admin.js @@ -0,0 +1,36 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + const now = new Date(); + await queryInterface.bulkInsert('accounts', [ + { + role: 'admin', + name_cn: '系统管理员', + name_en: 'Administrator', + gender: null, + phone: null, + email: 'admin@competition.com', + id_number: '000000000000000000', + password: '$2b$10$Nc6PCJItIwuOzpM22dTiAezB.jZMI8mzkCbg3oRZkAWx.QaI4co9.', + salt: '$2b$10$Nc6PCJItIwuOzpM22dTiAe', + position: '系统管理员', + major: null, + country: '中国', + region: null, + organization_cn: '赛事组委会', + organization_en: 'Competition Committee', + activated: 1, + score: null, + created_at: now, + updated_at: now, + }, + ], {}); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.bulkDelete('accounts', { + id_number: '000000000000000000', + }, {}); + }, +}; diff --git a/database/seeders/003-seed-carousel.js b/database/seeders/003-seed-carousel.js new file mode 100644 index 0000000..a95f7ea --- /dev/null +++ b/database/seeders/003-seed-carousel.js @@ -0,0 +1,36 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.bulkInsert('carousel', [ + { + image_url: 'https://images.unsplash.com/photo-1446776811953-b23d57bd21aa?w=1200&h=480&fit=crop', + sort_order: 1, + link_url: '', + enabled: 1, + created_at: new Date(), + updated_at: new Date(), + }, + { + image_url: 'https://images.unsplash.com/photo-1451187580459-43490279c0fa?w=1200&h=480&fit=crop', + sort_order: 2, + link_url: '', + enabled: 1, + created_at: new Date(), + updated_at: new Date(), + }, + { + image_url: 'https://images.unsplash.com/photo-1614728263952-84ea256f9679?w=1200&h=480&fit=crop', + sort_order: 3, + link_url: '', + enabled: 1, + created_at: new Date(), + updated_at: new Date(), + }, + ]); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.bulkDelete('carousel', null, {}); + }, +}; diff --git a/database/seeders/003-seed-news.js b/database/seeders/003-seed-news.js new file mode 100644 index 0000000..cec8d63 --- /dev/null +++ b/database/seeders/003-seed-news.js @@ -0,0 +1,85 @@ +'use strict'; + +module.exports = { + up: async (queryInterface) => { + const now = new Date(); + const newsItems = [ + { + title: '2026年通信与导航应用技术创新赛线上选拔赛通知', + content: '

为深入贯彻落实国家创新驱动发展战略,推动通信与导航技术领域的创新应用,现举办2026年通信与导航应用技术创新赛线上选拔赛。本次比赛面向全国职业院校和高等院校师生,欢迎广大师生踊跃报名参赛。

', + publish_time: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000), + pinned: true, + group: 'all', + created_at: now, + updated_at: now, + }, + { + title: '中职组比赛时间安排通知', + content: '

中职组线上选拔赛将于2026年7月15日上午9:00准时开始,请各位参赛选手提前做好准备。

', + publish_time: new Date(now.getTime() - 25 * 24 * 60 * 60 * 1000), + pinned: false, + group: 'zhongzhi', + created_at: now, + updated_at: now, + }, + { + title: '高职组比赛时间安排通知', + content: '

高职组线上选拔赛将于2026年7月16日上午9:00准时开始,比赛时长3小时。

', + publish_time: new Date(now.getTime() - 25 * 24 * 60 * 60 * 1000), + pinned: false, + group: 'gaozhi', + created_at: now, + updated_at: now, + }, + { + title: '本科组作品提交截止日期提醒', + content: '

本科组线上选拔赛作品提交截止日期为2026年7月20日23:59。

', + publish_time: new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000), + pinned: false, + group: 'benke', + created_at: now, + updated_at: now, + }, + { + title: '教师组作品提交截止日期提醒', + content: '

教师组线上选拔赛作品提交截止日期为2026年7月20日23:59。

', + publish_time: new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000), + pinned: false, + group: 'jiaoshi', + created_at: now, + updated_at: now, + }, + { + title: '关于账号激活说明', + content: '

各位参赛选手请使用身份证号进行账号激活。激活流程:输入身份证号验证,获取验证码,设置密码。

', + publish_time: new Date(now.getTime() - 15 * 24 * 60 * 60 * 1000), + pinned: true, + group: 'all', + created_at: now, + updated_at: now, + }, + { + title: '比赛系统操作指南', + content: '

为帮助各位选手熟悉比赛系统,请仔细阅读操作指南。中职/高职组在线答题,本科/教师组上传作品。

', + publish_time: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000), + pinned: false, + group: 'all', + created_at: now, + updated_at: now, + }, + { + title: '温馨提示:比赛期间注意事项', + content: '

为确保比赛顺利进行,请使用Chrome或Edge浏览器最新版本,保持网络稳定,比赛过程中请勿刷新页面。

', + publish_time: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000), + pinned: false, + group: 'all', + created_at: now, + updated_at: now, + }, + ]; + await queryInterface.bulkInsert('news', newsItems, {}); + }, + down: async (queryInterface) => { + await queryInterface.bulkDelete('news', null, {}); + }, +}; diff --git a/database/seeders/004-seed-exam-questions.js b/database/seeders/004-seed-exam-questions.js new file mode 100644 index 0000000..fba7224 --- /dev/null +++ b/database/seeders/004-seed-exam-questions.js @@ -0,0 +1,23 @@ +'use strict'; + +module.exports = { + up: async (queryInterface) => { + const now = new Date(); + const questions = [ + { group: 'zhongzhi', title: '通信系统的基本组成', content: '请简述通信系统的基本组成,包括信源、发送设备、信道、接收设备和信宿五大组成部分,并简要说明各部分的功能。', score: 20, sort_order: 1, created_at: now, updated_at: now }, + { group: 'zhongzhi', title: 'GPS导航原理', content: '请阐述GPS全球定位系统的基本工作原理,包括卫星星座、信号结构、伪距测量和定位解算等关键技术。', score: 20, sort_order: 2, created_at: now, updated_at: now }, + { group: 'zhongzhi', title: '信号调制与解调', content: '请比较分析AM调制、FM调制和数字调制(如QPSK)的基本原理、优缺点及适用场景。', score: 20, sort_order: 3, created_at: now, updated_at: now }, + { group: 'zhongzhi', title: '移动通信发展历程', content: '请简述移动通信从1G到5G的发展历程,重点说明各代通信技术的主要特点和技术突破。', score: 20, sort_order: 4, created_at: now, updated_at: now }, + { group: 'zhongzhi', title: '创新应用方案设计', content: '请结合通信与导航技术,设计一个创新应用方案,需包含应用场景描述、技术方案选型、预期效果分析等。', score: 20, sort_order: 5, created_at: now, updated_at: now }, + { group: 'gaozhi', title: 'OFDM技术原理与应用', content: '请详细阐述OFDM(正交频分复用)技术的基本原理,包括子载波正交性、循环前缀的作用以及在4G/5G中的应用。', score: 20, sort_order: 1, created_at: now, updated_at: now }, + { group: 'gaozhi', title: '北斗卫星导航系统', content: '请分析北斗卫星导航系统的发展历程、系统组成、服务类型,并与GPS系统进行对比分析。', score: 20, sort_order: 2, created_at: now, updated_at: now }, + { group: 'gaozhi', title: '5G网络架构与关键技术', content: '请描述5G网络的核心架构(包括核心网和接入网),并分析Massive MIMO、网络切片、边缘计算等关键技术。', score: 20, sort_order: 3, created_at: now, updated_at: now }, + { group: 'gaozhi', title: '通信网络安全', content: '请分析通信网络中常见的安全威胁(如窃听、篡改、拒绝服务攻击等),并提出相应的防护措施。', score: 20, sort_order: 4, created_at: now, updated_at: now }, + { group: 'gaozhi', title: '综合创新项目设计', content: '请设计一个通信与导航融合的创新应用项目,需包含项目背景、技术路线、实施方案、预期成果和可行性分析。', score: 20, sort_order: 5, created_at: now, updated_at: now }, + ]; + await queryInterface.bulkInsert('exam_questions', questions, {}); + }, + down: async (queryInterface) => { + await queryInterface.bulkDelete('exam_questions', null, {}); + }, +}; diff --git a/database/seeders/005-seed-competition-times.js b/database/seeders/005-seed-competition-times.js new file mode 100644 index 0000000..4632e4e --- /dev/null +++ b/database/seeders/005-seed-competition-times.js @@ -0,0 +1,61 @@ +'use strict'; + +/** + * 种子数据:初始化比赛时间配置 + * 中职组和高职组设置起止时间(在线答题模式) + * 本科组和教师组设置截止时间(作品提交模式) + */ + +module.exports = { + up: async (queryInterface) => { + const now = new Date(); + + const times = [ + // 中职组:在线答题,有起止时间 + { + group: 'zhongzhi', + start_time: new Date(now.getTime() + 1 * 24 * 60 * 60 * 1000), // 1天后开始 + end_time: new Date(now.getTime() + 1 * 24 * 60 * 60 * 1000 + 3 * 60 * 60 * 1000), // 持续3小时 + deadline: null, + created_at: now, + updated_at: now, + }, + // 高职组:在线答题,有起止时间 + { + group: 'gaozhi', + start_time: new Date(now.getTime() + 2 * 24 * 60 * 60 * 1000), // 2天后开始 + end_time: new Date(now.getTime() + 2 * 24 * 60 * 60 * 1000 + 3 * 60 * 60 * 1000), // 持续3小时 + deadline: null, + created_at: now, + updated_at: now, + }, + // 本科组:作品提交,只有截止时间 + { + group: 'benke', + start_time: null, + end_time: null, + deadline: new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000), // 7天后截止 + created_at: now, + updated_at: now, + }, + // 教师组:作品提交,只有截止时间 + { + group: 'jiaoshi', + start_time: null, + end_time: null, + deadline: new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000), // 7天后截止 + created_at: now, + updated_at: now, + }, + ]; + + await queryInterface.bulkInsert('competition_times', times, {}); + }, + + /** + * 回滚种子数据:删除所有时间配置 + */ + down: async (queryInterface) => { + await queryInterface.bulkDelete('competition_times', null, {}); + }, +}; \ No newline at end of file diff --git a/database/seeders/006-seed-choice-judge-word-questions.js b/database/seeders/006-seed-choice-judge-word-questions.js new file mode 100644 index 0000000..188db8e --- /dev/null +++ b/database/seeders/006-seed-choice-judge-word-questions.js @@ -0,0 +1,176 @@ +'use strict'; + +/** + * 种子文件:写入选择题、判断题、综合性任务模拟数据 + * + * 参考考试大纲"题型结构": + * - 专业基础任务:选择题 50题 × 1分 = 50分,判断题 20题 × 1分 = 20分 + * - 综合性任务:1题 × 30分 = 30分 + * - 总分:100分 + * + * 考试内容覆盖: + * - 第一篇 卫星通信技术(第1-6章)→ 16题 + * - 第二篇 卫星导航技术(第1-8章)→ 25题 + * - 第三篇 设备实操命令 → 29题(选择+判断总计) + */ + +module.exports = { + up: async (queryInterface) => { + const now = new Date(); + + // ==================== 选择题 ==================== + // 总分:50题 × 1分 = 50分 + // 覆盖:第1篇(16题)+ 第2篇(25题)+ 第3篇(9题)= 50题 + const choiceQuestions = [ + // ==================== 第一篇:卫星通信技术(第1-6章)==================== + // 第1章:卫星通信基础概论 + { group: 'zhongzhi', question: '卫星通信定义为?', options: '[{"key":"A","text":"利用卫星在地面站之间进行的通信"},{"key":"B","text":"利用卫星转发无线电信号实现多个地球站之间的通信"},{"key":"C","text":"卫星与飞机之间的通信"},{"key":"D","text":"卫星之间的宇宙通信"}]', answer: 'B', score: 1, sort_order: 1, created_at: now, updated_at: now }, + { group: 'zhongzhi', question: '卫星通信中,地球站向卫星发射信号的链路称为?', options: '[{"key":"A","text":"下行链路"},{"key":"B","text":"上行链路"},{"key":"C","text":"前向链路"},{"key":"D","text":"反向链路"}]', answer: 'B', score: 1, sort_order: 2, created_at: now, updated_at: now }, + { group: 'zhongzhi', question: '卫星通信的主要优点不包括以下哪项?', options: '[{"key":"A","text":"覆盖范围大"},{"key":"B","text":"通信容量大"},{"key":"C","text":"建站成本低"},{"key":"D","text":"不受地理条件限制"}]', answer: 'C', score: 1, sort_order: 3, created_at: now, updated_at: now }, + { group: 'zhongzhi', question: '通信系统中,将原始信号转换为适合信道传输的信号的过程称为?', options: '[{"key":"A","text":"编码"},{"key":"B","text":"调制"},{"key":"C","text":"解码"},{"key":"D","text":"解调"}]', answer: 'B', score: 1, sort_order: 4, created_at: now, updated_at: now }, + { group: 'zhongzhi', question: 'GEO卫星的轨道高度约为?', options: '[{"key":"A","text":"500-1500 km"},{"key":"B","text":"5000-12000 km"},{"key":"C","text":"20200 km"},{"key":"D","text":"35786 km"}]', answer: 'D', score: 1, sort_order: 5, created_at: now, updated_at: now }, + { group: 'zhongzhi', question: 'MEO卫星的轨道高度范围是?', options: '[{"key":"A","text":"低于500 km"},{"key":"B","text":"500-2000 km"},{"key":"C","text":"5000-20000 km"},{"key":"D","text":"35786 km"}]', answer: 'C', score: 1, sort_order: 6, created_at: now, updated_at: now }, + { group: 'zhongzhi', question: 'LEO卫星的轨道倾角通常为?', options: '[{"key":"A","text":"0度"},{"key":"B","text":"近极地轨道"},{"key":"C","text":"赤道轨道"},{"key":"D","text":"地球同步轨道"}]', answer: 'B', score: 1, sort_order: 7, created_at: now, updated_at: now }, + { group: 'zhongzhi', question: '卫星通信中,频率复用技术的主要作用是?', options: '[{"key":"A","text":"提高频率利用率"},{"key":"B","text":"降低发射功率"},{"key":"C","text":"减少卫星数量"},{"key":"D","text":"提高信号质量"}]', answer: 'A', score: 1, sort_order: 8, created_at: now, updated_at: now }, + { group: 'zhongzhi', question: '以下哪种不是卫星通信的多址方式?', options: '[{"key":"A","text":"FDMA"},{"key":"B","text":"TDMA"},{"key":"C","text":"CDMA"},{"key":"D","text":"ADMA"}]', answer: 'D', score: 1, sort_order: 9, created_at: now, updated_at: now }, + { group: 'zhongzhi', question: '卫星物联网中,星上处理的主要目的是?', options: '[{"key":"A","text":"增加卫星重量"},{"key":"B","text":"提高传输效率"},{"key":"C","text":"降低功率"},{"key":"D","text":"增加轨道高度"}]', answer: 'B', score: 1, sort_order: 10, created_at: now, updated_at: now }, + { group: 'zhongzhi', question: '低轨卫星星座的特点是?', options: '[{"key":"A","text":"单星覆盖大,但需要星上处理"},{"key":"B","text":"全球覆盖,大容量低时延"},{"key":"C","text":"只有一颗卫星"},{"key":"D","text":"不需要切换"}]', answer: 'B', score: 1, sort_order: 11, created_at: now, updated_at: now }, + { group: 'zhongzhi', question: '卫星通信系统的转发器作用是?', options: '[{"key":"A","text":"存储数据"},{"key":"B","text":"放大变频转发信号"},{"key":"C","text":"计算信号"},{"key":"D","text":"控制卫星姿态"}]', answer: 'B', score: 1, sort_order: 12, created_at: now, updated_at: now }, + { group: 'zhongzhi', question: '卫星天线的指向精度影响?', options: '[{"key":"A","text":"信号接收功率"},{"key":"B","text":"卫星重量"},{"key":"C","text":"轨道高度"},{"key":"D","text":"卫星寿命"}]', answer: 'A', score: 1, sort_order: 13, created_at: now, updated_at: now }, + { group: 'zhongzhi', question: '卫星电源系统主要采用?', options: '[{"key":"A","text":"燃料电池"},{"key":"B","text":"太阳能电池阵+蓄电池组"},{"key":"C","text":"核能发电"},{"key":"D","text":"化学电池"}]', answer: 'B', score: 1, sort_order: 14, created_at: now, updated_at: now }, + { group: 'zhongzhi', question: '以下哪个频段常用作卫星通信上行?', options: '[{"key":"A","text":"UHF"},{"key":"B","text":"C-band"},{"key":"C","text":"Ku-band"},{"key":"D","text":"Ka-band"}]', answer: 'D', score: 1, sort_order: 15, created_at: now, updated_at: now }, + { group: 'zhongzhi', question: '卫星姿态控制的目的是?', options: '[{"key":"A","text":"改变轨道高度"},{"key":"B","text":"保持天线指向目标区域"},{"key":"C","text":"改变速度"},{"key":"D","text":"减少功耗"}]', answer: 'B', score: 1, sort_order: 16, created_at: now, updated_at: now }, + + // ==================== 第二篇:卫星导航技术(第1-8章)==================== + // 第1章:GNSS概述 + { group: 'zhongzhi', question: 'GNSS的全称是?', options: '[{"key":"A","text":"Global Navigation Satellite System"},{"key":"B","text":"Global Network Service System"},{"key":"C","text":"General Navigation Support System"},{"key":"D","text":"Geo Navigation Satellite System"}]', answer: 'A', score: 1, sort_order: 17, created_at: now, updated_at: now }, + { group: 'zhongzhi', question: '以下哪个不属于四大全球导航卫星星座?', options: '[{"key":"A","text":"GPS"},{"key":"B","text":"北斗"},{"key":"C","text":"GLONASS"},{"key":"D","text":"WAAS"}]', answer: 'D', score: 1, sort_order: 18, created_at: now, updated_at: now }, + { group: 'zhongzhi', question: '北斗三号全球系统提供多少颗卫星?', options: '[{"key":"A","text":"24"},{"key":"B","text":"30"},{"key":"C","text":"35"},{"key":"D","text":"40"}]', answer: 'C', score: 1, sort_order: 19, created_at: now, updated_at: now }, + { group: 'zhongzhi', question: 'GPS卫星导航系统至少需要几颗卫星才能实现三维定位?', options: '[{"key":"A","text":"2颗"},{"key":"B","text":"3颗"},{"key":"C","text":"4颗"},{"key":"D","text":"5颗"}]', answer: 'C', score: 1, sort_order: 20, created_at: now, updated_at: now }, + { group: 'zhongzhi', question: '伪距测量测量的是?', options: '[{"key":"A","text":"卫星到接收机的几何距离"},{"key":"B","text":"卫星信号传播时间乘以光速得到的距离"},{"key":"C","text":"卫星到地心的距离"},{"key":"D","text":"接收机到地面的距离"}]', answer: 'B', score: 1, sort_order: 21, created_at: now, updated_at: now }, + { group: 'zhongzhi', question: '卫星定位误差中,属于系统误差的是?', options: '[{"key":"A","text":"电离层延迟误差"},{"key":"B","text":"接收机噪声"},{"key":"C","text":"多径效应随机误差"},{"key":"D","text":"测量噪声"}]', answer: 'A', score: 1, sort_order: 22, created_at: now, updated_at: now }, + { group: 'zhongzhi', question: '差分定位技术中,RTK的全称是?', options: '[{"key":"A","text":"Real-Time Kinematic"},{"key":"B","text":"Rapid Tracking Kinematic"},{"key":"C","text":"Real-Time Kalman"},{"key":"D","text":"Radio Transmission Kinematic"}]', answer: 'A', score: 1, sort_order: 23, created_at: now, updated_at: now }, + { group: 'zhongzhi', question: 'NMEA-0183协议中,GGA语句主要提供什么信息?', options: '[{"key":"A","text":"卫星星历"},{"key":"B","text":"定位数据"},{"key":"C","text":"卫星状态"},{"key":"D","text":"速度信息"}]', answer: 'B', score: 1, sort_order: 24, created_at: now, updated_at: now }, + { group: 'zhongzhi', question: '北斗系统独具特色的服务是?', options: '[{"key":"A","text":"仅定位"},{"key":"B","text":"短报文通信"},{"key":"C","text":"仅授时"},{"key":"D","text":"仅测速"}]', answer: 'B', score: 1, sort_order: 25, created_at: now, updated_at: now }, + { group: 'zhongzhi', question: '高精度差分定位中,NTRIP协议用于?', options: '[{"key":"A","text":"卫星轨道计算"},{"key":"B","text":"通过互联网传输差分改正数据"},{"key":"C","text":"信号调制"},{"key":"D","text":"误差计算"}]', answer: 'B', score: 1, sort_order: 26, created_at: now, updated_at: now }, + { group: 'zhongzhi', question: '卫星导航接收机的灵敏度主要影响?', options: '[{"key":"A","text":"捕获卫星信号的能力"},{"key":"B","text":"发射功率"},{"key":"C","text":"卫星轨道高度"},{"key":"D","text":"计算速度"}]', answer: 'A', score: 1, sort_order: 27, created_at: now, updated_at: now }, + { group: 'zhongzhi', question: '组合导航GNSS+IMU中,IMU的作用是?', options: '[{"key":"A","text":"提供加速度和角速度"},{"key":"B","text":"提供定位结果"},{"key":"C","text":"提供时间信息"},{"key":"D","text":"提供卫星星历"}]', answer: 'A', score: 1, sort_order: 28, created_at: now, updated_at: now }, + { group: 'zhongzhi', question: '多路径效应产生的原因是?', options: '[{"key":"A","text":"卫星信号经过反射后与直达信号干涉"},{"key":"B","text":"多颗卫星信号叠加"},{"key":"C","text":"多个接收机同时工作"},{"key":"D","text":"电离层反射信号"}]', answer: 'A', score: 1, sort_order: 29, created_at: now, updated_at: now }, + { group: 'zhongzhi', question: 'RTCM标准主要用于?', options: '[{"key":"A","text":"卫星导航电文标准"},{"key":"B","text":"差分改正数据标准"},{"key":"C","text":"NMEA输出标准"},{"key":"D","text":"接口通信标准"}]', answer: 'B', score: 1, sort_order: 30, created_at: now, updated_at: now }, + { group: 'zhongzhi', question: 'PDOP值越大表示?', options: '[{"key":"A","text":"定位精度越好"},{"key":"B","text":"定位几何误差越大"},{"key":"C","text":"卫星越多"},{"key":"D","text":"温度越高"}]', answer: 'B', score: 1, sort_order: 31, created_at: now, updated_at: now }, + { group: 'zhongzhi', question: '北斗卫星导航系统的服务区覆盖?', options: '[{"key":"A","text":"仅中国"},{"key":"B","text":"亚太地区"},{"key":"C","text":"全球"},{"key":"D","text":"仅北半球"}]', answer: 'C', score: 1, sort_order: 32, created_at: now, updated_at: now }, + { group: 'zhongzhi', question: 'GPS卫星星历更新周期通常为?', options: '[{"key":"A","text":"30秒"},{"key":"B","text":"2小时"},{"key":"C","text":"1天"},{"key":"D","text":"1周"}]', answer: 'B', score: 1, sort_order: 33, created_at: now, updated_at: now }, + { group: 'zhongzhi', question: '静态相对定位主要用于?', options: '[{"key":"A","text":"需要高精度大地测量"},{"key":"B","text":"车辆导航"},{"key":"C","text":"船舶导航"},{"key":"D","text":"飞机导航"}]', answer: 'A', score: 1, sort_order: 34, created_at: now, updated_at: now }, + { group: 'zhongzhi', question: '载波相位测量相比伪距测量?', options: '[{"key":"A","text":"精度更高"},{"key":"B","text":"精度更低"},{"key":"C","text":"精度相同"},{"key":"D","text":"无法比较"}]', answer: 'A', score: 1, sort_order: 35, created_at: now, updated_at: now }, + { group: 'zhongzhi', question: '电离层延迟对伪距测量的影响?', options: '[{"key":"A","text":"夜间比白天大"},{"key":"B","text":"白天比夜间大"},{"key":"C","text":"没有影响"},{"key":"D","text":"只在冬季有影响"}]', answer: 'B', score: 1, sort_order: 36, created_at: now, updated_at: now }, + { group: 'zhongzhi', question: '周跳发生在?', options: '[{"key":"A","text":"载波相位测量中"},{"key":"B","text":"伪距测量中"},{"key":"C","text":"信号发射时"},{"key":"D","text":"卫星星历计算时"}]', answer: 'A', score: 1, sort_order: 37, created_at: now, updated_at: now }, + { group: 'zhongzhi', question: 'GPS系统由多少颗卫星组成?', options: '[{"key":"A","text":"24"},{"key":"B","text":"31"},{"key":"C","text":"35"},{"key":"D","text":"40"}]', answer: 'A', score: 1, sort_order: 38, created_at: now, updated_at: now }, + { group: 'zhongzhi', question: '以下哪种不属于北斗增强系统?', options: '[{"key":"A","text":"SBAS"},{"key":"B","text":"GBAS"},{"key":"C","text":"BDS-IS"},{"key":"D","text":"GLONASS"}]', answer: 'D', score: 1, sort_order: 39, created_at: now, updated_at: now }, + { group: 'zhongzhi', question: '单点定位精度通常为?', options: '[{"key":"A","text":"厘米级"},{"key":"B","text":"米级"},{"key":"C","text":"分米级"},{"key":"D","text":"毫米级"}]', answer: 'B', score: 1, sort_order: 40, created_at: now, updated_at: now }, + { group: 'zhongzhi', question: 'RTK定位精度通常达到?', options: '[{"key":"A","text":"厘米级"},{"key":"B","text":"米级"},{"key":"C","text":"千米级"},{"key":"D","text":"十米级"}]', answer: 'A', score: 1, sort_order: 41, created_at: now, updated_at: now }, + { group: 'zhongzhi', question: 'NMEA-0183语句的结束标志是?', options: '[{"key":"A","text":"换行"},{"key":"B","text":"校验和"},{"key":"C","text":"星号"},{"key":"D","text":"回车"}]', answer: 'B', score: 1, sort_order: 42, created_at: now, updated_at: now }, + { group: 'zhongzhi', question: '以下哪个不是北斗卫星导航系统的特点?', options: '[{"key":"A","text":"有源定位"},{"key":"B","text":"短报文通信"},{"key":"C","text":"区域服务不能全球覆盖"},{"key":"D","text":"三频信号"}]', answer: 'C', score: 1, sort_order: 43, created_at: now, updated_at: now }, + { group: 'zhongzhi', question: '卫星导航接收机捕获阶段的主要作用是?', options: '[{"key":"A","text":"搜索卫星信号并确定频率码相位"},{"key":"B","text":"跟踪信号"},{"key":"C","text":"计算位置"},{"key":"D","text":"输出速度"}]', answer: 'A', score: 1, sort_order: 44, created_at: now, updated_at: now }, + { group: 'zhongzhi', question: '对流层延迟改正模型中,通常使用?', options: '[{"key":"A","text":"Hopfield模型"},{"key":"B","text":"Klobuchar模型"},{"key":"C","text":"Black模型"},{"key":"D","text":"高斯模型"}]', answer: 'A', score: 1, sort_order: 45, created_at: now, updated_at: now }, + { group: 'zhongzhi', question: '全球定位系统中,UTC表示?', options: '[{"key":"A","text":"世界时"},{"key":"B","text":"协调世界时"},{"key":"C","text":"原子时"},{"key":"D","text":"恒星时"}]', answer: 'B', score: 1, sort_order: 46, created_at: now, updated_at: now }, + { group: 'zhongzhi', question: '整周模糊度解算属于?', options: '[{"key":"A","text":"伪距测量问题"},{"key":"B","text":"载波相位测量问题"},{"key":"C","text":"卫星轨道问题"},{"key":"D","text":"电离层问题"}]', answer: 'B', score: 1, sort_order: 47, created_at: now, updated_at: now }, + { group: 'zhongzhi', question: '以下哪个频率是北斗B1I信号频率?', options: '[{"key":"A","text":"1561.098 MHz"},{"key":"B","text":"1268.52 MHz"},{"key":"C","text":"1176.45 MHz"},{"key":"D","text":"1575.42 MHz"}]', answer: 'A', score: 1, sort_order: 48, created_at: now, updated_at: now }, + { group: 'zhongzhi', question: 'GPS选择的PRN码特点是?', options: '[{"key":"A","text":"自相关特性好,互相关特性弱"},{"key":"B","text":"自相关特性弱,互相关特性强"},{"key":"C","text":"长度固定不变"},{"key":"D","text":"只有一个码"}]', answer: 'A', score: 1, sort_order: 49, created_at: now, updated_at: now }, + { group: 'zhongzhi', question: '下列哪项不属于卫星导航的应用?', options: '[{"key":"A","text":"车辆导航"},{"key":"B","text":"工程测量"},{"key":"C","text":"火力发电"},{"key":"D","text":"时间同步"}]', answer: 'C', score: 1, sort_order: 50, created_at: now, updated_at: now }, + + // 高职组继续补充题目(总计50题,上述中职组已有50题,高职组继续扩展) + { group: 'gaozhi', question: '卫星通信中,FDMA的含义是?', options: '[{"key":"A","text":"频分多址"},{"key":"B","text":"时分多址"},{"key":"C","text":"码分多址"},{"key":"D","text":"空分多址"}]', answer: 'A', score: 1, sort_order: 1, created_at: now, updated_at: now }, + { group: 'gaozhi', question: 'GEO卫星的星下点位置相对于地面观察者?', options: '[{"key":"A","text":"不断高速移动"},{"key":"B","text":"基本静止"},{"key":"C","text":"每天环绕一次"},{"key":"D","text":"每月环绕一次"}]', answer: 'B', score: 1, sort_order: 2, created_at: now, updated_at: now }, + { group: 'gaozhi', question: 'OFDM技术中,子载波之间保持正交性的目的是?', options: '[{"key":"A","text":"提高发射功率"},{"key":"B","text":"避免子载波间干扰"},{"key":"C","text":"增加带宽"},{"key":"D","text":"降低频率"}]', answer: 'B', score: 1, sort_order: 3, created_at: now, updated_at: now }, + { group: 'gaozhi', question: '北斗三号全球卫星导航系统具备以下哪种特色服务?', options: '[{"key":"A","text":"仅定位服务"},{"key":"B","text":"短报文通信"},{"key":"C","text":"仅授时服务"},{"key":"D","text":"仅测速服务"}]', answer: 'B', score: 1, sort_order: 4, created_at: now, updated_at: now }, + { group: 'gaozhi', question: '5G网络中,Massive MIMO技术的主要优势是?', options: '[{"key":"A","text":"降低功耗"},{"key":"B","text":"提升频谱效率和容量"},{"key":"C","text":"减少天线数量"},{"key":"D","text":"简化网络架构"}]', answer: 'B', score: 1, sort_order: 5, created_at: now, updated_at: now }, + { group: 'gaozhi', question: '网络切片技术属于5G的哪个核心特性?', options: '[{"key":"A","text":"eMBB"},{"key":"B","text":"URLLC"},{"key":"C","text":"mMTC"},{"key":"D","text":"网络能力开放"}]', answer: 'D', score: 1, sort_order: 6, created_at: now, updated_at: now }, + { group: 'gaozhi', question: '在通信安全中,以下哪种攻击属于被动攻击?', options: '[{"key":"A","text":"篡改"},{"key":"B","text":"窃听"},{"key":"C","text":"拒绝服务"},{"key":"D","text":"伪装"}]', answer: 'B', score: 1, sort_order: 7, created_at: now, updated_at: now }, + { group: 'gaozhi', question: 'NTRIP协议主要用于什么场景?', options: '[{"key":"A","text":"卫星遥测"},{"key":"B","text":"差分数据网络传输"},{"key":"C","text":"卫星轨道控制"},{"key":"D","text":"信号加密传输"}]', answer: 'B', score: 1, sort_order: 8, created_at: now, updated_at: now }, + { group: 'gaozhi', question: '组合导航(GNSS+IMU)的核心优势是什么?', options: '[{"key":"A","text":"降低成本"},{"key":"B","text":"无缝切换与误差互补"},{"key":"C","text":"减少卫星数量"},{"key":"D","text":"提高发射功率"}]', answer: 'B', score: 1, sort_order: 9, created_at: now, updated_at: now }, + { group: 'gaozhi', question: '5G网络中,URLLC场景的主要特征是?', options: '[{"key":"A","text":"高速率大带宽"},{"key":"B","text":"低时延高可靠"},{"key":"C","text":"海量连接"},{"key":"D","text":"广域覆盖"}]', answer: 'B', score: 1, sort_order: 10, created_at: now, updated_at: now }, + { group: 'gaozhi', question: '卫星定位中,伪距测量的误差来源不包括?', options: '[{"key":"A","text":"电离层延迟"},{"key":"B","text":"对流层延迟"},{"key":"C","text":"卫星轨道误差"},{"key":"D","text":"用户设备重量"}]', answer: 'D', score: 1, sort_order: 11, created_at: now, updated_at: now }, + { group: 'gaozhi', question: '低轨卫星物联网相比GEO卫星的主要优势是?', options: '[{"key":"A","text":"覆盖范围更大"},{"key":"B","text":"传输时延更低"},{"key":"C","text":"单星容量更大"},{"key":"D","text":"使用寿命更长"}]', answer: 'B', score: 1, sort_order: 12, created_at: now, updated_at: now }, + { group: 'gaozhi', question: 'QPSK调制属于?', options: '[{"key":"A","text":"模拟调制"},{"key":"B","text":"数字相位调制"},{"key":"C","text":"幅度调制"},{"key":"D","text":"频率调制"}]', answer: 'B', score: 1, sort_order: 13, created_at: now, updated_at: now }, + { group: 'gaozhi', question: '信道编码的主要目的是?', options: '[{"key":"A","text":"提高传输速率"},{"key":"B","text":"对抗传输错误"},{"key":"C","text":"增加功率"},{"key":"D","text":"压缩数据"}]', answer: 'B', score: 1, sort_order: 14, created_at: now, updated_at: now }, + { group: 'gaozhi', question: 'CDMA技术的特点是?', options: '[{"key":"A","text":"所有用户同时占用相同频段"},{"key":"B","text":"每个用户占用不同频率"},{"key":"C","text":"每个用户占用不同时隙"},{"key":"D","text":"用户按空间分隔"}]', answer: 'A', score: 1, sort_order: 15, created_at: now, updated_at: now }, + { group: 'gaozhi', question: 'M-QAM调制中,M越大?', options: '[{"key":"A","text":"频谱效率越高,误码性能越好"},{"key":"B","text":"频谱效率越高,误码性能越差"},{"key":"C","text":"频谱效率越低,误码性能越好"},{"key":"D","text":"频谱效率越低,误码性能越差"}]', answer: 'B', score: 1, sort_order: 16, created_at: now, updated_at: now }, + { group: 'gaozhi', question: '香农公式给出了?', options: '[{"key":"A","text":"信道容量与带宽信噪比的关系"},{"key":"B","text":"信号功率与频率的关系"},{"key":"C","text":"噪声功率与温度的关系"},{"key":"D","text":"带宽与频率的关系"}]', answer: 'A', score: 1, sort_order: 17, created_at: now, updated_at: now }, + { group: 'gaozhi', question: '均衡技术主要用于补偿?', options: '[{"key":"A","text":"信道衰落引起的码间干扰"},{"key":"B","text":"噪声干扰"},{"key":"C","text":"频率偏移"},{"key":"D","text":"相位噪声"}]', answer: 'A', score: 1, sort_order: 18, created_at: now, updated_at: now }, + { group: 'gaozhi', question: '编码效率等于?', options: '[{"key":"A","text":"信息位长度/总码长"},{"key":"B","text":"总码长/信息位长度"},{"key":"C","text":"监督位长度/总码长"},{"key":"D","text":"信息位长度/监督位长度"}]', answer: 'A', score: 1, sort_order: 19, created_at: now, updated_at: now }, + { group: 'gaozhi', question: 'RS编码属于?', options: '[{"key":"A","text":"分组码"},{"key":"B","text":"卷积码"},{"key":"C","text":"循环码"},{"key":"D","text":"RS不是编码"}]', answer: 'A', score: 1, sort_order: 20, created_at: now, updated_at: now }, + { group: 'gaozhi', question: 'TCM编码是?', options: '[{"key":"A","text":"网格编码调制"},{"key":"B","text":" Turbo编码"},{"key":"C","text":"卷积编码"},{"key":"D","text":"里德所罗门编码"}]', answer: 'A', score: 1, sort_order: 21, created_at: now, updated_at: now }, + { group: 'gaozhi', question: '扩频通信的主要优点不包括?', options: '[{"key":"A","text":"抗干扰能力强"},{"key":"B","text":"隐蔽性好"},{"key":"C","text":"功率大"},{"key":"D","text":"多址能力强"}]', answer: 'C', score: 1, sort_order: 22, created_at: now, updated_at: now }, + { group: 'gaozhi', question: 'DSSS表示?', options: '[{"key":"A","text":"跳频扩频"},{"key":"B","text":"直接序列扩频"},{"key":"C","text":"跳时扩频"},{"key":"D","text":"线性扩频"}]', answer: 'B', score: 1, sort_order: 23, created_at: now, updated_at: now }, + { group: 'gaozhi', question: 'AOS协议主要用于?', options: '[{"key":"A","text":"地面互联网传输"},{"key":"B","text":"星地链路 packet 传输"},{"key":"C","text":"卫星轨道测量"},{"key":"D","text":"信号调制"}]', answer: 'B', score: 1, sort_order: 24, created_at: now, updated_at: now }, + { group: 'gaozhi', question: 'CCSDS标准是?', options: '[{"key":"A","text":"空间数据系统咨询委员会标准"},{"key":"B","text":"通信系统标准"},{"key":"C","text":"卫星导航标准"},{"key":"D","text":"互联网标准"}]', answer: 'A', score: 1, sort_order: 25, created_at: now, updated_at: now }, + { group: 'gaozhi', question: 'LDPC编码属于?', options: '[{"key":"A","text":"低密度奇偶校验码"},{"key":"B","text":"低密度分组码"},{"key":"C","text":"循环码"},{"key":"D","text":"卷积码"}]', answer: 'A', score: 1, sort_order: 26, created_at: now, updated_at: now }, + { group: 'gaozhi', question: 'MIMO技术可以提高?', options: '[{"key":"A","text":"信道容量"},{"key":"B","text":"发射功率"},{"key":"C","text":"噪声功率"},{"key":"D","text":"传播距离"}]', answer: 'A', score: 1, sort_order: 27, created_at: now, updated_at: now }, + { group: 'gaozhi', question: 'OFDM循环前缀的作用是?', options: '[{"key":"A","text":"增加功率"},{"key":"B","text":"消除符号间干扰"},{"key":"C","text":"提高频率"},{"key":"D","text":"降低速度"}]', answer: 'B', score: 1, sort_order: 28, created_at: now, updated_at: now }, + { group: 'gaozhi', question: 'Turbo码的主要优点是?', options: '[{"key":"A","text":"接近香农极限的性能"},{"key":"B","text":"编码简单"},{"key":"C","text":"译码简单"},{"key":"D","text":"速率很高"}]', answer: 'A', score: 1, sort_order: 29, created_at: now, updated_at: now }, + { group: 'gaozhi', question: 'ARQ协议的作用是?', options: '[{"key":"A","text":"自动重传请求"},{"key":"B","text":"自动编码"},{"key":"C","text":"自动应答"},{"key":"D","text":"自动加密"}]', answer: 'A', score: 1, sort_order: 30, created_at: now, updated_at: now }, + { group: 'gaozhi', question: 'TCP/IP协议中,TCP提供?', options: '[{"key":"A","text":"面向连接可靠服务"},{"key":"B","text":"无连接不可靠服务"},{"key":"C","text":"广播服务"},{"key":"D","text":"组播服务"}]', answer: 'A', score: 1, sort_order: 31, created_at: now, updated_at: now }, + { group: 'gaozhi', question: 'UDP协议提供?', options: '[{"key":"A","text":"面向连接可靠服务"},{"key":"B","text":"无连接不可靠服务"},{"key":"C","text":"广播服务"},{"key":"D","text":"组播服务"}]', answer: 'B', score: 1, sort_order: 32, created_at: now, updated_at: now }, + { group: 'gaozhi', question: 'HTTPS协议使用什么端口?', options: '[{"key":"A","text":"80"},{"key":"B","text":"443"},{"key":"C","text":"8080"},{"key":"D","text":"22"}]', answer: 'B', score: 1, sort_order: 33, created_at: now, updated_at: now }, + { group: 'gaozhi', question: 'IPv6地址长度为?', options: '[{"key":"A","text":"32位"},{"key":"B","text":"64位"},{"key":"C","text":"128位"},{"key":"D","text":"256位"}]', answer: 'C', score: 1, sort_order: 34, created_at: now, updated_at: now }, + { group: 'gaozhi', question: 'OSI模型有几层?', options: '[{"key":"A","text":"5"},{"key":"B","text":"6"},{"key":"C","text":"7"},{"key":"D","text":"8"}]', answer: 'C', score: 1, sort_order: 35, created_at: now, updated_at: now }, + { group: 'gaozhi', question: '调制的主要作用是?', options: '[{"key":"A","text":"将基带信号搬移到高频载波"},{"key":"B","text":"将高频信号搬移到基带"},{"key":"C","text":"放大信号"},{"key":"D","text":"滤波信号"}]', answer: 'A', score: 1, sort_order: 36, created_at: now, updated_at: now }, + { group: 'gaozhi', question: '解调的主要作用是?', options: '[{"key":"A","text":"从高频载波提取基带信号"},{"key":"B","text":"放大信号"},{"key":"C","text":"滤波信号"},{"key":"D","text":"变频信号"}]', answer: 'A', score: 1, sort_order: 37, created_at: now, updated_at: now }, + { group: 'gaozhi', question: '信源编码的主要目的是?', options: '[{"key":"A","text":"提高传输可靠性"},{"key":"B","text":"压缩数据率"},{"key":"C","text":"增加冗余"},{"key":"D","text":"提高功率"}]', answer: 'B', score: 1, sort_order: 38, created_at: now, updated_at: now }, + { group: 'gaozhi', question: '信道编码的主要目的是?', options: '[{"key":"A","text":"压缩数据"},{"key":"B","text":"差错控制"},{"key":"C","text":"提高速率"},{"key":"D","text":"降低功率"}]', answer: 'B', score: 1, sort_order: 39, created_at: now, updated_at: now }, + { group: 'gaozhi', question: 'AM调制中,调幅指数m满足?', options: '[{"key":"A","text":"0 ≤ m ≤ 1"},{"key":"B","text":"m > 1"},{"key":"C","text":"m < 0"},{"key":"D","text":"m = 0"}]', answer: 'A', score: 1, sort_order: 40, created_at: now, updated_at: now }, + { group: 'gaozhi', question: 'FM调制相比AM调制?', options: '[{"key":"A","text":"抗噪声性能更好"},{"key":"B","text":"功率更小"},{"key":"C","text":"带宽更小"},{"key":"D","text":"实现更简单"}]', answer: 'A', score: 1, sort_order: 41, created_at: now, updated_at: now }, + { group: 'gaozhi', question: 'PCM编码三个步骤依次是?', options: '[{"key":"A","text":"抽样、量化、编码"},{"key":"B","text":"量化、抽样、编码"},{"key":"C","text":"编码、抽样、量化"},{"key":"D","text":"抽样、编码、量化"}]', answer: 'A', score: 1, sort_order: 42, created_at: now, updated_at: now }, + { group: 'gaozhi', question: '奈奎斯特第一准则解决什么问题?', options: '[{"key":"A","text":"无码间干扰的传输特性"},{"key":"B","text":"信噪比计算"},{"key":"C","text":"信道容量计算"},{"key":"D","text":"功率分配"}]', answer: 'A', score: 1, sort_order: 43, created_at: now, updated_at: now }, + { group: 'gaozhi', question: '匹配滤波器输出信噪比在什么时候最大?', options: '[{"key":"A","text":"抽样时刻"},{"key":"B","text":"发送时刻"},{"key":"C","text":"接收开始"},{"key":"D","text":"接收结束"}]', answer: 'A', score: 1, sort_order: 44, created_at: now, updated_at: now }, + { group: 'gaozhi', question: '升余弦滚降滤波器的作用是?', options: '[{"key":"A","text":"降低码间干扰"},{"key":"B","text":"增加码间干扰"},{"key":"C","text":"提高功率"},{"key":"D","text":"改变频率"}]', answer: 'A', score: 1, sort_order: 45, created_at: now, updated_at: now }, + { group: 'gaozhi', question: '希尔伯特变换主要用于?', options: '[{"key":"A","text":"产生解析信号"},{"key":"B","text":"滤波信号"},{"key":"C","text":"放大信号"},{"key":"D","text":"变频信号"}]', answer: 'A', score: 1, sort_order: 46, created_at: now, updated_at: now }, + { group: 'gaozhi', question: '随机过程的均值反映了?', options: '[{"key":"A","text":"信号的平均强度"},{"key":"B","text":"信号的起伏程度"},{"key":"C","text":"信号的相关性"},{"key":"D","text":"信号的频率"}]', answer: 'A', score: 1, sort_order: 47, created_at: now, updated_at: now }, + { group: 'gaozhi', question: '傅里叶变换将信号变换到?', options: '[{"key":"A","text":"时域"},{"key":"B","text":"频域"},{"key":"C","text":"空域"},{"key":"D","text":"码域"}]', answer: 'B', score: 1, sort_order: 48, created_at: now, updated_at: now }, + { group: 'gaozhi', question: '信噪比等于?', options: '[{"key":"A","text":"信号功率除以噪声功率"},{"key":"B","text":"噪声功率除以信号功率"},{"key":"C","text":"信号电压除以噪声电压"},{"key":"D","text":"信号电流除以噪声电流"}]', answer: 'A', score: 1, sort_order: 49, created_at: now, updated_at: now }, + { group: 'gaozhi', question: '纠错编码的主要作用是?', options: '[{"key":"A","text":"降低误码率"},{"key":"B","text":"提高误码率"},{"key":"C","text":"增加传输速率"},{"key":"D","text":"降低带宽占用"}]', answer: 'A', score: 1, sort_order: 50, created_at: now, updated_at: now }, + ]; + await queryInterface.bulkInsert('choice_questions', choiceQuestions, {}); + + // ==================== 判断题 ==================== + // 总分:20题 × 1分 = 20分 + const judgeQuestions = [ + { group: 'zhongzhi', question: '调制的目的是将低频信号搬移到高频载波上,以便于天线辐射和信道传输。', answer: 'true', score: 1, sort_order: 1, created_at: now, updated_at: now }, + { group: 'zhongzhi', question: 'GPS接收机只需要接收到3颗卫星的信号即可实现三维定位。', answer: 'false', score: 1, sort_order: 2, created_at: now, updated_at: now }, + { group: 'zhongzhi', question: 'AM调制的抗干扰能力优于FM调制。', answer: 'false', score: 1, sort_order: 3, created_at: now, updated_at: now }, + { group: 'zhongzhi', question: '卫星通信中的上行频率通常高于下行频率。', answer: 'true', score: 1, sort_order: 4, created_at: now, updated_at: now }, + { group: 'zhongzhi', question: 'NMEA-0183协议中每条语句都以"$"开头,以校验和结尾。', answer: 'true', score: 1, sort_order: 5, created_at: now, updated_at: now }, + { group: 'zhongzhi', question: '北斗卫星导航系统由空间段、地面段、用户段三部分组成。', answer: 'true', score: 1, sort_order: 6, created_at: now, updated_at: now }, + { group: 'zhongzhi', question: 'RTK定位可以达到厘米级精度。', answer: 'true', score: 1, sort_order: 7, created_at: now, updated_at: now }, + { group: 'zhongzhi', question: '电离层延迟误差与太阳活动无关。', answer: 'false', score: 1, sort_order: 8, created_at: now, updated_at: now }, + { group: 'zhongzhi', question: '差分定位可以提高定位精度。', answer: 'true', score: 1, sort_order: 9, created_at: now, updated_at: now }, + { group: 'zhongzhi', question: 'GPS卫星本身就是原子钟,提供精确的时间标准。', answer: 'true', score: 1, sort_order: 10, created_at: now, updated_at: now }, + + { group: 'gaozhi', question: 'OFDM系统中,循环前缀的长度必须大于信道的最大时延扩展。', answer: 'true', score: 1, sort_order: 11, created_at: now, updated_at: now }, + { group: 'gaozhi', question: '北斗系统只能提供区域导航定位服务,不能提供全球服务。', answer: 'false', score: 1, sort_order: 12, created_at: now, updated_at: now }, + { group: 'gaozhi', question: '边缘计算可以将计算任务从云端下沉到网络边缘,降低时延。', answer: 'true', score: 1, sort_order: 13, created_at: now, updated_at: now }, + { group: 'gaozhi', question: '数字签名技术可以防止通信内容被窃听。', answer: 'false', score: 1, sort_order: 14, created_at: now, updated_at: now }, + { group: 'gaozhi', question: 'Massive MIMO技术通过增加天线数量来提升系统容量。', answer: 'true', score: 1, sort_order: 15, created_at: now, updated_at: now }, + { group: 'gaozhi', question: '香农公式说明信道容量与带宽和信噪比都正相关。', answer: 'true', score: 1, sort_order: 16, created_at: now, updated_at: now }, + { group: 'gaozhi', question: 'LDPC码是一种高性能的信道编码。', answer: 'true', score: 1, sort_order: 17, created_at: now, updated_at: now }, + { group: 'gaozhi', question: 'TCP协议提供面向连接的可靠服务。', answer: 'true', score: 1, sort_order: 18, created_at: now, updated_at: now }, + { group: 'gaozhi', question: 'UDP协议提供流量控制和拥塞控制。', answer: 'false', score: 1, sort_order: 19, created_at: now, updated_at: now }, + { group: 'gaozhi', question: '奈奎斯特第一准则告诉我们理想低通信道的最高码元传输速率等于2B。', answer: 'true', score: 1, sort_order: 20, created_at: now, updated_at: now }, + ]; + await queryInterface.bulkInsert('judge_questions', judgeQuestions, {}); + + // ==================== 综合性任务 ==================== + // 总分:1题 × 30分 = 30分 + const wordQuestions = [ + { group: 'zhongzhi', title: '卫星通信系统基本原理', description: '请结合第一篇卫星通信技术,简述卫星通信系统的基本组成,包括空间段、地面段、用户段三大部分,并说明各部分的主要功能。分析GEO/MEO/LEO三种轨道的特点及其适用场景,举例说明典型应用。', score: 30, sort_order: 1, template_path: null, created_at: now, updated_at: now }, + { group: 'gaozhi', title: '卫星导航融合定位方案设计', description: '请结合第二篇卫星导航技术,设计一个GNSS/INS组合导航定位方案。要求:(1) 分析组合导航的必要性;(2) 说明GNSS和INS各自优缺点;(3) 给出卡尔曼滤波融合框架;(4) 分析预期性能改进。', score: 30, sort_order: 1, template_path: null, created_at: now, updated_at: now }, + ]; + await queryInterface.bulkInsert('word_questions', wordQuestions, {}); + }, + + down: async (queryInterface) => { + await queryInterface.bulkDelete('choice_questions', null, {}); + await queryInterface.bulkDelete('judge_questions', null, {}); + await queryInterface.bulkDelete('word_questions', null, {}); + }, +}; \ No newline at end of file diff --git a/dev.js b/dev.js new file mode 100644 index 0000000..2eaf26c --- /dev/null +++ b/dev.js @@ -0,0 +1,13 @@ +'use strict'; + +/** + * Egg.js 开发服务器启动脚本 + * 绕过 egg-bin 的依赖问题,直接使用 egg 启动集群 + */ +const egg = require('egg'); + +egg.startCluster({ + baseDir: __dirname, + workers: 1, + port: 7001, +}); \ No newline at end of file diff --git a/egg-compat-patch.js b/egg-compat-patch.js new file mode 100644 index 0000000..19adbcc --- /dev/null +++ b/egg-compat-patch.js @@ -0,0 +1,87 @@ +// egg-compat-patch.js +// 批量扫描并修复 egg.js v3 在 Node 22 下的全部缺失依赖 +const fs = require('fs'); +const path = require('path'); + +const BASE = path.resolve(__dirname, 'node_modules'); + +// === 已知缺失依赖 + shim 内容 === +const SHIMS = { + 'serialize-json': { + package: { name: 'serialize-json', version: '2.0.0', main: 'index.js' }, + index: `'use strict'; +// shim: serialize-json → native JSON +module.exports = JSON; +module.exports.parse = JSON.parse; +module.exports.stringify = JSON.stringify; +` + }, + 'sdk-base': { + package: { name: 'sdk-base', version: '4.0.0', main: 'index.js' }, + index: `'use strict'; +const EventEmitter = require('events'); +class Base extends EventEmitter { + constructor(opts) { super(); this.options = opts || {}; } + ready(cb) { if (cb) cb(); return Promise.resolve(); } +} +module.exports = Base; +` + }, + 'tcp-base': { + package: { name: 'tcp-base', version: '4.0.0', main: 'index.js' }, + index: `'use strict'; +const sdkBase = require('sdk-base'); +class TCPBase extends sdkBase { + constructor(opts) { super(opts); } + async close() {} + async connect() {} +} +module.exports = TCPBase; +` + }, + 'await-first': { + package: { name: 'await-first', version: '1.0.0', main: 'index.js' }, + index: `'use strict'; +module.exports = async function awaitFirst(emitter, event) { + return new Promise((resolve) => { + emitter.once(event, resolve); + }); +}; +` + }, + 'co': { + package: { name: 'co', version: '4.6.0', main: 'index.js' }, + index: `'use strict'; +// co shim: use Promise for basic generator support +module.exports = function co(gen) { + if (typeof gen === 'function') gen = gen(); + if (!gen || typeof gen.next !== 'function') return Promise.resolve(gen); + return new Promise((resolve, reject) => { + function step(next) { + try { + const result = next(); + if (result.done) return resolve(result.value); + Promise.resolve(result.value).then(v => step(() => gen.next(v)), e => step(() => gen.throw(e))); + } catch (e) { reject(e); } + } + step(() => gen.next()); + }); +}; +` + }, +}; + +// 创建所有 shim +for (const [name, files] of Object.entries(SHIMS)) { + const dir = path.join(BASE, name); + if (fs.existsSync(dir)) { + console.log(`[SKIP] ${name} already exists`); + continue; + } + fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify(files.package, null, 2)); + fs.writeFileSync(path.join(dir, 'index.js'), files.index); + console.log(`[CREATED] ${name}`); +} + +console.log('\n=== 补丁应用完成 ==='); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..2584841 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,11367 @@ +{ + "name": "competition-website-backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "competition-website-backend", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@alicloud/dysmsapi20170525": "^4.6.0", + "@alicloud/openapi-client": "^0.4.15", + "@alicloud/tea-util": "^1.4.11", + "@artus/core": "^2.2.4", + "@eggjs/router": "^3.0.6", + "archiver": "^8.0.0", + "bcryptjs": "^3.0.3", + "cliui": "^9.0.1", + "egg": "^3.34.0", + "egg-cors": "^2.2.4", + "egg-scripts": "^2.17.0", + "egg-sequelize": "^6.0.0", + "egg-validate": "^2.0.2", + "get-ready": "^2.0.1", + "jsonwebtoken": "^9.0.2", + "multer": "^1.4.5-lts.1", + "mysql2": "^3.6.5", + "mz-modules": "^2.1.0", + "svg-captcha": "^1.4.0", + "tslib": "^2.8.1", + "xlsx": "^0.18.0", + "ylru": "^1.4.0" + }, + "devDependencies": { + "egg-bin": "^6.3.0", + "egg-mock": "^5.0.0", + "sequelize-cli": "^6.6.2", + "string-width": "^4.2.3", + "wt": "^1.2.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@alicloud/credentials": { + "version": "2.4.5", + "resolved": "https://registry.npmmirror.com/@alicloud/credentials/-/credentials-2.4.5.tgz", + "integrity": "sha512-od1ufCxOO7cP2R4EVFliOB0kGo9lUXCibyj/mzmI6yLhxeqhqsegTzVsx5p2NJJsceKJnYcmye7FWKyLJAFBkw==", + "dependencies": { + "@alicloud/tea-typescript": "^1.8.0", + "httpx": "^2.3.3", + "ini": "^1.3.5", + "kitx": "^2.0.0" + } + }, + "node_modules/@alicloud/darabonba-array": { + "version": "0.1.2", + "resolved": "https://registry.npmmirror.com/@alicloud/darabonba-array/-/darabonba-array-0.1.2.tgz", + "integrity": "sha512-ZPuQ+bJyjrd8XVVm55kl+ypk7OQoi1ZH/DiToaAEQaGvgEjrTcvQkg71//vUX/6cvbLIF5piQDvhrLb+lUEIPQ==", + "dependencies": { + "@alicloud/tea-typescript": "^1.7.1" + } + }, + "node_modules/@alicloud/darabonba-encode-util": { + "version": "0.0.2", + "resolved": "https://registry.npmmirror.com/@alicloud/darabonba-encode-util/-/darabonba-encode-util-0.0.2.tgz", + "integrity": "sha512-mlsNctkeqmR0RtgE1Rngyeadi5snLOAHBCWEtYf68d7tyKskosXDTNeZ6VCD/UfrUu4N51ItO8zlpfXiOgeg3A==", + "dependencies": { + "moment": "^2.29.1" + } + }, + "node_modules/@alicloud/darabonba-map": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/@alicloud/darabonba-map/-/darabonba-map-0.0.1.tgz", + "integrity": "sha512-2ep+G3YDvuI+dRYVlmER1LVUQDhf9kEItmVB/bbEu1pgKzelcocCwAc79XZQjTcQGFgjDycf3vH87WLDGLFMlw==", + "dependencies": { + "@alicloud/tea-typescript": "^1.7.1" + } + }, + "node_modules/@alicloud/darabonba-signature-util": { + "version": "0.0.4", + "resolved": "https://registry.npmmirror.com/@alicloud/darabonba-signature-util/-/darabonba-signature-util-0.0.4.tgz", + "integrity": "sha512-I1TtwtAnzLamgqnAaOkN0IGjwkiti//0a7/auyVThdqiC/3kyafSAn6znysWOmzub4mrzac2WiqblZKFcN5NWg==", + "dependencies": { + "@alicloud/darabonba-encode-util": "^0.0.1" + } + }, + "node_modules/@alicloud/darabonba-signature-util/node_modules/@alicloud/darabonba-encode-util": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/@alicloud/darabonba-encode-util/-/darabonba-encode-util-0.0.1.tgz", + "integrity": "sha512-Sl5vCRVAYMqwmvXpJLM9hYoCHOMsQlGxaWSGhGWulpKk/NaUBArtoO1B0yHruJf1C5uHhEJIaylYcM48icFHgw==", + "dependencies": { + "@alicloud/tea-typescript": "^1.7.1", + "moment": "^2.29.1" + } + }, + "node_modules/@alicloud/darabonba-string": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/@alicloud/darabonba-string/-/darabonba-string-1.0.3.tgz", + "integrity": "sha512-NyWwrU8cAIesWk3uHL1Q7pTDTqLkCI/0PmJXC4/4A0MFNAZ9Ouq0iFBsRqvfyUujSSM+WhYLuTfakQXiVLkTMA==", + "dependencies": { + "@alicloud/tea-typescript": "^1.5.1" + } + }, + "node_modules/@alicloud/dysmsapi20170525": { + "version": "4.6.0", + "resolved": "https://registry.npmmirror.com/@alicloud/dysmsapi20170525/-/dysmsapi20170525-4.6.0.tgz", + "integrity": "sha512-cUAgK/nHEgG8TVOlvMDAEINJjPo9D/l/x9loCom7CqlE8U1uFeZh7Ko2LR2ksPoHbTzzx9NYbXQthFoWgdlThQ==", + "dependencies": { + "@alicloud/openapi-core": "^1.0.0", + "@darabonba/typescript": "^1.0.0" + } + }, + "node_modules/@alicloud/endpoint-util": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/@alicloud/endpoint-util/-/endpoint-util-0.0.1.tgz", + "integrity": "sha512-+pH7/KEXup84cHzIL6UJAaPqETvln4yXlD9JzlrqioyCSaWxbug5FUobsiI6fuUOpw5WwoB3fWAtGbFnJ1K3Yg==", + "dependencies": { + "@alicloud/tea-typescript": "^1.5.1", + "kitx": "^2.0.0" + } + }, + "node_modules/@alicloud/gateway-pop": { + "version": "0.0.6", + "resolved": "https://registry.npmmirror.com/@alicloud/gateway-pop/-/gateway-pop-0.0.6.tgz", + "integrity": "sha512-KF4I+JvfYuLKc3fWeWYIZ7lOVJ9jRW0sQXdXidZn1DKZ978ncfGf7i0LBfONGk4OxvNb/HD3/0yYhkgZgPbKtA==", + "dependencies": { + "@alicloud/credentials": "^2", + "@alicloud/darabonba-array": "^0.1.0", + "@alicloud/darabonba-encode-util": "^0.0.2", + "@alicloud/darabonba-map": "^0.0.1", + "@alicloud/darabonba-signature-util": "^0.0.4", + "@alicloud/darabonba-string": "^1.0.2", + "@alicloud/endpoint-util": "^0.0.1", + "@alicloud/gateway-spi": "^0.0.8", + "@alicloud/openapi-util": "^0.3.2", + "@alicloud/tea-typescript": "^1.7.1", + "@alicloud/tea-util": "^1.4.8" + } + }, + "node_modules/@alicloud/gateway-spi": { + "version": "0.0.8", + "resolved": "https://registry.npmmirror.com/@alicloud/gateway-spi/-/gateway-spi-0.0.8.tgz", + "integrity": "sha512-KM7fu5asjxZPmrz9sJGHJeSU+cNQNOxW+SFmgmAIrITui5hXL2LB+KNRuzWmlwPjnuA2X3/keq9h6++S9jcV5g==", + "dependencies": { + "@alicloud/credentials": "^2", + "@alicloud/tea-typescript": "^1.7.1" + } + }, + "node_modules/@alicloud/openapi-client": { + "version": "0.4.15", + "resolved": "https://registry.npmmirror.com/@alicloud/openapi-client/-/openapi-client-0.4.15.tgz", + "integrity": "sha512-4VE0/k5ZdQbAhOSTqniVhuX1k5DUeUMZv74degn3wIWjLY6Bq+hxjaGsaHYlLZ2gA5wUrs8NcI5TE+lIQS3iiA==", + "dependencies": { + "@alicloud/credentials": "^2.4.2", + "@alicloud/gateway-spi": "^0.0.8", + "@alicloud/openapi-util": "^0.3.2", + "@alicloud/tea-typescript": "^1.7.1", + "@alicloud/tea-util": "1.4.9", + "@alicloud/tea-xml": "0.0.3" + } + }, + "node_modules/@alicloud/openapi-client/node_modules/@alicloud/tea-util": { + "version": "1.4.9", + "resolved": "https://registry.npmmirror.com/@alicloud/tea-util/-/tea-util-1.4.9.tgz", + "integrity": "sha512-S0wz76rGtoPKskQtRTGqeuqBHFj8BqUn0Vh+glXKun2/9UpaaaWmuJwcmtImk6bJZfLYEShDF/kxDmDJoNYiTw==", + "dependencies": { + "@alicloud/tea-typescript": "^1.5.1", + "kitx": "^2.0.0" + } + }, + "node_modules/@alicloud/openapi-core": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/@alicloud/openapi-core/-/openapi-core-1.0.8.tgz", + "integrity": "sha512-xs8LdgMDcEUqv13kZ4nl+Vd+Fc1mixTF0g1lm5QSrNWDaLUUD5vqpuMtvUZnKNnq9PITfUQ+8KunAvNQJpFo8g==", + "hasInstallScript": true, + "dependencies": { + "@alicloud/credentials": "^2.4.2", + "@alicloud/gateway-pop": "0.0.6", + "@alicloud/gateway-spi": "^0.0.8", + "@darabonba/typescript": "^1.0.5" + } + }, + "node_modules/@alicloud/openapi-util": { + "version": "0.3.3", + "resolved": "https://registry.npmmirror.com/@alicloud/openapi-util/-/openapi-util-0.3.3.tgz", + "integrity": "sha512-vf0cQ/q8R2U7ZO88X5hDiu1yV3t/WexRj+YycWxRutkH/xVXfkmpRgps8lmNEk7Ar+0xnY8+daN2T+2OyB9F4A==", + "dependencies": { + "@alicloud/tea-typescript": "^1.7.1", + "@alicloud/tea-util": "^1.3.0", + "kitx": "^2.1.0", + "sm3": "^1.0.3" + } + }, + "node_modules/@alicloud/tea-typescript": { + "version": "1.8.0", + "resolved": "https://registry.npmmirror.com/@alicloud/tea-typescript/-/tea-typescript-1.8.0.tgz", + "integrity": "sha512-CWXWaquauJf0sW30mgJRVu9aaXyBth5uMBCUc+5vKTK1zlgf3hIqRUjJZbjlwHwQ5y9anwcu18r48nOZb7l2QQ==", + "dependencies": { + "@types/node": "^12.0.2", + "httpx": "^2.2.6" + } + }, + "node_modules/@alicloud/tea-typescript/node_modules/@types/node": { + "version": "12.20.55", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-12.20.55.tgz", + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==" + }, + "node_modules/@alicloud/tea-util": { + "version": "1.4.11", + "resolved": "https://registry.npmmirror.com/@alicloud/tea-util/-/tea-util-1.4.11.tgz", + "integrity": "sha512-HyPEEQ8F0WoZegiCp7sVdrdm6eBOB+GCvGl4182u69LDFktxfirGLcAx3WExUr1zFWkq2OSmBroTwKQ4w/+Yww==", + "dependencies": { + "@alicloud/tea-typescript": "^1.5.1", + "@darabonba/typescript": "^1.0.0", + "kitx": "^2.0.0" + } + }, + "node_modules/@alicloud/tea-xml": { + "version": "0.0.3", + "resolved": "https://registry.npmmirror.com/@alicloud/tea-xml/-/tea-xml-0.0.3.tgz", + "integrity": "sha512-+/9GliugjrLglsXVrd1D80EqqKgGpyA0eQ6+1ZdUOYCaRguaSwz44trX3PaxPu/HhIPJg9PsGQQ3cSLXWZjbAA==", + "dependencies": { + "@alicloud/tea-typescript": "^1", + "@types/xml2js": "^0.4.5", + "xml2js": "^0.6.0" + } + }, + "node_modules/@artus-cli/artus-cli": { + "version": "0.2.10", + "resolved": "https://registry.npmmirror.com/@artus-cli/artus-cli/-/artus-cli-0.2.10.tgz", + "integrity": "sha512-DmNKs8JwOtQTIqnLxGbLMy7cur1OUzcEvPIYNdBZpF9dFib7sYJt2+c4cnNVH9j8VeGAHIwWFPX2MQhFpUjIKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@artus-cli/plugin-help": "^1.0.0", + "@artus-cli/plugin-version": "^1.0.0", + "@artus/core": "^1.0.0", + "@artus/injection": "^0.5.1", + "@artus/pipeline": "^0.2.2", + "command-line-usage": "^6.1.3", + "koa-compose": "^4.1.0", + "lodash": "^4.17.21", + "pkg-up": "^3.1.0", + "reflect-metadata": "^0.1.13", + "tslib": "^2.4.0", + "type-fest": "^3.4.0", + "yargs-parser": "^21.1.1" + } + }, + "node_modules/@artus-cli/artus-cli/node_modules/@artus/core": { + "version": "1.0.9", + "resolved": "https://registry.npmmirror.com/@artus/core/-/core-1.0.9.tgz", + "integrity": "sha512-6/jaSThVFhV9jLR98aJ4u4JSB3ObQ2wt54XGn4YfJsYt2OpPw2rD6/vQFeZtRoZ9CpTiLeKgNuz6Fe4EzYXbsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@artus/injection": "^0.5.1", + "@artus/pipeline": "^0.2.2", + "deepmerge": "^4.2.2", + "minimatch": "^5.0.1" + }, + "peerDependencies": { + "reflect-metadata": "^0.1.13" + } + }, + "node_modules/@artus-cli/plugin-autocomplete": { + "version": "0.1.1", + "resolved": "https://registry.npmmirror.com/@artus-cli/plugin-autocomplete/-/plugin-autocomplete-0.1.1.tgz", + "integrity": "sha512-7xlYTRpD39HyAPMJ+NgIan8TsgvScuOWkAZP3pbmg2vzIi4QdEqpXoLRf86GwG3IUiUFQ2XIiCEcUz7Vb3Qejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "inquirer": "^8.2.5", + "tslib": "^2.4.0", + "yargs-parser": "^21.1.1" + } + }, + "node_modules/@artus-cli/plugin-help": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@artus-cli/plugin-help/-/plugin-help-1.1.0.tgz", + "integrity": "sha512-N4r6yIag06fwq41ROeIe6Gy7Oq5ShdbwShUQhET/XFT/e505mf+GUb65R6jWGbofp2+8gAGh98YgBNqa7+cUwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "command-line-usage": "^6.1.3", + "tslib": "^2.4.0" + } + }, + "node_modules/@artus-cli/plugin-version": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/@artus-cli/plugin-version/-/plugin-version-1.0.2.tgz", + "integrity": "sha512-gD/nJAoYdF6XAho0fcMlbwiekmAsunwqq4R7BHKaq3f+qOWVFHqoAFWa05evxx+TUQHd3+18RrwxqJN6LydVhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@artus/core": { + "version": "2.2.4", + "resolved": "https://registry.npmmirror.com/@artus/core/-/core-2.2.4.tgz", + "integrity": "sha512-uSY9b7QsY+FTaTLUdNEzHLejCGf/5w9gWF0+28RhSh/1j3wyqAy65ht8ZW9XXBP5p+wxhswwRh7qZ8h1GM+l+w==", + "license": "MIT", + "dependencies": { + "@artus/injection": "^0.5.1", + "deepmerge": "^4.2.2", + "minimatch": "^5.0.1", + "tslib": "^2.6.1" + }, + "peerDependencies": { + "reflect-metadata": "^0.1.13" + } + }, + "node_modules/@artus/injection": { + "version": "0.5.3", + "resolved": "https://registry.npmmirror.com/@artus/injection/-/injection-0.5.3.tgz", + "integrity": "sha512-0/F6EWZhT4Ki5Y5ugIDEehPMj6NFguG5lD6Q0UynaLN76A7Hbs7Ooiu2VWioouzodx6vCW3bthZ7V6vj+5Daog==", + "license": "ISC", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@artus/pipeline": { + "version": "0.2.3", + "resolved": "https://registry.npmmirror.com/@artus/pipeline/-/pipeline-0.2.3.tgz", + "integrity": "sha512-W4gBvkjD5bh7DScWn5DIdg3mRi93kabYCsOPTVN1sjtYVHlLX3xikyscdFphTjYlrmO4G+tP2lPH4pfBm3Sepg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.3.1" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmmirror.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@darabonba/typescript": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/@darabonba/typescript/-/typescript-1.0.5.tgz", + "integrity": "sha512-pfxHFVM8I3h8K2o8skpDQLMmR5iAeQ2eNpP1HrKDEm/9ZPF8aKwDKPwwEszT1NnIo0Y3++E7x1YsVkVpGjA/xw==", + "dependencies": { + "@alicloud/tea-typescript": "^1.5.1", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "httpx": "^2.3.2", + "lodash": "^4.17.21", + "moment": "^2.30.1", + "moment-timezone": "^0.5.45", + "socks-proxy-agent": "^6.2.1", + "ws": "^8.18.0", + "xml2js": "^0.6.2" + } + }, + "node_modules/@eggjs/ip": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/@eggjs/ip/-/ip-2.1.0.tgz", + "integrity": "sha512-jOsufOpySKBld+N1GG8IAW9EEQNmQ6bDaBnrRlYFk6AspoxG8gAzXSW93ZFfEPwscS5rL7UbDUKyIHUkmkTPyw==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@eggjs/router": { + "version": "3.0.6", + "resolved": "https://registry.npmmirror.com/@eggjs/router/-/router-3.0.6.tgz", + "integrity": "sha512-mqOFkq79x3wGHYoI/lWMwSZ4wqmET9okJ0WU+qeuEgwA7PNMt8HyUmZ9Edz8+o3TjCSl85uMhpC0K4WWfPh7yA==", + "license": "MIT", + "dependencies": { + "http-errors": "^2.0.0", + "inflection": "^3.0.0", + "is-type-of": "^2.1.0", + "koa-compose": "^4.1.0", + "methods": "^1.1.2", + "path-to-regexp": "^1.9.0", + "urijs": "^1.19.11", + "utility": "^2.1.0" + }, + "engines": { + "node": ">= 18.19.0" + } + }, + "node_modules/@eggjs/utils": { + "version": "4.4.1", + "resolved": "https://registry.npmmirror.com/@eggjs/utils/-/utils-4.4.1.tgz", + "integrity": "sha512-/bu4ORRNSLSjrQQ4KDBgtyF92EmgmtdgjoXBIhBexxEqoV5Tg6kX4xdcG42cw+Cq3WBnUq/PzSthEPHbSlkQgg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@hapi/bourne": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/@hapi/bourne/-/bourne-3.0.0.tgz", + "integrity": "sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w==", + "license": "BSD-3-Clause" + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmmirror.com/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmmirror.com/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmmirror.com/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types/node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmmirror.com/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@jest/types/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/types/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/types/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/types/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/types/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/types/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@koa/cors": { + "version": "3.4.3", + "resolved": "https://registry.npmmirror.com/@koa/cors/-/cors-3.4.3.tgz", + "integrity": "sha512-WPXQUaAeAMVaLTEFpoq3T2O1C+FstkjJnDQqy95Ck1UdILajsRhu6mhJ8H2f4NFPRBoCNN+qywTJfq/gGki5mw==", + "license": "MIT", + "dependencies": { + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmmirror.com/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@one-ini/wasm": { + "version": "0.1.1", + "resolved": "https://registry.npmmirror.com/@one-ini/wasm/-/wasm-0.1.1.tgz", + "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.10", + "resolved": "https://registry.npmmirror.com/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tootallnate/once": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/@tootallnate/once/-/once-2.0.1.tgz", + "integrity": "sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmmirror.com/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmmirror.com/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmmirror.com/@types/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/bluebird": { + "version": "3.5.42", + "resolved": "https://registry.npmmirror.com/@types/bluebird/-/bluebird-3.5.42.tgz", + "integrity": "sha512-Jhy+MWRlro6UjVi578V/4ZGNfeCOcNCp0YaFNIUGFKlImowqwb1O/22wDVk3FDGMLqxdpOV3qQHD5fPEH4hK6A==", + "license": "MIT" + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmmirror.com/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmmirror.com/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/content-disposition": { + "version": "0.5.9", + "resolved": "https://registry.npmmirror.com/@types/content-disposition/-/content-disposition-0.5.9.tgz", + "integrity": "sha512-8uYXI3Gw35MhiVYhG3s295oihrxRyytcRHjSjqnqZVDDy/xcGBRny7+Xj1Wgfhv5QzRtN2hB2dVRBUX9XW3UcQ==", + "license": "MIT" + }, + "node_modules/@types/continuation-local-storage": { + "version": "3.2.7", + "resolved": "https://registry.npmmirror.com/@types/continuation-local-storage/-/continuation-local-storage-3.2.7.tgz", + "integrity": "sha512-Q7dPOymVpRG5Zpz90/o26+OAqOG2Sw+FED7uQmTrJNCF/JAPTylclZofMxZKd6W7g1BDPmT9/C/jX0ZcSNTQwQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmmirror.com/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/cookies": { + "version": "0.9.2", + "resolved": "https://registry.npmmirror.com/@types/cookies/-/cookies-0.9.2.tgz", + "integrity": "sha512-1AvkDdZM2dbyFybL4fxpuNCaWyv//0AwsuUk2DWeXyM1/5ZKm6W3z6mQi24RZ4l2ucY+bkSHzbDVpySqPGuV8A==", + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/express": "*", + "@types/keygrip": "*", + "@types/node": "*" + } + }, + "node_modules/@types/dargs": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/@types/dargs/-/dargs-5.1.0.tgz", + "integrity": "sha512-2cXlO8pz13kVYMp6Zgr8Z5DACbaGfoBp7svqZqPGcO+qG3LQLWdB5BzPPASj+UI447XxGmFHi6KjLgUB0fzucQ==", + "license": "MIT" + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmmirror.com/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/depd": { + "version": "1.1.37", + "resolved": "https://registry.npmmirror.com/@types/depd/-/depd-1.1.37.tgz", + "integrity": "sha512-PkEYFHnqDFgs+bJXJX0L8mq7sn3DWh+TP0m8BBJUJfZ2WcjRm7jd7Cq68jIJt+c31R1gX0cwSK1ZXOECvN97Rg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmmirror.com/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-assert": { + "version": "1.5.6", + "resolved": "https://registry.npmmirror.com/@types/http-assert/-/http-assert-1.5.6.tgz", + "integrity": "sha512-TTEwmtjgVbYAzZYWyeHPrrtWnfVkm8tQkP8P21uQifPgMRgjrow3XDEYqucuC8SKZJT7pUnhU/JymvjggxO9vw==", + "license": "MIT" + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmmirror.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/keygrip": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/@types/keygrip/-/keygrip-1.0.6.tgz", + "integrity": "sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ==", + "license": "MIT" + }, + "node_modules/@types/koa": { + "version": "2.15.2", + "resolved": "https://registry.npmmirror.com/@types/koa/-/koa-2.15.2.tgz", + "integrity": "sha512-CB+iyjjh1uS5N6/CKwXvw0qA7USMS2WVc4Tjf660yCjhdvqzNr8gdFcIawB41zGGptOQ+d1fnpaQWIIUXYxR3w==", + "license": "MIT", + "dependencies": { + "@types/accepts": "*", + "@types/content-disposition": "*", + "@types/cookies": "*", + "@types/http-assert": "*", + "@types/http-errors": "*", + "@types/keygrip": "*", + "@types/koa-compose": "*", + "@types/node": "*" + } + }, + "node_modules/@types/koa-compose": { + "version": "3.2.9", + "resolved": "https://registry.npmmirror.com/@types/koa-compose/-/koa-compose-3.2.9.tgz", + "integrity": "sha512-BroAZ9FTvPiCy0Pi8tjD1OfJ7bgU1gQf0eR6e1Vm+JJATy9eKOG3hQMFtMciMawiSOVnLMdmUOC46s7HBhSTsA==", + "license": "MIT", + "dependencies": { + "@types/koa": "*" + } + }, + "node_modules/@types/koa-router": { + "version": "7.4.9", + "resolved": "https://registry.npmmirror.com/@types/koa-router/-/koa-router-7.4.9.tgz", + "integrity": "sha512-YibkqJrFWwwF55YtZo6dORpMj/1zfweHHIZA/qpA+2JPcVbBopAvLjejTcO1ojWAJfzf1tPC+CrPnLDuVRkUEA==", + "license": "MIT", + "dependencies": { + "@types/koa": "*" + } + }, + "node_modules/@types/lodash": { + "version": "4.17.24", + "resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.24.tgz", + "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", + "license": "MIT" + }, + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmmirror.com/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.9.3", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-25.9.3.tgz", + "integrity": "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==", + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmmirror.com/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmmirror.com/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/sequelize": { + "version": "4.28.20", + "resolved": "https://registry.npmmirror.com/@types/sequelize/-/sequelize-4.28.20.tgz", + "integrity": "sha512-XaGOKRhdizC87hDgQ0u3btxzbejlF+t6Hhvkek1HyphqCI4y7zVBIVAGmuc4cWJqGpxusZ1RiBToHHnNK/Edlw==", + "license": "MIT", + "dependencies": { + "@types/bluebird": "*", + "@types/continuation-local-storage": "*", + "@types/lodash": "*", + "@types/validator": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@types/superagent": { + "version": "8.1.10", + "resolved": "https://registry.npmmirror.com/@types/superagent/-/superagent-8.1.10.tgz", + "integrity": "sha512-nbt4IWXABhW0jGmmpRzCFNlbmwCTzZ2gTUsNIr+X+ItdqPms+PAJZbWsNzpS2USqXjcoNLQcO6nXo60zcPQiIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/supertest": { + "version": "2.0.16", + "resolved": "https://registry.npmmirror.com/@types/supertest/-/supertest-2.0.16.tgz", + "integrity": "sha512-6c2ogktZ06tr2ENoZivgm7YnprnhYE4ZoXGMY+oA7IuAf17M8FWvujXZGmxLv8y0PTyts4x5A+erSwVUFA8XSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/superagent": "*" + } + }, + "node_modules/@types/validator": { + "version": "13.15.10", + "resolved": "https://registry.npmmirror.com/@types/validator/-/validator-13.15.10.tgz", + "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==", + "license": "MIT" + }, + "node_modules/@types/xml2js": { + "version": "0.4.14", + "resolved": "https://registry.npmmirror.com/@types/xml2js/-/xml2js-0.4.14.tgz", + "integrity": "sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yargs": { + "version": "12.0.20", + "resolved": "https://registry.npmmirror.com/@types/yargs/-/yargs-12.0.20.tgz", + "integrity": "sha512-MjOKUoDmNattFOBJvAZng7X9KXIKSGy6XHoXY9mASkKwCn35X4Ckh+Ugv1DewXZXrWYXMNtLiXhlCfWlpcAV+Q==", + "license": "MIT" + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmmirror.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmmirror.com/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/address": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/address/-/address-2.0.3.tgz", + "integrity": "sha512-XNAb/a6TCqou+TufU8/u11HCu9x1gYvOoxLwtlXgIqmkrYQADVv6ljyW2zwiPhHz9R1gItAWpuDrdJMmrOBFEA==", + "license": "MIT", + "engines": { + "node": ">= 16.0.0" + } + }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmmirror.com/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmmirror.com/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmmirror.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/archiver": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/archiver/-/archiver-8.0.0.tgz", + "integrity": "sha512-fV1orZfsnPn9BaSByR/qE67rJCLJEy2Ox5bq7nJh+jquWaNh6Sfec75kJ2T6PtdGUbPQlrVoSVCEOa5SdiTQ1g==", + "license": "MIT", + "dependencies": { + "async": "^3.2.4", + "buffer-crc32": "^1.0.0", + "is-stream": "^4.0.0", + "lazystream": "^1.0.0", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0", + "readdir-glob": "^3.0.0", + "tar-stream": "^3.0.0", + "zip-stream": "^7.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmmirror.com/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-back": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/array-back/-/array-back-4.0.2.tgz", + "integrity": "sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/array-differ": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/array-differ/-/array-differ-3.0.0.tgz", + "integrity": "sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmmirror.com/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmmirror.com/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/await-event": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/await-event/-/await-event-2.1.0.tgz", + "integrity": "sha512-hADm2dFnyugZnfFoJ0Oug2T9xAT2gFdvxZXXnWUOFsHL+VTCvj4Q7oBOinUYzvAFeAD5HN1YSrP78iS3/SQ7iQ==", + "license": "MIT" + }, + "node_modules/await-first": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/await-first/-/await-first-1.0.0.tgz", + "integrity": "sha512-SK20HicVu6lXvNM0nS1flurrs4/1NdhvccvEn52Gf+vpERZnnkKBnJvAQDsYkzJnsHs1bRNNKEiobEet7a/0TA==", + "license": "MIT", + "dependencies": { + "ee-first": "^1.1.1" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/b4a": { + "version": "1.8.1", + "resolved": "https://registry.npmmirror.com/b4a/-/b4a-1.8.1.tgz", + "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/bare-events": { + "version": "2.9.1", + "resolved": "https://registry.npmmirror.com/bare-events/-/bare-events-2.9.1.tgz", + "integrity": "sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==", + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.7.2", + "resolved": "https://registry.npmmirror.com/bare-fs/-/bare-fs-4.7.2.tgz", + "integrity": "sha512-aTvMFUWkBmjzKtEQMDGGDNF8bkfpD5N1b/FCwt7A3wrU4t1o/e/85Wzkluh6JlODCjqVESYCkQCdTXqZ9G7VFg==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.9.1", + "resolved": "https://registry.npmmirror.com/bare-os/-/bare-os-3.9.1.tgz", + "integrity": "sha512-6M5XjcnsygQNPMCMPXSK379xrJFiZ/AEMNBmFEmQW8d/789VQATvriyi5r0HYTL9TkQ26rn3kgdTG3aisbrXkQ==", + "license": "Apache-2.0", + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/bare-path/-/bare-path-3.0.1.tgz", + "integrity": "sha512-ghj2DSK/2e99a1anTVPCV4m4YIYtrbXhfM7V3D7XZLOTsybnYyaJloymGqssQc8l/or0UoDyRtNQkmkEF/ysgQ==", + "license": "Apache-2.0", + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.13.3", + "resolved": "https://registry.npmmirror.com/bare-stream/-/bare-stream-2.13.3.tgz", + "integrity": "sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.8.1", + "streamx": "^2.25.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-abort-controller": "*", + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.4.5", + "resolved": "https://registry.npmmirror.com/bare-url/-/bare-url-2.4.5.tgz", + "integrity": "sha512-K+y9xF1tN+CdPu4qWwr0QiK1Al07eFPGYK5M2pDXcmHdMdgC/tT/bpmMe1hrmRHaidKLkXrC+cRNYf3XVDUhSQ==", + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bcryptjs": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/bcryptjs/-/bcryptjs-3.0.3.tgz", + "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/black-hole-stream": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/black-hole-stream/-/black-hole-stream-0.0.1.tgz", + "integrity": "sha512-FQSWhFQZmddoqWkwPMFeR5hJo9waZE796MuO7b0poStaPrm0Qr2em9FT4/TF1+0rOA1dRFabX+88rdX8YHxDTA==", + "license": "MIT" + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmmirror.com/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true, + "license": "ISC" + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmmirror.com/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/byte": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/byte/-/byte-2.0.0.tgz", + "integrity": "sha512-rNiK8YxOMvquToaBubKxA10sjRIZ/taDqtc/1jLQA4X7aNDlA1XGx4Ciml3YxL8DskFz1XX3WFskSp0peKYSKg==", + "license": "MIT", + "dependencies": { + "debug": "^3.1.0", + "long": "^4.0.0", + "utility": "^1.13.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/byte/node_modules/utility": { + "version": "1.18.0", + "resolved": "https://registry.npmmirror.com/utility/-/utility-1.18.0.tgz", + "integrity": "sha512-PYxZDA+6QtvRvm//++aGdmKG/cI07jNwbROz0Ql+VzFV1+Z0Dy55NI4zZ7RHc9KKpBePNFwoErqIuqQv/cjiTA==", + "license": "MIT", + "dependencies": { + "copy-to": "^2.0.1", + "escape-html": "^1.0.3", + "mkdirp": "^0.5.1", + "mz": "^2.7.0", + "unescape": "^1.0.1" + }, + "engines": { + "node": ">= 0.12.0" + } + }, + "node_modules/bytes": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/bytes/-/bytes-2.2.0.tgz", + "integrity": "sha512-zGRpnr2l5w/s8PxkrquUJoVeR06KvqPelrYqiSyQV7QEBqCYivpb6UzXYWC6JDBVtNFOT0rzJRFhkfJgxzmILA==", + "license": "MIT" + }, + "node_modules/c8": { + "version": "10.1.3", + "resolved": "https://registry.npmmirror.com/c8/-/c8-10.1.3.tgz", + "integrity": "sha512-LvcyrOAaOnrrlMpW22n690PUvxiq4Uf9WMhQwNJ9vgagkL/ph1+D4uvjvDA5XCbykrc0sx+ay6pVi9YZ1GnhyA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.1", + "@istanbuljs/schema": "^0.1.3", + "find-up": "^5.0.0", + "foreground-child": "^3.1.1", + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.1.6", + "test-exclude": "^7.0.1", + "v8-to-istanbul": "^9.0.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1" + }, + "bin": { + "c8": "bin/c8.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "monocart-coverage-reports": "^2" + }, + "peerDependenciesMeta": { + "monocart-coverage-reports": { + "optional": true + } + } + }, + "node_modules/cache-content-type": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/cache-content-type/-/cache-content-type-1.0.1.tgz", + "integrity": "sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==", + "license": "MIT", + "dependencies": { + "mime-types": "^2.1.18", + "ylru": "^1.2.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/camel-case": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/camel-case/-/camel-case-3.0.0.tgz", + "integrity": "sha512-+MbKztAYHXPr1jNTSKQF52VpcFjwY5RkR7fxksV8Doo4KAYc5Fl4UJRgthBbTmEx8C54DqahhbLJkDwjI3PI/w==", + "license": "MIT", + "dependencies": { + "no-case": "^2.2.0", + "upper-case": "^1.1.1" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmmirror.com/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/cfork": { + "version": "1.11.0", + "resolved": "https://registry.npmmirror.com/cfork/-/cfork-1.11.0.tgz", + "integrity": "sha512-pYyWhuXPq5OoaVPaQZXFf243oQ/+eCXu7ufOZxqi8HFI7TcTxbsi1/16CpT75dSuk78ErMJQlTkpzz9O82G/Ig==", + "license": "MIT", + "dependencies": { + "utility": "^1.12.0" + }, + "engines": { + "node": ">= 0.12.0" + } + }, + "node_modules/cfork/node_modules/utility": { + "version": "1.18.0", + "resolved": "https://registry.npmmirror.com/utility/-/utility-1.18.0.tgz", + "integrity": "sha512-PYxZDA+6QtvRvm//++aGdmKG/cI07jNwbROz0Ql+VzFV1+Z0Dy55NI4zZ7RHc9KKpBePNFwoErqIuqQv/cjiTA==", + "license": "MIT", + "dependencies": { + "copy-to": "^2.0.1", + "escape-html": "^1.0.3", + "mkdirp": "^0.5.1", + "mz": "^2.7.0", + "unescape": "^1.0.1" + }, + "engines": { + "node": ">= 0.12.0" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chan": { + "version": "0.6.1", + "resolved": "https://registry.npmmirror.com/chan/-/chan-0.6.1.tgz", + "integrity": "sha512-/TdBP2UhbBmw7qnqkzo9Mk4rzvwRv4dlNPXFerqWy90T8oBspKagJNZxrDbExKHhx9uXXHjo3f9mHgs9iKO3nQ==", + "license": "MIT" + }, + "node_modules/change-case": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/change-case/-/change-case-3.1.0.tgz", + "integrity": "sha512-2AZp7uJZbYEzRPsFoa+ijKdvp9zsrnnt6+yFokfwEpeJm0xuJDVoxiRCAaTzyJND8GJkofo2IcKWaUZ/OECVzw==", + "license": "MIT", + "dependencies": { + "camel-case": "^3.0.0", + "constant-case": "^2.0.0", + "dot-case": "^2.1.0", + "header-case": "^1.0.0", + "is-lower-case": "^1.1.0", + "is-upper-case": "^1.1.0", + "lower-case": "^1.1.1", + "lower-case-first": "^1.0.0", + "no-case": "^2.3.2", + "param-case": "^2.1.0", + "pascal-case": "^2.0.0", + "path-case": "^2.1.0", + "sentence-case": "^2.1.0", + "snake-case": "^2.1.0", + "swap-case": "^1.1.0", + "title-case": "^2.1.0", + "upper-case": "^1.1.1", + "upper-case-first": "^1.1.0" + } + }, + "node_modules/chardet": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmmirror.com/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/circular-json-for-egg": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/circular-json-for-egg/-/circular-json-for-egg-1.0.0.tgz", + "integrity": "sha512-BzMR1dg0+YqcFoMETHq0gFeQNNKliXI1Oe+C0nx/4npLaohsR7/Oj3UFht65MLwF7zs6x13gOr+f4+JeYni6vw==", + "license": "MIT" + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmmirror.com/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10" + } + }, + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmmirror.com/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/cluster-client": { + "version": "3.7.0", + "resolved": "https://registry.npmmirror.com/cluster-client/-/cluster-client-3.7.0.tgz", + "integrity": "sha512-n0pLGPWlAjaGDJrLKTenjF1qoHbDjuYFlvX4UBoWCt9NjUTZGQhfNafF6Gw4Rj7oJqqdBGrdiIdHSvtOMQX5AA==", + "license": "MIT", + "dependencies": { + "byte": "^2.0.0", + "co": "^4.6.0", + "egg-logger": "^3.5.0", + "is-type-of": "^1.4.0", + "json-stringify-safe": "^5.0.1", + "long": "^4.0.0", + "sdk-base": "^4.2.1", + "serialize-json": "^1.0.3", + "tcp-base": "^3.2.0", + "utility": "^2.1.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/cluster-client/node_modules/is-type-of": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/is-type-of/-/is-type-of-1.4.0.tgz", + "integrity": "sha512-EddYllaovi5ysMLMEN7yzHEKh8A850cZ7pykrY1aNRQGn/CDjRDE9qEWbIdt7xGEVJmjBXzU/fNnC4ABTm8tEQ==", + "license": "MIT", + "dependencies": { + "core-util-is": "^1.0.2", + "is-class-hotfix": "~0.0.6", + "isstream": "~0.1.2" + } + }, + "node_modules/cluster-reload": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/cluster-reload/-/cluster-reload-1.1.0.tgz", + "integrity": "sha512-lY3n9ohbJvDsDoaGGx/ER6eqaDKgVVmYjgoSL+XPxI0NHhkr4Ag60RSqVD1B9yZJz/q3FjB94bLfnuGuFuh7aw==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmmirror.com/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/co-body": { + "version": "6.2.0", + "resolved": "https://registry.npmmirror.com/co-body/-/co-body-6.2.0.tgz", + "integrity": "sha512-Kbpv2Yd1NdL1V/V4cwLVxraHDV6K8ayohr2rmH0J87Er8+zJjcTa6dAn9QMPC9CRgU8+aNajKbSf1TzDB1yKPA==", + "license": "MIT", + "dependencies": { + "@hapi/bourne": "^3.0.0", + "inflation": "^2.0.0", + "qs": "^6.5.2", + "raw-body": "^2.3.3", + "type-is": "^1.6.16" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/co-busboy": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/co-busboy/-/co-busboy-2.0.2.tgz", + "integrity": "sha512-xntc6PdplMQ9M92T5aKWRu8OBh4CbQcLHPnf2K3c+rD2xhAJcIjC5liblUquxDGTErsnHadMOsCcd6xhnpPAFA==", + "license": "MIT", + "dependencies": { + "black-hole-stream": "~0.0.1", + "busboy": "^1.6.0", + "chan": "^0.6.1", + "inflation": "^2.0.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmmirror.com/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/coffee": { + "version": "5.5.1", + "resolved": "https://registry.npmmirror.com/coffee/-/coffee-5.5.1.tgz", + "integrity": "sha512-ZKt9b/Iq0jhe7tYpDMXJggx8l/+YIcQFi2C+LvJRQ7lUSJnzayir1BGbsoHELKKyV+zevWMCsfmGAI1fREyRbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^6.0.5", + "debug": "^4.1.0", + "is-type-of": "^1.2.1" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/coffee/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/coffee/node_modules/is-type-of": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/is-type-of/-/is-type-of-1.4.0.tgz", + "integrity": "sha512-EddYllaovi5ysMLMEN7yzHEKh8A850cZ7pykrY1aNRQGn/CDjRDE9qEWbIdt7xGEVJmjBXzU/fNnC4ABTm8tEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "^1.0.2", + "is-class-hotfix": "~0.0.6", + "isstream": "~0.1.2" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/command-line-usage": { + "version": "6.1.3", + "resolved": "https://registry.npmmirror.com/command-line-usage/-/command-line-usage-6.1.3.tgz", + "integrity": "sha512-sH5ZSPr+7UStsloltmDh7Ce5fb8XPlHyoPzTpyyMuYCtervL65+ubVZ6Q61cFtFl62UyJlc8/JwERRbAFPUqgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-back": "^4.0.2", + "chalk": "^2.4.2", + "table-layout": "^1.0.2", + "typical": "^5.2.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmmirror.com/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/common-bin": { + "version": "2.9.2", + "resolved": "https://registry.npmmirror.com/common-bin/-/common-bin-2.9.2.tgz", + "integrity": "sha512-fw6YBX8dr4wgMCHqcOR5eIqWZxoZa6+0JAiqjZuOZFCh/pz9hW6EY2EGFc3QpHwp/DlLuw/du/Sr2InhrfAYSQ==", + "license": "MIT", + "dependencies": { + "@types/dargs": "^5.1.0", + "@types/node": "^10.12.18", + "@types/yargs": "^12.0.4", + "chalk": "^2.4.1", + "change-case": "^3.0.2", + "co": "^4.6.0", + "dargs": "^6.0.0", + "debug": "^4.1.0", + "is-type-of": "^1.2.1", + "semver": "^5.5.1", + "yargs": "^13.3.0", + "yargs-parser": "^13.1.2" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/common-bin/node_modules/@types/node": { + "version": "10.17.60", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-10.17.60.tgz", + "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==", + "license": "MIT" + }, + "node_modules/common-bin/node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/common-bin/node_modules/cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "license": "ISC", + "dependencies": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "node_modules/common-bin/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/common-bin/node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/common-bin/node_modules/emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "license": "MIT" + }, + "node_modules/common-bin/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "license": "MIT", + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/common-bin/node_modules/is-type-of": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/is-type-of/-/is-type-of-1.4.0.tgz", + "integrity": "sha512-EddYllaovi5ysMLMEN7yzHEKh8A850cZ7pykrY1aNRQGn/CDjRDE9qEWbIdt7xGEVJmjBXzU/fNnC4ABTm8tEQ==", + "license": "MIT", + "dependencies": { + "core-util-is": "^1.0.2", + "is-class-hotfix": "~0.0.6", + "isstream": "~0.1.2" + } + }, + "node_modules/common-bin/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "license": "MIT", + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/common-bin/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/common-bin/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/common-bin/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/common-bin/node_modules/string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/common-bin/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/common-bin/node_modules/wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/common-bin/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, + "node_modules/common-bin/node_modules/yargs": { + "version": "13.3.2", + "resolved": "https://registry.npmmirror.com/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "license": "MIT", + "dependencies": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" + } + }, + "node_modules/common-bin/node_modules/yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/compress-commons": { + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/compress-commons/-/compress-commons-7.0.1.tgz", + "integrity": "sha512-g0S8KAD8qf4+V//pr3BfB1aBnARLXNz2Gx+jmHU0LEriUuoQUOPOulVquHKTJ8+EAIIO7fhseNDr9wK5Q9FKBQ==", + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "crc32-stream": "^7.0.1", + "is-stream": "^4.0.0", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmmirror.com/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmmirror.com/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/concat-stream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/concat-stream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/concat-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/concat-stream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmmirror.com/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/constant-case": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/constant-case/-/constant-case-2.0.0.tgz", + "integrity": "sha512-eS0N9WwmjTqrOmR3o83F5vW8Z+9R1HnVz3xmzT2PMFug9ly+Au/fxRWlEBSb6LcZwspSsEn9Xs1uw9YgzAg1EQ==", + "license": "MIT", + "dependencies": { + "snake-case": "^2.1.0", + "upper-case": "^1.1.1" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmmirror.com/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookies": { + "version": "0.9.1", + "resolved": "https://registry.npmmirror.com/cookies/-/cookies-0.9.1.tgz", + "integrity": "sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "keygrip": "~1.1.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/copy-to": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/copy-to/-/copy-to-2.0.1.tgz", + "integrity": "sha512-3DdaFaU/Zf1AnpLiFDeNCD4TOWe3Zl2RZaTzUvWiIk5ERzcCodOE20Vqq4fzCbNoHURFHT4/us/Lfq+S2zyY4w==", + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/crc": { + "version": "3.8.0", + "resolved": "https://registry.npmmirror.com/crc/-/crc-3.8.0.tgz", + "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", + "license": "MIT", + "dependencies": { + "buffer": "^5.1.0" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/crc32-stream/-/crc32-stream-7.0.1.tgz", + "integrity": "sha512-IBWsY8xznyQrcHn8h4bC8/4ErNke5elzgG8GcqF4RFPw6aHkWWRc7Tgw6upjaTX/CT/yQgqYENkxYsTYN+hW2g==", + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cron-parser": { + "version": "2.18.0", + "resolved": "https://registry.npmmirror.com/cron-parser/-/cron-parser-2.18.0.tgz", + "integrity": "sha512-s4odpheTyydAbTBQepsqd2rNWGa2iV3cyo8g7zbI2QQYGLVsfbhmwukayS1XHppe02Oy1fg7mg6xoaraVJeEcg==", + "license": "MIT", + "dependencies": { + "is-nan": "^1.3.0", + "moment-timezone": "^0.5.31" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/cross-spawn": { + "version": "6.0.6", + "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/csrf": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/csrf/-/csrf-3.1.0.tgz", + "integrity": "sha512-uTqEnCvWRk042asU6JtapDTcJeeailFy4ydOQS28bj1hcLnYRiqi8SsD2jS412AY1I/4qdOwWZun774iqywf9w==", + "license": "MIT", + "dependencies": { + "rndm": "1.2.0", + "tsscmp": "1.0.6", + "uid-safe": "2.1.5" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cssfilter": { + "version": "0.0.10", + "resolved": "https://registry.npmmirror.com/cssfilter/-/cssfilter-0.0.10.tgz", + "integrity": "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==", + "license": "MIT" + }, + "node_modules/dargs": { + "version": "6.1.0", + "resolved": "https://registry.npmmirror.com/dargs/-/dargs-6.1.0.tgz", + "integrity": "sha512-5dVBvpBLBnPwSsYXqfybFyehMmC/EenKEcf23AhCTgTf48JFBbmJKqoZBsERDnjL0FyiVTYWdFsRfTLHxLyKdQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmmirror.com/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/dayjs": { + "version": "1.11.21", + "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.21.tgz", + "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", + "license": "MIT" + }, + "node_modules/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==", + "license": "MIT" + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmmirror.com/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-user-agent": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/default-user-agent/-/default-user-agent-1.0.0.tgz", + "integrity": "sha512-bDF7bg6OSNcSwFWPu4zYKpVkJZQYVrAANMYB8bc9Szem1D0yKdm4sa/rOCs2aC9+2GMqQ7KnwtZRvDhmLF0dXw==", + "license": "MIT", + "dependencies": { + "os-name": "~1.0.3" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT" + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-port": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/detect-port/-/detect-port-2.1.0.tgz", + "integrity": "sha512-epZuWb/6Q62L+nDHJc/hQAqf8pylsqgk3BpZXVBx1CDnr3nkrVNn73Uu1rXcFzkNcc+hkP3whuOg7JZYaQB65Q==", + "license": "MIT", + "dependencies": { + "address": "^2.0.1" + }, + "bin": { + "detect": "dist/commonjs/bin/detect-port.js", + "detect-port": "dist/commonjs/bin/detect-port.js" + }, + "engines": { + "node": ">= 16.0.0" + } + }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/diff": { + "version": "5.2.2", + "resolved": "https://registry.npmmirror.com/diff/-/diff-5.2.2.tgz", + "integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/digest-header": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/digest-header/-/digest-header-1.1.0.tgz", + "integrity": "sha512-glXVh42vz40yZb9Cq2oMOt70FIoWiv+vxNvdKdU8CwjLad25qHM3trLxhl9bVjdr6WaslIXhWpn0NO8T/67Qjg==", + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dot-case": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/dot-case/-/dot-case-2.1.1.tgz", + "integrity": "sha512-HnM6ZlFqcajLsyudHq7LeeLDr2rFAVYtDv/hV5qchQEidSck8j9OPUsXY9KwJv/lHMtYlX4DjRQqwFYa+0r8Ug==", + "license": "MIT", + "dependencies": { + "no-case": "^2.2.0" + } + }, + "node_modules/dot-prop": { + "version": "4.2.1", + "resolved": "https://registry.npmmirror.com/dot-prop/-/dot-prop-4.2.1.tgz", + "integrity": "sha512-l0p4+mIuJIua0mhxGoh4a+iNL9bmeK5DvnSVQa6T0OhrVmaEa1XScX5Etc673FePCJOArq/4Pa2cLGODUWTPOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-obj": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/dottie": { + "version": "2.0.6", + "resolved": "https://registry.npmmirror.com/dottie/-/dottie-2.0.6.tgz", + "integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==", + "license": "MIT" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmmirror.com/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "license": "MIT" + }, + "node_modules/duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmmirror.com/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "node_modules/duplexify/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/duplexify/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/duplexify/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/duplexify/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmmirror.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/editorconfig": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/editorconfig/-/editorconfig-1.0.7.tgz", + "integrity": "sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@one-ini/wasm": "0.1.1", + "commander": "^10.0.0", + "minimatch": "^9.0.1", + "semver": "^7.5.3" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/editorconfig/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmmirror.com/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/editorconfig/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/editorconfig/node_modules/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/egg": { + "version": "3.34.0", + "resolved": "https://registry.npmmirror.com/egg/-/egg-3.34.0.tgz", + "integrity": "sha512-Zs4gwrp/iY7Vkdfm4/UTy1cZrEcWuK9ZiBix30Sco5ennR6k7TcsssYfIS40lgVvI7U4HwcR9HOmhYML3A0A3w==", + "license": "MIT", + "dependencies": { + "@types/accepts": "^1.3.5", + "@types/koa": "^2.13.5", + "@types/koa-router": "^7.4.4", + "accepts": "^1.3.8", + "agentkeepalive": "^4.2.1", + "cache-content-type": "^1.0.1", + "circular-json-for-egg": "^1.0.0", + "cluster-client": "^3.3.0", + "delegates": "^1.0.0", + "egg-cluster": "^2.0.0", + "egg-cookies": "^2.6.1", + "egg-core": "^5.4.0", + "egg-development": "^3.0.0", + "egg-errors": "^2.3.1", + "egg-i18n": "^2.1.1", + "egg-jsonp": "^2.0.0", + "egg-logger": "^3.0.1", + "egg-logrotator": "^3.1.0", + "egg-multipart": "^3.1.0", + "egg-onerror": "^2.1.1", + "egg-schedule": "^4.0.0", + "egg-security": "^3.0.0", + "egg-session": "^3.3.0", + "egg-static": "^2.2.0", + "egg-view": "^2.1.3", + "egg-watcher": "^3.1.1", + "extend2": "^1.0.1", + "graceful": "^1.1.0", + "humanize-ms": "^1.2.1", + "is-type-of": "^2.1.0", + "koa-bodyparser": "^4.4.1", + "koa-is-json": "^1.0.0", + "koa-override": "^3.0.0", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "onelogger": "^1.0.0", + "sendmessage": "^2.0.0", + "urllib": "^2.33.0", + "urllib-next": "npm:urllib@^3.27.1", + "urllib4": "npm:urllib@^4.5.0", + "utility": "^2.1.0", + "ylru": "^1.3.2" + }, + "engines": { + "node": ">= 14.20.0" + } + }, + "node_modules/egg-bin": { + "version": "6.13.0", + "resolved": "https://registry.npmmirror.com/egg-bin/-/egg-bin-6.13.0.tgz", + "integrity": "sha512-dlztm1Y5DnSkofCeTaMf+O7kARPDGFbcbLDJ5cc029qRRbhStM7AsBBgLc+EEZuumZupejDHdvxraXRVut1ZTA==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@artus-cli/artus-cli": "^0.2.9", + "@artus-cli/plugin-autocomplete": "^0.1.1", + "@artus-cli/plugin-version": "^1.0.1", + "@eggjs/utils": "^4.0.2", + "c8": "^10.0.0", + "detect-port": "^2.0.0", + "egg-ts-helper": "^2.1.0", + "globby": "^11.1.0", + "jest-changed-files": "^29.4.2", + "mocha": "^10.2.0", + "mochawesome-with-mocha": "^7.1.3", + "runscript": "^2.0.0", + "ts-node": "^10.9.2", + "tsconfig-paths": "^4.1.2" + }, + "bin": { + "egg-bin": "dist/bin/cli.js" + }, + "engines": { + "node": ">= 16.19.0" + }, + "peerDependencies": { + "egg-mock": ">=5.10.2" + }, + "peerDependenciesMeta": { + "egg-mock": { + "optional": true + } + } + }, + "node_modules/egg-cluster": { + "version": "2.5.0", + "resolved": "https://registry.npmmirror.com/egg-cluster/-/egg-cluster-2.5.0.tgz", + "integrity": "sha512-5ySrghDOL6eWvjWpyGKzmLKPkhhD590TWEtdEGBvKRTY/ATquL5/AuQMG5qHOv9dbo7lCxTi9elyVNuJGe0qsA==", + "license": "MIT", + "dependencies": { + "await-event": "^2.1.0", + "cfork": "^1.7.1", + "cluster-reload": "^1.0.2", + "detect-port": "^2.0.1", + "egg-logger": "^3.3.0", + "egg-utils": "^2.4.1", + "get-ready": "^2.0.1", + "graceful-process": "^1.2.0", + "is-type-of": "^1.2.1", + "ps-tree": "^1.2.0", + "sendmessage": "^1.1.0", + "terminal-link": "^2.1.1", + "utility": "^1.15.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/egg-cluster/node_modules/is-type-of": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/is-type-of/-/is-type-of-1.4.0.tgz", + "integrity": "sha512-EddYllaovi5ysMLMEN7yzHEKh8A850cZ7pykrY1aNRQGn/CDjRDE9qEWbIdt7xGEVJmjBXzU/fNnC4ABTm8tEQ==", + "license": "MIT", + "dependencies": { + "core-util-is": "^1.0.2", + "is-class-hotfix": "~0.0.6", + "isstream": "~0.1.2" + } + }, + "node_modules/egg-cluster/node_modules/sendmessage": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/sendmessage/-/sendmessage-1.1.0.tgz", + "integrity": "sha512-riI/U2etmtMKaVPe7zMnr++eG46F191F6Zycwrkm+/sEiHzucNXJETPJ5dQryNaDuHTpYdzmesEJQ2le1DxlMQ==", + "license": "MIT", + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/egg-cluster/node_modules/utility": { + "version": "1.18.0", + "resolved": "https://registry.npmmirror.com/utility/-/utility-1.18.0.tgz", + "integrity": "sha512-PYxZDA+6QtvRvm//++aGdmKG/cI07jNwbROz0Ql+VzFV1+Z0Dy55NI4zZ7RHc9KKpBePNFwoErqIuqQv/cjiTA==", + "license": "MIT", + "dependencies": { + "copy-to": "^2.0.1", + "escape-html": "^1.0.3", + "mkdirp": "^0.5.1", + "mz": "^2.7.0", + "unescape": "^1.0.1" + }, + "engines": { + "node": ">= 0.12.0" + } + }, + "node_modules/egg-cookies": { + "version": "2.10.1", + "resolved": "https://registry.npmmirror.com/egg-cookies/-/egg-cookies-2.10.1.tgz", + "integrity": "sha512-fjlGF9jIu0GmU0bA+ZGOoz8OhbUECy18Y69wgpt8secmtaFzSbKay3DhKA13evqUDoiag5vQXQaN+N7Pp8vFsA==", + "license": "MIT", + "dependencies": { + "scmp": "^2.0.0", + "should-send-same-site-none": "^2.0.2", + "utility": "^1.14.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/egg-cookies/node_modules/utility": { + "version": "1.18.0", + "resolved": "https://registry.npmmirror.com/utility/-/utility-1.18.0.tgz", + "integrity": "sha512-PYxZDA+6QtvRvm//++aGdmKG/cI07jNwbROz0Ql+VzFV1+Z0Dy55NI4zZ7RHc9KKpBePNFwoErqIuqQv/cjiTA==", + "license": "MIT", + "dependencies": { + "copy-to": "^2.0.1", + "escape-html": "^1.0.3", + "mkdirp": "^0.5.1", + "mz": "^2.7.0", + "unescape": "^1.0.1" + }, + "engines": { + "node": ">= 0.12.0" + } + }, + "node_modules/egg-core": { + "version": "5.5.1", + "resolved": "https://registry.npmmirror.com/egg-core/-/egg-core-5.5.1.tgz", + "integrity": "sha512-6oLO6tLv40vraka+OxXHwaLeXHt/0XRuJlY7zlsz9tS6Z42xDGgjpYXUC2hsFEWQD+SdshC4kTEXllHvplxsqQ==", + "license": "MIT", + "dependencies": { + "@eggjs/router": "^2.2.0", + "@types/depd": "^1.1.32", + "@types/koa": "^2.13.5", + "co": "^4.6.0", + "debug": "^4.1.1", + "depd": "^2.0.0", + "egg-logger": "^3.1.0", + "egg-path-matching": "^1.2.0", + "extend2": "^1.0.0", + "gals": "^1.0.1", + "get-ready": "^2.0.1", + "globby": "^11.0.2", + "is-type-of": "^1.2.1", + "koa": "^2.14.0", + "koa-convert": "^1.2.0", + "node-homedir": "^1.1.1", + "ready-callback": "^3.0.0", + "tsconfig-paths": "^4.1.1", + "utility": "^1.16.1" + }, + "engines": { + "node": ">= 14.19.0" + } + }, + "node_modules/egg-core/node_modules/@eggjs/router": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/@eggjs/router/-/router-2.2.0.tgz", + "integrity": "sha512-EKnOrZ/pHMFQ1gAj3z0Yn1UnGYH/wSsc2jk06H6RrZp6ty9H6RWyP782e7FT8WL3pAevRY+SAQoH7aPh1SDSVA==", + "license": "MIT", + "dependencies": { + "co": "^4.6.0", + "http-errors": "^1.3.1", + "inflection": "^1.12.0", + "koa-compose": "^3.0.0", + "koa-convert": "^1.2.0", + "methods": "^1.0.1", + "path-to-regexp": "^1.9.0", + "urijs": "^1.19.0", + "utility": "^1.15.0" + }, + "engines": { + "node": ">= 8.5.0" + } + }, + "node_modules/egg-core/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/egg-core/node_modules/http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/egg-core/node_modules/http-errors/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/egg-core/node_modules/inflection": { + "version": "1.13.4", + "resolved": "https://registry.npmmirror.com/inflection/-/inflection-1.13.4.tgz", + "integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==", + "engines": [ + "node >= 0.4.0" + ], + "license": "MIT" + }, + "node_modules/egg-core/node_modules/is-type-of": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/is-type-of/-/is-type-of-1.4.0.tgz", + "integrity": "sha512-EddYllaovi5ysMLMEN7yzHEKh8A850cZ7pykrY1aNRQGn/CDjRDE9qEWbIdt7xGEVJmjBXzU/fNnC4ABTm8tEQ==", + "license": "MIT", + "dependencies": { + "core-util-is": "^1.0.2", + "is-class-hotfix": "~0.0.6", + "isstream": "~0.1.2" + } + }, + "node_modules/egg-core/node_modules/koa-compose": { + "version": "3.2.1", + "resolved": "https://registry.npmmirror.com/koa-compose/-/koa-compose-3.2.1.tgz", + "integrity": "sha512-8gen2cvKHIZ35eDEik5WOo8zbVp9t4cP8p4hW4uE55waxolLRexKKrqfCpwhGVppnB40jWeF8bZeTVg99eZgPw==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.1.0" + } + }, + "node_modules/egg-core/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/egg-core/node_modules/utility": { + "version": "1.18.0", + "resolved": "https://registry.npmmirror.com/utility/-/utility-1.18.0.tgz", + "integrity": "sha512-PYxZDA+6QtvRvm//++aGdmKG/cI07jNwbROz0Ql+VzFV1+Z0Dy55NI4zZ7RHc9KKpBePNFwoErqIuqQv/cjiTA==", + "license": "MIT", + "dependencies": { + "copy-to": "^2.0.1", + "escape-html": "^1.0.3", + "mkdirp": "^0.5.1", + "mz": "^2.7.0", + "unescape": "^1.0.1" + }, + "engines": { + "node": ">= 0.12.0" + } + }, + "node_modules/egg-cors": { + "version": "2.2.4", + "resolved": "https://registry.npmmirror.com/egg-cors/-/egg-cors-2.2.4.tgz", + "integrity": "sha512-6yc3pEbpZVLiCRdvR/Y+fT1sIO8/IldA31xQJeQDtgPrPWS99LXGyxWkszSovUS77yOpciSlcnpNoY9HAUP9hA==", + "dependencies": { + "@koa/cors": "^3.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/egg-development": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/egg-development/-/egg-development-3.0.2.tgz", + "integrity": "sha512-sxJzgw+EwfaZhrVcoGniLBT3B3Xd99VsNZnpLKrYp35upaLqw+tCB+kaTYmXZYqXv1Zm2c6RoGCvJbyqr+Mkjw==", + "license": "MIT", + "dependencies": { + "debounce": "^1.1.0", + "multimatch": "^5.0.0", + "utility": "^2.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/egg-errors": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/egg-errors/-/egg-errors-2.3.2.tgz", + "integrity": "sha512-E+Sx7IBVrfRyHSjFXaq4sCZ3Uk3ka9PYySaQ8VbRZmLEt9ENBCD99yVzLIeWUH2QfzvkrjY9El1eHmLeRx7cfw==", + "license": "MIT", + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/egg-i18n": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/egg-i18n/-/egg-i18n-2.1.1.tgz", + "integrity": "sha512-rpKP2nrUzeTOkjQObvlrLbb/BZAMtP7zeoGggwJLN9+zKbFKVkeF6Q0BrpuucDSekD+2oHQz7fC3w/5eQY1g8w==", + "license": "MIT", + "dependencies": { + "debug": "^3.1.0", + "koa-locales": "^1.11.0" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/egg-jsonp": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/egg-jsonp/-/egg-jsonp-2.0.0.tgz", + "integrity": "sha512-dno7BXAvSFO0WTb3GJD0aci9MFlaQhdgvEznwARam75GGJdLpg1K0XQq88/dEvYWHxEHa+iGIOXcNx5NOyyUHw==", + "license": "MIT", + "dependencies": { + "is-type-of": "^1.2.0", + "jsonp-body": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/egg-jsonp/node_modules/is-type-of": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/is-type-of/-/is-type-of-1.4.0.tgz", + "integrity": "sha512-EddYllaovi5ysMLMEN7yzHEKh8A850cZ7pykrY1aNRQGn/CDjRDE9qEWbIdt7xGEVJmjBXzU/fNnC4ABTm8tEQ==", + "license": "MIT", + "dependencies": { + "core-util-is": "^1.0.2", + "is-class-hotfix": "~0.0.6", + "isstream": "~0.1.2" + } + }, + "node_modules/egg-logger": { + "version": "3.6.1", + "resolved": "https://registry.npmmirror.com/egg-logger/-/egg-logger-3.6.1.tgz", + "integrity": "sha512-lGiEumARJAT7NiwMm577NFiOEBMX9FlZzfuBsLLS6eu14R9udcTI1IzXLGJ63SrCmFIXB+zsWLQv5U11e8fq0Q==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "circular-json-for-egg": "^1.0.0", + "depd": "^2.0.0", + "egg-errors": "^2.3.1", + "iconv-lite": "^0.6.3", + "utility": "^2.1.0" + }, + "engines": { + "node": ">=14.17.0" + } + }, + "node_modules/egg-logger/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/egg-logger/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/egg-logger/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/egg-logger/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/egg-logger/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/egg-logger/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/egg-logrotator": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/egg-logrotator/-/egg-logrotator-3.2.0.tgz", + "integrity": "sha512-2KYN4wwBd15FYbNtLxL7IoIkFL7JuHHA3eXWZd5zVKfWgeoy4J+oTnCLDixW1UhVIdehL0q1oQxEldWm5af1Eg==", + "license": "MIT", + "dependencies": { + "moment": "^2.24.0", + "mz": "^2.7.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/egg-mock": { + "version": "5.15.2", + "resolved": "https://registry.npmmirror.com/egg-mock/-/egg-mock-5.15.2.tgz", + "integrity": "sha512-8g+GzGcjfPaM/XwGaoej27wfIw7qeKR3tqYZPB3mIRWMzChYnyI2Bb+5qud23Ealj0raNamE+85ZsBKI2kq8nw==", + "dev": true, + "dependencies": { + "@eggjs/utils": "^4.0.2", + "@types/supertest": "^2.0.7", + "await-event": "^2.1.0", + "co": "^4.6.0", + "coffee": "^5.2.1", + "detect-port": "^2.0.1", + "egg-logger": "^3.5.0", + "extend2": "^1.0.0", + "get-ready": "^3.1.0", + "globby": "^11.1.0", + "is-type-of": "^2.2.0", + "merge-descriptors": "^1.0.1", + "methods": "^1.1.2", + "mm": "^3.0.2", + "sdk-base": "^4.2.1", + "supertest": "^6.2.4", + "urllib": "3" + }, + "engines": { + "node": ">= 14.18.0" + }, + "peerDependencies": { + "egg": "^3.12.0", + "mocha": "^10.2.0", + "urllib": "^3 || ^4" + } + }, + "node_modules/egg-mock/node_modules/get-ready": { + "version": "3.4.0", + "resolved": "https://registry.npmmirror.com/get-ready/-/get-ready-3.4.0.tgz", + "integrity": "sha512-D7N1WED4f5rQUveyl19GxfJupMeR+y106EVK6na/zI+w34FcTAShvetX0X2k61x+lJXCEiOnGHk0xro6YRyrjQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16.13.0" + } + }, + "node_modules/egg-mock/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/egg-mock/node_modules/urllib": { + "version": "3.27.3", + "resolved": "https://registry.npmmirror.com/urllib/-/urllib-3.27.3.tgz", + "integrity": "sha512-24ht/hHAkhWXbqSCM499B8gFPXaf4aFQJNqJH5pMsvfyo+aM3zKuQ/uiZ/mdz41Nnw6ItuiqMVMK5VDAlAHLlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-user-agent": "^1.0.0", + "digest-header": "^1.0.0", + "form-data-encoder": "^1.7.2", + "formdata-node": "^4.3.3", + "formstream": "^1.1.1", + "mime-types": "^2.1.35", + "pump": "^3.0.0", + "qs": "^6.11.2", + "type-fest": "^4.3.1", + "undici": "^5.28.2", + "ylru": "^1.3.2" + }, + "engines": { + "node": ">= 14.19.3" + } + }, + "node_modules/egg-multipart": { + "version": "3.5.0", + "resolved": "https://registry.npmmirror.com/egg-multipart/-/egg-multipart-3.5.0.tgz", + "integrity": "sha512-AWJ50gr+toCvixgtHvLXbtzemJuqnbtJtBZbBNZ8/ADONvOZ9Ib7lkvzcA4Wq0lypfa0rtlLDRq8A09wOd6tVA==", + "license": "MIT", + "dependencies": { + "co-busboy": "^2.0.0", + "dayjs": "^1.11.5", + "egg-path-matching": "^1.2.0", + "humanize-bytes": "^1.0.1" + }, + "engines": { + "node": ">= 14.17.0" + } + }, + "node_modules/egg-onerror": { + "version": "2.4.0", + "resolved": "https://registry.npmmirror.com/egg-onerror/-/egg-onerror-2.4.0.tgz", + "integrity": "sha512-btmUsP1m6H9awd1IaAlvlzqN+uIbUKzaxJ8ArZI71r5FQ9LShZGv2pOT5fhL9mSnGkrvHUsjb9I5KoX2AY63bA==", + "dependencies": { + "cookie": "^0.7.2", + "koa-onerror": "^4.0.0", + "mustache": "^2.3.0", + "stack-trace": "^0.0.10" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/egg-path-matching": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/egg-path-matching/-/egg-path-matching-1.2.0.tgz", + "integrity": "sha512-u88hY0tH8GfWu4iocWFxux/abXvi7bh4lr/GIRlUHCTJFk8RQNCCfk5H3Vm67pqTBljtBOCl0lLykOsxj8trXQ==", + "license": "MIT", + "dependencies": { + "path-to-regexp": "^1.9.0" + } + }, + "node_modules/egg-schedule": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/egg-schedule/-/egg-schedule-4.0.1.tgz", + "integrity": "sha512-9BgIEOA5+jsqqvW8qLj/0SbTHkr9t2vNRBbZQkqqscejocViGysGd866vN+rcpuqd2zmnP6EQ0YqPAV5fI8P6Q==", + "license": "MIT", + "dependencies": { + "cron-parser": "^2.16.3", + "humanize-ms": "^1.2.1", + "is-type-of": "^1.2.1", + "safe-timers": "^1.1.0", + "utility": "^1.16.3" + }, + "engines": { + "node": ">=14.17.0" + } + }, + "node_modules/egg-schedule/node_modules/is-type-of": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/is-type-of/-/is-type-of-1.4.0.tgz", + "integrity": "sha512-EddYllaovi5ysMLMEN7yzHEKh8A850cZ7pykrY1aNRQGn/CDjRDE9qEWbIdt7xGEVJmjBXzU/fNnC4ABTm8tEQ==", + "license": "MIT", + "dependencies": { + "core-util-is": "^1.0.2", + "is-class-hotfix": "~0.0.6", + "isstream": "~0.1.2" + } + }, + "node_modules/egg-schedule/node_modules/utility": { + "version": "1.18.0", + "resolved": "https://registry.npmmirror.com/utility/-/utility-1.18.0.tgz", + "integrity": "sha512-PYxZDA+6QtvRvm//++aGdmKG/cI07jNwbROz0Ql+VzFV1+Z0Dy55NI4zZ7RHc9KKpBePNFwoErqIuqQv/cjiTA==", + "license": "MIT", + "dependencies": { + "copy-to": "^2.0.1", + "escape-html": "^1.0.3", + "mkdirp": "^0.5.1", + "mz": "^2.7.0", + "unescape": "^1.0.1" + }, + "engines": { + "node": ">= 0.12.0" + } + }, + "node_modules/egg-scripts": { + "version": "2.17.0", + "resolved": "https://registry.npmmirror.com/egg-scripts/-/egg-scripts-2.17.0.tgz", + "integrity": "sha512-2OHW0HuKKhOsl3yczYW6vcweyDrtFVOBccJ5xfuZRJOM3wTLRKAIfvXO/oNV1Mo0LSVuphhHiWmrUp481hcRXA==", + "license": "MIT", + "dependencies": { + "await-event": "^2.1.0", + "common-bin": "^2.8.0", + "debug": "^4.1.0", + "egg-utils": "^2.4.1", + "moment": "^2.23.0", + "mz": "^2.7.0", + "mz-modules": "^2.1.0", + "node-homedir": "^1.1.1", + "runscript": "^1.3.0", + "source-map-support": "^0.5.9", + "zlogger": "^1.1.0" + }, + "bin": { + "egg-scripts": "bin/egg-scripts.js", + "eggctl": "bin/egg-scripts.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/egg-scripts/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/egg-scripts/node_modules/is-type-of": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/is-type-of/-/is-type-of-1.4.0.tgz", + "integrity": "sha512-EddYllaovi5ysMLMEN7yzHEKh8A850cZ7pykrY1aNRQGn/CDjRDE9qEWbIdt7xGEVJmjBXzU/fNnC4ABTm8tEQ==", + "license": "MIT", + "dependencies": { + "core-util-is": "^1.0.2", + "is-class-hotfix": "~0.0.6", + "isstream": "~0.1.2" + } + }, + "node_modules/egg-scripts/node_modules/runscript": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/runscript/-/runscript-1.6.0.tgz", + "integrity": "sha512-lI0ybcwtdC5Wz3aiVtMAK6U5jcTDeLseEBSXcz6ABtQeMmQGpj35dmzpmpy2C9Bn0k2wTjTRLZoya0NFt8Mxsg==", + "license": "MIT", + "dependencies": { + "is-type-of": "^1.1.0" + }, + "engines": { + "node": ">=4.2.3" + } + }, + "node_modules/egg-security": { + "version": "3.7.0", + "resolved": "https://registry.npmmirror.com/egg-security/-/egg-security-3.7.0.tgz", + "integrity": "sha512-bIlPTBUW2FZ7MHntPExv0F7xigKMMK3irfEo8ypeG7S2G2vg/uFoNZumzBv4PnJeNwTzjUuG+G/YDHFOgSbTsQ==", + "license": "MIT", + "dependencies": { + "@eggjs/ip": "^2.0.2", + "csrf": "^3.0.6", + "delegates": "^1.0.0", + "egg-path-matching": "^1.0.0", + "escape-html": "^1.0.3", + "extend": "^3.0.1", + "koa-compose": "^4.1.0", + "matcher": "^4.0.0", + "methods": "^1.1.2", + "nanoid": "^3.3.6", + "platform": "^1.3.4", + "statuses": "^2.0.1", + "type-is": "^1.6.15", + "xss": "^1.0.3" + }, + "engines": { + "node": ">=14.20.0" + } + }, + "node_modules/egg-sequelize": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/egg-sequelize/-/egg-sequelize-6.0.0.tgz", + "integrity": "sha512-uEeBWydcvkFNIVsshE9ZMZXgPmnOtSA+PzKXXoRgOsbN0DqzBRxcdpV4OiW0q3u5f9cfSg/h2HD7imw/z7LGnQ==", + "license": "MIT", + "dependencies": { + "@types/sequelize": "^4.27.24", + "mz-modules": "^2.1.0", + "sequelize": "^6.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/egg-session": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/egg-session/-/egg-session-3.3.0.tgz", + "integrity": "sha512-RCPWHLWi0Ak+xI/zXN71Wpva/wsqevmKDvkOGk2uu5UdwRHw5lKuTtYm161NMDGZi6lvcnzR2lN7xRcDuhGiIw==", + "dependencies": { + "koa-session": "^6.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/egg-static": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/egg-static/-/egg-static-2.3.1.tgz", + "integrity": "sha512-OODn2ccm4znFM/EdhiPUmIpgCkYvAdtjuVAy3N/Ub4cSRgqIPcjXajmDsbhEo43mCieukPhsOxgWD3vKZurzHQ==", + "license": "MIT", + "dependencies": { + "is-type-of": "^1.2.1", + "koa-compose": "^4.1.0", + "koa-range": "^0.3.0", + "koa-static-cache": "^5.1.2", + "ylru": "^1.3.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/egg-static/node_modules/is-type-of": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/is-type-of/-/is-type-of-1.4.0.tgz", + "integrity": "sha512-EddYllaovi5ysMLMEN7yzHEKh8A850cZ7pykrY1aNRQGn/CDjRDE9qEWbIdt7xGEVJmjBXzU/fNnC4ABTm8tEQ==", + "license": "MIT", + "dependencies": { + "core-util-is": "^1.0.2", + "is-class-hotfix": "~0.0.6", + "isstream": "~0.1.2" + } + }, + "node_modules/egg-ts-helper": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/egg-ts-helper/-/egg-ts-helper-2.1.1.tgz", + "integrity": "sha512-/uA9aPi9ptAvT8NIASv20bJftTdGZb5L3wFX2q74IBNGF9hBZW4CX9b80driXNFYpL9z6BusZ1qkGZwzRWkykA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "chokidar": "^3.0.0", + "commander": "^2.15.1", + "dot-prop": "^4.2.0", + "enquirer": "^2.3.0", + "globby": "^11.0.0", + "json5": "^2.2.3", + "ts-node": "^10.9.1", + "tslib": "^2.6.1", + "typescript": "^5.1.6", + "yn": "^3.0.0" + }, + "bin": { + "ets": "dist/bin.js" + }, + "engines": { + "node": ">= 14.21.3" + } + }, + "node_modules/egg-ts-helper/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/egg-ts-helper/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/egg-ts-helper/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/egg-ts-helper/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/egg-ts-helper/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/egg-ts-helper/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/egg-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmmirror.com/egg-utils/-/egg-utils-2.5.0.tgz", + "integrity": "sha512-zQDXcqD0v+6IDBxcxzpTWoDTMg0G3iISSSeOHN7dZzyJWXXmw4ijBPAwKQPVvqBoXB6jAXj5f8B8/LVM9AN//A==", + "license": "MIT", + "dependencies": { + "mkdirp": "^0.5.1", + "utility": "^1.15.0" + } + }, + "node_modules/egg-utils/node_modules/utility": { + "version": "1.18.0", + "resolved": "https://registry.npmmirror.com/utility/-/utility-1.18.0.tgz", + "integrity": "sha512-PYxZDA+6QtvRvm//++aGdmKG/cI07jNwbROz0Ql+VzFV1+Z0Dy55NI4zZ7RHc9KKpBePNFwoErqIuqQv/cjiTA==", + "license": "MIT", + "dependencies": { + "copy-to": "^2.0.1", + "escape-html": "^1.0.3", + "mkdirp": "^0.5.1", + "mz": "^2.7.0", + "unescape": "^1.0.1" + }, + "engines": { + "node": ">= 0.12.0" + } + }, + "node_modules/egg-validate": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/egg-validate/-/egg-validate-2.0.2.tgz", + "integrity": "sha512-pUTfMTHQSpzDKAWkH19ZpyQBK22UToAwNWIgW1eXCAMpz+qvULuI5ey7dgqGQ/ZNwV2w/Stg96BrzW5uigCuVg==", + "license": "MIT", + "dependencies": { + "parameter": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/egg-view": { + "version": "2.1.4", + "resolved": "https://registry.npmmirror.com/egg-view/-/egg-view-2.1.4.tgz", + "integrity": "sha512-8zJ/S7YU5SK3EbAYESlUCixLpDZPnyKimoowavrgW2vONnh8AJVuDGqgKCYmuymvWq3uaHoNx7mYbdMQwMTZ6A==", + "license": "MIT", + "dependencies": { + "mz": "^2.7.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/egg-watcher": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/egg-watcher/-/egg-watcher-3.1.1.tgz", + "integrity": "sha512-fLo8f2GD9kSrAKeDoXaCckl9MaMMwTEkqU9gVDYWYGPYLsmX79ugA+Wo/2RGQkytsxSCk3bn8YqhPdMzsozgWA==", + "dependencies": { + "camelcase": "^5.0.0", + "sdk-base": "^3.5.0", + "wt": "^1.2.0" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/egg-watcher/node_modules/is-type-of": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/is-type-of/-/is-type-of-1.4.0.tgz", + "integrity": "sha512-EddYllaovi5ysMLMEN7yzHEKh8A850cZ7pykrY1aNRQGn/CDjRDE9qEWbIdt7xGEVJmjBXzU/fNnC4ABTm8tEQ==", + "license": "MIT", + "dependencies": { + "core-util-is": "^1.0.2", + "is-class-hotfix": "~0.0.6", + "isstream": "~0.1.2" + } + }, + "node_modules/egg-watcher/node_modules/sdk-base": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/sdk-base/-/sdk-base-3.6.0.tgz", + "integrity": "sha512-jxHUIrRLlAoRFRwiXKhOGjd6BeFWO/jz7tv+E7lbMSef6F9jzFN2Sv3hLW58oDDKscKaBGG6vQdkbXn7isE7fw==", + "license": "MIT", + "dependencies": { + "await-event": "^2.1.0", + "await-first": "^1.0.0", + "co": "^4.6.0", + "is-type-of": "^1.2.1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmmirror.com/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmmirror.com/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/enquirer/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/enquirer/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/event-stream": { + "version": "3.3.4", + "resolved": "https://registry.npmmirror.com/event-stream/-/event-stream-3.3.4.tgz", + "integrity": "sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==", + "license": "MIT", + "dependencies": { + "duplexer": "~0.1.1", + "from": "~0", + "map-stream": "~0.1.0", + "pause-stream": "0.0.11", + "split": "0.3", + "stream-combiner": "~0.0.4", + "through": "~2.3.1" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/execa/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/execa/node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/execa/node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/execa/node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/execa/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extend2": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/extend2/-/extend2-1.0.1.tgz", + "integrity": "sha512-ISoKeVhtewd5YHzMo+r9KC3Zx0fdpNBqoRzot+6BeEQ3bWQYQQOt0jkkY5gLveI2e7j+vdCJKeszHJIbg2Uceg==", + "license": "MIT" + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmmirror.com/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmmirror.com/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/foreground-child/node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child/node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child/node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data-encoder": { + "version": "1.9.0", + "resolved": "https://registry.npmmirror.com/form-data-encoder/-/form-data-encoder-1.9.0.tgz", + "integrity": "sha512-rahaRMkN8P8d/tgK/BLPX+WBVM27NbvdXBxqQujBtkDAIFspaRqN7Od7lfdGQA6KAD+f82fYCLBq1ipvcu8qLw==", + "license": "MIT" + }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmmirror.com/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "license": "MIT", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, + "node_modules/formidable": { + "version": "2.1.5", + "resolved": "https://registry.npmmirror.com/formidable/-/formidable-2.1.5.tgz", + "integrity": "sha512-Oz5Hwvwak/DCaXVVUtPn4oLMLLy1CdclLKO1LFgU7XzDpVMUU5UjlSLpGMocyQNNk8F6IJW9M/YdooSn2MRI+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0", + "qs": "^6.11.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, + "node_modules/formstream": { + "version": "1.5.2", + "resolved": "https://registry.npmmirror.com/formstream/-/formstream-1.5.2.tgz", + "integrity": "sha512-NASf0lgxC1AyKNXQIrXTEYkiX99LhCEXTkiGObXAkpBui86a4u8FjH1o2bGb3PpqI3kafC+yw4zWeK6l6VHTgg==", + "license": "MIT", + "dependencies": { + "destroy": "^1.0.4", + "mime": "^2.5.2", + "node-hex": "^1.0.1", + "pause-stream": "~0.0.11" + } + }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmmirror.com/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/from": { + "version": "0.1.7", + "resolved": "https://registry.npmmirror.com/from/-/from-0.1.7.tgz", + "integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==", + "license": "MIT" + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs-readdir-recursive": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", + "integrity": "sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==", + "license": "MIT" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/fsu": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/fsu/-/fsu-1.1.1.tgz", + "integrity": "sha512-xQVsnjJ/5pQtcKh+KjUoZGzVWn4uNkchxTF6Lwjr4Gf7nQr8fmUfhKJ62zE77+xQg9xnxi5KUps7XGs+VC986A==", + "dev": true, + "license": "MIT" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gals": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/gals/-/gals-1.0.2.tgz", + "integrity": "sha512-h5c1Q6Q2cnRkO2v8ZxbuFCNRpM96CjGxGuoNcThoNF3dAEEYagF166EqJmaa9r2/I+ryij8TO3yMmqrMvQ1YXw==", + "license": "MIT", + "engines": { + "node": ">= 16.0.0" + } + }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-ready": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/get-ready/-/get-ready-2.0.1.tgz", + "integrity": "sha512-q2zoHPGbHognNvofksty0SOrviWowfUVxYcv3j3+Mf1BRRMHmq6q/1pciravEf8jkSL5iovloMWnonEc2QOpqA==", + "license": "MIT", + "dependencies": { + "is-type-of": "^1.0.0" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/get-ready/node_modules/is-type-of": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/is-type-of/-/is-type-of-1.4.0.tgz", + "integrity": "sha512-EddYllaovi5ysMLMEN7yzHEKh8A850cZ7pykrY1aNRQGn/CDjRDE9qEWbIdt7xGEVJmjBXzU/fNnC4ABTm8tEQ==", + "license": "MIT", + "dependencies": { + "core-util-is": "^1.0.2", + "is-class-hotfix": "~0.0.6", + "isstream": "~0.1.2" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmmirror.com/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmmirror.com/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/graceful/-/graceful-1.1.0.tgz", + "integrity": "sha512-sImEQVLLBo8hSeX4Vp6HVdWSdqCkHoVlZwBuBL65tNwcPTvnMaW40iWs1vdCbyP0znkCEuvv6rZ37QPloPi9Fw==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1", + "ps-tree": "^1.1.0" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/graceful-process": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/graceful-process/-/graceful-process-1.3.0.tgz", + "integrity": "sha512-tlldpcdxQgSgVD/A+OawlyjGU9Z+wbBTI6QroGo4u6znQNLgglp+V2KnixRHUXRttnrCpbQSSCJ6wvsMkzR9Aw==", + "license": "MIT", + "dependencies": { + "is-type-of": "^1.2.0", + "once": "^1.4.0" + } + }, + "node_modules/graceful-process/node_modules/is-type-of": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/is-type-of/-/is-type-of-1.4.0.tgz", + "integrity": "sha512-EddYllaovi5ysMLMEN7yzHEKh8A850cZ7pykrY1aNRQGn/CDjRDE9qEWbIdt7xGEVJmjBXzU/fNnC4ABTm8tEQ==", + "license": "MIT", + "dependencies": { + "core-util-is": "^1.0.2", + "is-class-hotfix": "~0.0.6", + "isstream": "~0.1.2" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/header-case": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/header-case/-/header-case-1.0.1.tgz", + "integrity": "sha512-i0q9mkOeSuhXw6bGgiQCCBgY/jlZuV/7dZXyZ9c6LcBrqwvT8eT719E9uxE5LiZftdl+z81Ugbg/VvXV4OJOeQ==", + "license": "MIT", + "dependencies": { + "no-case": "^2.2.0", + "upper-case": "^1.1.3" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-assert": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/http-assert/-/http-assert-1.5.0.tgz", + "integrity": "sha512-uPpH7OKX4H25hBmU6G1jWNaqJGpTXxey+YOUizJUAgu0AjLUeC8D73hTrhvDS5D+GJN1DN1+hhc/eF/wpxtp0w==", + "license": "MIT", + "dependencies": { + "deep-equal": "~1.0.1", + "http-errors": "~1.8.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-assert/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-assert/node_modules/http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-assert/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/httpx": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/httpx/-/httpx-2.3.3.tgz", + "integrity": "sha512-k1qv94u1b6e+XKCxVbLgYlOypVP9MPGpnN5G/vxFf6tDO4V3xpz3d6FUOY/s8NtPgaq5RBVVgSB+7IHpVxMYzw==", + "dependencies": { + "@types/node": "^20", + "debug": "^4.1.1" + } + }, + "node_modules/httpx/node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/httpx/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/httpx/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/humanize-bytes": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/humanize-bytes/-/humanize-bytes-1.0.1.tgz", + "integrity": "sha512-OZMIHt5YhQM4S3R82KGoKnWv4vaNnWuEhPAK+u5v7rD2tRM3DU2NghYEHipAPYwyTR6+fMVZQ9ETpZIZeZUapQ==", + "license": "MIT", + "dependencies": { + "bytes": "~2.2.0" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/inflation": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/inflation/-/inflation-2.1.0.tgz", + "integrity": "sha512-t54PPJHG1Pp7VQvxyVCJ9mBbjG3Hqryges9bXoOO6GExCPa+//i/d5GSuFtpx3ALLd7lgIAur6zrIlBQyJuMlQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/inflection": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/inflection/-/inflection-3.0.2.tgz", + "integrity": "sha512-+Bg3+kg+J6JUWn8J6bzFmOWkTQ6L/NHfDRSYU+EVvuKHDxUDHAXgqixHfVlzuBQaPOTac8hn43aPhMNk6rMe3g==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmmirror.com/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/inquirer": { + "version": "8.2.7", + "resolved": "https://registry.npmmirror.com/inquirer/-/inquirer-8.2.7.tgz", + "integrity": "sha512-UjOaSel/iddGZJ5xP/Eixh6dY1XghiBw4XK13rCCIJcJfyhhoul/7KhLLUGtebEj6GDYM6Vnx/mVsjx2L/mFIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/external-editor": "^1.0.0", + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.1", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "figures": "^3.0.0", + "lodash": "^4.17.21", + "mute-stream": "0.0.8", + "ora": "^5.4.1", + "run-async": "^2.4.0", + "rxjs": "^7.5.5", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6", + "wrap-ansi": "^6.0.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/inquirer/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/inquirer/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/inquirer/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/inquirer/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/inquirer/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmmirror.com/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-class-hotfix": { + "version": "0.0.6", + "resolved": "https://registry.npmmirror.com/is-class-hotfix/-/is-class-hotfix-0.0.6.tgz", + "integrity": "sha512-0n+pzCC6ICtVr/WXnN2f03TK/3BfXY7me4cjCAqT8TYXEl0+JBRoqBo94JJHXcyDSLUeWbNX8Fvy5g5RJdAstQ==", + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmmirror.com/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-lower-case": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/is-lower-case/-/is-lower-case-1.1.3.tgz", + "integrity": "sha512-+5A1e/WJpLLXZEDlgz4G//WYSHyQBD32qa4Jd3Lw06qQlv3fJHnp3YIHjTQSGzHMgzmVKz2ZP3rBxTHkPw/lxA==", + "license": "MIT", + "dependencies": { + "lower-case": "^1.1.0" + } + }, + "node_modules/is-nan": { + "version": "1.3.2", + "resolved": "https://registry.npmmirror.com/is-nan/-/is-nan-1.3.2.tgz", + "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-type-of": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/is-type-of/-/is-type-of-2.2.0.tgz", + "integrity": "sha512-72axShMJMnMy5HSU/jLGNOonZD5rWM0MwJSCYpKCTQCbggQZBJO/CLMMVP5HgS8kPSYFBkTysJexsD6NMvGKDQ==", + "license": "MIT" + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-upper-case": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/is-upper-case/-/is-upper-case-1.1.2.tgz", + "integrity": "sha512-GQYSJMgfeAmVwh9ixyk888l7OIhNAGKtY6QA+IrWlu9MDTCaXmeozOZ2S9Knj7bQwBO/H6J2kb+pbyTUiMNbsw==", + "license": "MIT", + "dependencies": { + "upper-case": "^1.1.0" + } + }, + "node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmmirror.com/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "license": "MIT" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmmirror.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmmirror.com/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmmirror.com/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmmirror.com/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-util/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-util/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-util/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-util/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-util/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/js-beautify": { + "version": "1.15.4", + "resolved": "https://registry.npmmirror.com/js-beautify/-/js-beautify-1.15.4.tgz", + "integrity": "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "config-chain": "^1.1.13", + "editorconfig": "^1.0.4", + "glob": "^10.4.2", + "js-cookie": "^3.0.5", + "nopt": "^7.2.1" + }, + "bin": { + "css-beautify": "js/bin/css-beautify.js", + "html-beautify": "js/bin/html-beautify.js", + "js-beautify": "js/bin/js-beautify.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/js-beautify/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmmirror.com/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/js-beautify/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/js-cookie": { + "version": "3.0.8", + "resolved": "https://registry.npmmirror.com/js-cookie/-/js-cookie-3.0.8.tgz", + "integrity": "sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "license": "ISC" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonp-body": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/jsonp-body/-/jsonp-body-1.1.0.tgz", + "integrity": "sha512-ZQyNWgHI8vvqclzz0l4/PatmeGureejrdMiLQGxTupMw0yQMHf+pQwS/7d5tpo2NBHsuwLCmXvZeNiz2RKdklA==", + "license": "MIT", + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmmirror.com/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keygrip": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/keygrip/-/keygrip-1.1.0.tgz", + "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", + "license": "MIT", + "dependencies": { + "tsscmp": "1.0.6" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/kitx": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/kitx/-/kitx-2.2.0.tgz", + "integrity": "sha512-tBMwe6AALTBQJb0woQDD40734NKzb0Kzi3k7wQj9ar3AbP9oqhoVrdXPh7rk2r00/glIgd0YbToIUJsnxWMiIg==", + "dependencies": { + "@types/node": "^22.5.4" + } + }, + "node_modules/kitx/node_modules/@types/node": { + "version": "22.20.1", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/kitx/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" + }, + "node_modules/ko-sleep": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/ko-sleep/-/ko-sleep-1.1.4.tgz", + "integrity": "sha512-s05WGpvvzyTuRlRE8fM7ru2Z3O+InbJuBcckTWKg2W+2c1k6SnFa3IfiSSt0/peFrlYAXgNoxuJWWVNmWh+K/A==", + "license": "MIT", + "dependencies": { + "ms": "*" + } + }, + "node_modules/koa": { + "version": "2.16.4", + "resolved": "https://registry.npmmirror.com/koa/-/koa-2.16.4.tgz", + "integrity": "sha512-3An0GCLDSR34tsCO4H8Tef8Pp2ngtaZDAZnsWJYelqXUK5wyiHvGItgK/xcSkmHLSTn1Jcho1mRQs2ehRzvKKw==", + "license": "MIT", + "dependencies": { + "accepts": "^1.3.5", + "cache-content-type": "^1.0.0", + "content-disposition": "~0.5.2", + "content-type": "^1.0.4", + "cookies": "~0.9.0", + "debug": "^4.3.2", + "delegates": "^1.0.0", + "depd": "^2.0.0", + "destroy": "^1.0.4", + "encodeurl": "^1.0.2", + "escape-html": "^1.0.3", + "fresh": "~0.5.2", + "http-assert": "^1.3.0", + "http-errors": "^1.6.3", + "is-generator-function": "^1.0.7", + "koa-compose": "^4.1.0", + "koa-convert": "^2.0.0", + "on-finished": "^2.3.0", + "only": "~0.0.2", + "parseurl": "^1.3.2", + "statuses": "^1.5.0", + "type-is": "^1.6.16", + "vary": "^1.1.2" + }, + "engines": { + "node": "^4.8.4 || ^6.10.1 || ^7.10.1 || >= 8.1.4" + } + }, + "node_modules/koa-bodyparser": { + "version": "4.4.1", + "resolved": "https://registry.npmmirror.com/koa-bodyparser/-/koa-bodyparser-4.4.1.tgz", + "integrity": "sha512-kBH3IYPMb+iAXnrxIhXnW+gXV8OTzCu8VPDqvcDHW9SQrbkHmqPQtiZwrltNmSq6/lpipHnT7k7PsjlVD7kK0w==", + "license": "MIT", + "dependencies": { + "co-body": "^6.0.0", + "copy-to": "^2.0.1", + "type-is": "^1.6.18" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/koa-compose": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/koa-compose/-/koa-compose-4.1.0.tgz", + "integrity": "sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==", + "license": "MIT" + }, + "node_modules/koa-convert": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/koa-convert/-/koa-convert-1.2.0.tgz", + "integrity": "sha512-K9XqjmEDStGX09v3oxR7t5uPRy0jqJdvodHa6wxWTHrTfDq0WUNnYTOOUZN6g8OM8oZQXprQASbiIXG2Ez8ehA==", + "license": "MIT", + "dependencies": { + "co": "^4.6.0", + "koa-compose": "^3.0.0" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/koa-convert/node_modules/koa-compose": { + "version": "3.2.1", + "resolved": "https://registry.npmmirror.com/koa-compose/-/koa-compose-3.2.1.tgz", + "integrity": "sha512-8gen2cvKHIZ35eDEik5WOo8zbVp9t4cP8p4hW4uE55waxolLRexKKrqfCpwhGVppnB40jWeF8bZeTVg99eZgPw==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.1.0" + } + }, + "node_modules/koa-is-json": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/koa-is-json/-/koa-is-json-1.0.0.tgz", + "integrity": "sha512-+97CtHAlWDx0ndt0J8y3P12EWLwTLMXIfMnYDev3wOTwH/RpBGMlfn4bDXlMEg1u73K6XRE9BbUp+5ZAYoRYWw==", + "license": "MIT" + }, + "node_modules/koa-locales": { + "version": "1.12.0", + "resolved": "https://registry.npmmirror.com/koa-locales/-/koa-locales-1.12.0.tgz", + "integrity": "sha512-lalx0OuvdZ39JppTVqHnYKIgJJbByon9xpt5KSKFOL6/VQS+XBMklMX83+fgkCXzsNacynWaN75ihWP2EGsWIQ==", + "license": "MIT", + "dependencies": { + "debug": "^2.6.0", + "humanize-ms": "^1.2.0", + "ini": "^1.3.4", + "object-assign": "^4.1.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/koa-locales/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/koa-locales/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/koa-onerror": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/koa-onerror/-/koa-onerror-4.2.0.tgz", + "integrity": "sha512-D15tp5rxevHqqcvOiEDbtQolG6z3NpBNupz3EUZz43pjYv5SGMom2Xz1FKM8oTya56+aq+hejPW/iBrNnC/UGQ==", + "license": "MIT", + "dependencies": { + "escape-html": "^1.0.3", + "stream-wormhole": "^1.1.0" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/koa-override": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/koa-override/-/koa-override-3.0.0.tgz", + "integrity": "sha512-w2rWCfapbQUZ8TrRBarj6iwryCTooEcdw9lr1hYC1q4FnaCZcAOhpjB1VpqtbODALVMgY3JGlzLSeYRXc5Ky0Q==", + "license": "MIT", + "dependencies": { + "methods": "^1.1.2" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/koa-range": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/koa-range/-/koa-range-0.3.0.tgz", + "integrity": "sha512-Ich3pCz6RhtbajYXRWjIl6O5wtrLs6kE3nkXc9XmaWe+MysJyZO7K4L3oce1Jpg/iMgCbj+5UCiMm/rqVtcDIg==", + "license": "MIT", + "dependencies": { + "stream-slice": "^0.1.2" + }, + "engines": { + "node": ">=7" + } + }, + "node_modules/koa-session": { + "version": "6.4.0", + "resolved": "https://registry.npmmirror.com/koa-session/-/koa-session-6.4.0.tgz", + "integrity": "sha512-h/dxmSOvNEXpHQPRs4TV03TZVFyZIjmYQiTAW5JBFTYBOZ0VdpZ8QEE6Dud75g8z9JNGXi3m++VqRmqToB+c2A==", + "license": "MIT", + "dependencies": { + "crc": "^3.8.0", + "debug": "^4.3.3", + "is-type-of": "^1.2.1", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/koa-session/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/koa-session/node_modules/is-type-of": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/is-type-of/-/is-type-of-1.4.0.tgz", + "integrity": "sha512-EddYllaovi5ysMLMEN7yzHEKh8A850cZ7pykrY1aNRQGn/CDjRDE9qEWbIdt7xGEVJmjBXzU/fNnC4ABTm8tEQ==", + "license": "MIT", + "dependencies": { + "core-util-is": "^1.0.2", + "is-class-hotfix": "~0.0.6", + "isstream": "~0.1.2" + } + }, + "node_modules/koa-static-cache": { + "version": "5.1.4", + "resolved": "https://registry.npmmirror.com/koa-static-cache/-/koa-static-cache-5.1.4.tgz", + "integrity": "sha512-abVWOHY6z6qSTvNtapWMAnvHS9SUiUCaQQQubClSAT9ybQPsZ6ioKcRarnownS4fMD0sXQgQ5ey8CYEuwoa1Yg==", + "license": "MIT", + "dependencies": { + "compressible": "^2.0.6", + "debug": "^3.1.0", + "fs-readdir-recursive": "^1.0.0", + "mime-types": "^2.1.8", + "mz": "^2.7.0" + }, + "engines": { + "node": ">= 7.6.0" + } + }, + "node_modules/koa/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/koa/node_modules/http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/koa/node_modules/http-errors/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/koa/node_modules/koa-convert": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/koa-convert/-/koa-convert-2.0.0.tgz", + "integrity": "sha512-asOvN6bFlSnxewce2e/DK3p4tltyfC4VM7ZwuTuepI7dEQVcvpyFuBcEARu1+Hxg8DIwytce2n7jrZtRlPrARA==", + "license": "MIT", + "dependencies": { + "co": "^4.6.0", + "koa-compose": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/koa/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isempty": { + "version": "4.4.0", + "resolved": "https://registry.npmmirror.com/lodash.isempty/-/lodash.isempty-4.4.0.tgz", + "integrity": "sha512-oKMuF3xEeqDltrGMfDxAPGIVMSSRv8tbRSODbrs4KGsRRLEhrW8N8Rd4DRgB2+621hY8A8XwwrTVhXWpxFvMzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isfunction": { + "version": "3.0.9", + "resolved": "https://registry.npmmirror.com/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", + "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isobject": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/lodash.isobject/-/lodash.isobject-3.0.2.tgz", + "integrity": "sha512-3/Qptq2vr7WeJbB4KHUSKlq8Pl7ASXi3UG6CMbBm8WRtXi8+GHm7mKaU3urfpSEzWe2wCIChs6/sdocUsTKJiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmmirror.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/log-symbols/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/log-symbols/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", + "license": "Apache-2.0" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lower-case": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/lower-case/-/lower-case-1.1.4.tgz", + "integrity": "sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA==", + "license": "MIT" + }, + "node_modules/lower-case-first": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/lower-case-first/-/lower-case-first-1.0.2.tgz", + "integrity": "sha512-UuxaYakO7XeONbKrZf5FEgkantPf5DUqDayzP5VXZrtRPdH86s4kN47I8B3TW10S4QKiE3ziHNf3kRN//okHjA==", + "license": "MIT", + "dependencies": { + "lower-case": "^1.1.2" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/lru.min": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/lru.min/-/lru.min-1.1.4.tgz", + "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmmirror.com/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/map-stream": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/map-stream/-/map-stream-0.1.0.tgz", + "integrity": "sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==" + }, + "node_modules/matcher": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/matcher/-/matcher-4.0.0.tgz", + "integrity": "sha512-S6x5wmcDmsDRRU/c2dkccDwQPXoFczc5+HpQ2lON8pnvHlnvHAHj5WlLVvw6n6vNyHuVugYrFohYxbS+pvFpKQ==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/matcher/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmmirror.com/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmmirror.com/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mm": { + "version": "3.4.0", + "resolved": "https://registry.npmmirror.com/mm/-/mm-3.4.0.tgz", + "integrity": "sha512-pXyBKWM0cMG0QD5NoXCUZok8XOhqe/Pf4ILOYFJ4Ur2RIHJni6YHdxt/zHOiwjf2FAMQH2fowczaQMtIDvq+bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-type-of": "^1.2.1", + "muk-prop": "^1.2.1", + "thenify": "^3.3.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/mm/node_modules/is-type-of": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/is-type-of/-/is-type-of-1.4.0.tgz", + "integrity": "sha512-EddYllaovi5ysMLMEN7yzHEKh8A850cZ7pykrY1aNRQGn/CDjRDE9qEWbIdt7xGEVJmjBXzU/fNnC4ABTm8tEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "^1.0.2", + "is-class-hotfix": "~0.0.6", + "isstream": "~0.1.2" + } + }, + "node_modules/mocha": { + "version": "10.8.2", + "resolved": "https://registry.npmmirror.com/mocha/-/mocha-10.8.2.tgz", + "integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^8.1.0", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/mocha/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/mocha/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmmirror.com/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/mocha/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/mocha/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mocha/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/mocha/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/mocha/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/mocha/node_modules/yargs": { + "version": "16.2.2", + "resolved": "https://registry.npmmirror.com/yargs/-/yargs-16.2.2.tgz", + "integrity": "sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/mochawesome-report-generator": { + "version": "6.3.2", + "resolved": "https://registry.npmmirror.com/mochawesome-report-generator/-/mochawesome-report-generator-6.3.2.tgz", + "integrity": "sha512-iB6iyOUMyMr8XOUYTNfrqYuZQLZka3K/Gr2GPc6CHPe7t2ZhxxfcoVkpMLOtyDKnWbY1zgu1/7VNRsigrvKnOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "dateformat": "^4.5.1", + "escape-html": "^1.0.3", + "fs-extra": "^10.0.0", + "fsu": "^1.1.1", + "lodash.isfunction": "^3.0.9", + "opener": "^1.5.2", + "prop-types": "^15.7.2", + "tcomb": "^3.2.17", + "tcomb-validation": "^3.3.0", + "yargs": "^17.2.1" + }, + "bin": { + "marge": "bin/cli.js" + } + }, + "node_modules/mochawesome-report-generator/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/mochawesome-report-generator/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/mochawesome-report-generator/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/mochawesome-report-generator/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mochawesome-report-generator/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/mochawesome-report-generator/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mochawesome-with-mocha": { + "version": "7.1.3", + "resolved": "https://registry.npmmirror.com/mochawesome-with-mocha/-/mochawesome-with-mocha-7.1.3.tgz", + "integrity": "sha512-C7df5V1hy2nvAlX7Z7H2YjvvFPUCyyqaK39JdSTK+owpfiTz2OjhGGpraAMW+DgimGbd3uLPn1RW62Ow5T7ZZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "diff": "^5.0.0", + "json-stringify-safe": "^5.0.1", + "lodash.isempty": "^4.4.0", + "lodash.isfunction": "^3.0.9", + "lodash.isobject": "^3.0.2", + "lodash.isstring": "^4.0.1", + "mocha": "^10.2.0", + "mochawesome-report-generator": "^6.2.0", + "strip-ansi": "^6.0.1", + "uuid": "^8.3.2" + } + }, + "node_modules/mochawesome-with-mocha/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/mochawesome-with-mocha/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/mochawesome-with-mocha/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/mochawesome-with-mocha/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/mochawesome-with-mocha/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mochawesome-with-mocha/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/mochawesome-with-mocha/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mochawesome-with-mocha/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmmirror.com/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/moment-timezone": { + "version": "0.5.48", + "resolved": "https://registry.npmmirror.com/moment-timezone/-/moment-timezone-0.5.48.tgz", + "integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==", + "license": "MIT", + "dependencies": { + "moment": "^2.29.4" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/muk-prop": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/muk-prop/-/muk-prop-1.2.1.tgz", + "integrity": "sha512-NdkOVav3GoIkBZqMUneU435HW0a90zitpuO1erPRhOQdPtl65dXD3G9/1k46G6/0ZMau4CJFFUHkMKVsyNZT+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/multer": { + "version": "1.4.5-lts.2", + "resolved": "https://registry.npmmirror.com/multer/-/multer-1.4.5-lts.2.tgz", + "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/multimatch": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/multimatch/-/multimatch-5.0.0.tgz", + "integrity": "sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA==", + "license": "MIT", + "dependencies": { + "@types/minimatch": "^3.0.3", + "array-differ": "^3.0.0", + "array-union": "^2.1.0", + "arrify": "^2.0.1", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/multimatch/node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/multimatch/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mustache": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/mustache/-/mustache-2.3.2.tgz", + "integrity": "sha512-KpMNwdQsYz3O/SBS1qJ/o3sqUJ5wSb8gb0pul8CO0S56b9Y2ALm8zCfsjPXsqGFfoNBkDwZuZIAjhsZI03gYVQ==", + "license": "MIT", + "bin": { + "mustache": "bin/mustache" + }, + "engines": { + "npm": ">=1.4.0" + } + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmmirror.com/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true, + "license": "ISC" + }, + "node_modules/mysql2": { + "version": "3.22.5", + "resolved": "https://registry.npmmirror.com/mysql2/-/mysql2-3.22.5.tgz", + "integrity": "sha512-95uZ2TrPWAZdwpB3vvvDbmEMcNG8yIeNCyu6GUcr/QnWEE/wXm7+mhOCsdQfWQDTV7qYT/PDUZ4U4UPP4AsXqQ==", + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.2", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.2", + "long": "^5.3.2", + "lru.min": "^1.1.4", + "named-placeholders": "^1.1.6", + "sql-escaper": "^1.3.3" + }, + "engines": { + "node": ">= 8.0" + }, + "peerDependencies": { + "@types/node": ">= 8" + } + }, + "node_modules/mysql2/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mysql2/node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmmirror.com/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/mz-modules": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/mz-modules/-/mz-modules-2.1.0.tgz", + "integrity": "sha512-sjk8lcRW3vrVYnZ+W+67L/2rL+jbO5K/N6PFGIcLWTiYytNr22Ah9FDXFs+AQntTM1boZcoHi5qS+CV1seuPog==", + "license": "MIT", + "dependencies": { + "glob": "^7.1.2", + "ko-sleep": "^1.0.3", + "mkdirp": "^0.5.1", + "pump": "^3.0.0", + "rimraf": "^2.6.1" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/mz-modules/node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/mz-modules/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mz-modules/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.6", + "resolved": "https://registry.npmmirror.com/named-placeholders/-/named-placeholders-1.1.6.tgz", + "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", + "license": "MIT", + "dependencies": { + "lru.min": "^1.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/ndir": { + "version": "0.1.5", + "resolved": "https://registry.npmmirror.com/ndir/-/ndir-0.1.5.tgz", + "integrity": "sha512-GEdViSiVHi63HASUGaQXNBbUNe5KDnuAYvOGBlY7y+CCCRT3NBeMWQlODb2GVV2cKwEIwHzo9KOCMsPBXlKsOg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/no-case": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/no-case/-/no-case-2.3.2.tgz", + "integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==", + "license": "MIT", + "dependencies": { + "lower-case": "^1.1.1" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-hex": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/node-hex/-/node-hex-1.0.1.tgz", + "integrity": "sha512-iwpZdvW6Umz12ICmu9IYPRxg0tOLGmU3Tq2tKetejCj3oZd7b2nUXwP3a7QA5M9glWy8wlPS1G3RwM/CdsUbdQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/node-homedir": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/node-homedir/-/node-homedir-1.1.1.tgz", + "integrity": "sha512-Xsmf94D/DdeDISAECUaxXVxhh+kHdbOQE4CnP4igo3HXL3BSmmUpD5M7orH434EZZwBTFF2xe5SgsQr/wOBuNw==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/nopt": { + "version": "7.2.1", + "resolved": "https://registry.npmmirror.com/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onelogger": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/onelogger/-/onelogger-1.0.1.tgz", + "integrity": "sha512-CssDwyG8cnY8fGm9HJurr2LNScfn2u//hnxgivLMgTj+B/S2IspJbbxgFboaRJ+V0CPzi59wqTLuikMcXyVeaw==", + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/only": { + "version": "0.0.2", + "resolved": "https://registry.npmmirror.com/only/-/only-0.0.2.tgz", + "integrity": "sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ==" + }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmmirror.com/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true, + "license": "(WTFPL OR MIT)", + "bin": { + "opener": "bin/opener-bin.js" + } + }, + "node_modules/opentype.js": { + "version": "0.7.3", + "resolved": "https://registry.npmmirror.com/opentype.js/-/opentype.js-0.7.3.tgz", + "integrity": "sha512-Veui5vl2bLonFJ/SjX/WRWJT3SncgiZNnKUyahmXCc2sa1xXW15u3R/3TN5+JFiP7RsjK5ER4HA5eWaEmV9deA==", + "license": "MIT", + "dependencies": { + "tiny-inflate": "^1.0.2" + }, + "bin": { + "ot": "bin/ot" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmmirror.com/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ora/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/ora/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ora/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/os-name": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/os-name/-/os-name-1.0.3.tgz", + "integrity": "sha512-f5estLO2KN8vgtTRaILIgEGBoBrMnZ3JQ7W9TMZCnOIGwHe8TRGSpcagnWDo+Dfhd/z08k9Xe75hvciJJ8Qaew==", + "license": "MIT", + "dependencies": { + "osx-release": "^1.0.0", + "win-release": "^1.0.0" + }, + "bin": { + "os-name": "cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/osx-release": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/osx-release/-/osx-release-1.1.0.tgz", + "integrity": "sha512-ixCMMwnVxyHFQLQnINhmIpWqXIfS2YOXchwQrk+OFzmo6nDjQ0E4KXAyyUh0T0MZgV4bUhkRrAbVqlE4yLVq4A==", + "license": "MIT", + "dependencies": { + "minimist": "^1.1.0" + }, + "bin": { + "osx-release": "cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-timeout": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/p-timeout/-/p-timeout-4.1.0.tgz", + "integrity": "sha512-+/wmHtzJuWii1sXn3HCuH/FTwGhrp4tmJTxSKJbfS+vkipci6osxXM5mY0jUiRzWKMTgUT8l7HFbeSwZAynqHw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/param-case": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/param-case/-/param-case-2.1.1.tgz", + "integrity": "sha512-eQE845L6ot89sk2N8liD8HAuH4ca6Vvr7VWAWwt7+kvvG5aBcPmmphQ68JsEG2qa9n1TykS2DLeMt363AAH8/w==", + "license": "MIT", + "dependencies": { + "no-case": "^2.2.0" + } + }, + "node_modules/parameter": { + "version": "3.7.0", + "resolved": "https://registry.npmmirror.com/parameter/-/parameter-3.7.0.tgz", + "integrity": "sha512-CRSlKBPn5pI1HQpdhyjVojTq9OyZFXcejkqt6vhkjmAwvZ4w0nK6Od/vNyniWY8wC3FwIPG+DUc59vG3FmkINg==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/pascal-case": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/pascal-case/-/pascal-case-2.0.1.tgz", + "integrity": "sha512-qjS4s8rBOJa2Xm0jmxXiyh1+OFf6ekCWOvUaRgAQSktzlTbMotS0nmG9gyYAybCWBcuP4fsBeRCKNwGBnMe2OQ==", + "license": "MIT", + "dependencies": { + "camel-case": "^3.0.0", + "upper-case-first": "^1.1.0" + } + }, + "node_modules/path-case": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/path-case/-/path-case-2.1.1.tgz", + "integrity": "sha512-Ou0N05MioItesaLr9q8TtHVWmJ6fxWdqKB2RohFmNWVyJ+2zeKIeDNWAN6B/Pe7wpzWChhZX6nONYmOnMeJQ/Q==", + "license": "MIT", + "dependencies": { + "no-case": "^2.2.0" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-to-regexp": { + "version": "1.9.0", + "resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-1.9.0.tgz", + "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", + "license": "MIT", + "dependencies": { + "isarray": "0.0.1" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pause-stream": { + "version": "0.0.11", + "resolved": "https://registry.npmmirror.com/pause-stream/-/pause-stream-0.0.11.tgz", + "integrity": "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==", + "license": [ + "MIT", + "Apache2" + ], + "dependencies": { + "through": "~2.3" + } + }, + "node_modules/pg-connection-string": { + "version": "2.13.0", + "resolved": "https://registry.npmmirror.com/pg-connection-string/-/pg-connection-string-2.13.0.tgz", + "integrity": "sha512-EMnU9E2fSULdsbErBbMaXJvFeD9B4+nPcM3f+4lsiCR0BHLPrLVjv3DbyM2hgQQviKJaTWIRRTjKjWlHg3p2ig==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-up": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/pkg-up/-/pkg-up-3.1.0.tgz", + "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-up/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-up/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/platform": { + "version": "1.3.6", + "resolved": "https://registry.npmmirror.com/platform/-/platform-1.3.6.tgz", + "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==", + "license": "MIT" + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmmirror.com/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmmirror.com/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true, + "license": "ISC" + }, + "node_modules/ps-tree": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/ps-tree/-/ps-tree-1.2.0.tgz", + "integrity": "sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA==", + "license": "MIT", + "dependencies": { + "event-stream": "=3.3.4" + }, + "bin": { + "ps-tree": "bin/ps-tree.js" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "license": "MIT", + "dependencies": { + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" + } + }, + "node_modules/pumpify/node_modules/pump": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmmirror.com/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmmirror.com/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmmirror.com/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/readable-stream/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmmirror.com/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/readdir-glob": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/readdir-glob/-/readdir-glob-3.0.0.tgz", + "integrity": "sha512-AhNB2KgKeVJr16nK9LLZbJNWnYoT23ZrumNKFDebHBdkC8KHSqWo871JAUhoWC/RtjEVdqNMFpM6qrwRbaUqpw==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^10.2.2" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/yqnn" + } + }, + "node_modules/readdir-glob/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/ready-callback": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/ready-callback/-/ready-callback-3.0.0.tgz", + "integrity": "sha512-wGhjJMrxVEX35Ec+QlM2FW+ZsXhThNCbmdk+x22vzwRPPsDUNgq7C6nU0EpJRvNJSK/MiwQ73XZljo/IKhovXA==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "get-ready": "^2.0.1", + "once": "^1.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ready-callback/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/reduce-flatten": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/reduce-flatten/-/reduce-flatten-2.0.0.tgz", + "integrity": "sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/reflect-metadata": { + "version": "0.1.14", + "resolved": "https://registry.npmmirror.com/reflect-metadata/-/reflect-metadata-0.1.14.tgz", + "integrity": "sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==", + "license": "Apache-2.0" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/retry-as-promised": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/retry-as-promised/-/retry-as-promised-7.1.1.tgz", + "integrity": "sha512-hMD7odLOt3LkTjcif8aRZqi/hybjpLNgSk5oF5FCowfCjok6LukpN2bDX7R5wDmbgBQFn7YoBxSagmtXHaJYJw==", + "license": "MIT" + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmmirror.com/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/rndm": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/rndm/-/rndm-1.2.0.tgz", + "integrity": "sha512-fJhQQI5tLrQvYIYFpOnFinzv9dwmR7hRnUz1XqP3OJ1jIweTNOd6aTO4jwQSgcBSFUB+/KHJxuGneime+FdzOw==", + "license": "MIT" + }, + "node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmmirror.com/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/runscript": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/runscript/-/runscript-2.0.1.tgz", + "integrity": "sha512-CjlpKusSKpE943TsCLkqU1dBDfC2nWwaaiaVlXu3IG3FvSa4IqSn7dMGT16Q6wDNUJX/51IO4mifbEipXnMKDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-type-of": "^2.2.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmmirror.com/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-timers": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/safe-timers/-/safe-timers-1.1.0.tgz", + "integrity": "sha512-9aqY+v5eMvmRaluUEtdRThV1EjlSElzO7HuCj0sTW9xvp++8iJ9t/RWGNWV6/WHcUJLHpyT2SNf/apoKTU2EpA==", + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/scmp": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/scmp/-/scmp-2.0.0.tgz", + "integrity": "sha512-FaHoAk75AYhT+rnBmMpkvHSIcQma4OHzYXOhn1XXtgNomi0FTV8YEXYuh2EIdCg5IKMVyFbXeJT4Cn96+fzABg==", + "license": "BSD-3-Clause" + }, + "node_modules/sdk-base": { + "version": "4.2.1", + "resolved": "https://registry.npmmirror.com/sdk-base/-/sdk-base-4.2.1.tgz", + "integrity": "sha512-B33iy/AkIpLyxexn/5v+8jmYHCyUaJizEQcmhvMY+SoiXViY1FEULbQibQH/EcOfzPwJqJ5UDeXyUJGO47Sq3g==", + "license": "MIT", + "dependencies": { + "await-event": "^2.1.0", + "await-first": "^1.0.0", + "co": "^4.6.0", + "p-timeout": "^4.1.0" + } + }, + "node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmmirror.com/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/sendmessage": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/sendmessage/-/sendmessage-2.0.0.tgz", + "integrity": "sha512-//UAqD/jhpCAQd4zRJ8KRv5FoaM2jeT94p+C2DlR0yGXZyjJTIQnl/fG7KpLo+x6tH+1SglUUh88C6LOxtaZbA==", + "license": "MIT", + "engines": { + "node": ">= 14.17.0" + } + }, + "node_modules/sentence-case": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/sentence-case/-/sentence-case-2.1.1.tgz", + "integrity": "sha512-ENl7cYHaK/Ktwk5OTD+aDbQ3uC8IByu/6Bkg+HDv8Mm+XnBnppVNalcfJTNsp1ibstKh030/JKQQWglDvtKwEQ==", + "license": "MIT", + "dependencies": { + "no-case": "^2.2.0", + "upper-case-first": "^1.1.2" + } + }, + "node_modules/sequelize": { + "version": "6.37.8", + "resolved": "https://registry.npmmirror.com/sequelize/-/sequelize-6.37.8.tgz", + "integrity": "sha512-HJ0IQFqcTsTiqbEgiuioYFMSD00TP6Cz7zoTti+zVVBwVe9fEhev9cH6WnM3XU31+ABS356durAb99ZuOthnKw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/sequelize" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.8", + "@types/validator": "^13.7.17", + "debug": "^4.3.4", + "dottie": "^2.0.6", + "inflection": "^1.13.4", + "lodash": "^4.17.21", + "moment": "^2.29.4", + "moment-timezone": "^0.5.43", + "pg-connection-string": "^2.6.1", + "retry-as-promised": "^7.0.4", + "semver": "^7.5.4", + "sequelize-pool": "^7.1.0", + "toposort-class": "^1.0.1", + "uuid": "^8.3.2", + "validator": "^13.9.0", + "wkx": "^0.5.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependenciesMeta": { + "ibm_db": { + "optional": true + }, + "mariadb": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "oracledb": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-hstore": { + "optional": true + }, + "snowflake-sdk": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "tedious": { + "optional": true + } + } + }, + "node_modules/sequelize-cli": { + "version": "6.6.5", + "resolved": "https://registry.npmmirror.com/sequelize-cli/-/sequelize-cli-6.6.5.tgz", + "integrity": "sha512-DqyISCULOaEbTM+rRQH4YvcUWeOC1XDiSKcjsC6TfAnT7W837mNkChJhtB/Z4FdCFHRCojmiP7zsrA4pARmacA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fs-extra": "^9.1.0", + "js-beautify": "1.15.4", + "lodash": "^4.17.21", + "picocolors": "^1.1.1", + "resolve": "^1.22.1", + "umzug": "^2.3.0", + "yargs": "^16.2.0" + }, + "bin": { + "sequelize": "lib/sequelize", + "sequelize-cli": "lib/sequelize" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/sequelize-cli/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/sequelize-cli/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/sequelize-cli/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmmirror.com/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/sequelize-cli/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/sequelize-cli/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/sequelize-cli/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sequelize-cli/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sequelize-cli/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/sequelize-cli/node_modules/yargs": { + "version": "16.2.2", + "resolved": "https://registry.npmmirror.com/yargs/-/yargs-16.2.2.tgz", + "integrity": "sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sequelize-cli/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/sequelize-pool": { + "version": "7.1.0", + "resolved": "https://registry.npmmirror.com/sequelize-pool/-/sequelize-pool-7.1.0.tgz", + "integrity": "sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/sequelize/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/sequelize/node_modules/inflection": { + "version": "1.13.4", + "resolved": "https://registry.npmmirror.com/inflection/-/inflection-1.13.4.tgz", + "integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==", + "engines": [ + "node >= 0.4.0" + ], + "license": "MIT" + }, + "node_modules/sequelize/node_modules/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serialize-json": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/serialize-json/-/serialize-json-1.0.3.tgz", + "integrity": "sha512-TJvXOXSUEH4Lh2FNy1mYzNkUyBG7Ti5fRKGAbcpaDX3mLq23aT/5unC+cIFc5JTDi4/BHTaYLhynrboCCYrFaQ==", + "license": "MIT", + "dependencies": { + "debug": "^3.2.6", + "is-type-of": "^1.2.1", + "utility": "^1.15.0" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/serialize-json/node_modules/is-type-of": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/is-type-of/-/is-type-of-1.4.0.tgz", + "integrity": "sha512-EddYllaovi5ysMLMEN7yzHEKh8A850cZ7pykrY1aNRQGn/CDjRDE9qEWbIdt7xGEVJmjBXzU/fNnC4ABTm8tEQ==", + "license": "MIT", + "dependencies": { + "core-util-is": "^1.0.2", + "is-class-hotfix": "~0.0.6", + "isstream": "~0.1.2" + } + }, + "node_modules/serialize-json/node_modules/utility": { + "version": "1.18.0", + "resolved": "https://registry.npmmirror.com/utility/-/utility-1.18.0.tgz", + "integrity": "sha512-PYxZDA+6QtvRvm//++aGdmKG/cI07jNwbROz0Ql+VzFV1+Z0Dy55NI4zZ7RHc9KKpBePNFwoErqIuqQv/cjiTA==", + "license": "MIT", + "dependencies": { + "copy-to": "^2.0.1", + "escape-html": "^1.0.3", + "mkdirp": "^0.5.1", + "mz": "^2.7.0", + "unescape": "^1.0.1" + }, + "engines": { + "node": ">= 0.12.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/should-send-same-site-none": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/should-send-same-site-none/-/should-send-same-site-none-2.0.5.tgz", + "integrity": "sha512-7dig49H7sKnv1v/GPoFQChGgJdEX9s2oy9TQBSD5RbUx7M9CCRjHMaFP06v+DZQNM0K+o8dBhvBAd4eEKirqbQ==", + "license": "MIT" + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/sm3": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/sm3/-/sm3-1.0.3.tgz", + "integrity": "sha512-KyFkIfr8QBlFG3uc3NaljaXdYcsbRy1KrSfc4tsQV8jW68jAktGeOcifu530Vx/5LC+PULHT0Rv8LiI8Gw+c1g==" + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/snake-case": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/snake-case/-/snake-case-2.1.0.tgz", + "integrity": "sha512-FMR5YoPFwOLuh4rRz92dywJjyKYZNLpMn1R5ujVpIYkbA9p01fq8RMg0FkO4M+Yobt4MjHeLTJVm5xFFBHSV2Q==", + "license": "MIT", + "dependencies": { + "no-case": "^2.2.0" + } + }, + "node_modules/socks": { + "version": "2.8.9", + "resolved": "https://registry.npmmirror.com/socks/-/socks-2.8.9.tgz", + "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "6.2.1", + "resolved": "https://registry.npmmirror.com/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", + "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/socks-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmmirror.com/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/split": { + "version": "0.3.3", + "resolved": "https://registry.npmmirror.com/split/-/split-0.3.3.tgz", + "integrity": "sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA==", + "license": "MIT", + "dependencies": { + "through": "2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/split2": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/split2/-/split2-2.2.0.tgz", + "integrity": "sha512-RAb22TG39LhI31MbreBgIuKiIKhVsawfTgEGqKHTK87aG+ul/PB8Sqoi3I7kVdRWiCfrKxK3uo4/YUkpNvhPbw==", + "license": "ISC", + "dependencies": { + "through2": "^2.0.2" + } + }, + "node_modules/sql-escaper": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/sql-escaper/-/sql-escaper-1.3.3.tgz", + "integrity": "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=2.0.0", + "node": ">=12.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/mysqljs/sql-escaper?sponsor=1" + } + }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmmirror.com/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmmirror.com/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stream-combiner": { + "version": "0.0.4", + "resolved": "https://registry.npmmirror.com/stream-combiner/-/stream-combiner-0.0.4.tgz", + "integrity": "sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==", + "license": "MIT", + "dependencies": { + "duplexer": "~0.1.1" + } + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "license": "MIT" + }, + "node_modules/stream-slice": { + "version": "0.1.2", + "resolved": "https://registry.npmmirror.com/stream-slice/-/stream-slice-0.1.2.tgz", + "integrity": "sha512-QzQxpoacatkreL6jsxnVb7X5R/pGw9OUv2qWTYWnmLpg4NdN31snPy/f3TdQE1ZUXaThRvj1Zw4/OGg0ZkaLMA==", + "license": "MIT" + }, + "node_modules/stream-wormhole": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/stream-wormhole/-/stream-wormhole-1.1.0.tgz", + "integrity": "sha512-gHFfL3px0Kctd6Po0M8TzEvt3De/xu6cnRrjlfYNhwbhLPLwigI2t1nc6jrzNuaYg5C4YF78PPFuQPzRiqn9ew==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/streamx": { + "version": "2.28.0", + "resolved": "https://registry.npmmirror.com/streamx/-/streamx-2.28.0.tgz", + "integrity": "sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==", + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/superagent": { + "version": "8.1.2", + "resolved": "https://registry.npmmirror.com/superagent/-/superagent-8.1.2.tgz", + "integrity": "sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.4", + "debug": "^4.3.4", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.0", + "formidable": "^2.1.2", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.0", + "semver": "^7.3.8" + }, + "engines": { + "node": ">=6.4.0 <13 || >=14" + } + }, + "node_modules/superagent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/superagent/node_modules/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/supertest": { + "version": "6.3.4", + "resolved": "https://registry.npmmirror.com/supertest/-/supertest-6.3.4.tgz", + "integrity": "sha512-erY3HFDG0dPnhw4U+udPfrzXa4xhSG+n4rxfRuZWCUvjFWwKl+OxWf/7zk50s84/fAAs7vf5QAb9uRa0cCykxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "methods": "^1.1.2", + "superagent": "^8.1.2" + }, + "engines": { + "node": ">=6.4.0" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-hyperlinks": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", + "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svg-captcha": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/svg-captcha/-/svg-captcha-1.4.0.tgz", + "integrity": "sha512-/fkkhavXPE57zRRCjNqAP3txRCSncpMx3NnNZL7iEoyAtYwUjPhJxW6FQTQPG5UPEmCrbFoXS10C3YdJlW7PDg==", + "license": "MIT", + "dependencies": { + "opentype.js": "^0.7.3" + }, + "engines": { + "node": ">=4.x" + } + }, + "node_modules/swap-case": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/swap-case/-/swap-case-1.1.2.tgz", + "integrity": "sha512-BAmWG6/bx8syfc6qXPprof3Mn5vQgf5dwdUNJhsNqU9WdPt5P+ES/wQ5bxfijy8zwZgZZHslC3iAsxsuQMCzJQ==", + "license": "MIT", + "dependencies": { + "lower-case": "^1.1.1", + "upper-case": "^1.1.1" + } + }, + "node_modules/table-layout": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/table-layout/-/table-layout-1.0.2.tgz", + "integrity": "sha512-qd/R7n5rQTRFi+Zf2sk5XVVd9UQl6ZkduPFC3S7WEGJAmetDTjY3qPN50eSKzwuzEyQKy5TN2TiZdkIjos2L6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-back": "^4.0.1", + "deep-extend": "~0.6.0", + "typical": "^5.2.0", + "wordwrapjs": "^4.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/tar-stream": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/tar-stream/-/tar-stream-3.2.0.tgz", + "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/tcomb": { + "version": "3.2.29", + "resolved": "https://registry.npmmirror.com/tcomb/-/tcomb-3.2.29.tgz", + "integrity": "sha512-di2Hd1DB2Zfw6StGv861JoAF5h/uQVu/QJp2g8KVbtfKnoHdBQl5M32YWq6mnSYBQ1vFFrns5B1haWJL7rKaOQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tcomb-validation": { + "version": "3.4.1", + "resolved": "https://registry.npmmirror.com/tcomb-validation/-/tcomb-validation-3.4.1.tgz", + "integrity": "sha512-urVVMQOma4RXwiVCa2nM2eqrAomHROHvWPuj6UkDGz/eb5kcy0x6P0dVt6kzpUZtYMNoAqJLWmz1BPtxrtjtrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tcomb": "^3.0.0" + } + }, + "node_modules/tcp-base": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/tcp-base/-/tcp-base-3.2.0.tgz", + "integrity": "sha512-fFAqH8QTbheuEbXLdbxTSe31Gkw6Lg3nq4loyrxIXM6+ILGdbYXEblgyuu7UltOkOHbP/q2iqaC+gIXXu0C5bg==", + "license": "MIT", + "dependencies": { + "is-type-of": "^1.0.0", + "sdk-base": "^3.1.1" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/tcp-base/node_modules/is-type-of": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/is-type-of/-/is-type-of-1.4.0.tgz", + "integrity": "sha512-EddYllaovi5ysMLMEN7yzHEKh8A850cZ7pykrY1aNRQGn/CDjRDE9qEWbIdt7xGEVJmjBXzU/fNnC4ABTm8tEQ==", + "license": "MIT", + "dependencies": { + "core-util-is": "^1.0.2", + "is-class-hotfix": "~0.0.6", + "isstream": "~0.1.2" + } + }, + "node_modules/tcp-base/node_modules/sdk-base": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/sdk-base/-/sdk-base-3.6.0.tgz", + "integrity": "sha512-jxHUIrRLlAoRFRwiXKhOGjd6BeFWO/jz7tv+E7lbMSef6F9jzFN2Sv3hLW58oDDKscKaBGG6vQdkbXn7isE7fw==", + "license": "MIT", + "dependencies": { + "await-event": "^2.1.0", + "await-first": "^1.0.0", + "co": "^4.6.0", + "is-type-of": "^1.2.1" + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/terminal-link": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/terminal-link/-/terminal-link-2.1.1.tgz", + "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.2.1", + "supports-hyperlinks": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/test-exclude": { + "version": "7.0.2", + "resolved": "https://registry.npmmirror.com/test-exclude/-/test-exclude-7.0.2.tgz", + "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^10.2.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/test-exclude/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmmirror.com/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmmirror.com/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmmirror.com/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "license": "MIT" + }, + "node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "license": "MIT", + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/through2/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/through2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/through2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/through2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "license": "MIT" + }, + "node_modules/title-case": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/title-case/-/title-case-2.1.1.tgz", + "integrity": "sha512-EkJoZ2O3zdCz3zJsYCsxyq2OC5hrxR9mfdd5I+w8h/tmFfeOxJ+vvkxsKxdmN0WtS9zLdHEgfgVOiMVgv+Po4Q==", + "license": "MIT", + "dependencies": { + "no-case": "^2.2.0", + "upper-case": "^1.0.3" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/toposort-class": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/toposort-class/-/toposort-class-1.0.1.tgz", + "integrity": "sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg==", + "license": "MIT" + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmmirror.com/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", + "license": "MIT", + "engines": { + "node": ">=0.6.x" + } + }, + "node_modules/type-fest": { + "version": "3.13.1", + "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-3.13.1.tgz", + "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmmirror.com/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmmirror.com/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typical": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/typical/-/typical-5.2.0.tgz", + "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmmirror.com/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "license": "MIT", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/umzug": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/umzug/-/umzug-2.3.0.tgz", + "integrity": "sha512-Z274K+e8goZK8QJxmbRPhl89HPO1K+ORFtm6rySPhFKfKc5GHhqdzD0SGhSWHkzoXasqJuItdhorSvY7/Cgflw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bluebird": "^3.7.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/undici": { + "version": "5.29.0", + "resolved": "https://registry.npmmirror.com/undici/-/undici-5.29.0.tgz", + "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "license": "MIT" + }, + "node_modules/unescape": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/unescape/-/unescape-1.0.1.tgz", + "integrity": "sha512-O0+af1Gs50lyH1nUu3ZyYS1cRh01Q/kUKatTOkSs7jukXE6/NebucDVxyiDsA9AQ4JC1V1jUH9EO8JX2nMDgGQ==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/upper-case": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/upper-case/-/upper-case-1.1.3.tgz", + "integrity": "sha512-WRbjgmYzgXkCV7zNVpy5YgrHgbBv126rMALQQMrmzOVC4GM2waQ9x7xtm8VU+1yF2kWyPzI9zbZ48n4vSxwfSA==", + "license": "MIT" + }, + "node_modules/upper-case-first": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/upper-case-first/-/upper-case-first-1.1.2.tgz", + "integrity": "sha512-wINKYvI3Db8dtjikdAqoBbZoP6Q+PZUyfMR7pmwHzjC2quzSkUq5DmPrTtPEqHaz8AGtmsB4TqwapMTM1QAQOQ==", + "license": "MIT", + "dependencies": { + "upper-case": "^1.1.1" + } + }, + "node_modules/urijs": { + "version": "1.19.11", + "resolved": "https://registry.npmmirror.com/urijs/-/urijs-1.19.11.tgz", + "integrity": "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==", + "license": "MIT" + }, + "node_modules/urllib": { + "version": "2.44.1", + "resolved": "https://registry.npmmirror.com/urllib/-/urllib-2.44.1.tgz", + "integrity": "sha512-vreOVvFizoiIz5NK9IYMgUknkriHHBVccn2VFfJhgKz6O2qwm0SgjFk4OpXFRDXpdrTx8EzM1DB0/pejrqXwPA==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.3.0", + "content-type": "^1.0.2", + "default-user-agent": "^1.0.0", + "digest-header": "^1.0.0", + "ee-first": "~1.1.1", + "formstream": "^1.1.0", + "humanize-ms": "^1.2.0", + "iconv-lite": "^0.6.3", + "pump": "^3.0.0", + "qs": "^6.4.0", + "statuses": "^1.3.1", + "utility": "^1.16.1" + }, + "engines": { + "node": ">= 0.10.0" + }, + "peerDependencies": { + "proxy-agent": "^5.0.0" + }, + "peerDependenciesMeta": { + "proxy-agent": { + "optional": true + } + } + }, + "node_modules/urllib-next": { + "name": "urllib", + "version": "3.27.3", + "resolved": "https://registry.npmmirror.com/urllib/-/urllib-3.27.3.tgz", + "integrity": "sha512-24ht/hHAkhWXbqSCM499B8gFPXaf4aFQJNqJH5pMsvfyo+aM3zKuQ/uiZ/mdz41Nnw6ItuiqMVMK5VDAlAHLlw==", + "license": "MIT", + "dependencies": { + "default-user-agent": "^1.0.0", + "digest-header": "^1.0.0", + "form-data-encoder": "^1.7.2", + "formdata-node": "^4.3.3", + "formstream": "^1.1.1", + "mime-types": "^2.1.35", + "pump": "^3.0.0", + "qs": "^6.11.2", + "type-fest": "^4.3.1", + "undici": "^5.28.2", + "ylru": "^1.3.2" + }, + "engines": { + "node": ">= 14.19.3" + } + }, + "node_modules/urllib-next/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/urllib/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/urllib/node_modules/utility": { + "version": "1.18.0", + "resolved": "https://registry.npmmirror.com/utility/-/utility-1.18.0.tgz", + "integrity": "sha512-PYxZDA+6QtvRvm//++aGdmKG/cI07jNwbROz0Ql+VzFV1+Z0Dy55NI4zZ7RHc9KKpBePNFwoErqIuqQv/cjiTA==", + "license": "MIT", + "dependencies": { + "copy-to": "^2.0.1", + "escape-html": "^1.0.3", + "mkdirp": "^0.5.1", + "mz": "^2.7.0", + "unescape": "^1.0.1" + }, + "engines": { + "node": ">= 0.12.0" + } + }, + "node_modules/urllib4": { + "name": "urllib", + "version": "4.9.0", + "resolved": "https://registry.npmmirror.com/urllib/-/urllib-4.9.0.tgz", + "integrity": "sha512-Rp3DBvVqy9arSTMH6oN5N7OAN01U3dQ2xc19AadP86MzC4j67MDeqOup5shpWnjkDVGWfLfWPOd8CyqOtd6u3w==", + "license": "MIT", + "dependencies": { + "form-data": "^4.0.1", + "formstream": "^1.5.1", + "mime-types": "^2.1.35", + "qs": "^6.12.1", + "type-fest": "^4.20.1", + "undici": "^7.1.1", + "ylru": "^2.0.0" + }, + "engines": { + "node": ">= 18.19.0" + } + }, + "node_modules/urllib4/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/urllib4/node_modules/undici": { + "version": "7.28.0", + "resolved": "https://registry.npmmirror.com/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/urllib4/node_modules/ylru": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ylru/-/ylru-2.0.0.tgz", + "integrity": "sha512-T6hTrKcr9lKeUG0MQ/tO72D3UGptWVohgzpHG8ljU1jeBt2RCjcWxvsTPD8ZzUq1t1FvwROAw1kxg2euvg/THg==", + "license": "MIT", + "engines": { + "node": ">= 18.19.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utility": { + "version": "2.5.0", + "resolved": "https://registry.npmmirror.com/utility/-/utility-2.5.0.tgz", + "integrity": "sha512-lDbOVde5UAKgtxrSyZNhqrTA7f7anba6DTqbsDWgUFk6PZlmr7djqPYw0FnL5a6TbJvRt38VmYqt07zVLzXG2A==", + "license": "MIT", + "dependencies": { + "escape-html": "^1.0.3", + "unescape": "^1.0.1", + "ylru": "^2.0.0" + }, + "engines": { + "node": ">= 16.0.0" + } + }, + "node_modules/utility/node_modules/ylru": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ylru/-/ylru-2.0.0.tgz", + "integrity": "sha512-T6hTrKcr9lKeUG0MQ/tO72D3UGptWVohgzpHG8ljU1jeBt2RCjcWxvsTPD8ZzUq1t1FvwROAw1kxg2euvg/THg==", + "license": "MIT", + "engines": { + "node": ">= 18.19.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmmirror.com/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmmirror.com/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/v8-to-istanbul/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/validator": { + "version": "13.15.35", + "resolved": "https://registry.npmmirror.com/validator/-/validator-13.15.35.tgz", + "integrity": "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmmirror.com/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, + "node_modules/win-release": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/win-release/-/win-release-1.1.1.tgz", + "integrity": "sha512-iCRnKVvGxOQdsKhcQId2PXV1vV3J/sDPXKA4Oe9+Eti2nb2ESEsYHRYls/UjoUW3bIc5ZDO8dTH50A/5iVN+bw==", + "license": "MIT", + "dependencies": { + "semver": "^5.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wkx": { + "version": "0.5.0", + "resolved": "https://registry.npmmirror.com/wkx/-/wkx-0.5.0.tgz", + "integrity": "sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/wordwrapjs": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/wordwrapjs/-/wordwrapjs-4.0.1.tgz", + "integrity": "sha512-kKlNACbvHrkpIw6oPeYDSmdCTu2hdMHoyXLTcUKala++lx5Y+wjJ/e474Jqv5abnVmwxw08DiTuHmw69lJGksA==", + "dev": true, + "license": "MIT", + "dependencies": { + "reduce-flatten": "^2.0.0", + "typical": "^5.2.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/workerpool": { + "version": "6.5.1", + "resolved": "https://registry.npmmirror.com/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==" + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.21.1", + "resolved": "https://registry.npmmirror.com/ws/-/ws-8.21.1.tgz", + "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/wt": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/wt/-/wt-1.2.0.tgz", + "integrity": "sha512-nJttxFyxnIvWBWYcy7Px4XkXbRoEe77by66d0FDof266Mv00vVBNcUGa00rL1+0DdsXG75LG1rwBnBfUa2bNLA==", + "license": "MIT", + "dependencies": { + "debug": "^2.2.0", + "ndir": "^0.1.5", + "sdk-base": "^2.0.1" + }, + "engines": { + "node": ">= 0.12.9" + } + }, + "node_modules/wt/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/wt/node_modules/get-ready": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/get-ready/-/get-ready-1.0.0.tgz", + "integrity": "sha512-mFXCZPJIlcYcth+N8267+mghfYN9h3EhsDa6JSnbA3Wrhh/XFpuowviFcsDeYZtKspQyWyJqfs4O6P8CHeTwzw==", + "license": "MIT" + }, + "node_modules/wt/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/wt/node_modules/sdk-base": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/sdk-base/-/sdk-base-2.0.1.tgz", + "integrity": "sha512-eeG26wRwhtwYuKGCDM3LixCaxY27Pa/5lK4rLKhQa7HBjJ3U3Y+f81MMZQRsDw/8SC2Dao/83yJTXJ8aULuN8Q==", + "license": "MIT", + "dependencies": { + "get-ready": "~1.0.0" + } + }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmmirror.com/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmmirror.com/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmmirror.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xss": { + "version": "1.0.15", + "resolved": "https://registry.npmmirror.com/xss/-/xss-1.0.15.tgz", + "integrity": "sha512-FVdlVVC67WOIPvfOwhoMETV72f6GbW7aOabBC3WxN/oUdoEMDyLz4OgRv5/gck2ZeNqEQu+Tb0kloovXOfpYVg==", + "license": "MIT", + "dependencies": { + "commander": "^2.20.3", + "cssfilter": "0.0.10" + }, + "bin": { + "xss": "bin/xss" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmmirror.com/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmmirror.com/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/yargs/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmmirror.com/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/yargs/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/ylru": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ylru/-/ylru-1.4.0.tgz", + "integrity": "sha512-2OQsPNEmBCvXuFlIni/a+Rn+R2pHW9INm0BxXJ4hVDA8TirqMj+J/Rp9ItLatT/5pZqWwefVrTQcHpixsxnVlA==", + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zip-stream": { + "version": "7.0.5", + "resolved": "https://registry.npmmirror.com/zip-stream/-/zip-stream-7.0.5.tgz", + "integrity": "sha512-dSvYKdvLsAHCDqPOhIwk/q5CvuWtTB3Dgpoe0uVEFjTzIOAmsQpprX25InCvrvJsirEbu1OHyy67n/kAj1Sw/w==", + "license": "MIT", + "dependencies": { + "compress-commons": "^7.0.0", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/zlogger": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/zlogger/-/zlogger-1.1.0.tgz", + "integrity": "sha512-WjRPkjHe4rajXun45zZRAnViTSLkIr2PEFI303u+3NYIkhWqxhpTr0j67XPzhTJMmDH27pGoTSxGg/qP8FiaJA==", + "license": "MIT", + "dependencies": { + "pumpify": "^1.3.5", + "split2": "^2.1.0", + "through2": "^2.0.1" + }, + "engines": { + "node": ">=4.3.1" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..e4c8731 --- /dev/null +++ b/package.json @@ -0,0 +1,70 @@ +{ + "name": "competition-website-backend", + "version": "1.0.0", + "description": "通信与导航应用技术创新赛线上选拔赛网站后端", + "private": true, + "egg": { + "declarations": false, + "typescript": false + }, + "dependencies": { + "@alicloud/dysmsapi20170525": "^4.6.0", + "@alicloud/openapi-client": "^0.4.15", + "@alicloud/tea-util": "^1.4.11", + "@artus/core": "^2.2.4", + "@eggjs/router": "^3.0.6", + "archiver": "^8.0.0", + "bcryptjs": "^3.0.3", + "cliui": "^9.0.1", + "egg": "^3.34.0", + "egg-cors": "^2.2.4", + "egg-scripts": "^2.17.0", + "egg-sequelize": "^6.0.0", + "egg-validate": "^2.0.2", + "get-ready": "^2.0.1", + "jsonwebtoken": "^9.0.2", + "multer": "^1.4.5-lts.1", + "mysql2": "^3.6.5", + "mz-modules": "^2.1.0", + "svg-captcha": "^1.4.0", + "tslib": "^2.8.1", + "xlsx": "^0.18.0", + "ylru": "^1.4.0" + }, + "devDependencies": { + "egg-bin": "^6.3.0", + "egg-mock": "^5.0.0", + "sequelize-cli": "^6.6.2", + "string-width": "^4.2.3", + "wt": "^1.2.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "scripts": { + "start": "egg-scripts start --daemon --title=competition-website-backend", + "stop": "egg-scripts stop --title=competition-website-backend", + "dev": "egg-bin dev", + "debug": "egg-bin debug", + "test": "npm run lint -- --fix && npm run test-local", + "test-local": "egg-bin test", + "cov": "egg-bin cov", + "lint": "eslint .", + "ci": "npm run cov" + }, + "ci": { + "version": "14, 16", + "type": "github" + }, + "repository": { + "type": "git", + "url": "" + }, + "keywords": [ + "egg", + "competition", + "backend" + ], + "author": "", + "license": "MIT" +} diff --git a/scripts/diagnose_upload_download.cjs b/scripts/diagnose_upload_download.cjs new file mode 100644 index 0000000..98a684b --- /dev/null +++ b/scripts/diagnose_upload_download.cjs @@ -0,0 +1,195 @@ +'use strict'; + +/** + * 生产环境 上传/下载 链路诊断脚本 + * --------------------------------------------------------------------------- + * 用途:在生产后端目录下运行,回答三个问题: + * 1) app/public/{works,answers,materials,tmp} 目录是否存在、是否可写? + * 2) 代码里「上传存盘路径」与「下载读取路径」是否真的指向同一文件?(路径自洽性) + * 3) 数据库里已有的 submission 记录,其 file_path 是否对应磁盘上真实存在的文件? + * + * 运行: + * cd <生产 backend 目录> + * node scripts/diagnose_upload_download.cjs + * # 如需抽查数据库(可选): + * DB_HOST=127.0.0.1 DB_USER=root DB_PASS=74191221 DB_NAME=competition_db \ + * node scripts/diagnose_upload_download.cjs + * + * 不依赖 Egg 启动,纯 Node + fs 即可;DB 部分为可选(拿不到模块/连接失败则跳过)。 + */ + +const fs = require('fs'); +const path = require('path'); + +// ---------- 1. 定位 backend 根目录(app/public 的上一级) ---------- +function resolveBackendRoot() { + // 脚本放在 backend/scripts/ 下时,根目录是 scripts 的上一级 + let here = __dirname; + for (let i = 0; i < 4; i++) { + if (fs.existsSync(path.join(here, 'app', 'public'))) return here; + const parent = path.dirname(here); + if (parent === here) break; + here = parent; + } + // 兜底:以当前工作目录为根 + return process.cwd(); +} + +const root = resolveBackendRoot(); +const publicDir = path.join(root, 'app', 'public'); + +console.log('=================================================='); +console.log(' 上传/下载 链路诊断'); +console.log('=================================================='); +console.log('backend 根目录 :', root); +console.log('app/public :', publicDir); +console.log('app/public 存在:', fs.existsSync(publicDir)); +console.log(''); + +// ---------- 2. 检查各业务目录的存在性与可写性 ---------- +const subDirs = ['works', 'answers', 'materials', 'tmp']; +const groups = ['zhongzhi', 'gaozhi', 'benke', 'jiaoshi']; + +function checkDir(p) { + const exists = fs.existsSync(p); + let type = exists ? (fs.statSync(p).isDirectory() ? '目录' : '非目录!') : '不存在'; + let writable = false; + if (exists) { + try { + const probe = path.join(p, '.write_probe_' + Date.now()); + fs.writeFileSync(probe, 'x'); + fs.unlinkSync(probe); + writable = true; + } catch (e) { + writable = false; + } + } + return { exists, type, writable }; +} + +console.log('--- 2.1 业务目录存在性 / 可写性 ---'); +const dirReport = {}; +for (const d of subDirs) { + const full = path.join(publicDir, d); + const r = checkDir(full); + dirReport[d] = r; + console.log( + ` app/public/${d.padEnd(9)} : ${r.type.padEnd(8)} 可写=${r.writable}` + ); + if (d === 'works' || d === 'materials') { + for (const g of groups) { + const gp = path.join(full, g); + const gr = checkDir(gp); + console.log( + ` └─ ${g.padEnd(9)} : ${gr.type.padEnd(8)} 可写=${gr.writable}` + ); + } + } +} +console.log(''); + +// ---------- 3. 路径自洽性模拟(与源码逻辑一致) ---------- +console.log('--- 3. 路径自洽性(模拟一次上传→下载应指向同一文件)---'); +function simulate(group, safeName) { + // 上传存盘:service.upload.saveWorkFile / admin.uploadMaterial / exam.uploadWord + const savedAbs = path.join(publicDir, 'works', group, safeName); // 例:works/benke/... + // 下载读取:admin.downloadSubmissionFile -> path.join(app.baseDir,'app/public',submission.file_path) + const dbRelative = path.join('works', group, safeName).replace(/\\/g, '/'); + const readAbs = path.join(publicDir, dbRelative); + const ok = path.resolve(savedAbs) === path.resolve(readAbs); + console.log(` ${group}/${safeName}`); + console.log(` 上传落盘路径 : ${savedAbs}`); + console.log(` 下载读取路径 : ${readAbs}`); + console.log(` 自洽一致 : ${ok ? '✓ YES' : '✗ NO —— 代码有路径错配!'}`); + return ok; +} +const selfConsistent = + simulate('benke', 'work_1700000000000_ab12cd.docx') && + simulate('zhongzhi', 'exam_1_1700000000000.docx') && + simulate('zhongzhi', '报告.pdf'); +console.log(''); + +// ---------- 4. 抽查数据库里 submission 的 file_path 是否真实存在 ---------- +console.log('--- 4. 数据库 submission.file_path 真实性抽查(可选)---'); +const dbCfg = { + host: process.env.DB_HOST || '127.0.0.1', + user: process.env.DB_USER || 'root', + password: process.env.DB_PASS || '74191221', + database: process.env.DB_NAME || 'competition_db', +}; + +(async () => { + let mysqlOk = false; + try { + const mysql = require(path.join(root, 'node_modules', 'mysql2', 'promise')); + const conn = await mysql.createConnection({ + host: dbCfg.host, + user: dbCfg.user, + password: dbCfg.password, + database: dbCfg.database, + connectTimeout: 4000, + }); + mysqlOk = true; + console.log(` 已连接数据库 ${dbCfg.database} @ ${dbCfg.host}`); + + const tables = [ + { name: 'benke_submissions', label: '本科', col: 'file_path' }, + { name: 'jiaoshi_submissions', label: '教师', col: 'file_path' }, + { name: 'exam_submissions', label: '中职/高职(word)', col: 'word_file_path' }, + ]; + for (const t of tables) { + try { + const [rows] = await conn.query( + `SELECT id, ${t.col} FROM ${t.name} WHERE ${t.col} IS NOT NULL AND ${t.col} <> '' LIMIT 10` + ); + console.log(` [${t.label}] ${t.name}.${t.col} 抽样 ${rows.length} 条:`); + if (rows.length === 0) { + console.log(' (无带路径的记录,或该表无文件)'); + } + for (const r of rows) { + const fp = r[t.col]; + const abs = path.join(publicDir, fp); + const exists = fs.existsSync(abs); + console.log( + ` id=${r.id} file_path="${fp}" -> 磁盘${exists ? '✓ 存在' : '✗ 缺失!'}` + ); + if (!exists) console.log(` 解析绝对路径: ${abs}`); + } + } catch (e) { + console.log(` [${t.label}] 查询 ${t.name} 失败: ${e.message}`); + } + } + await conn.end(); + } catch (e) { + console.log(` 数据库检查跳过(未连接: ${e.message})`); + console.log(' 如需抽查,请在环境变量中提供 DB_HOST/DB_USER/DB_PASS/DB_NAME 后重跑。'); + } + console.log(''); + + // ---------- 5. 结论 ---------- + console.log('=================================================='); + console.log(' 诊断结论'); + console.log('=================================================='); + const dirsMissing = subDirs.filter(d => !dirReport[d].exists); + const dirsNotWritable = subDirs.filter(d => dirReport[d].exists && !dirReport[d].writable); + if (dirsMissing.length) { + console.log(`✗ 关键目录缺失: app/public/${dirsMissing.join(', ')}`); + console.log(' → 上传文件无处落盘,下载必然 404。请在服务器后端目录执行:'); + for (const d of dirsMissing) { + console.log(` mkdir -p app/public/${d}/{zhongzhi,gaozhi,benke,jiaoshi}`); + } + } else { + console.log('✓ 业务目录均存在'); + } + if (dirsNotWritable.length) { + console.log(`✗ 目录不可写: app/public/${dirsNotWritable.join(', ')}`); + console.log(' → 检查进程用户对该目录的写权限(chown/chmod)。'); + } else { + console.log('✓ 业务目录均可写'); + } + console.log(selfConsistent ? '✓ 代码路径自洽(上传/下载指向同一文件)' : '✗ 代码路径不自洽,需修代码'); + if (mysqlOk) { + console.log(' → 查看上方「磁盘缺失」标记:若有,说明文件没落盘(上传未成功或目录曾缺失)。'); + } + console.log('=================================================='); +})(); diff --git a/seed-carousel.js b/seed-carousel.js new file mode 100644 index 0000000..8c3467e --- /dev/null +++ b/seed-carousel.js @@ -0,0 +1,40 @@ +const { Sequelize } = require('./node_modules/sequelize'); +const sequelize = new Sequelize('competition_db', 'root', '74191221', { + host: 'localhost', port: 3306, dialect: 'mysql', + logging: false +}); + +(async () => { + try { + await sequelize.authenticate(); + console.log('DB connected'); + + // MariaDB 5.5 compatible: only one TIMESTAMP can have DEFAULT CURRENT_TIMESTAMP + await sequelize.query(` + CREATE TABLE IF NOT EXISTS carousel ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT, + image_url VARCHAR(500) NOT NULL, + sort_order INT NOT NULL DEFAULT 0, + link_url VARCHAR(500) NULL, + enabled TINYINT(1) NOT NULL DEFAULT 1, + created_at DATETIME NOT NULL, + updated_at DATETIME NOT NULL, + PRIMARY KEY (id), + KEY idx_sort_order (sort_order), + KEY idx_enabled (enabled) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + `); + + const [rows] = await sequelize.query('SELECT COUNT(*) as cnt FROM carousel'); + console.log('Existing:', rows[0].cnt); + + if (rows[0].cnt === 0) { + await sequelize.query("INSERT INTO carousel (image_url, sort_order, enabled, created_at, updated_at) VALUES ('https://images.unsplash.com/photo-1446776811953-b23d57bd21aa?w=1200', 1, 1, NOW(), NOW())"); + await sequelize.query("INSERT INTO carousel (image_url, sort_order, enabled, created_at, updated_at) VALUES ('https://images.unsplash.com/photo-1451187580459-43490279c0fa?w=1200', 2, 1, NOW(), NOW())"); + await sequelize.query("INSERT INTO carousel (image_url, sort_order, enabled, created_at, updated_at) VALUES ('https://images.unsplash.com/photo-1614728263952-84ea256f9679?w=1200', 3, 1, NOW(), NOW())"); + console.log('Inserted 3 carousel slides'); + } + console.log('Done'); + } catch(e) { console.error(e.message); } + finally { await sequelize.close(); } +})(); diff --git a/show_tables.js b/show_tables.js new file mode 100644 index 0000000..4df1a98 --- /dev/null +++ b/show_tables.js @@ -0,0 +1,9 @@ +const mysql = require('mysql2/promise'); +(async () => { + const c = await mysql.createConnection({ + host: 'localhost', user: 'root', password: '74191221', database: 'competition_db' + }); + const [t] = await c.query('SHOW TABLES'); + t.forEach(r => console.log(Object.values(r)[0])); + await c.end(); +})(); \ No newline at end of file diff --git a/start-dev.js b/start-dev.js new file mode 100644 index 0000000..7c4a0b7 --- /dev/null +++ b/start-dev.js @@ -0,0 +1,8 @@ +// Direct egg cluster starter - avoids PowerShell JSON quoting issues +const egg = require('./node_modules/egg'); +egg.startCluster({ + baseDir: __dirname, + workers: 1, + port: 7001, + framework: __dirname + '\\node_modules\\egg', +}); \ No newline at end of file diff --git a/unit_auth_catch.cjs b/unit_auth_catch.cjs new file mode 100644 index 0000000..37013bd --- /dev/null +++ b/unit_auth_catch.cjs @@ -0,0 +1,18 @@ +const jwt = require('jsonwebtoken'); +const authFactory = require('./app/middleware/auth.js'); +const SECRET = 'unit-test-secret'; +const token = jwt.sign({ id: 1, role: 'contestant' }, SECRET); +function makeCtx() { + return { get: (h) => (h === 'Authorization' ? 'Bearer ' + token : ''), + status: 200, body: null, state: {}, + app: { config: { jwt: { secret: SECRET } } } }; +} +const throwingNext = async () => { throw new Error('EACCES: readdirSync failed'); }; +const mw = authFactory({}, { config: { jwt: { secret: SECRET } } }); +(async () => { + try { await mw(makeCtx(), throwingNext); console.log('RESULT: 未抛错(不应发生)'); } + catch (e) { + console.log('RESULT: 中间件重新抛出异常 ->', e.message); + console.log('=> 异常被 Egg 全局错误处理为 500+日志,【不会】再被伪装成 401「认证失败」'); + } +})();