This commit is contained in:
biss committed 2026-07-20 20:09:26 +08:00
1 parent a6e07eebbb
commit 5a33a6bcc4
5 files changed
+91 -39

No files matched your search

+26 -7
View File
@@ -166,15 +166,34 @@ npm run seed-test-data:mysql -- --force
测试数据脚本会提供以下账号;其中初始超级管理员也可能由正常首次建库创建,并可通过环境变量改名、改密,其余校级、班级和考生账号不会在正常启动时创建: 测试数据脚本会提供以下账号;其中初始超级管理员也可能由正常首次建库创建,并可通过环境变量改名、改密,其余校级、班级和考生账号不会在正常启动时创建:
为了便于临时联调,所有预置样例账号统一使用密码 `12345678`,且预置考生不会在首次登录时被要求改密。通过系统业务流程后续新建的账号仍按正式规则生成随机初始密码。
| 角色 | 账号 | 密码 | | 角色 | 账号 | 密码 |
| --- | --- | --- | | --- | --- | --- |
| 超级管理员 | `admin` | `Admin123!` | | 超级管理员 | `admin` | `12345678` |
| 超级管理员(监督演示) | `supervisor` | `Admin123!` | | 超级管理员(监督演示) | `supervisor` | `12345678` |
| 校级管理员 | `school_admin` | `School123!` | | 校级管理员 | `school_admin` | `12345678` |
| 同校校级管理员(转交演示) | `school_admin_2` | `School123!` | | 同校校级管理员(转交演示) | `school_admin_2` | `12345678` |
| 班级管理员 | `class_admin` | `Class123!` | | 班级管理员 | `class_admin` | `12345678` |
| 同班班级管理员(均分演示) | `class_admin_2` | `Class123!` | | 同班班级管理员(均分演示) | `class_admin_2` | `12345678` |
| 考生(首次登录需改密) | `2026-HZ01-F-0001` | `Candidate123!` | | 考生 | `2026-HZ01-F-0001` | `12345678` |
## 测试结束后初始化系统
先停止应用,然后运行以下命令。命令会读取 `.env` 并自动选择 SQLite 或 MySQL,删除样例学校、考生、考试、报名等业务数据,恢复系统基础配置和一个初始超级管理员:
```powershell
npm run initialize-system
```
也可以明确指定数据库类型:
```powershell
npm run initialize-system:sqlite
npm run initialize-system:mysql
```
MySQL 模式会自动识别由本项目生成的批量样例数据并清理。若目标包含无法识别为样例数据的业务记录,命令会拒绝执行;只有明确确认目标可完全清空时才可运行 `npm run initialize-system:mysql -- --force`。初始化完成后,初始管理员账号由 `.env` 中的 `INITIAL_ADMIN_USERNAME`、`INITIAL_ADMIN_PASSWORD`、`INITIAL_ADMIN_DISPLAY_NAME` 决定,然后再重新启动应用。
## 自动化测试 ## 自动化测试
+4 -1
View File
@@ -9,7 +9,10 @@
"reset-db": "node scripts/reset-dev-database.mjs", "reset-db": "node scripts/reset-dev-database.mjs",
"seed-test-data": "node scripts/import-test-data.mjs", "seed-test-data": "node scripts/import-test-data.mjs",
"seed-test-data:sqlite": "node scripts/import-test-data.mjs --sqlite", "seed-test-data:sqlite": "node scripts/import-test-data.mjs --sqlite",
"seed-test-data:mysql": "node scripts/import-test-data.mjs --mysql" "seed-test-data:mysql": "node scripts/import-test-data.mjs --mysql",
"initialize-system": "node scripts/import-test-data.mjs --empty",
"initialize-system:sqlite": "node scripts/import-test-data.mjs --empty --sqlite",
"initialize-system:mysql": "node scripts/import-test-data.mjs --empty --mysql"
}, },
"dependencies": { "dependencies": {
"ckeditor5": "^48.3.1", "ckeditor5": "^48.3.1",
+38 -11
View File
@@ -4,6 +4,7 @@ import { rm } from 'node:fs/promises';
import { isAbsolute, join, relative, resolve } from 'node:path'; import { isAbsolute, join, relative, resolve } from 'node:path';
import { loadEnvFile } from 'node:process'; import { loadEnvFile } from 'node:process';
import { buildSeedOperations, createDatabase, relationalTables } from '../database.mjs'; import { buildSeedOperations, createDatabase, relationalTables } from '../database.mjs';
import { createBaseDatabase } from '../src/data/base.mjs';
import { createSeedDatabase } from '../src/data/seed.mjs'; import { createSeedDatabase } from '../src/data/seed.mjs';
const root = resolve(process.cwd()); const root = resolve(process.cwd());
@@ -17,13 +18,24 @@ const client = String(configuredClient || (process.env.NODE_ENV === 'production'
if (!['sqlite', 'mysql'].includes(client)) throw new Error(`不支持的 DATABASE_CLIENT:${client}`); if (!['sqlite', 'mysql'].includes(client)) throw new Error(`不支持的 DATABASE_CLIENT:${client}`);
process.env.DATABASE_CLIENT = client; process.env.DATABASE_CLIENT = client;
const force = options.has('--force'); const force = options.has('--force');
const initializeEmpty = options.has('--empty');
function hashPassword(password, salt = randomBytes(16).toString('hex')) { function hashPassword(password, salt = randomBytes(16).toString('hex')) {
const hash = pbkdf2Sync(password, salt, 120000, 32, 'sha256').toString('hex'); const hash = pbkdf2Sync(password, salt, 120000, 32, 'sha256').toString('hex');
return `${salt}:${hash}`; return `${salt}:${hash}`;
} }
const seed = () => createSeedDatabase({ nowIso: () => new Date().toISOString(), hashPassword }); const createTargetState = () => initializeEmpty
? createBaseDatabase({
nowIso: () => new Date().toISOString(),
hashPassword,
initialAdmin: {
username: process.env.INITIAL_ADMIN_USERNAME,
password: process.env.INITIAL_ADMIN_PASSWORD,
displayName: process.env.INITIAL_ADMIN_DISPLAY_NAME
}
})
: createSeedDatabase({ nowIso: () => new Date().toISOString(), hashPassword });
async function prepareSqlite() { async function prepareSqlite() {
const databasePath = resolve(process.env.SQLITE_PATH || join(root, 'data', 'exam.sqlite')); const databasePath = resolve(process.env.SQLITE_PATH || join(root, 'data', 'exam.sqlite'));
@@ -100,19 +112,28 @@ async function prepareMysql() {
} }
const [[userRow]] = await connection.query('SELECT COUNT(*) AS count FROM users'); const [[userRow]] = await connection.query('SELECT COUNT(*) AS count FROM users');
if (Number(userRow.count) > 1) nonEmpty.push(`users=${userRow.count}`); if (Number(userRow.count) > 1) nonEmpty.push(`users=${userRow.count}`);
if (nonEmpty.length && !force) { let recognizedTestData = false;
if (initializeEmpty && nonEmpty.length) {
const [[bulkUsers]] = await connection.query("SELECT COUNT(*) AS count FROM users WHERE id LIKE 'usr_bulk_%'");
const [[testSchools]] = await connection.query("SELECT COUNT(*) AS count FROM schools WHERE id IN ('school_hz1', 'school_hz3', 'school_hz5', 'school_hz7')");
recognizedTestData = Number(bulkUsers.count) >= 300 && Number(testSchools.count) === 4;
}
if (nonEmpty.length && !force && !recognizedTestData) {
const forceCommand = initializeEmpty
? 'npm run initialize-system:mysql -- --force'
: 'npm run seed-test-data:mysql -- --force';
throw new Error( throw new Error(
`MySQL 数据库 ${databaseName} 已有业务数据(${nonEmpty.slice(0, 8).join(', ')}${nonEmpty.length > 8 ? ', ...' : ''})。` + `MySQL 数据库 ${databaseName} 已有业务数据(${nonEmpty.slice(0, 8).join(', ')}${nonEmpty.length > 8 ? ', ...' : ''})。` +
'如确认这是可覆盖的测试库,请运行 npm run seed-test-data:mysql -- --force' `如确认这是可覆盖的测试库,请运行 ${forceCommand}`
); );
} }
if (nonEmpty.length) { if (nonEmpty.length) {
console.warn(`[force] Replacing existing business data in MySQL database ${databaseName}`); console.warn(`[${recognizedTestData ? 'test-data cleanup' : 'force'}] Replacing existing business data in MySQL database ${databaseName}`);
} else { } else {
console.log(`Preparing empty MySQL database ${databaseName} for test data`); console.log(`Preparing empty MySQL database ${databaseName} for ${initializeEmpty ? 'system initialization' : 'test data'}`);
} }
const state = seed(); const state = createTargetState();
await connection.query('SET FOREIGN_KEY_CHECKS = 0'); await connection.query('SET FOREIGN_KEY_CHECKS = 0');
await connection.beginTransaction(); await connection.beginTransaction();
try { try {
@@ -145,7 +166,7 @@ if (client === 'sqlite') {
} }
const database = await createDatabase({ const database = await createDatabase({
root, root,
seed seed: createTargetState
}); });
const state = await database.read(); const state = await database.read();
await database.close(); await database.close();
@@ -154,7 +175,13 @@ const pending = state.registrations.filter(item => item.status === 'pending').le
const rejected = state.registrations.filter(item => item.status === 'rejected').length; const rejected = state.registrations.filter(item => item.status === 'rejected').length;
const unpaid = state.registrations.filter(item => item.status === 'approved' && item.paymentStatus === 'unpaid').length; const unpaid = state.registrations.filter(item => item.status === 'approved' && item.paymentStatus === 'unpaid').length;
const paid = state.registrations.filter(item => item.status === 'approved' && item.paymentStatus === 'paid').length; const paid = state.registrations.filter(item => item.status === 'approved' && item.paymentStatus === 'paid').length;
console.log(`Imported test data into ${location}${mysqlAlreadyImported ? ' (transactional replace)' : ''}`); if (initializeEmpty) {
console.log(`${state.schools.length} schools, ${state.candidateProfiles.length} candidates, ${state.registrations.length} registrations`); console.log(`Initialized empty system in ${location}${mysqlAlreadyImported ? ' (transactional replace)' : ''}`);
console.log(`pending ${pending}, rejected ${rejected}, approved/unpaid ${unpaid}, approved/paid ${paid}`); console.log(`${state.users.length} initial administrator, ${state.schools.length} schools, ${state.candidateProfiles.length} candidates, ${state.registrations.length} registrations`);
console.log(`arrangement plans ${state.arrangementPlans.length}, admit cards ${state.registrations.filter(item => item.admitCard).length}`); } else {
console.log(`Imported test data into ${location}${mysqlAlreadyImported ? ' (transactional replace)' : ''}`);
console.log(`${state.schools.length} schools, ${state.candidateProfiles.length} candidates, ${state.registrations.length} registrations`);
console.log(`pending ${pending}, rejected ${rejected}, approved/unpaid ${unpaid}, approved/paid ${paid}`);
console.log(`arrangement plans ${state.arrangementPlans.length}, admit cards ${state.registrations.filter(item => item.admitCard).length}`);
console.log('all predefined test account passwords: 12345678');
}
Loaded 3 of 5 files, more files were not shown because too many files have changed in this diff. Show more