gongxiangp2/scripts/verify-migration.ts
chenpei ea9076df45 feat: 实现统一的下载错误处理系统并修复产品全案数据库Schema
- 创建统一的下载错误处理模块 (lib/download-errors.ts)
- 实现13种错误类型分类和中文错误消息
- 后端统一使用 createDownloadError 创建标准化错误响应
- 前端统一使用 parseDownloadError 解析错误并显示
- 修复正式系统数据库Schema,添加产品全案相关字段和表
- 执行数据库迁移:添加 task_no, task_status, task_fetched_at 字段
- 创建 material_assets, api_keys, api_call_logs 表
2026-01-05 17:52:42 +08:00

481 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 迁移验证脚本
*
* 功能:
* 验证各个阶段的迁移结果,确保数据完整性
*
* 使用方法:
* npx tsx scripts/verify-migration.ts [stage]
*
* 参数:
* stage: 可选,验证特定阶段(1/2/3/4),不指定则验证所有阶段
*/
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
interface VerificationResult {
stage: string;
passed: boolean;
issues: string[];
stats: Record<string, number>;
}
async function verifyStage1(): Promise<VerificationResult> {
const result: VerificationResult = {
stage: '阶段一:用户状态管理',
passed: true,
issues: [],
stats: {}
};
console.log('🔍 验证阶段一:用户状态管理...\n');
try {
// 1. 检查 status 字段是否存在
try {
await prisma.user.findFirst({ select: { status: true } });
} catch (error: any) {
if (error.message?.includes('Unknown arg `status`')) {
result.passed = false;
result.issues.push('status 字段不存在,Schema 未更新');
return result;
}
throw error;
}
// 2. 统计各状态用户数
const totalUsers = await prisma.user.count();
const activeUsers = await prisma.user.count({ where: { status: 'active' } });
const pendingUsers = await prisma.user.count({ where: { status: 'pending' } });
const suspendedUsers = await prisma.user.count({ where: { status: 'suspended' } });
const archivedUsers = await prisma.user.count({ where: { status: 'archived' } });
// status 字段有默认值 'pending',所以不会有 null 值
// 只检查空字符串的情况
const nullStatusUsers = await prisma.user.count({ where: { status: '' } });
result.stats = {
total: totalUsers,
active: activeUsers,
pending: pendingUsers,
suspended: suspendedUsers,
archived: archivedUsers,
nullStatus: nullStatusUsers
};
// 3. 检查是否有 status 为 null 的用户
if (nullStatusUsers > 0) {
result.passed = false;
result.issues.push(`${nullStatusUsers} 个用户的 status 字段为 null`);
}
// 4. 检查 isActive 和 status 的一致性
const inconsistentActive = await prisma.user.count({
where: {
isActive: true,
status: { not: 'active' }
}
});
const inconsistentPending = await prisma.user.count({
where: {
isActive: false,
status: { not: 'pending' }
}
});
if (inconsistentActive > 0) {
result.passed = false;
result.issues.push(`${inconsistentActive} 个用户的 isActive=true 但 status!='active'`);
}
if (inconsistentPending > 0) {
result.passed = false;
result.issues.push(`${inconsistentPending} 个用户的 isActive=false 但 status!='pending'`);
}
// 5. 检查审计字段
const hasLastLoginAt = await prisma.user.findFirst({
select: { lastLoginAt: true }
}).then(() => true).catch(() => false);
if (!hasLastLoginAt) {
result.issues.push('lastLoginAt 字段不存在(可选,不影响功能)');
}
console.log(' 统计:');
console.log(` 总用户数: ${totalUsers}`);
console.log(` active: ${activeUsers}`);
console.log(` pending: ${pendingUsers}`);
console.log(` suspended: ${suspendedUsers}`);
console.log(` archived: ${archivedUsers}`);
if (nullStatusUsers > 0) {
console.log(` ⚠️ null: ${nullStatusUsers}`);
}
if (result.issues.length === 0) {
console.log(' ✅ 验证通过');
} else {
console.log(' ❌ 发现问题:');
result.issues.forEach(issue => console.log(` - ${issue}`));
}
} catch (error) {
result.passed = false;
result.issues.push(`验证过程出错: ${error}`);
console.error(' ❌ 验证失败:', error);
}
return result;
}
async function verifyStage2(): Promise<VerificationResult> {
const result: VerificationResult = {
stage: '阶段二:部门外键关联',
passed: true,
issues: [],
stats: {}
};
console.log('\n🔍 验证阶段二:部门外键关联...\n');
try {
// 1. 检查 departmentId 字段是否存在
try {
await prisma.user.findFirst({ select: { departmentId: true } });
} catch (error: any) {
if (error.message?.includes('Unknown arg `departmentId`')) {
result.passed = false;
result.issues.push('departmentId 字段不存在,Schema 未更新');
return result;
}
throw error;
}
// 2. 统计
const totalUsers = await prisma.user.count();
const usersWithDeptName = await prisma.user.count({
where: { department: { not: '' } }
});
const usersWithDeptId = await prisma.user.count({
where: { departmentId: { not: null } }
});
const usersWithBoth = await prisma.user.count({
where: {
department: { not: '' },
departmentId: { not: null }
}
});
const usersWithDeptNameButNoId = await prisma.user.count({
where: {
department: { not: '' },
departmentId: null
}
});
result.stats = {
total: totalUsers,
withDeptName: usersWithDeptName,
withDeptId: usersWithDeptId,
withBoth: usersWithBoth,
missingId: usersWithDeptNameButNoId
};
// 3. 检查 departmentId 和 department 的一致性
const usersWithBothFields = await prisma.user.findMany({
where: {
department: { not: '' },
departmentId: { not: null }
},
include: {
departmentRef: true
}
});
const inconsistent = usersWithBothFields.filter(
u => u.department !== u.departmentRef?.name
);
if (inconsistent.length > 0) {
result.passed = false;
result.issues.push(`${inconsistent.length} 个用户的 department 和 departmentRef.name 不一致`);
}
// 4. 检查是否有无效的 departmentId
const usersWithInvalidDeptId = await prisma.user.findMany({
where: {
departmentId: { not: null }
},
include: {
departmentRef: true
}
});
const invalidDeptIds = usersWithInvalidDeptId.filter(u => !u.departmentRef);
if (invalidDeptIds.length > 0) {
result.passed = false;
result.issues.push(`${invalidDeptIds.length} 个用户的 departmentId 指向不存在的部门`);
}
// 5. 检查是否有 department 名称但无对应部门记录
const usersWithDeptNameList = await prisma.user.findMany({
where: {
department: { not: '' }
},
select: {
department: true
},
distinct: ['department']
});
const deptNames = usersWithDeptNameList.map(u => u.department.trim());
const existingDepts = await prisma.department.findMany({
select: { name: true }
});
const existingNames = new Set(existingDepts.map(d => d.name));
const missingDepts = deptNames.filter(name => !existingNames.has(name));
if (missingDepts.length > 0) {
result.passed = false;
result.issues.push(`${missingDepts.length} 个用户部门在 Department 表中不存在: ${missingDepts.join(', ')}`);
}
console.log(' 统计:');
console.log(` 总用户数: ${totalUsers}`);
console.log(` 有部门名称: ${usersWithDeptName}`);
console.log(` 有部门ID: ${usersWithDeptId}`);
console.log(` 两者都有: ${usersWithBoth}`);
if (usersWithDeptNameButNoId > 0) {
console.log(` ⚠️ 有部门名称但无ID: ${usersWithDeptNameButNoId}`);
}
if (result.issues.length === 0) {
console.log(' ✅ 验证通过');
} else {
console.log(' ❌ 发现问题:');
result.issues.forEach(issue => console.log(` - ${issue}`));
}
} catch (error) {
result.passed = false;
result.issues.push(`验证过程出错: ${error}`);
console.error(' ❌ 验证失败:', error);
}
return result;
}
async function verifyStage3(): Promise<VerificationResult> {
const result: VerificationResult = {
stage: '阶段三:部门层级结构',
passed: true,
issues: [],
stats: {}
};
console.log('\n🔍 验证阶段三:部门层级结构...\n');
try {
// 检查 level 字段是否存在
try {
await prisma.department.findFirst({ select: { level: true } });
} catch (error: any) {
// 字段不存在,这是正常的(阶段三尚未实施)
if (error.message?.includes('Unknown arg `level`')) {
result.passed = true;
result.issues.push('level 字段不存在(阶段三尚未实施,这是正常的)');
console.log(' ⏭️ 阶段三尚未实施,跳过验证');
return result;
}
throw error;
}
// 如果字段存在,进行验证
// 由于level字段有默认值,直接统计所有部门
const totalDepts = await prisma.department.count();
const deptsWithParent = await prisma.department.count({
where: { parentId: { not: null } }
});
// 使用原生SQL查询level统计(因为level字段有默认值,不能查询null)
const levelStatsRaw = await prisma.$queryRaw<Array<{
level1: bigint;
level2: bigint;
level3: bigint;
}>>`
SELECT
COUNT(*) FILTER (WHERE level = 1) as level1,
COUNT(*) FILTER (WHERE level = 2) as level2,
COUNT(*) FILTER (WHERE level = 3) as level3
FROM departments
`.catch(() => [{ level1: BigInt(0), level2: BigInt(0), level3: BigInt(0) }]);
const levelStats = levelStatsRaw[0] || { level1: BigInt(0), level2: BigInt(0), level3: BigInt(0) };
const deptsWithLevel = totalDepts; // 所有部门都应该有level(因为有默认值)
result.stats = {
total: totalDepts,
withLevel: deptsWithLevel,
withParent: deptsWithParent,
level1: Number(levelStats.level1),
level2: Number(levelStats.level2),
level3: Number(levelStats.level3),
};
console.log(' 统计:');
console.log(` 总部门数: ${totalDepts}`);
console.log(` 有层级: ${deptsWithLevel} (所有部门,因为有默认值)`);
console.log(` 有父部门: ${deptsWithParent}`);
console.log(` 一级部门: ${Number(levelStats.level1)}`);
console.log(` 二级部门: ${Number(levelStats.level2)}`);
console.log(` 三级部门: ${Number(levelStats.level3)}`);
if (result.issues.length === 0) {
console.log(' ✅ 验证通过');
} else {
console.log(' ⚠️ 提示:');
result.issues.forEach(issue => console.log(` - ${issue}`));
}
} catch (error) {
// 如果出错,可能是字段不存在,这是正常的
result.passed = true;
result.issues.push(`阶段三尚未实施(这是正常的)`);
console.log(' ⏭️ 阶段三尚未实施,跳过验证');
}
return result;
}
async function verifyStage4(): Promise<VerificationResult> {
const result: VerificationResult = {
stage: '阶段四:角色枚举扩展',
passed: true,
issues: [],
stats: {}
};
console.log('\n🔍 验证阶段四:角色枚举扩展...\n');
try {
const totalUsers = await prisma.user.count();
const userRole = await prisma.user.count({ where: { role: 'USER' } });
const adminRole = await prisma.user.count({ where: { role: 'ADMIN' } });
// 阶段四尚未实施,DEPT_ADMIN 和 SYSTEM_ADMIN 角色还不存在
// 使用原生查询检查这些角色是否存在
let deptAdminRole = 0;
let systemAdminRole = 0;
try {
// 尝试查询新角色(如果存在)
const deptAdminResult = await prisma.$queryRaw<Array<{ count: bigint }>>`
SELECT COUNT(*) as count FROM users WHERE role = 'DEPT_ADMIN'
`.catch(() => null);
const systemAdminResult = await prisma.$queryRaw<Array<{ count: bigint }>>`
SELECT COUNT(*) as count FROM users WHERE role = 'SYSTEM_ADMIN'
`.catch(() => null);
if (deptAdminResult) {
deptAdminRole = Number(deptAdminResult[0]?.count || 0);
}
if (systemAdminResult) {
systemAdminRole = Number(systemAdminResult[0]?.count || 0);
}
} catch (error) {
// 角色不存在,这是正常的(阶段四尚未实施)
result.issues.push('DEPT_ADMIN 和 SYSTEM_ADMIN 角色不存在(阶段四尚未实施,这是正常的)');
}
result.stats = {
total: totalUsers,
USER: userRole,
ADMIN: adminRole,
DEPT_ADMIN: deptAdminRole,
SYSTEM_ADMIN: systemAdminRole
};
console.log(' 统计:');
console.log(` 总用户数: ${totalUsers}`);
console.log(` USER: ${userRole}`);
console.log(` ADMIN: ${adminRole}`);
if (deptAdminRole > 0 || systemAdminRole > 0) {
console.log(` DEPT_ADMIN: ${deptAdminRole}`);
console.log(` SYSTEM_ADMIN: ${systemAdminRole}`);
} else {
console.log(` ⏭️ DEPT_ADMIN 和 SYSTEM_ADMIN 角色尚未添加(阶段四尚未实施)`);
}
if (result.issues.length === 0) {
console.log(' ✅ 验证通过');
} else {
console.log(' ⚠️ 提示:');
result.issues.forEach(issue => console.log(` - ${issue}`));
}
} catch (error) {
// 如果出错,可能是角色不存在,这是正常的
result.passed = true;
result.issues.push(`阶段四尚未实施(这是正常的)`);
console.log(' ⏭️ 阶段四尚未实施,跳过验证');
}
return result;
}
async function main() {
const stage = process.argv[2];
console.log('📋 开始迁移验证...\n');
const results: VerificationResult[] = [];
if (!stage || stage === '1') {
results.push(await verifyStage1());
}
if (!stage || stage === '2') {
results.push(await verifyStage2());
}
if (!stage || stage === '3') {
results.push(await verifyStage3());
}
if (!stage || stage === '4') {
results.push(await verifyStage4());
}
// 总结
console.log('\n' + '='.repeat(50));
console.log('📊 验证总结\n');
const allPassed = results.every(r => r.passed);
results.forEach(result => {
const status = result.passed ? '✅' : '❌';
console.log(`${status} ${result.stage}`);
if (!result.passed && result.issues.length > 0) {
result.issues.forEach(issue => console.log(` - ${issue}`));
}
});
if (allPassed) {
console.log('\n✅ 所有验证通过!');
} else {
console.log('\n❌ 部分验证未通过,请检查上述问题');
process.exit(1);
}
}
main()
.catch((error) => {
console.error('❌ 验证失败:', error);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});