diff --git a/docs/pricing-page-improvements.md b/docs/pricing-page-improvements.md new file mode 100644 index 0000000..e02f833 --- /dev/null +++ b/docs/pricing-page-improvements.md @@ -0,0 +1,185 @@ +# 定价页面改进说明 + +## 概述 + +定价页面现在实现了智能的套餐过滤和按钮显示逻辑,确保只显示有效的可订阅套餐,并提供清晰的用户体验。 + +## 主要改进 + +### 1. 智能套餐过滤 + +定价页面现在只显示以下套餐: + +1. **免费套餐** - 总是显示 +2. **用户当前套餐** - 总是显示(即使没有 stripePriceId) +3. **有效的付费套餐** - 必须有 `stripePriceId` 才显示 + +#### 过滤逻辑 +```typescript +const filteredPlans = plans.filter((plan: SubscriptionPlan) => { + // 免费套餐总是显示 + if (isPlanFree(plan)) return true + + // 用户当前套餐总是显示 + if (userData && isCurrentPlan(plan.id)) return true + + // 其他套餐必须有 stripePriceId 才能显示(可订阅) + return plan.stripePriceId && plan.stripePriceId.trim() !== '' +}) +``` + +### 2. 智能按钮显示 + +按钮显示遵循以下逻辑: + +#### 免费套餐 +- **当前套餐**: 显示 "当前方案" 按钮(禁用) +- **非当前套餐**: 不显示任何按钮 + +#### 付费套餐 +- **当前套餐**: 显示 "当前方案" 按钮(禁用) +- **可订阅套餐**: 显示 "立即订阅" 按钮 +- **无价格ID套餐**: 不显示按钮(理论上不会出现,因为已过滤) + +#### 按钮逻辑代码 +```typescript +{(() => { + // 免费套餐逻辑 + if (isFree) { + if (isCurrent) { + return + } + return null // 免费套餐且非当前套餐,不显示按钮 + } + + // 付费套餐逻辑 + if (isCurrent) { + return + } + + // 可订阅的付费套餐 + if (plan.stripePriceId && plan.stripePriceId.trim() !== '') { + return ( + + {t('subscribeNow')} + + ) + } + + return null +})()} +``` + +### 3. 多语言支持 + +添加了新的翻译键: + +#### 英文 (en.json) +```json +{ + "pricing": { + "subscribeNow": "Subscribe Now", + "currentPlan": "Current Plan" + } +} +``` + +#### 中文 (zh.json) +```json +{ + "pricing": { + "subscribeNow": "立即订阅", + "currentPlan": "当前方案" + } +} +``` + +## 用户体验场景 + +### 1. 匿名用户 +- 看到:免费套餐(无按钮)+ 有价格ID的付费套餐(立即订阅按钮) +- 不看到:没有价格ID的付费套餐 + +### 2. 免费用户 +- 看到:免费套餐(当前方案按钮)+ 有价格ID的付费套餐(立即订阅按钮) +- 不看到:没有价格ID的付费套餐 + +### 3. Pro 用户 +- 看到:免费套餐(无按钮)+ 当前Pro套餐(当前方案按钮)+ 其他有价格ID的套餐(立即订阅按钮) +- 不看到:没有价格ID的付费套餐 + +## 技术实现 + +### 1. 动态数据获取 +```typescript +useEffect(() => { + fetchPlans() +}, [userData]) // 依赖 userData,确保用户数据加载后再过滤套餐 +``` + +### 2. 套餐判定工具 +使用统一的工具函数: +- `isPlanFree(plan)` - 判断是否为免费套餐 +- `isPlanPro(plan)` - 判断是否为Pro套餐 +- `isCurrentPlan(planId)` - 判断是否为用户当前套餐 + +### 3. 类型安全 +```typescript +const filteredPlans = (data.plans || []).filter((plan: SubscriptionPlan) => { + // 过滤逻辑 +}) +``` + +## 配置要求 + +### 1. 套餐配置 +确保付费套餐有有效的 `stripePriceId`: + +```sql +-- 检查套餐配置 +SELECT id, name, "displayName", price, "stripePriceId", "isActive" +FROM subscription_plans +WHERE "isActive" = true; + +-- 设置价格ID +UPDATE subscription_plans +SET "stripePriceId" = 'price_your_stripe_price_id' +WHERE name = 'pro'; +``` + +### 2. 验证工具 +使用测试脚本验证配置: + +```bash +npx tsx scripts/test-pricing-page-filtering.ts +``` + +## 优势 + +1. **用户友好**: 只显示用户可以实际订阅的套餐 +2. **防止错误**: 避免显示无法订阅的套餐 +3. **清晰导航**: 明确的按钮状态和操作 +4. **灵活配置**: 支持动态套餐管理 +5. **多语言**: 完整的国际化支持 + +## 故障排除 + +### 问题:套餐不显示 +**原因**: 付费套餐没有设置 `stripePriceId` +**解决**: 在数据库中设置正确的 Stripe 价格 ID + +### 问题:按钮不显示 +**原因**: 套餐被过滤掉或逻辑判断问题 +**解决**: 检查套餐配置和用户状态 + +### 问题:翻译缺失 +**原因**: 缺少 `subscribeNow` 翻译键 +**解决**: 在对应语言文件中添加翻译 + +## 测试建议 + +1. 测试不同用户状态下的页面显示 +2. 验证按钮功能和状态 +3. 检查多语言显示 +4. 确认套餐过滤逻辑 +5. 测试订阅流程 diff --git a/messages/en.json b/messages/en.json index 072624d..d48b136 100644 --- a/messages/en.json +++ b/messages/en.json @@ -220,6 +220,7 @@ "startProTrial": "Upgrade to Pro", "currentPlan": "Current Plan", "upgradeToPro": "Upgrade to Pro", + "subscribeNow": "Subscribe Now", "manageSubscription": "Manage Subscription" }, "subscription": { diff --git a/messages/zh.json b/messages/zh.json index f0392e3..fcd880f 100644 --- a/messages/zh.json +++ b/messages/zh.json @@ -220,6 +220,7 @@ "startProTrial": "升级到专业版", "currentPlan": "当前方案", "upgradeToPro": "升级到专业版", + "subscribeNow": "立即订阅", "manageSubscription": "管理订阅" }, "subscription": { diff --git a/scripts/test-pricing-page-filtering.ts b/scripts/test-pricing-page-filtering.ts new file mode 100644 index 0000000..c7419db --- /dev/null +++ b/scripts/test-pricing-page-filtering.ts @@ -0,0 +1,122 @@ +import { PrismaClient } from '@prisma/client' +import { SubscriptionService } from '../src/lib/subscription-service' +import { isPlanFree } from '../src/lib/subscription-utils' + +const prisma = new PrismaClient() + +async function testPricingPageFiltering() { + console.log('🧪 Testing pricing page filtering logic...') + + try { + // 1. 获取所有套餐 + console.log('\n1. Getting all available plans...') + const allPlans = await SubscriptionService.getAvailablePlans() + console.log(`📊 Found ${allPlans.length} active plans:`) + + allPlans.forEach(plan => { + console.log(` - ${plan.displayName} (${plan.id}):`) + console.log(` * Price: $${plan.price}`) + console.log(` * Stripe Price ID: ${plan.stripePriceId || 'Not set'}`) + console.log(` * Is Free: ${isPlanFree(plan)}`) + }) + + // 2. 模拟定价页面的过滤逻辑 + console.log('\n2. Testing pricing page filtering logic...') + + // 模拟不同用户场景 + const testScenarios = [ + { name: 'Anonymous User', userData: null }, + { name: 'Free User', userData: { subscriptionPlanId: 'free' } }, + { name: 'Pro User', userData: { subscriptionPlanId: 'pro' } } + ] + + for (const scenario of testScenarios) { + console.log(`\n📋 Scenario: ${scenario.name}`) + + const filteredPlans = allPlans.filter(plan => { + // 免费套餐总是显示 + if (isPlanFree(plan)) return true + + // 用户当前套餐总是显示 + if (scenario.userData && scenario.userData.subscriptionPlanId === plan.id) return true + + // 其他套餐必须有 stripePriceId 才能显示(可订阅) + return plan.stripePriceId && plan.stripePriceId.trim() !== '' + }) + + console.log(` Visible plans: ${filteredPlans.length}`) + filteredPlans.forEach(plan => { + const isCurrent = scenario.userData?.subscriptionPlanId === plan.id + const canSubscribe = !isPlanFree(plan) && plan.stripePriceId && !isCurrent + + console.log(` - ${plan.displayName}:`) + console.log(` * Current plan: ${isCurrent}`) + console.log(` * Can subscribe: ${canSubscribe}`) + console.log(` * Button action: ${getButtonAction(plan, isCurrent, canSubscribe)}`) + }) + } + + // 3. 检查套餐配置问题 + console.log('\n3. Checking for potential issues...') + + const plansWithoutPriceId = allPlans.filter(plan => + !isPlanFree(plan) && (!plan.stripePriceId || plan.stripePriceId.trim() === '') + ) + + if (plansWithoutPriceId.length > 0) { + console.log(`⚠️ Found ${plansWithoutPriceId.length} paid plans without Stripe Price ID:`) + plansWithoutPriceId.forEach(plan => { + console.log(` - ${plan.displayName} ($${plan.price}) - will not be visible to non-subscribers`) + }) + } else { + console.log('✅ All paid plans have valid Stripe Price IDs') + } + + // 4. 验证免费套餐 + const freePlans = allPlans.filter(isPlanFree) + console.log(`\n4. Free plans validation:`) + console.log(` Found ${freePlans.length} free plans`) + freePlans.forEach(plan => { + console.log(` - ${plan.displayName}: Always visible, no subscription button`) + }) + + console.log('\n🎉 Pricing page filtering test completed!') + + } catch (error) { + console.error('❌ Test failed:', error) + throw error + } finally { + await prisma.$disconnect() + } +} + +function getButtonAction(plan: any, isCurrent: boolean, canSubscribe: boolean): string { + if (isPlanFree(plan)) { + return isCurrent ? 'Current Plan (disabled)' : 'No button' + } + + if (isCurrent) { + return 'Current Plan (disabled)' + } + + if (canSubscribe) { + return 'Subscribe Now' + } + + return 'No button (no price ID)' +} + +// 运行测试 +if (require.main === module) { + testPricingPageFiltering() + .then(() => { + console.log('✅ All tests completed!') + process.exit(0) + }) + .catch((error) => { + console.error('❌ Tests failed:', error) + process.exit(1) + }) +} + +export { testPricingPageFiltering } diff --git a/src/app/pricing/page.tsx b/src/app/pricing/page.tsx index f6acc95..e7916c2 100644 --- a/src/app/pricing/page.tsx +++ b/src/app/pricing/page.tsx @@ -25,16 +25,23 @@ export default function PricingPage() { const [plans, setPlans] = useState([]) const [loading, setLoading] = useState(true) - useEffect(() => { - fetchPlans() - }, []) - const fetchPlans = async () => { try { const response = await fetch('/api/subscription-plans') if (response.ok) { const data = await response.json() - setPlans(data.plans || []) + // 过滤套餐:只显示免费套餐、用户当前套餐,以及有 stripePriceId 的套餐 + const filteredPlans = (data.plans || []).filter((plan: SubscriptionPlan) => { + // 免费套餐总是显示 + if (isPlanFree(plan)) return true + + // 用户当前套餐总是显示 + if (userData && isCurrentPlan(plan.id)) return true + + // 其他套餐必须有 stripePriceId 才能显示(可订阅) + return plan.stripePriceId && plan.stripePriceId.trim() !== '' + }) + setPlans(filteredPlans) } } catch (error) { console.error('Error fetching plans:', error) @@ -43,13 +50,11 @@ export default function PricingPage() { } } - const handleGetStarted = () => { - if (user) { - window.location.href = '/studio' - } else { - window.location.href = '/signup' - } - } + useEffect(() => { + fetchPlans() + }, [userData]) // 依赖 userData,确保用户数据加载后再过滤套餐 + + const isCurrentPlan = (planId: string) => { if (!userData) return false @@ -170,25 +175,55 @@ export default function PricingPage() { )} - {isFree ? ( - - ) : ( - - {isCurrent ? t('currentPlan') : t('upgradePlan')} - - )} + {(() => { + // 免费套餐逻辑 + if (isFree) { + // 如果是当前套餐,显示"当前套餐"按钮 + if (isCurrent) { + return ( + + ) + } + // 免费套餐且非当前套餐,不显示按钮 + return null + } + + // 付费套餐逻辑 + if (isCurrent) { + // 当前套餐,显示"当前套餐"按钮 + return ( + + ) + } + + // 可订阅的付费套餐,显示"立即订阅"按钮 + if (plan.stripePriceId && plan.stripePriceId.trim() !== '') { + return ( + + {t('subscribeNow')} + + ) + } + + // 没有 stripePriceId 的套餐不显示按钮(理论上不会到这里,因为已经过滤了) + return null + })()} ) })}