mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-21 13:59:03 +08:00
先提交上去
This commit is contained in:
@@ -0,0 +1,245 @@
|
||||
import { Context, Service } from '../../shared/kernel'
|
||||
import { DataSource } from 'typeorm'
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import { StudentEntity } from './entities/StudentEntity'
|
||||
import { ReasonEntity } from './entities/ReasonEntity'
|
||||
import { ScoreEventEntity } from './entities/ScoreEventEntity'
|
||||
import { SettlementEntity } from './entities/SettlementEntity'
|
||||
import { SettingEntity } from './entities/SettingEntity'
|
||||
import { TagEntity } from './entities/TagEntity'
|
||||
import { StudentTagEntity } from './entities/StudentTagEntity'
|
||||
import { migrations } from './migrations'
|
||||
|
||||
declare module '../../shared/kernel' {
|
||||
interface Context {
|
||||
db: DbManager
|
||||
}
|
||||
}
|
||||
|
||||
interface PostgreSQLConfig {
|
||||
host: string
|
||||
port: number
|
||||
username: string
|
||||
password: string
|
||||
database: string
|
||||
ssl?: boolean
|
||||
sslmode?: string
|
||||
channelBinding?: string
|
||||
}
|
||||
|
||||
interface QueueItem {
|
||||
id: string
|
||||
operation: () => Promise<any>
|
||||
resolve: (value: any) => void
|
||||
reject: (error: Error) => void
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
export function parsePostgreSQLConnectionString(connectionString: string): PostgreSQLConfig | null {
|
||||
if (!connectionString.startsWith('postgresql://')) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(connectionString)
|
||||
const config: PostgreSQLConfig = {
|
||||
host: url.hostname,
|
||||
port: url.port ? parseInt(url.port, 10) : 5432,
|
||||
username: url.username,
|
||||
password: decodeURIComponent(url.password || ''),
|
||||
database: url.pathname.slice(1),
|
||||
ssl: false
|
||||
}
|
||||
|
||||
const params = new URLSearchParams(url.search)
|
||||
if (params.get('sslmode')) {
|
||||
config.ssl = true
|
||||
config.sslmode = params.get('sslmode') || 'require'
|
||||
}
|
||||
if (params.get('channel_binding')) {
|
||||
config.channelBinding = params.get('channel_binding') || 'require'
|
||||
}
|
||||
|
||||
return config
|
||||
} catch (e) {
|
||||
console.error('Failed to parse PostgreSQL connection string:', e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export class DbManager extends Service {
|
||||
private _dataSource: DataSource | null = null
|
||||
private _isPostgreSQL: boolean = false
|
||||
private dbPath: string
|
||||
private operationQueue: QueueItem[] = []
|
||||
private isProcessingQueue: boolean = false
|
||||
private queueIdCounter: number = 0
|
||||
|
||||
constructor(ctx: Context, dbPath: string, pgConnectionString?: string) {
|
||||
super(ctx, 'db')
|
||||
this.dbPath = dbPath
|
||||
this._isPostgreSQL = !!pgConnectionString
|
||||
}
|
||||
|
||||
get dataSource(): DataSource {
|
||||
if (!this._dataSource) {
|
||||
throw new Error('Database not initialized. Call initialize() first.')
|
||||
}
|
||||
return this._dataSource
|
||||
}
|
||||
|
||||
get isPostgreSQL(): boolean {
|
||||
return this._isPostgreSQL
|
||||
}
|
||||
|
||||
private createDataSource(pgConnectionString?: string): DataSource {
|
||||
const pgConfig = pgConnectionString ? parsePostgreSQLConnectionString(pgConnectionString) : null
|
||||
|
||||
if (pgConfig) {
|
||||
this._isPostgreSQL = true
|
||||
return new DataSource({
|
||||
type: 'postgres',
|
||||
host: pgConfig.host,
|
||||
port: pgConfig.port,
|
||||
username: pgConfig.username,
|
||||
password: pgConfig.password,
|
||||
database: pgConfig.database,
|
||||
ssl: pgConfig.ssl ? { rejectUnauthorized: false } : false,
|
||||
extra: {
|
||||
ssl: pgConfig.ssl ? { rejectUnauthorized: false } : undefined,
|
||||
sslmode: pgConfig.sslmode,
|
||||
channelBinding: pgConfig.channelBinding
|
||||
},
|
||||
entities: [
|
||||
StudentEntity,
|
||||
ReasonEntity,
|
||||
ScoreEventEntity,
|
||||
SettlementEntity,
|
||||
SettingEntity,
|
||||
TagEntity,
|
||||
StudentTagEntity
|
||||
],
|
||||
migrations,
|
||||
synchronize: false,
|
||||
logging: false,
|
||||
poolSize: 10,
|
||||
connectTimeoutMS: 30000
|
||||
})
|
||||
} else {
|
||||
this._isPostgreSQL = false
|
||||
const dbDir = path.dirname(this.dbPath)
|
||||
if (!fs.existsSync(dbDir)) {
|
||||
fs.mkdirSync(dbDir, { recursive: true })
|
||||
}
|
||||
return new DataSource({
|
||||
type: 'better-sqlite3',
|
||||
database: this.dbPath,
|
||||
entities: [
|
||||
StudentEntity,
|
||||
ReasonEntity,
|
||||
ScoreEventEntity,
|
||||
SettlementEntity,
|
||||
SettingEntity,
|
||||
TagEntity,
|
||||
StudentTagEntity
|
||||
],
|
||||
migrations,
|
||||
synchronize: false,
|
||||
logging: false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async initialize(pgConnectionString?: string) {
|
||||
if (this._dataSource?.isInitialized) {
|
||||
await this.dispose()
|
||||
}
|
||||
|
||||
this._dataSource = this.createDataSource(pgConnectionString)
|
||||
await this._dataSource.initialize()
|
||||
|
||||
if (!this._isPostgreSQL) {
|
||||
await this._dataSource.query('PRAGMA foreign_keys = ON')
|
||||
}
|
||||
|
||||
await this._dataSource.runMigrations()
|
||||
}
|
||||
|
||||
async dispose() {
|
||||
if (!this._dataSource?.isInitialized) return
|
||||
await this._dataSource.destroy()
|
||||
this._dataSource = null
|
||||
}
|
||||
|
||||
async switchConnection(pgConnectionString?: string): Promise<{ type: 'sqlite' | 'postgresql' }> {
|
||||
await this.dispose()
|
||||
await this.initialize(pgConnectionString)
|
||||
return { type: this._isPostgreSQL ? 'postgresql' : 'sqlite' }
|
||||
}
|
||||
|
||||
getDatabaseType(): 'sqlite' | 'postgresql' {
|
||||
return this._isPostgreSQL ? 'postgresql' : 'sqlite'
|
||||
}
|
||||
|
||||
async enqueueOperation<T>(operation: () => Promise<T>): Promise<T> {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const item: QueueItem = {
|
||||
id: `op-${++this.queueIdCounter}`,
|
||||
operation,
|
||||
resolve: resolve as (value: any) => void,
|
||||
reject,
|
||||
timestamp: Date.now()
|
||||
}
|
||||
|
||||
this.operationQueue.push(item)
|
||||
this.processQueue()
|
||||
})
|
||||
}
|
||||
|
||||
private async processQueue() {
|
||||
if (this.isProcessingQueue || this.operationQueue.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
this.isProcessingQueue = true
|
||||
|
||||
while (this.operationQueue.length > 0) {
|
||||
const item = this.operationQueue.shift()!
|
||||
try {
|
||||
const result = await item.operation()
|
||||
item.resolve(result)
|
||||
} catch (error) {
|
||||
item.reject(error as Error)
|
||||
}
|
||||
}
|
||||
|
||||
this.isProcessingQueue = false
|
||||
}
|
||||
|
||||
async withQueue<T>(operation: () => Promise<T>): Promise<T> {
|
||||
if (this._isPostgreSQL) {
|
||||
return this.enqueueOperation(operation)
|
||||
}
|
||||
return operation()
|
||||
}
|
||||
|
||||
async syncToRemote(): Promise<{ success: boolean; message?: string }> {
|
||||
if (!this._isPostgreSQL) {
|
||||
return { success: false, message: '当前不是远程数据库模式' }
|
||||
}
|
||||
|
||||
if (!this._dataSource?.isInitialized) {
|
||||
return { success: false, message: '数据库未初始化' }
|
||||
}
|
||||
|
||||
try {
|
||||
return await this.enqueueOperation(async () => {
|
||||
await this._dataSource!.query('SELECT 1')
|
||||
return { success: true, message: '同步成功' }
|
||||
})
|
||||
} catch (error: any) {
|
||||
return { success: false, message: error?.message || '同步失败' }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
import type { DataSource } from 'typeorm'
|
||||
import {
|
||||
ReasonEntity,
|
||||
ScoreEventEntity,
|
||||
SettlementEntity,
|
||||
SettingEntity,
|
||||
StudentEntity
|
||||
} from '../entities'
|
||||
|
||||
type exportBundle = {
|
||||
students: StudentEntity[]
|
||||
reasons: ReasonEntity[]
|
||||
events: ScoreEventEntity[]
|
||||
settlements: SettlementEntity[]
|
||||
settings: SettingEntity[]
|
||||
}
|
||||
|
||||
type importResult = { success: true } | { success: false; message: string }
|
||||
|
||||
export class DataBackupRepository {
|
||||
constructor(private readonly dataSource: DataSource) {}
|
||||
|
||||
async exportJson(): Promise<string> {
|
||||
const bundle = await this.exportBundle()
|
||||
return JSON.stringify(bundle, null, 2)
|
||||
}
|
||||
|
||||
private async exportBundle(): Promise<exportBundle> {
|
||||
const students = await this.dataSource.getRepository(StudentEntity).find()
|
||||
const reasons = await this.dataSource.getRepository(ReasonEntity).find()
|
||||
const events = await this.dataSource.getRepository(ScoreEventEntity).find()
|
||||
const settlements = await this.dataSource
|
||||
.getRepository(SettlementEntity)
|
||||
.find({ order: { id: 'ASC' } })
|
||||
const settings = await this.dataSource
|
||||
.getRepository(SettingEntity)
|
||||
.createQueryBuilder('s')
|
||||
.where("s.key NOT LIKE 'security_%'")
|
||||
.getMany()
|
||||
return { students, reasons, events, settlements, settings }
|
||||
}
|
||||
|
||||
async importJson(jsonText: string): Promise<importResult> {
|
||||
let parsed: any
|
||||
try {
|
||||
parsed = JSON.parse(String(jsonText ?? ''))
|
||||
} catch {
|
||||
return { success: false, message: 'Invalid JSON' }
|
||||
}
|
||||
|
||||
const students = Array.isArray(parsed?.students) ? parsed.students : []
|
||||
const reasons = Array.isArray(parsed?.reasons) ? parsed.reasons : []
|
||||
const events = Array.isArray(parsed?.events) ? parsed.events : []
|
||||
const settlements = Array.isArray(parsed?.settlements) ? parsed.settlements : []
|
||||
const settings = Array.isArray(parsed?.settings) ? parsed.settings : []
|
||||
|
||||
try {
|
||||
await this.dataSource.transaction(async (manager) => {
|
||||
await manager.clear(ScoreEventEntity)
|
||||
await manager.clear(SettlementEntity)
|
||||
await manager.clear(StudentEntity)
|
||||
await manager.clear(ReasonEntity)
|
||||
await manager
|
||||
.getRepository(SettingEntity)
|
||||
.createQueryBuilder()
|
||||
.delete()
|
||||
.where("key NOT LIKE 'security_%'")
|
||||
.execute()
|
||||
|
||||
const insertStudents: Partial<StudentEntity>[] = []
|
||||
for (const s of students) {
|
||||
const name = String(s?.name ?? '').trim()
|
||||
if (!name) continue
|
||||
const score = Number(s?.score ?? 0)
|
||||
const extraJson = s?.extra_json != null ? String(s.extra_json) : null
|
||||
const createdAt = s?.created_at != null ? String(s.created_at) : new Date().toISOString()
|
||||
const updatedAt = s?.updated_at != null ? String(s.updated_at) : new Date().toISOString()
|
||||
insertStudents.push({
|
||||
name,
|
||||
score: Number.isFinite(score) ? score : 0,
|
||||
extra_json: extraJson,
|
||||
created_at: createdAt,
|
||||
updated_at: updatedAt
|
||||
})
|
||||
}
|
||||
if (insertStudents.length) {
|
||||
await manager.getRepository(StudentEntity).insert(insertStudents)
|
||||
}
|
||||
|
||||
const insertReasons: Partial<ReasonEntity>[] = []
|
||||
for (const r of reasons) {
|
||||
const content = String(r?.content ?? '').trim()
|
||||
if (!content) continue
|
||||
const category = String(r?.category ?? '其他')
|
||||
const delta = Number(r?.delta ?? 0)
|
||||
const isSystem = Number(r?.is_system ?? 0) ? 1 : 0
|
||||
const updatedAt = r?.updated_at != null ? String(r.updated_at) : new Date().toISOString()
|
||||
insertReasons.push({
|
||||
content,
|
||||
category,
|
||||
delta: Number.isFinite(delta) ? delta : 0,
|
||||
is_system: isSystem,
|
||||
updated_at: updatedAt
|
||||
})
|
||||
}
|
||||
if (insertReasons.length) {
|
||||
await manager.getRepository(ReasonEntity).insert(insertReasons)
|
||||
}
|
||||
|
||||
const insertSettlements: Partial<SettlementEntity>[] = []
|
||||
for (const s of settlements) {
|
||||
const id = Number(s?.id)
|
||||
const startTime = String(s?.start_time ?? '').trim()
|
||||
const endTime = String(s?.end_time ?? '').trim()
|
||||
const createdAt = String(s?.created_at ?? new Date().toISOString())
|
||||
if (!Number.isFinite(id) || !startTime || !endTime) continue
|
||||
insertSettlements.push({
|
||||
id,
|
||||
start_time: startTime,
|
||||
end_time: endTime,
|
||||
created_at: createdAt
|
||||
})
|
||||
}
|
||||
if (insertSettlements.length) {
|
||||
await manager.getRepository(SettlementEntity).insert(insertSettlements)
|
||||
}
|
||||
|
||||
const insertEvents: Partial<ScoreEventEntity>[] = []
|
||||
for (const e of events) {
|
||||
const uuid = String(e?.uuid ?? '').trim()
|
||||
const studentName = String(e?.student_name ?? '').trim()
|
||||
const reasonContent = String(e?.reason_content ?? '').trim()
|
||||
if (!uuid || !studentName || !reasonContent) continue
|
||||
const delta = Number(e?.delta ?? 0)
|
||||
const valPrev = Number(e?.val_prev ?? 0)
|
||||
const valCurr = Number(e?.val_curr ?? 0)
|
||||
const eventTime = String(e?.event_time ?? new Date().toISOString())
|
||||
const settlementIdRaw = e?.settlement_id
|
||||
const settlementId =
|
||||
settlementIdRaw === null || settlementIdRaw === undefined
|
||||
? null
|
||||
: Number(settlementIdRaw)
|
||||
insertEvents.push({
|
||||
uuid,
|
||||
student_name: studentName,
|
||||
reason_content: reasonContent,
|
||||
delta: Number.isFinite(delta) ? delta : 0,
|
||||
val_prev: Number.isFinite(valPrev) ? valPrev : 0,
|
||||
val_curr: Number.isFinite(valCurr) ? valCurr : 0,
|
||||
event_time: eventTime,
|
||||
settlement_id: Number.isFinite(settlementId as any) ? (settlementId as any) : null
|
||||
})
|
||||
}
|
||||
if (insertEvents.length) {
|
||||
await manager.getRepository(ScoreEventEntity).insert(insertEvents)
|
||||
}
|
||||
|
||||
for (const it of settings) {
|
||||
const key = String(it?.key ?? '').trim()
|
||||
if (!key || key.startsWith('security_')) continue
|
||||
await manager.getRepository(SettingEntity).save({ key, value: String(it?.value ?? '') })
|
||||
}
|
||||
})
|
||||
} catch (e: any) {
|
||||
return { success: false, message: e?.message || 'Import failed' }
|
||||
}
|
||||
|
||||
return { success: true }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export { StudentEntity } from './entities/StudentEntity'
|
||||
export { ReasonEntity } from './entities/ReasonEntity'
|
||||
export { ScoreEventEntity } from './entities/ScoreEventEntity'
|
||||
export { SettlementEntity } from './entities/SettlementEntity'
|
||||
export { SettingEntity } from './entities/SettingEntity'
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'
|
||||
|
||||
@Entity({ name: 'reasons' })
|
||||
export class ReasonEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: number
|
||||
|
||||
@Index({ unique: true })
|
||||
@Column({ type: 'text' })
|
||||
content!: string
|
||||
|
||||
@Column({ type: 'text', default: '其他' })
|
||||
category!: string
|
||||
|
||||
@Column({ type: 'integer' })
|
||||
delta!: number
|
||||
|
||||
@Column({ type: 'integer', default: 0 })
|
||||
is_system!: number
|
||||
|
||||
@Column({ type: 'text', default: () => 'CURRENT_TIMESTAMP' })
|
||||
updated_at!: string
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'
|
||||
|
||||
@Entity({ name: 'score_events' })
|
||||
export class ScoreEventEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: number
|
||||
|
||||
@Index({ unique: true })
|
||||
@Column({ type: 'text' })
|
||||
uuid!: string
|
||||
|
||||
@Column({ type: 'text' })
|
||||
student_name!: string
|
||||
|
||||
@Column({ type: 'text' })
|
||||
reason_content!: string
|
||||
|
||||
@Column({ type: 'integer' })
|
||||
delta!: number
|
||||
|
||||
@Column({ type: 'integer' })
|
||||
val_prev!: number
|
||||
|
||||
@Column({ type: 'integer' })
|
||||
val_curr!: number
|
||||
|
||||
@Column({ type: 'text', default: () => 'CURRENT_TIMESTAMP' })
|
||||
event_time!: string
|
||||
|
||||
@Index()
|
||||
@Column({ type: 'integer', nullable: true })
|
||||
settlement_id!: number | null
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Column, Entity, PrimaryColumn } from 'typeorm'
|
||||
|
||||
@Entity({ name: 'settings' })
|
||||
export class SettingEntity {
|
||||
@PrimaryColumn({ type: 'text' })
|
||||
key!: string
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
value!: string | null
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'
|
||||
|
||||
@Entity({ name: 'settlements' })
|
||||
export class SettlementEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: number
|
||||
|
||||
@Column({ type: 'text' })
|
||||
start_time!: string
|
||||
|
||||
@Column({ type: 'text' })
|
||||
end_time!: string
|
||||
|
||||
@Column({ type: 'text', default: () => 'CURRENT_TIMESTAMP' })
|
||||
created_at!: string
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Column, Entity, PrimaryGeneratedColumn, ManyToMany, JoinTable } from 'typeorm'
|
||||
import { TagEntity } from './TagEntity'
|
||||
|
||||
@Entity({ name: 'students' })
|
||||
export class StudentEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: number
|
||||
|
||||
@Column({ type: 'text' })
|
||||
name!: string
|
||||
|
||||
@Column({ type: 'text', default: '[]' })
|
||||
tags!: string
|
||||
|
||||
@Column({ type: 'integer', default: 0 })
|
||||
score!: number
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
extra_json!: string | null
|
||||
|
||||
@Column({ type: 'text', default: () => 'CURRENT_TIMESTAMP' })
|
||||
created_at!: string
|
||||
|
||||
@Column({ type: 'text', default: () => 'CURRENT_TIMESTAMP' })
|
||||
updated_at!: string
|
||||
|
||||
@ManyToMany(() => TagEntity, { cascade: true })
|
||||
@JoinTable({
|
||||
name: 'student_tags',
|
||||
joinColumn: { name: 'student_id', referencedColumnName: 'id' },
|
||||
inverseJoinColumn: { name: 'tag_id', referencedColumnName: 'id' }
|
||||
})
|
||||
tagEntities!: TagEntity[]
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Column, Entity, PrimaryGeneratedColumn, ManyToOne, JoinColumn, Index } from 'typeorm'
|
||||
import { StudentEntity } from './StudentEntity'
|
||||
import { TagEntity } from './TagEntity'
|
||||
|
||||
@Entity({ name: 'student_tags' })
|
||||
@Index(['student_id', 'tag_id'], { unique: true })
|
||||
export class StudentTagEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: number
|
||||
|
||||
@Column({ name: 'student_id', type: 'integer' })
|
||||
student_id!: number
|
||||
|
||||
@Column({ name: 'tag_id', type: 'integer' })
|
||||
tag_id!: number
|
||||
|
||||
@ManyToOne(() => StudentEntity, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'student_id' })
|
||||
student!: StudentEntity
|
||||
|
||||
@ManyToOne(() => TagEntity, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'tag_id' })
|
||||
tag!: TagEntity
|
||||
|
||||
@Column({ type: 'text', default: () => 'CURRENT_TIMESTAMP' })
|
||||
created_at!: string
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'
|
||||
|
||||
@Entity({ name: 'tags' })
|
||||
export class TagEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: number
|
||||
|
||||
@Column({ type: 'text', unique: true })
|
||||
name!: string
|
||||
|
||||
@Column({ type: 'text', default: () => 'CURRENT_TIMESTAMP' })
|
||||
created_at!: string
|
||||
|
||||
@Column({ type: 'text', default: () => 'CURRENT_TIMESTAMP' })
|
||||
updated_at!: string
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export { StudentEntity } from './StudentEntity'
|
||||
export { ReasonEntity } from './ReasonEntity'
|
||||
export { ScoreEventEntity } from './ScoreEventEntity'
|
||||
export { SettlementEntity } from './SettlementEntity'
|
||||
export { SettingEntity } from './SettingEntity'
|
||||
export { TagEntity } from './TagEntity'
|
||||
export { StudentTagEntity } from './StudentTagEntity'
|
||||
@@ -0,0 +1,155 @@
|
||||
import type { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class InitSchema2026011800000 implements MigrationInterface {
|
||||
name = 'InitSchema2026011800000'
|
||||
|
||||
private isPostgres(queryRunner: QueryRunner): boolean {
|
||||
return queryRunner.connection.options.type === 'postgres'
|
||||
}
|
||||
|
||||
async up(queryRunner: QueryRunner): Promise<void> {
|
||||
const isPg = this.isPostgres(queryRunner)
|
||||
const pkSerial = isPg ? 'SERIAL PRIMARY KEY' : 'integer PRIMARY KEY AUTOINCREMENT'
|
||||
const now = isPg ? 'CURRENT_TIMESTAMP' : '(CURRENT_TIMESTAMP)'
|
||||
const defaultZero = isPg ? 'DEFAULT 0' : 'DEFAULT (0)'
|
||||
const defaultEmptyArr = isPg ? "DEFAULT '[]'" : "DEFAULT '[]'"
|
||||
|
||||
if (!(await queryRunner.hasTable('students'))) {
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE "students" (
|
||||
"id" ${pkSerial} NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"score" integer NOT NULL ${defaultZero},
|
||||
"extra_json" text,
|
||||
"tags" text NOT NULL ${defaultEmptyArr},
|
||||
"created_at" text NOT NULL DEFAULT ${now},
|
||||
"updated_at" text NOT NULL DEFAULT ${now}
|
||||
)
|
||||
`)
|
||||
}
|
||||
|
||||
if (!(await queryRunner.hasTable('reasons'))) {
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE "reasons" (
|
||||
"id" ${pkSerial} NOT NULL,
|
||||
"content" text NOT NULL,
|
||||
"category" text NOT NULL DEFAULT '其他',
|
||||
"delta" integer NOT NULL,
|
||||
"is_system" integer NOT NULL ${defaultZero},
|
||||
"updated_at" text NOT NULL DEFAULT ${now},
|
||||
CONSTRAINT "UQ_reasons_content" UNIQUE ("content")
|
||||
)
|
||||
`)
|
||||
}
|
||||
|
||||
if (!(await queryRunner.hasTable('settlements'))) {
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE "settlements" (
|
||||
"id" ${pkSerial} NOT NULL,
|
||||
"start_time" text NOT NULL,
|
||||
"end_time" text NOT NULL,
|
||||
"created_at" text NOT NULL DEFAULT ${now}
|
||||
)
|
||||
`)
|
||||
}
|
||||
|
||||
if (!(await queryRunner.hasTable('score_events'))) {
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE "score_events" (
|
||||
"id" ${pkSerial} NOT NULL,
|
||||
"uuid" text NOT NULL,
|
||||
"student_name" text NOT NULL,
|
||||
"reason_content" text NOT NULL,
|
||||
"delta" integer NOT NULL,
|
||||
"val_prev" integer NOT NULL,
|
||||
"val_curr" integer NOT NULL,
|
||||
"event_time" text NOT NULL DEFAULT ${now},
|
||||
"settlement_id" integer,
|
||||
CONSTRAINT "UQ_score_events_uuid" UNIQUE ("uuid")
|
||||
)
|
||||
`)
|
||||
}
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX IF NOT EXISTS "IDX_score_events_settlement_id" ON "score_events" ("settlement_id")`
|
||||
)
|
||||
|
||||
if (!(await queryRunner.hasTable('settings'))) {
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE "settings" (
|
||||
"key" text PRIMARY KEY NOT NULL,
|
||||
"value" text
|
||||
)
|
||||
`)
|
||||
}
|
||||
|
||||
const reasonValues = `('上课发言','课堂表现',1,1,CURRENT_TIMESTAMP),('作业优秀','作业情况',2,1,CURRENT_TIMESTAMP),('纪律良好','纪律',1,1,CURRENT_TIMESTAMP),('帮助同学','品德',2,1,CURRENT_TIMESTAMP),('迟到','纪律',-1,1,CURRENT_TIMESTAMP),('未交作业','作业情况',-2,1,CURRENT_TIMESTAMP)`
|
||||
|
||||
if (isPg) {
|
||||
await queryRunner.query(`
|
||||
INSERT INTO "reasons" ("content","category","delta","is_system","updated_at") VALUES
|
||||
${reasonValues}
|
||||
ON CONFLICT DO NOTHING
|
||||
`)
|
||||
} else {
|
||||
await queryRunner.query(`
|
||||
INSERT OR IGNORE INTO "reasons" ("content","category","delta","is_system","updated_at") VALUES
|
||||
${reasonValues}
|
||||
`)
|
||||
}
|
||||
|
||||
if (!(await queryRunner.hasTable('tags'))) {
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE "tags" (
|
||||
"id" ${pkSerial} NOT NULL,
|
||||
"name" text NOT NULL UNIQUE,
|
||||
"created_at" text NOT NULL DEFAULT ${now},
|
||||
"updated_at" text NOT NULL DEFAULT ${now}
|
||||
)
|
||||
`)
|
||||
}
|
||||
|
||||
if (!(await queryRunner.hasTable('student_tags'))) {
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE "student_tags" (
|
||||
"id" ${pkSerial} NOT NULL,
|
||||
"student_id" integer NOT NULL,
|
||||
"tag_id" integer NOT NULL,
|
||||
"created_at" text NOT NULL DEFAULT ${now},
|
||||
FOREIGN KEY ("student_id") REFERENCES "students"("id") ON DELETE CASCADE,
|
||||
FOREIGN KEY ("tag_id") REFERENCES "tags"("id") ON DELETE CASCADE,
|
||||
UNIQUE("student_id", "tag_id")
|
||||
)
|
||||
`)
|
||||
}
|
||||
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX IF NOT EXISTS "IDX_student_tags_student_id" ON "student_tags" ("student_id")`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX IF NOT EXISTS "IDX_student_tags_tag_id" ON "student_tags" ("tag_id")`
|
||||
)
|
||||
|
||||
const tagValues = `('优秀生'),('班干部'),('勤奋'),('活跃'),('进步快')`
|
||||
|
||||
if (isPg) {
|
||||
await queryRunner.query(`
|
||||
INSERT INTO "tags" ("name") VALUES ${tagValues}
|
||||
ON CONFLICT DO NOTHING
|
||||
`)
|
||||
} else {
|
||||
await queryRunner.query(`
|
||||
INSERT OR IGNORE INTO "tags" ("name") VALUES ${tagValues}
|
||||
`)
|
||||
}
|
||||
}
|
||||
|
||||
async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS "score_events"`)
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS "settlements"`)
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS "students"`)
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS "reasons"`)
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS "settings"`)
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS "student_tags"`)
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS "tags"`)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { InitSchema2026011800000 } from './InitSchema2026011800000'
|
||||
|
||||
export const migrations = [InitSchema2026011800000]
|
||||
Reference in New Issue
Block a user