- 全模块类型化:controller 的 req: any → AuthenticatedRequest/RequestUser, 聚合查询 getRawMany 泛型标注、导入行/响应体定义具体 interface、 catch (e: any) → unknown + 收窄、no-base-to-string 用 String() 显式转换 - 第三方无类型库边界(pdfkit/exceljs)文件级或单行 disable 并注明理由 - 顺带修复:get-business-context.tool 两个 require-await error、 bills.controller 参数顺序隐患、main.ts compression 调用 - 运行时逻辑零改动;测试 142 套件 / 1065 用例全部通过
191 lines
6.6 KiB
TypeScript
191 lines
6.6 KiB
TypeScript
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<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 = (): Promise<OrganizationRow[]> =>
|
|
runner.query('SELECT * FROM organizations WHERE is_host = 1 LIMIT 1') as Promise<OrganizationRow[]>;
|
|
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<string, unknown>
|
|
>;
|
|
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<void> {
|
|
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<UpdateResultLike | undefined>((manager) =>
|
|
manager.query(`
|
|
UPDATE classes
|
|
SET
|
|
${assignments}
|
|
WHERE
|
|
${predicates}
|
|
`),
|
|
);
|
|
|
|
const affected = typeof result?.changes === 'number' ? result.changes : result?.affectedRows;
|
|
if (affected) logger.log(`已规范化 ${affected} 条班级日期数据`);
|
|
}
|