已完成迁移验收和清理。
删除全部 Node.js 后端、.mjs 测试、辅助脚本、package*.json、node_modules 和迁移对照工具。 浏览器前端保留并迁入 [Eis.Web/wwwroot](C:/Users/BI/Documents/EIS-dotnet/src/Eis.Web/wwwroot/index.html),统一改为 .js 模块。 修复一个隐藏遗漏:.NET 仍依赖 schema.mjs,且 MySQL 结构解析可能被转义反引号截断。现已改为原生 [SQLite SQL](C:/Users/BI/Documents/EIS-dotnet/src/Eis.Infrastructure/Data/Schema.sqlite.sql) 和 [MySQL SQL](C:/Users/BI/Documents/EIS-dotnet/src/Eis.Infrastructure/Data/Schema.mysql.sql) 资源。 更新了 [DatabaseInitializer.cs](C:/Users/BI/Documents/EIS-dotnet/src/Eis.Infrastructure/Data/DatabaseInitializer.cs)、项目配置、README 和迁移文档。 data 目录和现有数据库未修改;测试仅使用系统临时 SQLite 数据库。
This commit is contained in:
@@ -20,7 +20,7 @@ internal sealed class RegionCatalog
|
||||
{
|
||||
var assembly = typeof(RegionCatalog).Assembly;
|
||||
var resourceName = assembly.GetManifestResourceNames()
|
||||
.Single(name => name.EndsWith("china-regions.mjs", StringComparison.Ordinal));
|
||||
.Single(name => name.EndsWith("china-regions.js", StringComparison.Ordinal));
|
||||
using var stream = assembly.GetManifestResourceStream(resourceName)
|
||||
?? throw new InvalidOperationException("无法读取内嵌行政区划数据");
|
||||
using var reader = new StreamReader(stream);
|
||||
|
||||
@@ -2,32 +2,34 @@ using System.Data.Common;
|
||||
using System.Globalization;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using Eis.Infrastructure.Authentication;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Eis.Infrastructure.Data;
|
||||
|
||||
internal sealed partial class DatabaseInitializer(
|
||||
internal sealed class DatabaseInitializer(
|
||||
IRelationalConnectionFactory connectionFactory,
|
||||
DatabaseOptions options,
|
||||
PasswordCompatibilityService passwords)
|
||||
{
|
||||
private const int SchemaVersion = 20;
|
||||
private const string MySqlStatementSeparator = "\n-- EIS-STATEMENT\n";
|
||||
|
||||
public async Task InitializeAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await using var connection = await connectionFactory.OpenAsync(cancellationToken);
|
||||
var source = await ReadSchemaSourceAsync(cancellationToken);
|
||||
var schema = await ReadSchemaAsync(options.Client, cancellationToken);
|
||||
if (options.Client == "sqlite")
|
||||
{
|
||||
await ExecuteAsync(connection, ExtractSqliteSchema(source), cancellationToken);
|
||||
await ExecuteAsync(connection, schema, cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (Match match in MySqlStatementPattern().Matches(ExtractMySqlSection(source)))
|
||||
foreach (var statement in schema.ReplaceLineEndings("\n").Split(
|
||||
MySqlStatementSeparator,
|
||||
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||
{
|
||||
await ExecuteAsync(connection, match.Groups["sql"].Value, cancellationToken);
|
||||
await ExecuteAsync(connection, statement, cancellationToken);
|
||||
}
|
||||
}
|
||||
await SeedBaseStateAsync(connection, null, cancellationToken);
|
||||
@@ -264,39 +266,18 @@ internal sealed partial class DatabaseInitializer(
|
||||
await command.ExecuteNonQueryAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private static async Task<string> ReadSchemaSourceAsync(CancellationToken cancellationToken)
|
||||
private static async Task<string> ReadSchemaAsync(string client, CancellationToken cancellationToken)
|
||||
{
|
||||
var assembly = typeof(DatabaseInitializer).Assembly;
|
||||
var resourceSuffix = $"Schema.{client}.sql";
|
||||
var name = assembly.GetManifestResourceNames()
|
||||
.Single(item => item.EndsWith("database-schema.mjs", StringComparison.Ordinal));
|
||||
.Single(item => item.EndsWith(resourceSuffix, StringComparison.Ordinal));
|
||||
await using var stream = assembly.GetManifestResourceStream(name)
|
||||
?? throw new InvalidOperationException("内置数据库结构资源不存在");
|
||||
using var reader = new StreamReader(stream);
|
||||
return await reader.ReadToEndAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private static string ExtractSqliteSchema(string source)
|
||||
{
|
||||
const string startToken = "export const sqliteSchema = `";
|
||||
var start = source.IndexOf(startToken, StringComparison.Ordinal);
|
||||
var mysqlStart = source.IndexOf("export const mysqlSchema", StringComparison.Ordinal);
|
||||
var end = mysqlStart < 0 ? -1 : source.LastIndexOf('`', mysqlStart);
|
||||
if (start < 0 || end <= start) throw new InvalidOperationException("无法读取 SQLite 数据库结构");
|
||||
return source[(start + startToken.Length)..end];
|
||||
}
|
||||
|
||||
private static string ExtractMySqlSection(string source)
|
||||
{
|
||||
const string startToken = "export const mysqlSchema = [";
|
||||
var start = source.IndexOf(startToken, StringComparison.Ordinal);
|
||||
var end = source.IndexOf("];", start, StringComparison.Ordinal);
|
||||
if (start < 0 || end <= start) throw new InvalidOperationException("无法读取 MySQL 数据库结构");
|
||||
return source[(start + startToken.Length)..end];
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"`(?<sql>[\s\S]*?)`\s*,?", RegexOptions.CultureInvariant)]
|
||||
private static partial Regex MySqlStatementPattern();
|
||||
|
||||
private sealed record AdmissionRule(
|
||||
string Id, string Code, string Name, string Description, string Example, AdmissionSegment[] Segments);
|
||||
private sealed record AdmissionSegment(string Source, string Label, int? Width);
|
||||
|
||||
@@ -0,0 +1,570 @@
|
||||
CREATE TABLE IF NOT EXISTS schema_metadata (
|
||||
id TINYINT UNSIGNED NOT NULL,
|
||||
schema_version INT UNSIGNED NOT NULL DEFAULT 1,
|
||||
app_version INT UNSIGNED NOT NULL DEFAULT 1,
|
||||
self_registration_enabled BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_at VARCHAR(35) NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
CONSTRAINT chk_schema_metadata_singleton CHECK (id = 1)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
|
||||
-- EIS-STATEMENT
|
||||
CREATE TABLE IF NOT EXISTS organization (
|
||||
id TINYINT UNSIGNED NOT NULL,
|
||||
name VARCHAR(120) NOT NULL,
|
||||
code VARCHAR(60) NOT NULL,
|
||||
phone VARCHAR(60) NOT NULL,
|
||||
address VARCHAR(255) NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
CONSTRAINT chk_organization_singleton CHECK (id = 1)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
|
||||
-- EIS-STATEMENT
|
||||
CREATE TABLE IF NOT EXISTS schools (
|
||||
id VARCHAR(64) NOT NULL,
|
||||
name VARCHAR(160) NOT NULL,
|
||||
code VARCHAR(40) NOT NULL,
|
||||
address VARCHAR(255) NULL,
|
||||
is_source_school BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
is_admission_school BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uq_schools_name (name),
|
||||
UNIQUE KEY uq_schools_code (code)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
|
||||
-- EIS-STATEMENT
|
||||
CREATE TABLE IF NOT EXISTS school_classes (
|
||||
id VARCHAR(64) NOT NULL,
|
||||
school_id VARCHAR(64) NOT NULL,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
grade VARCHAR(60) NOT NULL,
|
||||
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uq_classes_school_name (school_id, name),
|
||||
CONSTRAINT fk_classes_school FOREIGN KEY (school_id) REFERENCES schools(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
|
||||
-- EIS-STATEMENT
|
||||
CREATE TABLE IF NOT EXISTS school_student_partitions (
|
||||
school_id VARCHAR(64) NOT NULL,
|
||||
partition_key VARCHAR(32) NOT NULL,
|
||||
students_table VARCHAR(64) NOT NULL,
|
||||
created_at VARCHAR(35) NOT NULL,
|
||||
updated_at VARCHAR(35) NOT NULL,
|
||||
PRIMARY KEY (school_id),
|
||||
UNIQUE KEY uq_school_partitions_key (partition_key),
|
||||
UNIQUE KEY uq_school_partitions_table (students_table),
|
||||
CONSTRAINT fk_school_partitions_school FOREIGN KEY (school_id) REFERENCES schools(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
|
||||
-- EIS-STATEMENT
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id VARCHAR(64) NOT NULL,
|
||||
username VARCHAR(100) NOT NULL,
|
||||
candidate_number VARCHAR(120) NULL,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
role ENUM('admin', 'candidate', 'admission_school') NOT NULL,
|
||||
admin_level ENUM('super', 'school', 'class') NULL,
|
||||
school_id VARCHAR(64) NULL,
|
||||
class_id VARCHAR(64) NULL,
|
||||
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
must_change_password BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
totp_enabled BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
totp_secret_encrypted VARCHAR(512) NULL,
|
||||
totp_recovery_codes VARCHAR(2048) NOT NULL DEFAULT '[]',
|
||||
totp_last_used_step BIGINT NULL,
|
||||
archived_at VARCHAR(35) NULL,
|
||||
archived_by VARCHAR(64) NULL,
|
||||
display_name VARCHAR(100) NOT NULL,
|
||||
created_at VARCHAR(35) NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uq_users_username (username),
|
||||
UNIQUE KEY uq_users_candidate_number (candidate_number),
|
||||
KEY idx_users_admin_scope (role, admin_level, school_id, class_id),
|
||||
KEY idx_users_archive_scope (role, school_id, class_id, archived_at),
|
||||
CONSTRAINT fk_users_school FOREIGN KEY (school_id) REFERENCES schools(id) ON DELETE SET NULL,
|
||||
CONSTRAINT fk_users_class FOREIGN KEY (class_id) REFERENCES school_classes(id) ON DELETE SET NULL,
|
||||
CONSTRAINT fk_users_archiver FOREIGN KEY (archived_by) REFERENCES users(id) ON DELETE RESTRICT
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
|
||||
-- EIS-STATEMENT
|
||||
CREATE TABLE IF NOT EXISTS candidate_profiles (
|
||||
id VARCHAR(64) NOT NULL,
|
||||
user_id VARCHAR(64) NOT NULL,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
gender VARCHAR(20) NULL,
|
||||
id_number VARCHAR(60) NOT NULL,
|
||||
phone VARCHAR(60) NOT NULL,
|
||||
email VARCHAR(160) NULL,
|
||||
school VARCHAR(160) NULL,
|
||||
grade VARCHAR(100) NULL,
|
||||
school_id VARCHAR(64) NULL,
|
||||
class_id VARCHAR(64) NULL,
|
||||
province_code VARCHAR(6) NULL,
|
||||
province_name VARCHAR(80) NULL,
|
||||
city_code VARCHAR(6) NULL,
|
||||
city_name VARCHAR(100) NULL,
|
||||
district_code VARCHAR(6) NULL,
|
||||
district_name VARCHAR(100) NULL,
|
||||
address VARCHAR(255) NULL,
|
||||
emergency_contact VARCHAR(100) NULL,
|
||||
emergency_phone VARCHAR(60) NULL,
|
||||
native_place VARCHAR(160) NULL,
|
||||
birth_date VARCHAR(20) NULL,
|
||||
ethnicity VARCHAR(60) NULL,
|
||||
postal_code VARCHAR(20) NULL,
|
||||
guardian_name VARCHAR(100) NULL,
|
||||
guardian_phone VARCHAR(60) NULL,
|
||||
specialty_category VARCHAR(30) NULL,
|
||||
specialty_type VARCHAR(40) NULL,
|
||||
specialty_types JSON NOT NULL DEFAULT (JSON_ARRAY()),
|
||||
specialty_certificate VARCHAR(255) NULL,
|
||||
policy_eligibility VARCHAR(255) NULL,
|
||||
profile_completed BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
status ENUM('pending', 'approved', 'rejected') NOT NULL,
|
||||
review_note VARCHAR(500) NULL,
|
||||
reviewed_at VARCHAR(35) NULL,
|
||||
reviewer_id VARCHAR(64) NULL,
|
||||
updated_at VARCHAR(35) NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uq_profiles_user (user_id),
|
||||
UNIQUE KEY uq_profiles_id_number (id_number),
|
||||
KEY idx_profiles_status (status),
|
||||
CONSTRAINT fk_profiles_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_profiles_reviewer FOREIGN KEY (reviewer_id) REFERENCES users(id) ON DELETE SET NULL,
|
||||
CONSTRAINT fk_profiles_school FOREIGN KEY (school_id) REFERENCES schools(id) ON DELETE SET NULL,
|
||||
CONSTRAINT fk_profiles_class FOREIGN KEY (class_id) REFERENCES school_classes(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
|
||||
-- EIS-STATEMENT
|
||||
CREATE TABLE IF NOT EXISTS notices (
|
||||
id VARCHAR(64) NOT NULL,
|
||||
title VARCHAR(240) NOT NULL,
|
||||
summary VARCHAR(500) NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
category VARCHAR(60) NOT NULL,
|
||||
pinned BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
status ENUM('draft', 'published') NOT NULL,
|
||||
publish_at VARCHAR(35) NULL,
|
||||
created_at VARCHAR(35) NULL,
|
||||
author VARCHAR(100) NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_notices_status_publish (status, publish_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
|
||||
-- EIS-STATEMENT
|
||||
CREATE TABLE IF NOT EXISTS exams (
|
||||
id VARCHAR(64) NOT NULL,
|
||||
code VARCHAR(60) NOT NULL,
|
||||
name VARCHAR(200) NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
registration_start VARCHAR(35) NOT NULL,
|
||||
registration_end VARCHAR(35) NOT NULL,
|
||||
exam_start VARCHAR(35) NOT NULL,
|
||||
exam_end VARCHAR(35) NOT NULL,
|
||||
admit_download_start VARCHAR(35) NOT NULL,
|
||||
admit_download_end VARCHAR(35) NOT NULL,
|
||||
location VARCHAR(200) NOT NULL,
|
||||
pass_policy ENUM('fixed_score', 'score_ratio', 'rank_percent', 'subject_scores', 'none') NOT NULL DEFAULT 'rank_percent',
|
||||
pass_value DOUBLE NOT NULL DEFAULT 60,
|
||||
status ENUM('draft', 'published', 'closed') NOT NULL,
|
||||
archived_at VARCHAR(35) NULL,
|
||||
archived_by VARCHAR(64) NULL,
|
||||
created_at VARCHAR(35) NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uq_exams_code (code),
|
||||
KEY idx_exams_status_registration (status, registration_start, registration_end),
|
||||
KEY idx_exams_archive (archived_at, exam_end),
|
||||
CONSTRAINT fk_exams_archived_by FOREIGN KEY (archived_by) REFERENCES users(id) ON DELETE RESTRICT
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
|
||||
-- EIS-STATEMENT
|
||||
CREATE TABLE IF NOT EXISTS exam_data_partitions (
|
||||
exam_id VARCHAR(64) NOT NULL,
|
||||
partition_key VARCHAR(32) NOT NULL,
|
||||
candidates_table VARCHAR(64) NOT NULL,
|
||||
admissions_table VARCHAR(64) NOT NULL,
|
||||
results_table VARCHAR(64) NOT NULL,
|
||||
centers_table VARCHAR(64) NOT NULL,
|
||||
created_at VARCHAR(35) NOT NULL,
|
||||
updated_at VARCHAR(35) NOT NULL,
|
||||
PRIMARY KEY (exam_id),
|
||||
UNIQUE KEY uq_exam_partitions_key (partition_key),
|
||||
UNIQUE KEY uq_exam_partitions_candidates (candidates_table),
|
||||
UNIQUE KEY uq_exam_partitions_admissions (admissions_table),
|
||||
UNIQUE KEY uq_exam_partitions_results (results_table),
|
||||
UNIQUE KEY uq_exam_partitions_centers (centers_table),
|
||||
CONSTRAINT fk_exam_partitions_exam FOREIGN KEY (exam_id) REFERENCES exams(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
|
||||
-- EIS-STATEMENT
|
||||
CREATE TABLE IF NOT EXISTS exam_subjects (
|
||||
id VARCHAR(64) NOT NULL,
|
||||
exam_id VARCHAR(64) NOT NULL,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
subject_date VARCHAR(35) NOT NULL,
|
||||
start_time VARCHAR(20) NOT NULL,
|
||||
end_time VARCHAR(20) NOT NULL,
|
||||
fee DOUBLE NOT NULL DEFAULT 0,
|
||||
full_score DOUBLE NOT NULL DEFAULT 150,
|
||||
pass_score DOUBLE NOT NULL DEFAULT 90,
|
||||
pass_rule ENUM('fixed_score', 'score_ratio', 'none') NOT NULL DEFAULT 'fixed_score',
|
||||
pass_value DOUBLE NOT NULL DEFAULT 90,
|
||||
position INT UNSIGNED NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uq_subjects_exam_position (exam_id, position),
|
||||
KEY idx_subjects_exam (exam_id, position),
|
||||
CONSTRAINT fk_subjects_exam FOREIGN KEY (exam_id) REFERENCES exams(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
|
||||
-- EIS-STATEMENT
|
||||
CREATE TABLE IF NOT EXISTS registrations (
|
||||
id VARCHAR(64) NOT NULL,
|
||||
user_id VARCHAR(64) NOT NULL,
|
||||
exam_id VARCHAR(64) NOT NULL,
|
||||
status ENUM('pending', 'approved', 'rejected') NOT NULL,
|
||||
payment_status ENUM('unpaid', 'paid') NOT NULL,
|
||||
paid_at VARCHAR(35) NULL,
|
||||
paid_by VARCHAR(64) NULL,
|
||||
created_at VARCHAR(35) NOT NULL,
|
||||
reviewed_at VARCHAR(35) NULL,
|
||||
review_note VARCHAR(500) NULL,
|
||||
registration_number VARCHAR(120) NULL,
|
||||
number_rule_id VARCHAR(64) NULL,
|
||||
feature_score DECIMAL(8,2) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uq_registrations_user_exam (user_id, exam_id),
|
||||
KEY idx_registrations_status (status),
|
||||
KEY idx_registrations_payment (payment_status, paid_at),
|
||||
KEY idx_registrations_exam (exam_id),
|
||||
CONSTRAINT fk_registrations_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_registrations_exam FOREIGN KEY (exam_id) REFERENCES exams(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_registrations_paid_by FOREIGN KEY (paid_by) REFERENCES users(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
|
||||
-- EIS-STATEMENT
|
||||
CREATE TABLE IF NOT EXISTS registration_subjects (
|
||||
registration_id VARCHAR(64) NOT NULL,
|
||||
subject_id VARCHAR(64) NOT NULL,
|
||||
PRIMARY KEY (registration_id, subject_id),
|
||||
CONSTRAINT fk_registration_subjects_registration FOREIGN KEY (registration_id) REFERENCES registrations(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_registration_subjects_subject FOREIGN KEY (subject_id) REFERENCES exam_subjects(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
|
||||
-- EIS-STATEMENT
|
||||
CREATE TABLE IF NOT EXISTS results (
|
||||
id VARCHAR(64) NOT NULL,
|
||||
registration_id VARCHAR(64) NOT NULL,
|
||||
subject_id VARCHAR(64) NOT NULL,
|
||||
score DOUBLE NOT NULL,
|
||||
grade VARCHAR(20) NOT NULL,
|
||||
published BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
updated_at VARCHAR(35) NULL,
|
||||
published_at VARCHAR(35) NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uq_results_registration_subject (registration_id, subject_id),
|
||||
KEY idx_results_registration (registration_id, published),
|
||||
CONSTRAINT chk_results_score CHECK (score >= 0 AND score <= 150),
|
||||
CONSTRAINT fk_results_registration FOREIGN KEY (registration_id) REFERENCES registrations(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_results_subject FOREIGN KEY (subject_id) REFERENCES exam_subjects(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
|
||||
-- EIS-STATEMENT
|
||||
CREATE TABLE IF NOT EXISTS test_centers (
|
||||
id VARCHAR(64) NOT NULL,
|
||||
school_id VARCHAR(64) NOT NULL,
|
||||
code VARCHAR(40) NOT NULL,
|
||||
name VARCHAR(200) NOT NULL,
|
||||
province_code VARCHAR(6) NOT NULL,
|
||||
province_name VARCHAR(80) NOT NULL,
|
||||
city_code VARCHAR(6) NOT NULL,
|
||||
city_name VARCHAR(100) NOT NULL,
|
||||
district_code VARCHAR(6) NOT NULL,
|
||||
district_name VARCHAR(100) NOT NULL,
|
||||
address VARCHAR(255) NOT NULL,
|
||||
contact VARCHAR(100) NULL,
|
||||
manager_name VARCHAR(100) NULL,
|
||||
manager_phone VARCHAR(60) NULL,
|
||||
emergency_phone VARCHAR(60) NULL,
|
||||
gate_open_time VARCHAR(40) NULL,
|
||||
transport VARCHAR(500) NULL,
|
||||
status ENUM('active', 'inactive') NOT NULL DEFAULT 'active',
|
||||
notes VARCHAR(1000) NULL,
|
||||
rooms TEXT NOT NULL,
|
||||
updated_at VARCHAR(35) NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uq_centers_code (code),
|
||||
UNIQUE KEY uq_centers_school_name (school_id, name),
|
||||
CONSTRAINT fk_centers_school FOREIGN KEY (school_id) REFERENCES schools(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
|
||||
-- EIS-STATEMENT
|
||||
CREATE TABLE IF NOT EXISTS test_rooms (
|
||||
id VARCHAR(64) NOT NULL,
|
||||
center_id VARCHAR(64) NOT NULL,
|
||||
code VARCHAR(40) NOT NULL,
|
||||
name VARCHAR(120) NOT NULL,
|
||||
building VARCHAR(120) NOT NULL,
|
||||
floor VARCHAR(40) NULL,
|
||||
capacity INT UNSIGNED NOT NULL,
|
||||
seat_plan VARCHAR(500) NULL,
|
||||
seat_start INT UNSIGNED NOT NULL DEFAULT 1,
|
||||
seat_end INT UNSIGNED NOT NULL,
|
||||
room_type ENUM('standard', 'computer', 'accessible', 'spare') NOT NULL,
|
||||
status ENUM('active', 'inactive') NOT NULL DEFAULT 'active',
|
||||
notes VARCHAR(500) NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uq_rooms_center_code (center_id, code),
|
||||
KEY idx_rooms_center (center_id, status, code),
|
||||
CONSTRAINT fk_rooms_center FOREIGN KEY (center_id) REFERENCES test_centers(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
|
||||
-- EIS-STATEMENT
|
||||
CREATE TABLE IF NOT EXISTS admission_number_rules (
|
||||
id VARCHAR(64) NOT NULL,
|
||||
code VARCHAR(80) NOT NULL,
|
||||
name VARCHAR(160) NOT NULL,
|
||||
description VARCHAR(500) NOT NULL,
|
||||
`separator` VARCHAR(10) NOT NULL DEFAULT '',
|
||||
segments_json JSON NOT NULL,
|
||||
example VARCHAR(120) NOT NULL,
|
||||
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at VARCHAR(35) NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uq_admission_rules_code (code)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
|
||||
-- EIS-STATEMENT
|
||||
CREATE TABLE IF NOT EXISTS exam_arrangement_plans (
|
||||
id VARCHAR(64) NOT NULL,
|
||||
exam_id VARCHAR(64) NOT NULL,
|
||||
number_rule_id VARCHAR(64) NOT NULL,
|
||||
mixing_scope ENUM('class', 'school', 'district', 'city', 'province') NOT NULL,
|
||||
random_seed VARCHAR(80) NOT NULL,
|
||||
candidate_count INT UNSIGNED NOT NULL,
|
||||
center_count INT UNSIGNED NOT NULL,
|
||||
subject_assignment_count INT UNSIGNED NOT NULL,
|
||||
subject_combination_count INT UNSIGNED NOT NULL,
|
||||
same_school_center_rate DOUBLE NOT NULL,
|
||||
warnings_json JSON NOT NULL,
|
||||
generated_by VARCHAR(64) NULL,
|
||||
generated_at VARCHAR(35) NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uq_arrangement_plans_exam (exam_id),
|
||||
CONSTRAINT fk_arrangement_plans_exam FOREIGN KEY (exam_id) REFERENCES exams(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_arrangement_plans_rule FOREIGN KEY (number_rule_id) REFERENCES admission_number_rules(id),
|
||||
CONSTRAINT fk_arrangement_plans_generator FOREIGN KEY (generated_by) REFERENCES users(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
|
||||
-- EIS-STATEMENT
|
||||
CREATE TABLE IF NOT EXISTS admit_cards (
|
||||
registration_id VARCHAR(64) NOT NULL,
|
||||
plan_id VARCHAR(64) NOT NULL,
|
||||
card_number VARCHAR(100) NOT NULL,
|
||||
center_id VARCHAR(64) NULL,
|
||||
test_center VARCHAR(200) NOT NULL,
|
||||
center_code VARCHAR(40) NOT NULL,
|
||||
center_address VARCHAR(500) NOT NULL,
|
||||
generated_at VARCHAR(35) NOT NULL,
|
||||
PRIMARY KEY (registration_id),
|
||||
UNIQUE KEY uq_admit_cards_number (card_number),
|
||||
CONSTRAINT fk_admit_cards_registration FOREIGN KEY (registration_id) REFERENCES registrations(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_admit_cards_plan FOREIGN KEY (plan_id) REFERENCES exam_arrangement_plans(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_admit_cards_center FOREIGN KEY (center_id) REFERENCES test_centers(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
|
||||
-- EIS-STATEMENT
|
||||
CREATE TABLE IF NOT EXISTS admit_card_subjects (
|
||||
registration_id VARCHAR(64) NOT NULL,
|
||||
subject_id VARCHAR(64) NOT NULL,
|
||||
room_id VARCHAR(64) NULL,
|
||||
room VARCHAR(120) NOT NULL,
|
||||
room_code VARCHAR(40) NOT NULL,
|
||||
exam_room_code VARCHAR(20) NOT NULL,
|
||||
building VARCHAR(120) NOT NULL,
|
||||
floor VARCHAR(80) NOT NULL,
|
||||
seat VARCHAR(30) NOT NULL,
|
||||
subject_signature VARCHAR(1000) NOT NULL,
|
||||
PRIMARY KEY (registration_id, subject_id),
|
||||
UNIQUE KEY uq_admit_subject_room_seat (subject_id, room_id, seat),
|
||||
KEY idx_admit_subjects_room (subject_id, room_id, seat),
|
||||
CONSTRAINT fk_admit_subjects_card FOREIGN KEY (registration_id) REFERENCES admit_cards(registration_id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_admit_subjects_subject FOREIGN KEY (subject_id) REFERENCES exam_subjects(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_admit_subjects_room FOREIGN KEY (room_id) REFERENCES test_rooms(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
|
||||
-- EIS-STATEMENT
|
||||
CREATE TABLE IF NOT EXISTS center_change_requests (
|
||||
id VARCHAR(64) NOT NULL,
|
||||
center_id VARCHAR(64) NULL,
|
||||
school_id VARCHAR(64) NOT NULL,
|
||||
request_type ENUM('create', 'update') NOT NULL,
|
||||
code VARCHAR(40) NOT NULL,
|
||||
name VARCHAR(200) NOT NULL,
|
||||
province_code VARCHAR(6) NOT NULL,
|
||||
province_name VARCHAR(80) NOT NULL,
|
||||
city_code VARCHAR(6) NOT NULL,
|
||||
city_name VARCHAR(100) NOT NULL,
|
||||
district_code VARCHAR(6) NOT NULL,
|
||||
district_name VARCHAR(100) NOT NULL,
|
||||
address VARCHAR(255) NOT NULL,
|
||||
contact VARCHAR(100) NULL,
|
||||
manager_name VARCHAR(100) NULL,
|
||||
manager_phone VARCHAR(60) NULL,
|
||||
emergency_phone VARCHAR(60) NULL,
|
||||
gate_open_time VARCHAR(40) NULL,
|
||||
transport VARCHAR(500) NULL,
|
||||
center_status ENUM('active', 'inactive') NOT NULL,
|
||||
notes VARCHAR(1000) NULL,
|
||||
status ENUM('pending', 'approved', 'rejected') NOT NULL,
|
||||
review_note VARCHAR(500) NULL,
|
||||
requested_by VARCHAR(64) NULL,
|
||||
created_at VARCHAR(35) NOT NULL,
|
||||
reviewed_at VARCHAR(35) NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_center_changes_school (school_id, status, created_at),
|
||||
CONSTRAINT fk_center_change_center FOREIGN KEY (center_id) REFERENCES test_centers(id) ON DELETE SET NULL,
|
||||
CONSTRAINT fk_center_change_school FOREIGN KEY (school_id) REFERENCES schools(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_center_change_requester FOREIGN KEY (requested_by) REFERENCES users(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
|
||||
-- EIS-STATEMENT
|
||||
CREATE TABLE IF NOT EXISTS center_change_rooms (
|
||||
id VARCHAR(64) NOT NULL,
|
||||
request_id VARCHAR(64) NOT NULL,
|
||||
room_id VARCHAR(64) NULL,
|
||||
code VARCHAR(40) NOT NULL,
|
||||
name VARCHAR(120) NOT NULL,
|
||||
building VARCHAR(120) NOT NULL,
|
||||
floor VARCHAR(40) NULL,
|
||||
capacity INT UNSIGNED NOT NULL,
|
||||
seat_plan VARCHAR(500) NULL,
|
||||
seat_start INT UNSIGNED NOT NULL DEFAULT 1,
|
||||
seat_end INT UNSIGNED NOT NULL,
|
||||
room_type ENUM('standard', 'computer', 'accessible', 'spare') NOT NULL,
|
||||
status ENUM('active', 'inactive') NOT NULL,
|
||||
notes VARCHAR(500) NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uq_center_change_rooms_code (request_id, code),
|
||||
CONSTRAINT fk_center_change_rooms_request FOREIGN KEY (request_id) REFERENCES center_change_requests(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
|
||||
-- EIS-STATEMENT
|
||||
CREATE TABLE IF NOT EXISTS number_rules (
|
||||
id VARCHAR(64) NOT NULL,
|
||||
name VARCHAR(120) NOT NULL,
|
||||
`separator` VARCHAR(10) NOT NULL DEFAULT '',
|
||||
active BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_by VARCHAR(64) NULL,
|
||||
updated_at VARCHAR(35) NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
CONSTRAINT fk_number_rules_creator FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
|
||||
-- EIS-STATEMENT
|
||||
CREATE TABLE IF NOT EXISTS number_rule_segments (
|
||||
id VARCHAR(64) NOT NULL,
|
||||
rule_id VARCHAR(64) NOT NULL,
|
||||
position INT UNSIGNED NOT NULL,
|
||||
type ENUM('year', 'school_code', 'gender', 'sequence', 'literal') NOT NULL,
|
||||
value VARCHAR(60) NULL,
|
||||
width INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uq_rule_segments_position (rule_id, position),
|
||||
CONSTRAINT fk_rule_segments_rule FOREIGN KEY (rule_id) REFERENCES number_rules(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
|
||||
-- EIS-STATEMENT
|
||||
CREATE TABLE IF NOT EXISTS candidate_account_batches (
|
||||
id VARCHAR(64) NOT NULL,
|
||||
school_id VARCHAR(64) NOT NULL,
|
||||
requested_by VARCHAR(64) NULL,
|
||||
status ENUM('pending', 'approved', 'rejected') NOT NULL,
|
||||
review_note VARCHAR(500) NULL,
|
||||
created_at VARCHAR(35) NOT NULL,
|
||||
reviewed_at VARCHAR(35) NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_account_batches_school (school_id, status, created_at),
|
||||
CONSTRAINT fk_account_batches_school FOREIGN KEY (school_id) REFERENCES schools(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_account_batches_requester FOREIGN KEY (requested_by) REFERENCES users(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
|
||||
-- EIS-STATEMENT
|
||||
CREATE TABLE IF NOT EXISTS candidate_account_batch_items (
|
||||
id VARCHAR(64) NOT NULL,
|
||||
batch_id VARCHAR(64) NOT NULL,
|
||||
class_id VARCHAR(64) NOT NULL,
|
||||
position INT UNSIGNED NOT NULL,
|
||||
candidate_number VARCHAR(120) NULL,
|
||||
initial_password VARCHAR(120) NULL,
|
||||
user_id VARCHAR(64) NULL,
|
||||
created_at VARCHAR(35) NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uq_account_batch_position (batch_id, position),
|
||||
UNIQUE KEY uq_account_batch_number (candidate_number),
|
||||
UNIQUE KEY uq_account_batch_user (user_id),
|
||||
KEY idx_account_batch_items (batch_id, class_id, position),
|
||||
CONSTRAINT fk_account_batch_items_batch FOREIGN KEY (batch_id) REFERENCES candidate_account_batches(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_account_batch_items_class FOREIGN KEY (class_id) REFERENCES school_classes(id),
|
||||
CONSTRAINT fk_account_batch_items_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
|
||||
-- EIS-STATEMENT
|
||||
CREATE TABLE IF NOT EXISTS workflow_definitions (
|
||||
id VARCHAR(64) NOT NULL,
|
||||
business_type ENUM('profile_change', 'registration_review', 'center_change', 'candidate_account_batch', 'score_appeal') NOT NULL,
|
||||
name VARCHAR(120) NOT NULL,
|
||||
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
updated_by VARCHAR(64) NULL,
|
||||
updated_at VARCHAR(35) NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uq_workflow_type_active (business_type, active),
|
||||
CONSTRAINT fk_workflow_updater FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
|
||||
-- EIS-STATEMENT
|
||||
CREATE TABLE IF NOT EXISTS workflow_steps (
|
||||
id VARCHAR(64) NOT NULL,
|
||||
workflow_id VARCHAR(64) NOT NULL,
|
||||
position INT UNSIGNED NOT NULL,
|
||||
name VARCHAR(120) NOT NULL,
|
||||
admin_level ENUM('class', 'school', 'super') NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uq_workflow_steps_position (workflow_id, position),
|
||||
CONSTRAINT fk_workflow_steps_definition FOREIGN KEY (workflow_id) REFERENCES workflow_definitions(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
|
||||
-- EIS-STATEMENT
|
||||
CREATE TABLE IF NOT EXISTS workflow_instances (
|
||||
id VARCHAR(64) NOT NULL,
|
||||
workflow_id VARCHAR(64) NOT NULL,
|
||||
business_type VARCHAR(40) NOT NULL,
|
||||
business_id VARCHAR(64) NOT NULL,
|
||||
status ENUM('pending', 'approved', 'rejected') NOT NULL,
|
||||
current_step INT UNSIGNED NOT NULL DEFAULT 1,
|
||||
assignee_id VARCHAR(64) NULL,
|
||||
created_at VARCHAR(35) NOT NULL,
|
||||
completed_at VARCHAR(35) NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_workflow_inbox (status, assignee_id, business_type),
|
||||
CONSTRAINT fk_workflow_instance_definition FOREIGN KEY (workflow_id) REFERENCES workflow_definitions(id),
|
||||
CONSTRAINT fk_workflow_instance_assignee FOREIGN KEY (assignee_id) REFERENCES users(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
|
||||
-- EIS-STATEMENT
|
||||
CREATE TABLE IF NOT EXISTS workflow_actions (
|
||||
id VARCHAR(64) NOT NULL,
|
||||
instance_id VARCHAR(64) NOT NULL,
|
||||
actor_id VARCHAR(64) NULL,
|
||||
action ENUM('submit', 'approve', 'reject', 'transfer', 'return', 'supervise') NOT NULL,
|
||||
note VARCHAR(500) NULL,
|
||||
from_assignee_id VARCHAR(64) NULL,
|
||||
to_assignee_id VARCHAR(64) NULL,
|
||||
created_at VARCHAR(35) NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_workflow_actions_instance (instance_id, created_at),
|
||||
CONSTRAINT fk_workflow_action_instance FOREIGN KEY (instance_id) REFERENCES workflow_instances(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_workflow_action_actor FOREIGN KEY (actor_id) REFERENCES users(id) ON DELETE SET NULL,
|
||||
CONSTRAINT fk_workflow_action_from FOREIGN KEY (from_assignee_id) REFERENCES users(id) ON DELETE SET NULL,
|
||||
CONSTRAINT fk_workflow_action_to FOREIGN KEY (to_assignee_id) REFERENCES users(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
|
||||
-- EIS-STATEMENT
|
||||
CREATE TABLE IF NOT EXISTS admission_records (
|
||||
id VARCHAR(64) NOT NULL,
|
||||
kind ENUM('setting', 'plan', 'preference', 'placement', 'notification', 'indicator_qualification', 'qualification_publication', 'cutoff_publication') NOT NULL,
|
||||
exam_id VARCHAR(64) NOT NULL,
|
||||
user_id VARCHAR(64) NULL,
|
||||
school_id VARCHAR(64) NULL,
|
||||
status VARCHAR(40) NOT NULL,
|
||||
payload_json JSON NOT NULL,
|
||||
created_at VARCHAR(35) NOT NULL,
|
||||
updated_at VARCHAR(35) NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_admission_records_lookup (kind, exam_id, school_id, user_id, status),
|
||||
CONSTRAINT fk_admission_record_exam FOREIGN KEY (exam_id) REFERENCES exams(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_admission_record_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_admission_record_school FOREIGN KEY (school_id) REFERENCES schools(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
|
||||
-- EIS-STATEMENT
|
||||
CREATE TABLE IF NOT EXISTS audit_logs (
|
||||
id VARCHAR(64) NOT NULL,
|
||||
actor_id VARCHAR(64) NULL,
|
||||
action VARCHAR(100) NOT NULL,
|
||||
detail VARCHAR(1000) NOT NULL,
|
||||
created_at VARCHAR(35) NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_audit_created (created_at),
|
||||
CONSTRAINT fk_audit_actor FOREIGN KEY (actor_id) REFERENCES users(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
|
||||
@@ -0,0 +1,501 @@
|
||||
PRAGMA foreign_keys = ON;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS schema_metadata (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
schema_version INTEGER NOT NULL DEFAULT 1,
|
||||
app_version INTEGER NOT NULL DEFAULT 1,
|
||||
self_registration_enabled INTEGER NOT NULL DEFAULT 0 CHECK (self_registration_enabled IN (0, 1)),
|
||||
created_at TEXT NOT NULL
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS organization (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
name TEXT NOT NULL,
|
||||
code TEXT NOT NULL,
|
||||
phone TEXT NOT NULL,
|
||||
address TEXT NOT NULL
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS schools (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
code TEXT NOT NULL UNIQUE,
|
||||
address TEXT,
|
||||
is_source_school INTEGER NOT NULL DEFAULT 1 CHECK (is_source_school IN (0, 1)),
|
||||
is_admission_school INTEGER NOT NULL DEFAULT 1 CHECK (is_admission_school IN (0, 1)),
|
||||
active INTEGER NOT NULL DEFAULT 1 CHECK (active IN (0, 1))
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS school_classes (
|
||||
id TEXT PRIMARY KEY,
|
||||
school_id TEXT NOT NULL REFERENCES schools(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
grade TEXT NOT NULL,
|
||||
active INTEGER NOT NULL DEFAULT 1 CHECK (active IN (0, 1)),
|
||||
UNIQUE (school_id, name)
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS school_student_partitions (
|
||||
school_id TEXT PRIMARY KEY REFERENCES schools(id) ON DELETE CASCADE,
|
||||
partition_key TEXT NOT NULL UNIQUE,
|
||||
students_table TEXT NOT NULL UNIQUE,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id TEXT PRIMARY KEY,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
candidate_number TEXT UNIQUE,
|
||||
password_hash TEXT NOT NULL,
|
||||
role TEXT NOT NULL CHECK (role IN ('admin', 'candidate', 'admission_school')),
|
||||
admin_level TEXT CHECK (admin_level IN ('super', 'school', 'class')),
|
||||
school_id TEXT REFERENCES schools(id) ON DELETE SET NULL,
|
||||
class_id TEXT REFERENCES school_classes(id) ON DELETE SET NULL,
|
||||
active INTEGER NOT NULL DEFAULT 1 CHECK (active IN (0, 1)),
|
||||
must_change_password INTEGER NOT NULL DEFAULT 0 CHECK (must_change_password IN (0, 1)),
|
||||
totp_enabled INTEGER NOT NULL DEFAULT 0 CHECK (totp_enabled IN (0, 1)),
|
||||
totp_secret_encrypted TEXT,
|
||||
totp_recovery_codes TEXT NOT NULL DEFAULT '[]',
|
||||
totp_last_used_step INTEGER,
|
||||
archived_at TEXT,
|
||||
archived_by TEXT REFERENCES users(id) ON DELETE RESTRICT,
|
||||
display_name TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS candidate_profiles (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL UNIQUE REFERENCES users(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
gender TEXT,
|
||||
id_number TEXT NOT NULL UNIQUE,
|
||||
phone TEXT NOT NULL,
|
||||
email TEXT,
|
||||
school TEXT,
|
||||
grade TEXT,
|
||||
school_id TEXT REFERENCES schools(id) ON DELETE SET NULL,
|
||||
class_id TEXT REFERENCES school_classes(id) ON DELETE SET NULL,
|
||||
province_code TEXT,
|
||||
province_name TEXT,
|
||||
city_code TEXT,
|
||||
city_name TEXT,
|
||||
district_code TEXT,
|
||||
district_name TEXT,
|
||||
address TEXT,
|
||||
emergency_contact TEXT,
|
||||
emergency_phone TEXT,
|
||||
native_place TEXT,
|
||||
birth_date TEXT,
|
||||
ethnicity TEXT,
|
||||
postal_code TEXT,
|
||||
guardian_name TEXT,
|
||||
guardian_phone TEXT,
|
||||
specialty_category TEXT,
|
||||
specialty_type TEXT,
|
||||
specialty_types TEXT NOT NULL DEFAULT '[]',
|
||||
specialty_certificate TEXT,
|
||||
policy_eligibility TEXT,
|
||||
profile_completed INTEGER NOT NULL DEFAULT 0 CHECK (profile_completed IN (0, 1)),
|
||||
status TEXT NOT NULL CHECK (status IN ('pending', 'approved', 'rejected')),
|
||||
review_note TEXT,
|
||||
reviewed_at TEXT,
|
||||
reviewer_id TEXT REFERENCES users(id) ON DELETE SET NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS notices (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
summary TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
category TEXT NOT NULL,
|
||||
pinned INTEGER NOT NULL DEFAULT 0 CHECK (pinned IN (0, 1)),
|
||||
status TEXT NOT NULL CHECK (status IN ('draft', 'published')),
|
||||
publish_at TEXT,
|
||||
created_at TEXT,
|
||||
author TEXT NOT NULL
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS exams (
|
||||
id TEXT PRIMARY KEY,
|
||||
code TEXT NOT NULL UNIQUE,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
registration_start TEXT NOT NULL,
|
||||
registration_end TEXT NOT NULL,
|
||||
exam_start TEXT NOT NULL,
|
||||
exam_end TEXT NOT NULL,
|
||||
admit_download_start TEXT NOT NULL,
|
||||
admit_download_end TEXT NOT NULL,
|
||||
location TEXT NOT NULL,
|
||||
pass_policy TEXT NOT NULL DEFAULT 'rank_percent' CHECK (pass_policy IN ('fixed_score', 'score_ratio', 'rank_percent', 'subject_scores', 'none')),
|
||||
pass_value REAL NOT NULL DEFAULT 60,
|
||||
status TEXT NOT NULL CHECK (status IN ('draft', 'published', 'closed')),
|
||||
archived_at TEXT,
|
||||
archived_by TEXT REFERENCES users(id) ON DELETE RESTRICT,
|
||||
created_at TEXT NOT NULL
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS exam_data_partitions (
|
||||
exam_id TEXT PRIMARY KEY REFERENCES exams(id) ON DELETE CASCADE,
|
||||
partition_key TEXT NOT NULL UNIQUE,
|
||||
candidates_table TEXT NOT NULL UNIQUE,
|
||||
admissions_table TEXT NOT NULL UNIQUE,
|
||||
results_table TEXT NOT NULL UNIQUE,
|
||||
centers_table TEXT NOT NULL UNIQUE,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS exam_subjects (
|
||||
id TEXT PRIMARY KEY,
|
||||
exam_id TEXT NOT NULL REFERENCES exams(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
subject_date TEXT NOT NULL,
|
||||
start_time TEXT NOT NULL,
|
||||
end_time TEXT NOT NULL,
|
||||
fee REAL NOT NULL DEFAULT 0,
|
||||
full_score REAL NOT NULL DEFAULT 150,
|
||||
pass_score REAL NOT NULL DEFAULT 90,
|
||||
pass_rule TEXT NOT NULL DEFAULT 'fixed_score' CHECK (pass_rule IN ('fixed_score', 'score_ratio', 'none')),
|
||||
pass_value REAL NOT NULL DEFAULT 90,
|
||||
position INTEGER NOT NULL,
|
||||
UNIQUE (exam_id, position)
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS registrations (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
exam_id TEXT NOT NULL REFERENCES exams(id) ON DELETE CASCADE,
|
||||
status TEXT NOT NULL CHECK (status IN ('pending', 'approved', 'rejected')),
|
||||
payment_status TEXT NOT NULL CHECK (payment_status IN ('unpaid', 'paid')),
|
||||
paid_at TEXT,
|
||||
paid_by TEXT REFERENCES users(id) ON DELETE SET NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
reviewed_at TEXT,
|
||||
review_note TEXT,
|
||||
registration_number TEXT,
|
||||
number_rule_id TEXT,
|
||||
feature_score REAL NOT NULL DEFAULT 0 CHECK (feature_score >= 0),
|
||||
UNIQUE (user_id, exam_id)
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS registration_subjects (
|
||||
registration_id TEXT NOT NULL REFERENCES registrations(id) ON DELETE CASCADE,
|
||||
subject_id TEXT NOT NULL REFERENCES exam_subjects(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (registration_id, subject_id)
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS admission_number_rules (
|
||||
id TEXT PRIMARY KEY,
|
||||
code TEXT NOT NULL UNIQUE,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
separator TEXT NOT NULL DEFAULT '',
|
||||
segments_json TEXT NOT NULL,
|
||||
example TEXT NOT NULL,
|
||||
active INTEGER NOT NULL DEFAULT 1 CHECK (active IN (0, 1)),
|
||||
created_at TEXT NOT NULL
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS exam_arrangement_plans (
|
||||
id TEXT PRIMARY KEY,
|
||||
exam_id TEXT NOT NULL UNIQUE REFERENCES exams(id) ON DELETE CASCADE,
|
||||
number_rule_id TEXT NOT NULL REFERENCES admission_number_rules(id),
|
||||
mixing_scope TEXT NOT NULL CHECK (mixing_scope IN ('class', 'school', 'district', 'city', 'province')),
|
||||
random_seed TEXT NOT NULL,
|
||||
candidate_count INTEGER NOT NULL CHECK (candidate_count >= 0),
|
||||
center_count INTEGER NOT NULL CHECK (center_count >= 0),
|
||||
subject_assignment_count INTEGER NOT NULL CHECK (subject_assignment_count >= 0),
|
||||
subject_combination_count INTEGER NOT NULL CHECK (subject_combination_count >= 0),
|
||||
same_school_center_rate REAL NOT NULL,
|
||||
warnings_json TEXT NOT NULL,
|
||||
generated_by TEXT REFERENCES users(id) ON DELETE SET NULL,
|
||||
generated_at TEXT NOT NULL
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS admit_cards (
|
||||
registration_id TEXT PRIMARY KEY REFERENCES registrations(id) ON DELETE CASCADE,
|
||||
plan_id TEXT NOT NULL REFERENCES exam_arrangement_plans(id) ON DELETE CASCADE,
|
||||
card_number TEXT NOT NULL UNIQUE,
|
||||
center_id TEXT REFERENCES test_centers(id) ON DELETE SET NULL,
|
||||
test_center TEXT NOT NULL,
|
||||
center_code TEXT NOT NULL,
|
||||
center_address TEXT NOT NULL,
|
||||
generated_at TEXT NOT NULL
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS admit_card_subjects (
|
||||
registration_id TEXT NOT NULL REFERENCES admit_cards(registration_id) ON DELETE CASCADE,
|
||||
subject_id TEXT NOT NULL REFERENCES exam_subjects(id) ON DELETE CASCADE,
|
||||
room_id TEXT REFERENCES test_rooms(id) ON DELETE SET NULL,
|
||||
room TEXT NOT NULL,
|
||||
room_code TEXT NOT NULL,
|
||||
exam_room_code TEXT NOT NULL,
|
||||
building TEXT NOT NULL,
|
||||
floor TEXT NOT NULL,
|
||||
seat TEXT NOT NULL,
|
||||
subject_signature TEXT NOT NULL,
|
||||
PRIMARY KEY (registration_id, subject_id),
|
||||
UNIQUE (subject_id, room_id, seat)
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS results (
|
||||
id TEXT PRIMARY KEY,
|
||||
registration_id TEXT NOT NULL REFERENCES registrations(id) ON DELETE CASCADE,
|
||||
subject_id TEXT NOT NULL REFERENCES exam_subjects(id) ON DELETE CASCADE,
|
||||
score REAL NOT NULL CHECK (score >= 0),
|
||||
grade TEXT NOT NULL,
|
||||
published INTEGER NOT NULL DEFAULT 0 CHECK (published IN (0, 1)),
|
||||
updated_at TEXT,
|
||||
published_at TEXT,
|
||||
UNIQUE (registration_id, subject_id)
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS test_centers (
|
||||
id TEXT PRIMARY KEY,
|
||||
school_id TEXT NOT NULL REFERENCES schools(id) ON DELETE CASCADE,
|
||||
code TEXT NOT NULL UNIQUE,
|
||||
name TEXT NOT NULL,
|
||||
province_code TEXT NOT NULL,
|
||||
province_name TEXT NOT NULL,
|
||||
city_code TEXT NOT NULL,
|
||||
city_name TEXT NOT NULL,
|
||||
district_code TEXT NOT NULL,
|
||||
district_name TEXT NOT NULL,
|
||||
address TEXT NOT NULL,
|
||||
contact TEXT,
|
||||
manager_name TEXT,
|
||||
manager_phone TEXT,
|
||||
emergency_phone TEXT,
|
||||
gate_open_time TEXT,
|
||||
transport TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'inactive')),
|
||||
notes TEXT,
|
||||
rooms TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
UNIQUE (school_id, name)
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS test_rooms (
|
||||
id TEXT PRIMARY KEY,
|
||||
center_id TEXT NOT NULL REFERENCES test_centers(id) ON DELETE CASCADE,
|
||||
code TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
building TEXT NOT NULL,
|
||||
floor TEXT,
|
||||
capacity INTEGER NOT NULL CHECK (capacity > 0),
|
||||
seat_plan TEXT,
|
||||
seat_start INTEGER NOT NULL DEFAULT 1 CHECK (seat_start > 0),
|
||||
seat_end INTEGER NOT NULL CHECK (seat_end >= seat_start),
|
||||
room_type TEXT NOT NULL CHECK (room_type IN ('standard', 'computer', 'accessible', 'spare')),
|
||||
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'inactive')),
|
||||
notes TEXT,
|
||||
UNIQUE (center_id, code)
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS center_change_requests (
|
||||
id TEXT PRIMARY KEY,
|
||||
center_id TEXT REFERENCES test_centers(id) ON DELETE SET NULL,
|
||||
school_id TEXT NOT NULL REFERENCES schools(id) ON DELETE CASCADE,
|
||||
request_type TEXT NOT NULL CHECK (request_type IN ('create', 'update')),
|
||||
code TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
province_code TEXT NOT NULL,
|
||||
province_name TEXT NOT NULL,
|
||||
city_code TEXT NOT NULL,
|
||||
city_name TEXT NOT NULL,
|
||||
district_code TEXT NOT NULL,
|
||||
district_name TEXT NOT NULL,
|
||||
address TEXT NOT NULL,
|
||||
contact TEXT,
|
||||
manager_name TEXT,
|
||||
manager_phone TEXT,
|
||||
emergency_phone TEXT,
|
||||
gate_open_time TEXT,
|
||||
transport TEXT,
|
||||
center_status TEXT NOT NULL CHECK (center_status IN ('active', 'inactive')),
|
||||
notes TEXT,
|
||||
status TEXT NOT NULL CHECK (status IN ('pending', 'approved', 'rejected')),
|
||||
review_note TEXT,
|
||||
requested_by TEXT REFERENCES users(id) ON DELETE SET NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
reviewed_at TEXT
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS center_change_rooms (
|
||||
id TEXT PRIMARY KEY,
|
||||
request_id TEXT NOT NULL REFERENCES center_change_requests(id) ON DELETE CASCADE,
|
||||
room_id TEXT,
|
||||
code TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
building TEXT NOT NULL,
|
||||
floor TEXT,
|
||||
capacity INTEGER NOT NULL CHECK (capacity > 0),
|
||||
seat_plan TEXT,
|
||||
seat_start INTEGER NOT NULL DEFAULT 1,
|
||||
seat_end INTEGER NOT NULL,
|
||||
room_type TEXT NOT NULL CHECK (room_type IN ('standard', 'computer', 'accessible', 'spare')),
|
||||
status TEXT NOT NULL CHECK (status IN ('active', 'inactive')),
|
||||
notes TEXT,
|
||||
UNIQUE (request_id, code)
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS number_rules (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
separator TEXT NOT NULL DEFAULT '',
|
||||
active INTEGER NOT NULL DEFAULT 0 CHECK (active IN (0, 1)),
|
||||
created_by TEXT REFERENCES users(id) ON DELETE SET NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS number_rule_segments (
|
||||
id TEXT PRIMARY KEY,
|
||||
rule_id TEXT NOT NULL REFERENCES number_rules(id) ON DELETE CASCADE,
|
||||
position INTEGER NOT NULL,
|
||||
type TEXT NOT NULL CHECK (type IN ('year', 'school_code', 'gender', 'sequence', 'literal')),
|
||||
value TEXT,
|
||||
width INTEGER NOT NULL DEFAULT 0,
|
||||
UNIQUE (rule_id, position)
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS candidate_account_batches (
|
||||
id TEXT PRIMARY KEY,
|
||||
school_id TEXT NOT NULL REFERENCES schools(id) ON DELETE CASCADE,
|
||||
requested_by TEXT REFERENCES users(id) ON DELETE SET NULL,
|
||||
status TEXT NOT NULL CHECK (status IN ('pending', 'approved', 'rejected')),
|
||||
review_note TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
reviewed_at TEXT
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS candidate_account_batch_items (
|
||||
id TEXT PRIMARY KEY,
|
||||
batch_id TEXT NOT NULL REFERENCES candidate_account_batches(id) ON DELETE CASCADE,
|
||||
class_id TEXT NOT NULL REFERENCES school_classes(id) ON DELETE RESTRICT,
|
||||
position INTEGER NOT NULL,
|
||||
candidate_number TEXT UNIQUE,
|
||||
initial_password TEXT,
|
||||
user_id TEXT UNIQUE REFERENCES users(id) ON DELETE SET NULL,
|
||||
created_at TEXT,
|
||||
UNIQUE (batch_id, position)
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS workflow_definitions (
|
||||
id TEXT PRIMARY KEY,
|
||||
business_type TEXT NOT NULL CHECK (business_type IN ('profile_change', 'registration_review', 'center_change', 'candidate_account_batch', 'score_appeal')),
|
||||
name TEXT NOT NULL,
|
||||
active INTEGER NOT NULL DEFAULT 1 CHECK (active IN (0, 1)),
|
||||
updated_by TEXT REFERENCES users(id) ON DELETE SET NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
UNIQUE (business_type, active)
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS workflow_steps (
|
||||
id TEXT PRIMARY KEY,
|
||||
workflow_id TEXT NOT NULL REFERENCES workflow_definitions(id) ON DELETE CASCADE,
|
||||
position INTEGER NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
admin_level TEXT NOT NULL CHECK (admin_level IN ('class', 'school', 'super')),
|
||||
UNIQUE (workflow_id, position)
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS workflow_instances (
|
||||
id TEXT PRIMARY KEY,
|
||||
workflow_id TEXT NOT NULL REFERENCES workflow_definitions(id) ON DELETE RESTRICT,
|
||||
business_type TEXT NOT NULL,
|
||||
business_id TEXT NOT NULL,
|
||||
status TEXT NOT NULL CHECK (status IN ('pending', 'approved', 'rejected')),
|
||||
current_step INTEGER NOT NULL DEFAULT 1,
|
||||
assignee_id TEXT REFERENCES users(id) ON DELETE SET NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
completed_at TEXT
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS workflow_actions (
|
||||
id TEXT PRIMARY KEY,
|
||||
instance_id TEXT NOT NULL REFERENCES workflow_instances(id) ON DELETE CASCADE,
|
||||
actor_id TEXT REFERENCES users(id) ON DELETE SET NULL,
|
||||
action TEXT NOT NULL CHECK (action IN ('submit', 'approve', 'reject', 'transfer', 'return', 'supervise')),
|
||||
note TEXT,
|
||||
from_assignee_id TEXT REFERENCES users(id) ON DELETE SET NULL,
|
||||
to_assignee_id TEXT REFERENCES users(id) ON DELETE SET NULL,
|
||||
created_at TEXT NOT NULL
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS audit_logs (
|
||||
id TEXT PRIMARY KEY,
|
||||
actor_id TEXT REFERENCES users(id) ON DELETE SET NULL,
|
||||
action TEXT NOT NULL,
|
||||
detail TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS admission_records (
|
||||
id TEXT PRIMARY KEY,
|
||||
kind TEXT NOT NULL CHECK (kind IN ('setting', 'plan', 'preference', 'placement', 'notification', 'indicator_qualification', 'qualification_publication', 'cutoff_publication')),
|
||||
exam_id TEXT NOT NULL REFERENCES exams(id) ON DELETE CASCADE,
|
||||
user_id TEXT REFERENCES users(id) ON DELETE CASCADE,
|
||||
school_id TEXT REFERENCES schools(id) ON DELETE CASCADE,
|
||||
status TEXT NOT NULL,
|
||||
payload_json TEXT NOT NULL DEFAULT '{}',
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
) STRICT;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_profiles_status ON candidate_profiles(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_archive_scope ON users(role, school_id, class_id, archived_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_exams_archive ON exams(archived_at, exam_end);
|
||||
CREATE INDEX IF NOT EXISTS idx_profiles_scope ON candidate_profiles(school_id, class_id, status);
|
||||
CREATE INDEX IF NOT EXISTS idx_notices_status_publish ON notices(status, publish_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_exams_status_registration ON exams(status, registration_start, registration_end);
|
||||
CREATE INDEX IF NOT EXISTS idx_subjects_exam ON exam_subjects(exam_id, position);
|
||||
CREATE INDEX IF NOT EXISTS idx_registrations_status ON registrations(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_registrations_payment ON registrations(payment_status, paid_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_registrations_exam ON registrations(exam_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_arrangement_plans_exam ON exam_arrangement_plans(exam_id, generated_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_admit_subjects_room ON admit_card_subjects(subject_id, room_id, seat);
|
||||
CREATE INDEX IF NOT EXISTS idx_workflow_inbox ON workflow_instances(status, assignee_id, business_type);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_test_centers_code ON test_centers(code);
|
||||
CREATE INDEX IF NOT EXISTS idx_rooms_center ON test_rooms(center_id, status, code);
|
||||
CREATE INDEX IF NOT EXISTS idx_center_changes_school ON center_change_requests(school_id, status, created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_account_batches_school ON candidate_account_batches(school_id, status, created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_account_batch_items ON candidate_account_batch_items(batch_id, class_id, position);
|
||||
CREATE INDEX IF NOT EXISTS idx_results_registration ON results(registration_id, published);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_created ON audit_logs(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_admission_records_lookup ON admission_records(kind, exam_id, school_id, user_id, status);
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS trg_results_lock_archived_insert
|
||||
BEFORE INSERT ON results
|
||||
WHEN EXISTS (
|
||||
SELECT 1 FROM registrations registration
|
||||
JOIN exams exam ON exam.id = registration.exam_id
|
||||
WHERE registration.id = NEW.registration_id AND exam.archived_at IS NOT NULL
|
||||
)
|
||||
BEGIN
|
||||
SELECT RAISE(ABORT, '归档考试成绩已永久锁定');
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS trg_results_lock_archived_update
|
||||
BEFORE UPDATE ON results
|
||||
WHEN EXISTS (
|
||||
SELECT 1 FROM registrations registration
|
||||
JOIN exams exam ON exam.id = registration.exam_id
|
||||
WHERE registration.id IN (OLD.registration_id, NEW.registration_id) AND exam.archived_at IS NOT NULL
|
||||
)
|
||||
BEGIN
|
||||
SELECT RAISE(ABORT, '归档考试成绩已永久锁定');
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS trg_results_lock_archived_delete
|
||||
BEFORE DELETE ON results
|
||||
WHEN EXISTS (
|
||||
SELECT 1 FROM registrations registration
|
||||
JOIN exams exam ON exam.id = registration.exam_id
|
||||
WHERE registration.id = OLD.registration_id AND exam.archived_at IS NOT NULL
|
||||
)
|
||||
BEGIN
|
||||
SELECT RAISE(ABORT, '归档考试成绩已永久锁定');
|
||||
END;
|
||||
@@ -17,8 +17,9 @@
|
||||
<PackageReference Include="StackExchange.Redis" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="..\data\china-regions.mjs" Link="Data\china-regions.mjs" />
|
||||
<EmbeddedResource Include="..\..\src\database\schema.mjs" Link="Data\database-schema.mjs" />
|
||||
<EmbeddedResource Include="..\Eis.Web\wwwroot\js\data\china-regions.js" Link="Data\china-regions.js" />
|
||||
<EmbeddedResource Include="Data\Schema.sqlite.sql" />
|
||||
<EmbeddedResource Include="Data\Schema.mysql.sql" />
|
||||
<EmbeddedResource Include="Data\demo-seed-operations.json.gz" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -106,7 +106,7 @@ public sealed partial class PublicQueryService(
|
||||
public async Task<JsonObject?> GetNoticeAsync(string id, CancellationToken cancellationToken)
|
||||
{
|
||||
// Detail reads deliberately bypass the list cache. During the compatibility
|
||||
// window Node.js may publish a notice in another process without a shared
|
||||
// window another application instance may publish a notice without a shared
|
||||
// Redis cache, and a just-created notice must still be immediately readable.
|
||||
var home = await LoadHomeAsync(cancellationToken);
|
||||
var notices = home["notices"]?.AsArray();
|
||||
|
||||
Reference in New Issue
Block a user