- 创建统一的下载错误处理模块 (lib/download-errors.ts) - 实现13种错误类型分类和中文错误消息 - 后端统一使用 createDownloadError 创建标准化错误响应 - 前端统一使用 parseDownloadError 解析错误并显示 - 修复正式系统数据库Schema,添加产品全案相关字段和表 - 执行数据库迁移:添加 task_no, task_status, task_fetched_at 字段 - 创建 material_assets, api_keys, api_call_logs 表
481 lines
14 KiB
TypeScript
481 lines
14 KiB
TypeScript
/**
|
||
* 迁移验证脚本
|
||
*
|
||
* 功能:
|
||
* 验证各个阶段的迁移结果,确保数据完整性
|
||
*
|
||
* 使用方法:
|
||
* 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();
|
||
});
|
||
|