refactor: 拆分数据库迁移基建并新增审计日志工具
This commit is contained in:
180
apps/server/src/database/database-migrations.backfill.ts
Normal file
180
apps/server/src/database/database-migrations.backfill.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { uuidV7 } from '../common/uuid-v7';
|
||||
import { withQueryRunner } from './database-migrations.runner';
|
||||
|
||||
export async function backfillOrganizations(
|
||||
dataSource: DataSource,
|
||||
): Promise<void> {
|
||||
await withQueryRunner(dataSource, async (runner) => {
|
||||
const tables = await runner.getTables([
|
||||
'tenants',
|
||||
'organizations',
|
||||
'students',
|
||||
'occupancies',
|
||||
'classroom_rentals',
|
||||
]);
|
||||
const tableNames = new Set(tables.map((table) => table.name));
|
||||
if (!tableNames.has('organizations')) return;
|
||||
|
||||
const organizationRows = () =>
|
||||
runner.query('SELECT * FROM organizations WHERE is_host = 1 LIMIT 1');
|
||||
let host = (await organizationRows())[0];
|
||||
if (!host) {
|
||||
await runner.query(
|
||||
`INSERT INTO organizations (public_id, code, name, is_host, color, notes, status, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`,
|
||||
[
|
||||
uuidV7(),
|
||||
'HOST',
|
||||
process.env.HOST_ORGANIZATION_NAME || '本机构',
|
||||
1,
|
||||
'#1677ff',
|
||||
'系统默认运营主体',
|
||||
'active',
|
||||
],
|
||||
);
|
||||
host = (await organizationRows())[0];
|
||||
}
|
||||
if (!host) return;
|
||||
|
||||
if (tableNames.has('tenants')) {
|
||||
const legacyTenants: Array<Record<string, unknown>> =
|
||||
await runner.query('SELECT * FROM tenants');
|
||||
for (const legacy of legacyTenants) {
|
||||
const name = String(legacy.name || '').trim();
|
||||
if (!name) continue;
|
||||
let external = (
|
||||
await runner.query('SELECT * FROM organizations WHERE name = ? LIMIT 1', [name])
|
||||
)[0];
|
||||
if (!external) {
|
||||
await runner.query(
|
||||
`INSERT INTO organizations (public_id, code, name, is_host, contact_name, phone, color, notes, status, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`,
|
||||
[
|
||||
uuidV7(),
|
||||
`ORG_${legacy.id}`,
|
||||
name,
|
||||
0,
|
||||
legacy.contact || null,
|
||||
legacy.phone || null,
|
||||
legacy.color || null,
|
||||
legacy.notes || null,
|
||||
legacy.status || 'active',
|
||||
],
|
||||
);
|
||||
external = (
|
||||
await runner.query('SELECT * FROM organizations WHERE name = ? LIMIT 1', [name])
|
||||
)[0];
|
||||
}
|
||||
if (!external) continue;
|
||||
if (tableNames.has('students')) {
|
||||
await runner
|
||||
.query(
|
||||
'UPDATE students SET organization_id = ? WHERE organization_id IS NULL AND tenant_id = ?',
|
||||
[external.id, legacy.id],
|
||||
)
|
||||
.catch(() => undefined);
|
||||
}
|
||||
if (tableNames.has('occupancies')) {
|
||||
await runner
|
||||
.query(
|
||||
'UPDATE occupancies SET responsible_organization_id = ? WHERE responsible_organization_id IS NULL AND tenant_id = ?',
|
||||
[external.id, legacy.id],
|
||||
)
|
||||
.catch(() => undefined);
|
||||
}
|
||||
if (tableNames.has('classroom_rentals')) {
|
||||
await runner
|
||||
.query(
|
||||
'UPDATE classroom_rentals SET lessee_organization_id = ?, lessor_organization_id = ? WHERE lessee_organization_id IS NULL AND tenant_id = ?',
|
||||
[external.id, host.id, legacy.id],
|
||||
)
|
||||
.catch(() => undefined);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (tableNames.has('students')) {
|
||||
await runner.query(
|
||||
'UPDATE students SET organization_id = ? WHERE organization_id IS NULL',
|
||||
[host.id],
|
||||
);
|
||||
}
|
||||
if (tableNames.has('occupancies')) {
|
||||
await runner.query(
|
||||
`UPDATE occupancies
|
||||
SET responsible_organization_id = COALESCE(
|
||||
(SELECT organization_id FROM students WHERE students.id = occupancies.student_id), ?
|
||||
)
|
||||
WHERE responsible_organization_id IS NULL`,
|
||||
[host.id],
|
||||
);
|
||||
}
|
||||
if (tableNames.has('classroom_rentals')) {
|
||||
await runner.query(
|
||||
'UPDATE classroom_rentals SET lessor_organization_id = ? WHERE lessor_organization_id IS NULL',
|
||||
[host.id],
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function normalizeClassDates(
|
||||
dataSource: DataSource,
|
||||
logger: Logger,
|
||||
): Promise<void> {
|
||||
const driver = dataSource.options.type;
|
||||
let columns: Array<'start_date' | 'end_date'> = ['start_date', 'end_date'];
|
||||
|
||||
await withQueryRunner(dataSource, async (runner) => {
|
||||
const table = await runner.getTable('classes');
|
||||
if (!table) return;
|
||||
|
||||
// Fresh MySQL schemas created by TypeORM already use native DATE columns.
|
||||
// This cleanup is only for legacy schemas that stored dates as strings;
|
||||
// comparing a native DATE column with '' raises ER_TRUNCATED_WRONG_VALUE
|
||||
// in strict SQL mode.
|
||||
if (driver === 'mysql') {
|
||||
columns = columns.filter((columnName) => {
|
||||
const column = table.columns.find((item) => item.name === columnName);
|
||||
const type = String(column?.type ?? '').toLowerCase();
|
||||
return !['date', 'datetime', 'timestamp'].includes(type);
|
||||
});
|
||||
if (columns.length === 0) return;
|
||||
}
|
||||
});
|
||||
|
||||
const columnText = (column: string) =>
|
||||
driver === 'mysql' ? `CAST(${column} AS CHAR)` : column;
|
||||
const firstTenChars = (column: string) =>
|
||||
driver === 'mysql'
|
||||
? `NULLIF(LEFT(${columnText(column)}, 10), '')`
|
||||
: `NULLIF(substr(${column}, 1, 10), '')`;
|
||||
const normalizedDate = (column: string) => `CASE
|
||||
WHEN ${column} IS NULL THEN NULL
|
||||
ELSE ${firstTenChars(column)}
|
||||
END`;
|
||||
const lengthFunction = driver === 'mysql' ? 'CHAR_LENGTH' : 'length';
|
||||
const needsNormalization = (column: string) => `(
|
||||
${column} IS NOT NULL
|
||||
AND (${columnText(column)} = '' OR ${lengthFunction}(${columnText(column)}) > 10)
|
||||
)`;
|
||||
|
||||
const assignments = columns
|
||||
.map((column) => `${column} = ${normalizedDate(column)}`)
|
||||
.join(',\n ');
|
||||
const predicates = columns.map((column) => needsNormalization(column)).join('\n OR ');
|
||||
const result = await dataSource.transaction((manager) =>
|
||||
manager.query(`
|
||||
UPDATE classes
|
||||
SET
|
||||
${assignments}
|
||||
WHERE
|
||||
${predicates}
|
||||
`),
|
||||
);
|
||||
|
||||
const affected = typeof result?.changes === 'number' ? result.changes : result?.affectedRows;
|
||||
if (affected) logger.log(`已规范化 ${affected} 条班级日期数据`);
|
||||
}
|
||||
Reference in New Issue
Block a user