From 543a76fba49f721feb9367b986bddda025387d00 Mon Sep 17 00:00:00 2001 From: songtianlun Date: Tue, 5 Aug 2025 23:42:48 +0800 Subject: [PATCH] fix seed --- docs/deployment-fix.md | 147 +++++++++++++++++++++++++++++++++++++++++ package.json | 2 + prisma/seed.ts | 66 ++++++++++-------- 3 files changed, 189 insertions(+), 26 deletions(-) create mode 100644 docs/deployment-fix.md diff --git a/docs/deployment-fix.md b/docs/deployment-fix.md new file mode 100644 index 0000000..4ce825d --- /dev/null +++ b/docs/deployment-fix.md @@ -0,0 +1,147 @@ +# 部署修复说明 + +## 问题解决 + +已修复在推送到现有环境时遇到的外键约束错误: + +``` +Error: insert or update on table "users" violates foreign key constraint "users_subscriptionPlanId_fkey" +``` + +## 解决方案 + +通过修改种子脚本 (`prisma/seed.ts`),确保在数据库推送时: + +1. **创建必需的套餐**:使用 `upsert` 确保 'free' 和 'pro' 套餐存在 +2. **修复用户数据**:自动修复无效的套餐引用 +3. **验证数据完整性**:确保所有用户都有有效的套餐关联 + +## 使用方法 + +现在可以直接使用现有命令进行部署: + +```bash +npm run db:push +``` + +这个命令会: +1. 推送数据库架构 (`prisma db push`) +2. 运行种子脚本 (`npm run db:seed`) +3. 自动处理数据修复 + +## 种子脚本改进 + +### 主要变更 + +1. **使用 upsert 而不是 create** + ```typescript + // 之前:如果套餐存在就跳过 + if (existingPlans > 0) return + + // 现在:确保必需套餐存在 + await prisma.subscriptionPlan.upsert({ + where: { id: 'free' }, + update: { name: 'free', isActive: true }, + create: { /* 完整套餐数据 */ } + }) + ``` + +2. **智能用户数据修复** + ```typescript + // 检查所有用户的套餐引用 + const validPlanIds = ['free', 'pro'] + const usersToUpdate = users.filter(user => { + const hasValidSubscribePlan = validPlanIds.includes(user.subscribePlan) + const hasValidSubscriptionPlanId = validPlanIds.includes(user.subscriptionPlanId) + return !hasValidSubscribePlan || !hasValidSubscriptionPlanId + }) + + // 修复无效引用 + for (const user of usersToUpdate) { + await prisma.user.update({ + where: { id: user.id }, + data: { + subscribePlan: validPlanIds.includes(user.subscribePlan) ? user.subscribePlan : 'free', + subscriptionPlanId: validPlanIds.includes(user.subscribePlan) ? user.subscribePlan : 'free' + } + }) + } + ``` + +### 执行流程 + +1. **创建/更新套餐**:确保 'free' 和 'pro' 套餐存在 +2. **检查用户数据**:找出有无效套餐引用的用户 +3. **修复用户数据**:将无效引用改为 'free' +4. **验证结果**:确认所有数据都是一致的 + +## 测试验证 + +种子脚本运行结果: +``` +🌱 Starting database seeding... +📦 Ensuring essential subscription plans exist... +✅ Created subscription plan: Free Plan +✅ Created subscription plan: Pro Plan +👥 Updating existing users... +👥 No users need subscription plan updates +📊 Seeding completed: + • 2 subscription plans created + • 2 users have valid plan associations +🎉 Database seeding finished successfully! +``` + +## 部署到现有环境 + +现在可以安全地部署到任何现有环境: + +1. **本地测试**: + ```bash + npm run db:push + npm run build + ``` + +2. **生产部署**: + ```bash + npm run db:push + npm start + ``` + +## 优势 + +1. **无需额外脚本**:使用现有的 `npm run db:push` 命令 +2. **自动修复**:种子脚本自动处理数据不一致问题 +3. **向后兼容**:不会破坏现有数据 +4. **幂等操作**:可以重复运行而不会出错 + +## 注意事项 + +1. **Stripe 配置**:部署后需要设置 Pro 套餐的 Stripe 价格 ID + ```sql + UPDATE subscription_plans + SET "stripePriceId" = 'price_your_stripe_price_id' + WHERE name = 'pro'; + ``` + +2. **数据验证**:部署后验证定价页面和订阅功能 +3. **备份建议**:在生产环境部署前建议备份数据库 + +## 故障排除 + +如果仍然遇到问题: + +1. **检查套餐**: + ```sql + SELECT id, name, "isActive" FROM subscription_plans; + ``` + +2. **检查用户**: + ```sql + SELECT "subscribePlan", COUNT(*) FROM users GROUP BY "subscribePlan"; + ``` + +3. **手动修复**: + ```sql + UPDATE users SET "subscribePlan" = 'free' + WHERE "subscribePlan" NOT IN ('free', 'pro'); + ``` diff --git a/package.json b/package.json index d8705d4..e7756f1 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,8 @@ "lint": "next lint", "db:generate": "prisma generate", "db:push": "prisma db push && npm run db:seed", + "db:deploy": "./scripts/deploy-with-migration.sh", + "db:safe-migrate": "tsx scripts/safe-db-migration.ts", "db:migrate": "prisma migrate dev", "db:studio": "prisma studio", "db:reset": "prisma migrate reset", diff --git a/prisma/seed.ts b/prisma/seed.ts index 8525d3a..22a46ed 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -5,19 +5,17 @@ const prisma = new PrismaClient() async function main() { console.log('🌱 Starting database seeding...') - // 检查是否已存在订阅套餐 - const existingPlans = await prisma.subscriptionPlan.count() - - if (existingPlans > 0) { - console.log('📦 Subscription plans already exist, skipping seed...') - return - } + console.log('📦 Ensuring essential subscription plans exist...') - console.log('📦 Creating default subscription plans...') - - // 创建免费套餐 - const freePlan = await prisma.subscriptionPlan.create({ - data: { + // 确保免费套餐存在 + const freePlan = await prisma.subscriptionPlan.upsert({ + where: { id: 'free' }, + update: { + // 更新现有套餐的关键字段 + name: 'free', + isActive: true + }, + create: { id: 'free', name: 'Free', displayName: 'Free Plan', @@ -42,9 +40,15 @@ async function main() { } }) - // 创建 Pro 套餐 - const proPlan = await prisma.subscriptionPlan.create({ - data: { + // 确保 Pro 套餐存在 + const proPlan = await prisma.subscriptionPlan.upsert({ + where: { id: 'pro' }, + update: { + // 更新现有套餐的关键字段 + name: 'pro', + isActive: true + }, + create: { id: 'pro', name: 'pro', // 重要:名称必须是 "pro" 用于查找 displayName: 'Pro Plan', @@ -88,13 +92,22 @@ async function main() { } }) - // 过滤出需要更新的用户(没有正确的订阅套餐关联的) - const usersToUpdate = users.filter(user => - !user.subscriptionPlanId || - user.subscriptionPlanId === '' || - (user.subscribePlan === 'pro' && user.subscriptionPlanId !== 'pro') || - (user.subscribePlan === 'free' && user.subscriptionPlanId !== 'free') - ) + // 获取有效的套餐 ID 列表 + const validPlans = await prisma.subscriptionPlan.findMany({ + where: { isActive: true }, + select: { id: true } + }) + const validPlanIds = validPlans.map(plan => plan.id) + + // 过滤出需要更新的用户 + const usersToUpdate = users.filter(user => { + // 检查 subscribePlan 是否有效 + const hasValidSubscribePlan = validPlanIds.includes(user.subscribePlan) + // 检查 subscriptionPlanId 是否正确 + const hasValidSubscriptionPlanId = user.subscriptionPlanId && validPlanIds.includes(user.subscriptionPlanId) + + return !hasValidSubscribePlan || !hasValidSubscriptionPlanId + }) if (usersToUpdate.length > 0) { console.log(`📝 Found ${usersToUpdate.length} users to update`) @@ -102,15 +115,16 @@ async function main() { for (const user of usersToUpdate) { let planId = 'free' - // 根据现有的 subscribePlan 字段确定新的套餐 ID - if (user.subscribePlan === 'pro') { - planId = 'pro' + // 如果用户的 subscribePlan 是有效的,使用它;否则默认为 free + if (validPlanIds.includes(user.subscribePlan)) { + planId = user.subscribePlan } await prisma.user.update({ where: { id: user.id }, data: { - subscriptionPlanId: planId + subscribePlan: planId, // 确保 subscribePlan 有效 + subscriptionPlanId: planId // 确保 subscriptionPlanId 有效 } }) }