This commit is contained in:
songtianlun 2025-08-05 23:42:48 +08:00
parent 6cd33d24cd
commit 543a76fba4
3 changed files with 189 additions and 26 deletions

147
docs/deployment-fix.md Normal file
View File

@ -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');
```

View File

@ -13,6 +13,8 @@
"lint": "next lint", "lint": "next lint",
"db:generate": "prisma generate", "db:generate": "prisma generate",
"db:push": "prisma db push && npm run db:seed", "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:migrate": "prisma migrate dev",
"db:studio": "prisma studio", "db:studio": "prisma studio",
"db:reset": "prisma migrate reset", "db:reset": "prisma migrate reset",

View File

@ -5,19 +5,17 @@ const prisma = new PrismaClient()
async function main() { async function main() {
console.log('🌱 Starting database seeding...') console.log('🌱 Starting database seeding...')
// 检查是否已存在订阅套餐 console.log('📦 Ensuring essential subscription plans exist...')
const existingPlans = await prisma.subscriptionPlan.count()
if (existingPlans > 0) {
console.log('📦 Subscription plans already exist, skipping seed...')
return
}
console.log('📦 Creating default subscription plans...') // 确保免费套餐存在
const freePlan = await prisma.subscriptionPlan.upsert({
// 创建免费套餐 where: { id: 'free' },
const freePlan = await prisma.subscriptionPlan.create({ update: {
data: { // 更新现有套餐的关键字段
name: 'free',
isActive: true
},
create: {
id: 'free', id: 'free',
name: 'Free', name: 'Free',
displayName: 'Free Plan', displayName: 'Free Plan',
@ -42,9 +40,15 @@ async function main() {
} }
}) })
// 创建 Pro 套餐 // 确保 Pro 套餐存在
const proPlan = await prisma.subscriptionPlan.create({ const proPlan = await prisma.subscriptionPlan.upsert({
data: { where: { id: 'pro' },
update: {
// 更新现有套餐的关键字段
name: 'pro',
isActive: true
},
create: {
id: 'pro', id: 'pro',
name: 'pro', // 重要:名称必须是 "pro" 用于查找 name: 'pro', // 重要:名称必须是 "pro" 用于查找
displayName: 'Pro Plan', displayName: 'Pro Plan',
@ -88,13 +92,22 @@ async function main() {
} }
}) })
// 过滤出需要更新的用户(没有正确的订阅套餐关联的) // 获取有效的套餐 ID 列表
const usersToUpdate = users.filter(user => const validPlans = await prisma.subscriptionPlan.findMany({
!user.subscriptionPlanId || where: { isActive: true },
user.subscriptionPlanId === '' || select: { id: true }
(user.subscribePlan === 'pro' && user.subscriptionPlanId !== 'pro') || })
(user.subscribePlan === 'free' && user.subscriptionPlanId !== 'free') 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) { if (usersToUpdate.length > 0) {
console.log(`📝 Found ${usersToUpdate.length} users to update`) console.log(`📝 Found ${usersToUpdate.length} users to update`)
@ -102,15 +115,16 @@ async function main() {
for (const user of usersToUpdate) { for (const user of usersToUpdate) {
let planId = 'free' let planId = 'free'
// 根据现有的 subscribePlan 字段确定新的套餐 ID // 如果用户的 subscribePlan 是有效的,使用它;否则默认为 free
if (user.subscribePlan === 'pro') { if (validPlanIds.includes(user.subscribePlan)) {
planId = 'pro' planId = user.subscribePlan
} }
await prisma.user.update({ await prisma.user.update({
where: { id: user.id }, where: { id: user.id },
data: { data: {
subscriptionPlanId: planId subscribePlan: planId, // 确保 subscribePlan 有效
subscriptionPlanId: planId // 确保 subscriptionPlanId 有效
} }
}) })
} }