import { Logger } from '@nestjs/common'; import { DataSource } from 'typeorm'; import { uuidV7 } from '../common/uuid-v7'; import { withQueryRunner } from './database-migrations.runner'; /** 迁移脚本中用到的 organizations 表最小行结构。 */ interface OrganizationRow { id: number; } /** String() 包装:避免 unknown 收窄后触发 no-base-to-string。 */ function stringify(value: unknown): string { return String(value); } export async function backfillOrganizations( dataSource: DataSource, ): Promise { 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 = (): Promise => runner.query('SELECT * FROM organizations WHERE is_host = 1 LIMIT 1') as Promise; 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 = (await runner.query('SELECT * FROM tenants')) as Array< Record >; for (const legacy of legacyTenants) { const name = stringify(legacy.name || '').trim(); if (!name) continue; const externalRows = (await runner.query( 'SELECT * FROM organizations WHERE name = ? LIMIT 1', [name], )) as OrganizationRow[]; let external = externalRows[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_${String(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])) as OrganizationRow[] )[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 { 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. 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) => `CAST(${column} AS CHAR)`; const firstTenChars = (column: string) => `NULLIF(LEFT(${columnText(column)}, 10), '')`; const normalizedDate = (column: string) => `CASE WHEN ${column} IS NULL THEN NULL ELSE ${firstTenChars(column)} END`; const lengthFunction = 'CHAR_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 '); interface UpdateResultLike { changes?: number; affectedRows?: number; } 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} 条班级日期数据`); }