修复
This commit is contained in:
1 parent
a6e07eebbb
commit
5a33a6bcc4
5 files changed
+91
-39
No files matched your search
@@ -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
@@ -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",
|
||||||
|
|||||||
@@ -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
Reference in new issue
Block a user