better subscribe
This commit is contained in:
parent
bbdfb54c84
commit
c5c69645c5
200
docs/pro-plan-configuration.md
Normal file
200
docs/pro-plan-configuration.md
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
# Pro 套餐配置指南
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
系统现在完全从数据库动态获取 Pro 套餐的配置,不再依赖环境变量 `NEXT_PUBLIC_STRIPE_PRO_PRICE_ID`。
|
||||||
|
|
||||||
|
## 核心原理
|
||||||
|
|
||||||
|
1. **动态查找**: 系统通过查找数据库中 `name = 'pro'` 的套餐来确定 Pro 套餐
|
||||||
|
2. **价格判定**: 任何价格超过 $19 的套餐都被视为 Pro 级别
|
||||||
|
3. **实时获取**: 订阅时动态获取 Pro 套餐的 Stripe 价格 ID
|
||||||
|
|
||||||
|
## 配置步骤
|
||||||
|
|
||||||
|
### 1. 确保 Pro 套餐存在
|
||||||
|
|
||||||
|
运行种子脚本会自动创建 Pro 套餐:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run db:seed
|
||||||
|
```
|
||||||
|
|
||||||
|
或者手动创建:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
INSERT INTO subscription_plans (
|
||||||
|
id, name, "displayName", description, price, currency, interval,
|
||||||
|
"stripePriceId", "isActive", "sortOrder", features, limits,
|
||||||
|
"createdAt", "updatedAt"
|
||||||
|
) VALUES (
|
||||||
|
'pro', 'pro', 'Pro Plan', 'Advanced features for power users',
|
||||||
|
19.9, 'usd', 'month', NULL, true, 2,
|
||||||
|
'{"promptLimit": true, "versionsPerPrompt": true, "prioritySupport": true}',
|
||||||
|
'{"maxVersionLimit": 10, "promptLimit": 5000, "creditMonthly": 20}',
|
||||||
|
NOW(), NOW()
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 设置 Stripe 价格 ID
|
||||||
|
|
||||||
|
在 Stripe Dashboard 中创建价格后,更新数据库:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
UPDATE subscription_plans
|
||||||
|
SET "stripePriceId" = 'price_your_actual_stripe_price_id'
|
||||||
|
WHERE name = 'pro';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 验证配置
|
||||||
|
|
||||||
|
运行测试脚本验证配置:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx tsx scripts/test-pro-plan-detection.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
## API 端点
|
||||||
|
|
||||||
|
### 获取 Pro 价格 ID
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/subscription/pro-price-id
|
||||||
|
```
|
||||||
|
|
||||||
|
**成功响应**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"priceId": "price_1RryBL9mNQExFr2OpYGPdBVJ"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**错误响应**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "Pro plan not found or not configured"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 前端集成
|
||||||
|
|
||||||
|
### QuickUpgradeButton 组件
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { QuickUpgradeButton } from '@/components/subscription/SubscribeButton'
|
||||||
|
|
||||||
|
// 自动获取 Pro 套餐价格 ID
|
||||||
|
<QuickUpgradeButton className="btn-primary">
|
||||||
|
Upgrade to Pro
|
||||||
|
</QuickUpgradeButton>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 定价页面
|
||||||
|
|
||||||
|
定价页面会自动从数据库获取所有套餐,包括 Pro 套餐的价格 ID。
|
||||||
|
|
||||||
|
## 服务端方法
|
||||||
|
|
||||||
|
### SubscriptionService 方法
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { SubscriptionService } from '@/lib/subscription-service'
|
||||||
|
|
||||||
|
// 获取 Pro 套餐
|
||||||
|
const proPlan = await SubscriptionService.getProPlan()
|
||||||
|
|
||||||
|
// 获取 Pro 价格 ID
|
||||||
|
const proPriceId = await SubscriptionService.getProPriceId()
|
||||||
|
|
||||||
|
// 判断套餐是否为 Pro
|
||||||
|
const isPro = SubscriptionService.isPlanPro(plan)
|
||||||
|
|
||||||
|
// 判断用户是否为 Pro
|
||||||
|
const isUserPro = await SubscriptionService.isUserPro(userId)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 故障排除
|
||||||
|
|
||||||
|
### 问题:订阅创建失败
|
||||||
|
|
||||||
|
**原因**: Pro 套餐未配置或 Stripe 价格 ID 未设置
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
1. 检查数据库中是否存在 `name = 'pro'` 的套餐
|
||||||
|
2. 确认该套餐的 `stripePriceId` 字段不为空
|
||||||
|
3. 验证 Stripe 价格 ID 是否有效
|
||||||
|
|
||||||
|
### 问题:API 返回 404
|
||||||
|
|
||||||
|
**原因**: 找不到名称为 "pro" 的套餐
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
```sql
|
||||||
|
-- 检查套餐名称
|
||||||
|
SELECT id, name, "displayName" FROM subscription_plans WHERE "isActive" = true;
|
||||||
|
|
||||||
|
-- 如果名称不正确,更新为 "pro"
|
||||||
|
UPDATE subscription_plans SET name = 'pro' WHERE id = 'pro';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 问题:前端显示 "Pro plan unavailable"
|
||||||
|
|
||||||
|
**原因**: API 无法获取 Pro 价格 ID
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
1. 检查 API 端点 `/api/subscription/pro-price-id` 是否正常
|
||||||
|
2. 确认数据库连接正常
|
||||||
|
3. 验证 Pro 套餐配置
|
||||||
|
|
||||||
|
## 测试工具
|
||||||
|
|
||||||
|
### 检测脚本
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 测试 Pro 套餐检测
|
||||||
|
npx tsx scripts/test-pro-plan-detection.ts
|
||||||
|
|
||||||
|
# 修复 Pro 套餐名称
|
||||||
|
npx tsx scripts/fix-pro-plan-name.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
### 手动测试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 测试 API 端点
|
||||||
|
curl http://localhost:3000/api/subscription/pro-price-id
|
||||||
|
|
||||||
|
# 检查数据库
|
||||||
|
npx prisma studio
|
||||||
|
```
|
||||||
|
|
||||||
|
## 迁移指南
|
||||||
|
|
||||||
|
### 从环境变量迁移
|
||||||
|
|
||||||
|
如果你之前使用 `NEXT_PUBLIC_STRIPE_PRO_PRICE_ID`:
|
||||||
|
|
||||||
|
1. 将价格 ID 更新到数据库:
|
||||||
|
```sql
|
||||||
|
UPDATE subscription_plans
|
||||||
|
SET "stripePriceId" = 'your_old_price_id'
|
||||||
|
WHERE name = 'pro';
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 移除环境变量:
|
||||||
|
```bash
|
||||||
|
# 从 .env 文件中删除这一行
|
||||||
|
# NEXT_PUBLIC_STRIPE_PRO_PRICE_ID="price_..."
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 重新构建应用:
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## 优势
|
||||||
|
|
||||||
|
1. **灵活性**: 可以在数据库中动态修改 Pro 套餐配置
|
||||||
|
2. **安全性**: 价格 ID 不再暴露在客户端环境变量中
|
||||||
|
3. **可扩展性**: 支持多个 Pro 级别套餐
|
||||||
|
4. **一致性**: 所有套餐配置都在数据库中统一管理
|
@ -43,13 +43,36 @@
|
|||||||
# Stripe 配置
|
# Stripe 配置
|
||||||
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="pk_test_your_publishable_key_here"
|
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="pk_test_your_publishable_key_here"
|
||||||
STRIPE_SECRET_KEY="sk_test_your_secret_key_here"
|
STRIPE_SECRET_KEY="sk_test_your_secret_key_here"
|
||||||
NEXT_PUBLIC_STRIPE_PRO_PRICE_ID="price_your_pro_price_id_here"
|
|
||||||
NEXT_PUBLIC_APP_URL="http://localhost:3000"
|
NEXT_PUBLIC_APP_URL="http://localhost:3000"
|
||||||
|
|
||||||
# Webhook 密钥(稍后配置)
|
# Webhook 密钥(稍后配置)
|
||||||
STRIPE_WEBHOOK_SECRET="whsec_your_webhook_secret_here"
|
STRIPE_WEBHOOK_SECRET="whsec_your_webhook_secret_here"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**注意**: 不再需要 `NEXT_PUBLIC_STRIPE_PRO_PRICE_ID` 环境变量,价格 ID 现在从数据库动态获取。
|
||||||
|
|
||||||
|
### 3.1 配置 Pro 套餐价格 ID
|
||||||
|
|
||||||
|
创建 Stripe 价格后,需要在数据库中更新 Pro 套餐的 `stripePriceId` 字段:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 在数据库中更新 Pro 套餐的 Stripe 价格 ID
|
||||||
|
UPDATE subscription_plans
|
||||||
|
SET "stripePriceId" = 'price_your_actual_pro_price_id_here'
|
||||||
|
WHERE name = 'pro';
|
||||||
|
```
|
||||||
|
|
||||||
|
或者使用管理员 API 更新:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X PUT http://localhost:3000/api/admin/subscription-plans \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"id": "pro",
|
||||||
|
"stripePriceId": "price_your_actual_pro_price_id_here"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
## 4. Webhook 配置
|
## 4. Webhook 配置
|
||||||
|
|
||||||
### 4.1 创建 Webhook 端点
|
### 4.1 创建 Webhook 端点
|
||||||
|
107
docs/subscription-pro-detection.md
Normal file
107
docs/subscription-pro-detection.md
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
# Pro 套餐判定逻辑
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
系统使用统一的价格判定逻辑来确定套餐是否为 Pro 级别,而不是依赖硬编码的套餐 ID。这提供了更大的灵活性,允许动态创建不同价格的套餐。
|
||||||
|
|
||||||
|
## 判定规则
|
||||||
|
|
||||||
|
### Pro 套餐判定
|
||||||
|
```typescript
|
||||||
|
function isPlanPro(plan): boolean {
|
||||||
|
return plan.price > 19
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**规则**: 任何价格超过 $19 的套餐都被视为 Pro 级别
|
||||||
|
|
||||||
|
### 套餐层级
|
||||||
|
```typescript
|
||||||
|
function getPlanTier(plan): 'free' | 'pro' | 'enterprise' {
|
||||||
|
if (!plan || plan.price === 0) return 'free'
|
||||||
|
if (plan.price > 50) return 'enterprise' // 为未来预留
|
||||||
|
if (plan.price > 19) return 'pro'
|
||||||
|
return 'free'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**层级划分**:
|
||||||
|
- **Free**: $0
|
||||||
|
- **Basic**: $0.01 - $19.00 (仍被视为免费层级)
|
||||||
|
- **Pro**: $19.01 - $50.00
|
||||||
|
- **Enterprise**: $50.01+ (为未来扩展预留)
|
||||||
|
|
||||||
|
## 实际应用示例
|
||||||
|
|
||||||
|
| 套餐名称 | 价格 | isPro() | getPlanTier() | 说明 |
|
||||||
|
|---------|------|---------|---------------|------|
|
||||||
|
| Free | $0 | false | free | 免费套餐 |
|
||||||
|
| Basic | $9.99 | false | free | 低价套餐,仍为免费层级 |
|
||||||
|
| Pro | $19.99 | true | pro | 标准 Pro 套餐 |
|
||||||
|
| Premium | $29.99 | true | pro | 高级 Pro 套餐 |
|
||||||
|
| Enterprise | $99.99 | true | enterprise | 企业级套餐 |
|
||||||
|
|
||||||
|
## 使用方式
|
||||||
|
|
||||||
|
### 在组件中使用
|
||||||
|
```typescript
|
||||||
|
import { isPlanPro, getPlanTier, getPlanTheme } from '@/lib/subscription-utils'
|
||||||
|
|
||||||
|
// 判断是否为 Pro
|
||||||
|
const isPro = isPlanPro(plan)
|
||||||
|
|
||||||
|
// 获取套餐层级
|
||||||
|
const tier = getPlanTier(plan)
|
||||||
|
|
||||||
|
// 获取对应的主题样式
|
||||||
|
const theme = getPlanTheme(plan)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 在服务端使用
|
||||||
|
```typescript
|
||||||
|
import { SubscriptionService } from '@/lib/subscription-service'
|
||||||
|
|
||||||
|
// 判断用户是否为 Pro
|
||||||
|
const isUserPro = await SubscriptionService.isUserPro(userId)
|
||||||
|
|
||||||
|
// 判断套餐是否为 Pro
|
||||||
|
const isPro = SubscriptionService.isPlanPro(plan)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 优势
|
||||||
|
|
||||||
|
1. **灵活性**: 可以创建任意价格的套餐,系统自动判定层级
|
||||||
|
2. **一致性**: 所有 Pro 判定都使用统一的逻辑
|
||||||
|
3. **可扩展性**: 易于添加新的套餐层级(如 Enterprise)
|
||||||
|
4. **维护性**: 修改判定规则只需要更新一个地方
|
||||||
|
|
||||||
|
## 样式主题
|
||||||
|
|
||||||
|
系统会根据套餐层级自动应用对应的主题样式:
|
||||||
|
|
||||||
|
- **Free**: 灰色主题,Star 图标
|
||||||
|
- **Pro**: 橙色/琥珀色主题,Crown 图标
|
||||||
|
- **Enterprise**: 紫色主题,Building 图标
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. 价格判定基于美元金额
|
||||||
|
2. 判定逻辑支持部分套餐数据(只需要 `price` 字段)
|
||||||
|
3. 所有显示逻辑都应该使用这些工具函数,而不是硬编码套餐 ID
|
||||||
|
4. 修改价格阈值时需要考虑现有套餐的影响
|
||||||
|
|
||||||
|
## 迁移指南
|
||||||
|
|
||||||
|
如果需要修改 Pro 套餐的价格阈值:
|
||||||
|
|
||||||
|
1. 更新 `src/lib/subscription-utils.ts` 中的判定逻辑
|
||||||
|
2. 运行测试确保所有现有套餐仍然正确分类
|
||||||
|
3. 更新文档中的示例
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 例如,将 Pro 阈值改为 $25
|
||||||
|
export function isPlanPro(plan: PlanLike): boolean {
|
||||||
|
if (!plan) return false
|
||||||
|
return plan.price > 25 // 从 19 改为 25
|
||||||
|
}
|
||||||
|
```
|
520
package-lock.json
generated
520
package-lock.json
generated
@ -35,6 +35,7 @@
|
|||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "15.4.4",
|
"eslint-config-next": "15.4.4",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
|
"tsx": "^4.20.3",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@ -118,6 +119,448 @@
|
|||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
|
"version": "0.25.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz",
|
||||||
|
"integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"aix"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-arm": {
|
||||||
|
"version": "0.25.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.8.tgz",
|
||||||
|
"integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-arm64": {
|
||||||
|
"version": "0.25.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz",
|
||||||
|
"integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-x64": {
|
||||||
|
"version": "0.25.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.8.tgz",
|
||||||
|
"integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/darwin-arm64": {
|
||||||
|
"version": "0.25.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz",
|
||||||
|
"integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/darwin-x64": {
|
||||||
|
"version": "0.25.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz",
|
||||||
|
"integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/freebsd-arm64": {
|
||||||
|
"version": "0.25.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz",
|
||||||
|
"integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/freebsd-x64": {
|
||||||
|
"version": "0.25.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz",
|
||||||
|
"integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-arm": {
|
||||||
|
"version": "0.25.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz",
|
||||||
|
"integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-arm64": {
|
||||||
|
"version": "0.25.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz",
|
||||||
|
"integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-ia32": {
|
||||||
|
"version": "0.25.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz",
|
||||||
|
"integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-loong64": {
|
||||||
|
"version": "0.25.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz",
|
||||||
|
"integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==",
|
||||||
|
"cpu": [
|
||||||
|
"loong64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-mips64el": {
|
||||||
|
"version": "0.25.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz",
|
||||||
|
"integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==",
|
||||||
|
"cpu": [
|
||||||
|
"mips64el"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-ppc64": {
|
||||||
|
"version": "0.25.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz",
|
||||||
|
"integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-riscv64": {
|
||||||
|
"version": "0.25.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz",
|
||||||
|
"integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-s390x": {
|
||||||
|
"version": "0.25.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz",
|
||||||
|
"integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-x64": {
|
||||||
|
"version": "0.25.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz",
|
||||||
|
"integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/netbsd-arm64": {
|
||||||
|
"version": "0.25.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz",
|
||||||
|
"integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"netbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/netbsd-x64": {
|
||||||
|
"version": "0.25.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz",
|
||||||
|
"integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"netbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openbsd-arm64": {
|
||||||
|
"version": "0.25.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz",
|
||||||
|
"integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openbsd-x64": {
|
||||||
|
"version": "0.25.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz",
|
||||||
|
"integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openharmony-arm64": {
|
||||||
|
"version": "0.25.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz",
|
||||||
|
"integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openharmony"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/sunos-x64": {
|
||||||
|
"version": "0.25.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz",
|
||||||
|
"integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"sunos"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-arm64": {
|
||||||
|
"version": "0.25.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz",
|
||||||
|
"integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-ia32": {
|
||||||
|
"version": "0.25.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz",
|
||||||
|
"integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-x64": {
|
||||||
|
"version": "0.25.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz",
|
||||||
|
"integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@eslint-community/eslint-utils": {
|
"node_modules/@eslint-community/eslint-utils": {
|
||||||
"version": "4.7.0",
|
"version": "4.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
|
||||||
@ -3032,6 +3475,48 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/esbuild": {
|
||||||
|
"version": "0.25.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz",
|
||||||
|
"integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"esbuild": "bin/esbuild"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@esbuild/aix-ppc64": "0.25.8",
|
||||||
|
"@esbuild/android-arm": "0.25.8",
|
||||||
|
"@esbuild/android-arm64": "0.25.8",
|
||||||
|
"@esbuild/android-x64": "0.25.8",
|
||||||
|
"@esbuild/darwin-arm64": "0.25.8",
|
||||||
|
"@esbuild/darwin-x64": "0.25.8",
|
||||||
|
"@esbuild/freebsd-arm64": "0.25.8",
|
||||||
|
"@esbuild/freebsd-x64": "0.25.8",
|
||||||
|
"@esbuild/linux-arm": "0.25.8",
|
||||||
|
"@esbuild/linux-arm64": "0.25.8",
|
||||||
|
"@esbuild/linux-ia32": "0.25.8",
|
||||||
|
"@esbuild/linux-loong64": "0.25.8",
|
||||||
|
"@esbuild/linux-mips64el": "0.25.8",
|
||||||
|
"@esbuild/linux-ppc64": "0.25.8",
|
||||||
|
"@esbuild/linux-riscv64": "0.25.8",
|
||||||
|
"@esbuild/linux-s390x": "0.25.8",
|
||||||
|
"@esbuild/linux-x64": "0.25.8",
|
||||||
|
"@esbuild/netbsd-arm64": "0.25.8",
|
||||||
|
"@esbuild/netbsd-x64": "0.25.8",
|
||||||
|
"@esbuild/openbsd-arm64": "0.25.8",
|
||||||
|
"@esbuild/openbsd-x64": "0.25.8",
|
||||||
|
"@esbuild/openharmony-arm64": "0.25.8",
|
||||||
|
"@esbuild/sunos-x64": "0.25.8",
|
||||||
|
"@esbuild/win32-arm64": "0.25.8",
|
||||||
|
"@esbuild/win32-ia32": "0.25.8",
|
||||||
|
"@esbuild/win32-x64": "0.25.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/escape-string-regexp": {
|
"node_modules/escape-string-regexp": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||||
@ -3614,6 +4099,21 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fsevents": {
|
||||||
|
"version": "2.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
|
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/function-bind": {
|
"node_modules/function-bind": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
@ -6294,6 +6794,26 @@
|
|||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
|
||||||
},
|
},
|
||||||
|
"node_modules/tsx": {
|
||||||
|
"version": "4.20.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.3.tgz",
|
||||||
|
"integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"esbuild": "~0.25.0",
|
||||||
|
"get-tsconfig": "^4.7.5"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"tsx": "dist/cli.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "~2.3.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/type-check": {
|
"node_modules/type-check": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||||
|
@ -12,13 +12,16 @@
|
|||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"db:generate": "prisma generate",
|
"db:generate": "prisma generate",
|
||||||
"db:push": "prisma db push",
|
"db:push": "prisma db push && npm run db:seed",
|
||||||
"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",
|
||||||
"db:seed": "prisma db seed",
|
"db:seed": "tsx prisma/seed.ts",
|
||||||
"postinstall": "prisma generate"
|
"postinstall": "prisma generate"
|
||||||
},
|
},
|
||||||
|
"prisma": {
|
||||||
|
"seed": "tsx prisma/seed.ts"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "^6.12.0",
|
"@prisma/client": "^6.12.0",
|
||||||
"@stripe/stripe-js": "^7.8.0",
|
"@stripe/stripe-js": "^7.8.0",
|
||||||
@ -46,6 +49,7 @@
|
|||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "15.4.4",
|
"eslint-config-next": "15.4.4",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
|
"tsx": "^4.20.3",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,21 +21,55 @@ model User {
|
|||||||
bio String?
|
bio String?
|
||||||
language String @default("en")
|
language String @default("en")
|
||||||
isAdmin Boolean @default(false) // 管理员标记
|
isAdmin Boolean @default(false) // 管理员标记
|
||||||
versionLimit Int @default(3) // 版本数量限制,可在用户配置中设置
|
versionLimit Int @default(3) // 用户自定义版本数量限制,不能超过套餐限制
|
||||||
|
|
||||||
|
// 新的订阅系统字段
|
||||||
|
subscriptionPlanId String @default("free") // 关联的订阅套餐ID
|
||||||
|
|
||||||
|
// 旧的订阅系统字段(保留用于迁移,后期会移除)
|
||||||
subscribePlan String @default("free") // 订阅计划: "free", "pro"
|
subscribePlan String @default("free") // 订阅计划: "free", "pro"
|
||||||
maxVersionLimit Int @default(3) // 基于订阅的最大版本限制
|
maxVersionLimit Int @default(3) // 基于订阅的最大版本限制
|
||||||
promptLimit Int @default(500) // 提示词数量限制,更新为500
|
promptLimit Int? // 提示词数量限制(已弃用,忽略此字段)
|
||||||
creditBalance Float @default(0.0) // 信用余额,移除默认值
|
creditBalance Float @default(0.0) // 信用余额,移除默认值
|
||||||
|
|
||||||
stripeCustomerId String? @unique // Stripe 客户 ID
|
stripeCustomerId String? @unique // Stripe 客户 ID
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
prompts Prompt[]
|
// 关联关系
|
||||||
credits Credit[]
|
subscriptionPlan SubscriptionPlan @relation(fields: [subscriptionPlanId], references: [id])
|
||||||
|
prompts Prompt[]
|
||||||
|
credits Credit[]
|
||||||
|
|
||||||
@@map("users")
|
@@map("users")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 订阅套餐模型
|
||||||
|
model SubscriptionPlan {
|
||||||
|
id String @id // "free", "pro" 等
|
||||||
|
name String // 套餐名称
|
||||||
|
displayName String // 显示名称(支持国际化)
|
||||||
|
description String? // 套餐描述
|
||||||
|
price Float @default(0) // 价格(美元)
|
||||||
|
currency String @default("usd") // 货币
|
||||||
|
interval String @default("month") // 计费周期: "month", "year"
|
||||||
|
stripePriceId String? @unique // Stripe 价格 ID
|
||||||
|
isActive Boolean @default(true) // 是否激活
|
||||||
|
sortOrder Int @default(0) // 排序顺序
|
||||||
|
|
||||||
|
// 权益配置 (JSON 格式存储)
|
||||||
|
features Json // 功能特性配置
|
||||||
|
limits Json // 限制配置
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
// 关联关系
|
||||||
|
users User[]
|
||||||
|
|
||||||
|
@@map("subscription_plans")
|
||||||
|
}
|
||||||
|
|
||||||
model Prompt {
|
model Prompt {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String
|
name String
|
||||||
|
147
prisma/seed.ts
Normal file
147
prisma/seed.ts
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
import { PrismaClient } from '@prisma/client'
|
||||||
|
|
||||||
|
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('📦 Creating default subscription plans...')
|
||||||
|
|
||||||
|
// 创建免费套餐
|
||||||
|
const freePlan = await prisma.subscriptionPlan.create({
|
||||||
|
data: {
|
||||||
|
id: 'free',
|
||||||
|
name: 'Free',
|
||||||
|
displayName: 'Free Plan',
|
||||||
|
description: 'Basic features for getting started with AI prompt management',
|
||||||
|
price: 0,
|
||||||
|
currency: 'usd',
|
||||||
|
interval: 'month',
|
||||||
|
stripePriceId: null,
|
||||||
|
isActive: true,
|
||||||
|
sortOrder: 1,
|
||||||
|
features: {
|
||||||
|
promptLimit: true,
|
||||||
|
versionsPerPrompt: true,
|
||||||
|
basicSupport: true,
|
||||||
|
publicSharing: true
|
||||||
|
},
|
||||||
|
limits: {
|
||||||
|
maxVersionLimit: 3,
|
||||||
|
promptLimit: 500,
|
||||||
|
creditMonthly: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 创建 Pro 套餐
|
||||||
|
const proPlan = await prisma.subscriptionPlan.create({
|
||||||
|
data: {
|
||||||
|
id: 'pro',
|
||||||
|
name: 'pro', // 重要:名称必须是 "pro" 用于查找
|
||||||
|
displayName: 'Pro Plan',
|
||||||
|
description: 'Advanced features for power users and professionals',
|
||||||
|
price: 19.9,
|
||||||
|
currency: 'usd',
|
||||||
|
interval: 'month',
|
||||||
|
stripePriceId: null, // 需要手动在数据库中设置 Stripe 价格 ID
|
||||||
|
isActive: true,
|
||||||
|
sortOrder: 2,
|
||||||
|
features: {
|
||||||
|
promptLimit: true,
|
||||||
|
versionsPerPrompt: true,
|
||||||
|
prioritySupport: true,
|
||||||
|
advancedAnalytics: true,
|
||||||
|
apiAccess: true,
|
||||||
|
teamCollaboration: true,
|
||||||
|
exportImport: true,
|
||||||
|
customBranding: true
|
||||||
|
},
|
||||||
|
limits: {
|
||||||
|
maxVersionLimit: 10,
|
||||||
|
promptLimit: 5000,
|
||||||
|
creditMonthly: 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(`✅ Created subscription plan: ${freePlan.displayName}`)
|
||||||
|
console.log(`✅ Created subscription plan: ${proPlan.displayName}`)
|
||||||
|
|
||||||
|
// 更新现有用户的订阅套餐关联
|
||||||
|
console.log('👥 Updating existing users...')
|
||||||
|
|
||||||
|
// 获取所有用户,不管是否已有订阅套餐关联
|
||||||
|
const users = await prisma.user.findMany({
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
subscribePlan: true,
|
||||||
|
subscriptionPlanId: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 过滤出需要更新的用户(没有正确的订阅套餐关联的)
|
||||||
|
const usersToUpdate = users.filter(user =>
|
||||||
|
!user.subscriptionPlanId ||
|
||||||
|
user.subscriptionPlanId === '' ||
|
||||||
|
(user.subscribePlan === 'pro' && user.subscriptionPlanId !== 'pro') ||
|
||||||
|
(user.subscribePlan === 'free' && user.subscriptionPlanId !== 'free')
|
||||||
|
)
|
||||||
|
|
||||||
|
if (usersToUpdate.length > 0) {
|
||||||
|
console.log(`📝 Found ${usersToUpdate.length} users to update`)
|
||||||
|
|
||||||
|
for (const user of usersToUpdate) {
|
||||||
|
let planId = 'free'
|
||||||
|
|
||||||
|
// 根据现有的 subscribePlan 字段确定新的套餐 ID
|
||||||
|
if (user.subscribePlan === 'pro') {
|
||||||
|
planId = 'pro'
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.user.update({
|
||||||
|
where: { id: user.id },
|
||||||
|
data: {
|
||||||
|
subscriptionPlanId: planId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✅ Updated ${usersToUpdate.length} users with subscription plan associations`)
|
||||||
|
} else {
|
||||||
|
console.log('👥 No users need subscription plan updates')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证种子数据
|
||||||
|
const planCount = await prisma.subscriptionPlan.count()
|
||||||
|
const usersWithPlans = await prisma.user.count({
|
||||||
|
where: {
|
||||||
|
subscriptionPlanId: {
|
||||||
|
not: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(`\n📊 Seeding completed:`)
|
||||||
|
console.log(` • ${planCount} subscription plans created`)
|
||||||
|
console.log(` • ${usersWithPlans} users have valid plan associations`)
|
||||||
|
console.log(`\n🎉 Database seeding finished successfully!`)
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
.then(async () => {
|
||||||
|
await prisma.$disconnect()
|
||||||
|
})
|
||||||
|
.catch(async (e) => {
|
||||||
|
console.error('❌ Seeding failed:', e)
|
||||||
|
await prisma.$disconnect()
|
||||||
|
process.exit(1)
|
||||||
|
})
|
80
scripts/fix-pro-plan-name.ts
Normal file
80
scripts/fix-pro-plan-name.ts
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import { PrismaClient } from '@prisma/client'
|
||||||
|
|
||||||
|
const prisma = new PrismaClient()
|
||||||
|
|
||||||
|
async function fixProPlanName() {
|
||||||
|
console.log('🔧 Fixing Pro plan name in database...')
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 查找当前的 Pro 套餐
|
||||||
|
const currentProPlan = await prisma.subscriptionPlan.findFirst({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ name: 'Pro' },
|
||||||
|
{ name: 'pro' },
|
||||||
|
{ id: 'pro' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!currentProPlan) {
|
||||||
|
console.log('❌ No Pro plan found in database')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`📋 Found Pro plan:`)
|
||||||
|
console.log(` - ID: ${currentProPlan.id}`)
|
||||||
|
console.log(` - Current name: "${currentProPlan.name}"`)
|
||||||
|
console.log(` - Display name: ${currentProPlan.displayName}`)
|
||||||
|
|
||||||
|
// 更新名称为小写 "pro"
|
||||||
|
if (currentProPlan.name !== 'pro') {
|
||||||
|
console.log('\n🔄 Updating name to "pro"...')
|
||||||
|
|
||||||
|
const updatedPlan = await prisma.subscriptionPlan.update({
|
||||||
|
where: { id: currentProPlan.id },
|
||||||
|
data: { name: 'pro' }
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(`✅ Successfully updated plan name to: "${updatedPlan.name}"`)
|
||||||
|
} else {
|
||||||
|
console.log('✅ Plan name is already correct')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证更新
|
||||||
|
console.log('\n🔍 Verifying update...')
|
||||||
|
const verifyPlan = await prisma.subscriptionPlan.findFirst({
|
||||||
|
where: { name: 'pro' }
|
||||||
|
})
|
||||||
|
|
||||||
|
if (verifyPlan) {
|
||||||
|
console.log(`✅ Verification successful: Found plan with name "pro"`)
|
||||||
|
console.log(` - ID: ${verifyPlan.id}`)
|
||||||
|
console.log(` - Display name: ${verifyPlan.displayName}`)
|
||||||
|
console.log(` - Price: $${verifyPlan.price}`)
|
||||||
|
} else {
|
||||||
|
console.log('❌ Verification failed: No plan found with name "pro"')
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error fixing Pro plan name:', error)
|
||||||
|
throw error
|
||||||
|
} finally {
|
||||||
|
await prisma.$disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 运行修复
|
||||||
|
if (require.main === module) {
|
||||||
|
fixProPlanName()
|
||||||
|
.then(() => {
|
||||||
|
console.log('\n🎉 Pro plan name fix completed!')
|
||||||
|
process.exit(0)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('❌ Fix failed:', error)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export { fixProPlanName }
|
147
scripts/migrate-subscription-system.ts
Normal file
147
scripts/migrate-subscription-system.ts
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
import { PrismaClient } from '@prisma/client'
|
||||||
|
|
||||||
|
const prisma = new PrismaClient()
|
||||||
|
|
||||||
|
async function migrateSubscriptionSystem() {
|
||||||
|
console.log('Starting subscription system migration...')
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 创建默认的订阅套餐
|
||||||
|
console.log('Creating default subscription plans...')
|
||||||
|
|
||||||
|
// 免费套餐
|
||||||
|
await prisma.subscriptionPlan.upsert({
|
||||||
|
where: { id: 'free' },
|
||||||
|
update: {},
|
||||||
|
create: {
|
||||||
|
id: 'free',
|
||||||
|
name: 'Free',
|
||||||
|
displayName: 'Free Plan',
|
||||||
|
description: 'Basic features for getting started',
|
||||||
|
price: 0,
|
||||||
|
currency: 'usd',
|
||||||
|
interval: 'month',
|
||||||
|
stripePriceId: null,
|
||||||
|
isActive: true,
|
||||||
|
sortOrder: 1,
|
||||||
|
features: {
|
||||||
|
promptLimit: true,
|
||||||
|
versionsPerPrompt: true,
|
||||||
|
basicSupport: true
|
||||||
|
},
|
||||||
|
limits: {
|
||||||
|
maxVersionLimit: 3,
|
||||||
|
promptLimit: 500,
|
||||||
|
creditMonthly: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Pro 套餐
|
||||||
|
await prisma.subscriptionPlan.upsert({
|
||||||
|
where: { id: 'pro' },
|
||||||
|
update: {},
|
||||||
|
create: {
|
||||||
|
id: 'pro',
|
||||||
|
name: 'pro', // 重要:名称必须是 "pro" 用于查找
|
||||||
|
displayName: 'Pro Plan',
|
||||||
|
description: 'Advanced features for power users',
|
||||||
|
price: 19.9,
|
||||||
|
currency: 'usd',
|
||||||
|
interval: 'month',
|
||||||
|
stripePriceId: null, // 需要手动在数据库中设置 Stripe 价格 ID
|
||||||
|
isActive: true,
|
||||||
|
sortOrder: 2,
|
||||||
|
features: {
|
||||||
|
promptLimit: true,
|
||||||
|
versionsPerPrompt: true,
|
||||||
|
prioritySupport: true,
|
||||||
|
advancedAnalytics: true,
|
||||||
|
apiAccess: true
|
||||||
|
},
|
||||||
|
limits: {
|
||||||
|
maxVersionLimit: 10,
|
||||||
|
promptLimit: 5000,
|
||||||
|
creditMonthly: 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('Default subscription plans created successfully')
|
||||||
|
|
||||||
|
// 2. 更新现有用户的订阅套餐关联
|
||||||
|
console.log('Updating existing users...')
|
||||||
|
|
||||||
|
// 获取所有用户
|
||||||
|
const users = await prisma.user.findMany({
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
subscribePlan: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(`Found ${users.length} users to update`)
|
||||||
|
|
||||||
|
// 批量更新用户的订阅套餐关联
|
||||||
|
for (const user of users) {
|
||||||
|
let planId = 'free'
|
||||||
|
|
||||||
|
// 根据现有的 subscribePlan 字段确定新的套餐 ID
|
||||||
|
if (user.subscribePlan === 'pro') {
|
||||||
|
planId = 'pro'
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.user.update({
|
||||||
|
where: { id: user.id },
|
||||||
|
data: {
|
||||||
|
subscriptionPlanId: planId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('User subscription plan associations updated successfully')
|
||||||
|
|
||||||
|
// 3. 验证迁移结果
|
||||||
|
console.log('Verifying migration results...')
|
||||||
|
|
||||||
|
const planCount = await prisma.subscriptionPlan.count()
|
||||||
|
const userCount = await prisma.user.count()
|
||||||
|
const usersWithValidPlans = await prisma.user.count({
|
||||||
|
where: {
|
||||||
|
subscriptionPlanId: {
|
||||||
|
not: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(`Migration completed successfully:`)
|
||||||
|
console.log(`- Created ${planCount} subscription plans`)
|
||||||
|
console.log(`- Updated ${userCount} users`)
|
||||||
|
console.log(`- ${usersWithValidPlans} users have valid plan associations`)
|
||||||
|
|
||||||
|
if (userCount !== usersWithValidPlans) {
|
||||||
|
console.warn(`Warning: ${userCount - usersWithValidPlans} users don't have valid plan associations`)
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Migration failed:', error)
|
||||||
|
throw error
|
||||||
|
} finally {
|
||||||
|
await prisma.$disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 运行迁移
|
||||||
|
if (require.main === module) {
|
||||||
|
migrateSubscriptionSystem()
|
||||||
|
.then(() => {
|
||||||
|
console.log('Migration completed successfully')
|
||||||
|
process.exit(0)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Migration failed:', error)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export { migrateSubscriptionSystem }
|
113
scripts/test-pro-plan-detection.ts
Normal file
113
scripts/test-pro-plan-detection.ts
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import { PrismaClient } from '@prisma/client'
|
||||||
|
import { SubscriptionService } from '../src/lib/subscription-service'
|
||||||
|
|
||||||
|
const prisma = new PrismaClient()
|
||||||
|
|
||||||
|
async function testProPlanDetection() {
|
||||||
|
console.log('🧪 Testing Pro plan detection and price ID retrieval...')
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 测试获取 Pro 套餐
|
||||||
|
console.log('\n1. Testing Pro plan retrieval...')
|
||||||
|
const proPlan = await SubscriptionService.getProPlan()
|
||||||
|
|
||||||
|
if (proPlan) {
|
||||||
|
console.log(`✅ Pro plan found:`)
|
||||||
|
console.log(` - ID: ${proPlan.id}`)
|
||||||
|
console.log(` - Name: ${proPlan.name}`)
|
||||||
|
console.log(` - Display Name: ${proPlan.displayName}`)
|
||||||
|
console.log(` - Price: $${proPlan.price}`)
|
||||||
|
console.log(` - Stripe Price ID: ${proPlan.stripePriceId || 'Not set'}`)
|
||||||
|
console.log(` - Active: ${proPlan.isActive}`)
|
||||||
|
} else {
|
||||||
|
console.log('❌ Pro plan not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 测试获取 Pro 价格 ID
|
||||||
|
console.log('\n2. Testing Pro price ID retrieval...')
|
||||||
|
const proPriceId = await SubscriptionService.getProPriceId()
|
||||||
|
|
||||||
|
if (proPriceId) {
|
||||||
|
console.log(`✅ Pro price ID found: ${proPriceId}`)
|
||||||
|
} else {
|
||||||
|
console.log('⚠️ Pro price ID not set (this is expected for new installations)')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 测试 Pro 判定逻辑
|
||||||
|
console.log('\n3. Testing Pro plan detection logic...')
|
||||||
|
if (proPlan) {
|
||||||
|
const isPro = SubscriptionService.isPlanPro(proPlan)
|
||||||
|
console.log(`✅ Pro plan detection: ${isPro} (expected: true for price > $19)`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 验证数据库中的套餐配置
|
||||||
|
console.log('\n4. Validating database plan configuration...')
|
||||||
|
|
||||||
|
const allPlans = await SubscriptionService.getAvailablePlans()
|
||||||
|
console.log(`📊 Found ${allPlans.length} active plans:`)
|
||||||
|
|
||||||
|
allPlans.forEach(plan => {
|
||||||
|
const isPro = SubscriptionService.isPlanPro(plan)
|
||||||
|
console.log(` - ${plan.displayName} (${plan.name}): $${plan.price} - ${isPro ? 'Pro' : 'Free'} tier`)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 5. 检查是否有名称为 "pro" 的套餐
|
||||||
|
console.log('\n5. Checking for plans with name "pro"...')
|
||||||
|
const proNamedPlans = allPlans.filter(plan => plan.name === 'pro')
|
||||||
|
|
||||||
|
if (proNamedPlans.length === 0) {
|
||||||
|
console.log('❌ No plans found with name "pro"')
|
||||||
|
console.log(' This will cause subscription creation to fail!')
|
||||||
|
} else if (proNamedPlans.length === 1) {
|
||||||
|
console.log(`✅ Found exactly one plan with name "pro": ${proNamedPlans[0].displayName}`)
|
||||||
|
} else {
|
||||||
|
console.log(`⚠️ Found ${proNamedPlans.length} plans with name "pro" - this may cause conflicts`)
|
||||||
|
proNamedPlans.forEach((plan, index) => {
|
||||||
|
console.log(` ${index + 1}. ${plan.displayName} (ID: ${plan.id})`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 模拟 API 调用测试
|
||||||
|
console.log('\n6. Testing API endpoint simulation...')
|
||||||
|
try {
|
||||||
|
const testPriceId = await SubscriptionService.getProPriceId()
|
||||||
|
if (testPriceId) {
|
||||||
|
console.log(`✅ API would return price ID: ${testPriceId}`)
|
||||||
|
} else {
|
||||||
|
console.log('⚠️ API would return 404 - Pro plan not configured')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`❌ API would return 500 - Error: ${error}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n🎉 Pro plan detection test completed!')
|
||||||
|
|
||||||
|
// 7. 提供配置建议
|
||||||
|
if (!proPriceId && proPlan) {
|
||||||
|
console.log('\n💡 Configuration needed:')
|
||||||
|
console.log(' To enable Pro subscriptions, set the stripePriceId for the Pro plan:')
|
||||||
|
console.log(` UPDATE subscription_plans SET "stripePriceId" = 'price_your_stripe_price_id' WHERE name = 'pro';`)
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Test failed:', error)
|
||||||
|
throw error
|
||||||
|
} finally {
|
||||||
|
await prisma.$disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 运行测试
|
||||||
|
if (require.main === module) {
|
||||||
|
testProPlanDetection()
|
||||||
|
.then(() => {
|
||||||
|
console.log('✅ All tests completed!')
|
||||||
|
process.exit(0)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('❌ Tests failed:', error)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export { testProPlanDetection }
|
138
scripts/test-subscription-system.ts
Normal file
138
scripts/test-subscription-system.ts
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
import { PrismaClient } from '@prisma/client'
|
||||||
|
import { SubscriptionService } from '../src/lib/subscription-service'
|
||||||
|
import { isPlanPro, isPlanFree, getPlanTier } from '../src/lib/subscription-utils'
|
||||||
|
|
||||||
|
const prisma = new PrismaClient()
|
||||||
|
|
||||||
|
async function testSubscriptionSystem() {
|
||||||
|
console.log('🧪 Testing subscription system...')
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 测试获取套餐
|
||||||
|
console.log('\n1. Testing plan retrieval...')
|
||||||
|
const plans = await SubscriptionService.getAvailablePlans()
|
||||||
|
console.log(`✅ Found ${plans.length} plans:`)
|
||||||
|
plans.forEach(plan => {
|
||||||
|
const limits = plan.limits as any
|
||||||
|
const isPro = isPlanPro(plan)
|
||||||
|
const tier = getPlanTier(plan)
|
||||||
|
console.log(` - ${plan.displayName}: ${limits.promptLimit} prompts, ${limits.maxVersionLimit} versions (${tier}, isPro: ${isPro})`)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 2. 测试获取特定套餐
|
||||||
|
console.log('\n2. Testing specific plan retrieval...')
|
||||||
|
const freePlan = await SubscriptionService.getPlanById('free')
|
||||||
|
const proPlan = await SubscriptionService.getPlanById('pro')
|
||||||
|
|
||||||
|
if (freePlan) {
|
||||||
|
console.log(`✅ Free plan found: ${freePlan.displayName}`)
|
||||||
|
} else {
|
||||||
|
console.log('❌ Free plan not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (proPlan) {
|
||||||
|
console.log(`✅ Pro plan found: ${proPlan.displayName}`)
|
||||||
|
} else {
|
||||||
|
console.log('❌ Pro plan not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 测试用户权限(需要一个测试用户)
|
||||||
|
console.log('\n3. Testing user permissions...')
|
||||||
|
|
||||||
|
// 查找第一个用户进行测试
|
||||||
|
const testUser = await prisma.user.findFirst({
|
||||||
|
select: { id: true, email: true, subscriptionPlanId: true }
|
||||||
|
})
|
||||||
|
|
||||||
|
if (testUser) {
|
||||||
|
console.log(`📝 Testing with user: ${testUser.email} (plan: ${testUser.subscriptionPlanId})`)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const permissions = await SubscriptionService.getUserPermissions(testUser.id)
|
||||||
|
console.log(`✅ User permissions:`)
|
||||||
|
console.log(` - Prompt limit: ${permissions.promptLimit}`)
|
||||||
|
console.log(` - Max version limit: ${permissions.maxVersionLimit}`)
|
||||||
|
console.log(` - Can create prompt: ${permissions.canCreatePrompt}`)
|
||||||
|
console.log(` - Features: ${permissions.features.join(', ')}`)
|
||||||
|
|
||||||
|
// 测试订阅状态
|
||||||
|
const subscriptionStatus = await SubscriptionService.getUserSubscriptionStatus(testUser.id)
|
||||||
|
console.log(`✅ Subscription status:`)
|
||||||
|
console.log(` - Active: ${subscriptionStatus.isActive}`)
|
||||||
|
console.log(` - Plan ID: ${subscriptionStatus.planId}`)
|
||||||
|
if (subscriptionStatus.stripeSubscriptionId) {
|
||||||
|
console.log(` - Stripe subscription: ${subscriptionStatus.stripeSubscriptionId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`❌ Error getting user permissions: ${error}`)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('⚠️ No users found for testing')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 验证数据完整性
|
||||||
|
console.log('\n4. Validating data integrity...')
|
||||||
|
|
||||||
|
const userCount = await prisma.user.count()
|
||||||
|
const usersWithPlans = await prisma.user.count({
|
||||||
|
where: {
|
||||||
|
subscriptionPlanId: {
|
||||||
|
not: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(`📊 Data integrity:`)
|
||||||
|
console.log(` - Total users: ${userCount}`)
|
||||||
|
console.log(` - Users with valid plans: ${usersWithPlans}`)
|
||||||
|
|
||||||
|
if (userCount === usersWithPlans) {
|
||||||
|
console.log(`✅ All users have valid subscription plans`)
|
||||||
|
} else {
|
||||||
|
console.log(`⚠️ ${userCount - usersWithPlans} users don't have valid subscription plans`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 测试 Pro 判定逻辑
|
||||||
|
console.log('\n5. Testing Pro plan detection logic...')
|
||||||
|
|
||||||
|
// 测试不同价格的套餐判定
|
||||||
|
const testPlans = [
|
||||||
|
{ price: 0, name: 'Free' },
|
||||||
|
{ price: 9.99, name: 'Basic' },
|
||||||
|
{ price: 19.99, name: 'Pro' },
|
||||||
|
{ price: 29.99, name: 'Premium' },
|
||||||
|
{ price: 99.99, name: 'Enterprise' }
|
||||||
|
]
|
||||||
|
|
||||||
|
testPlans.forEach(testPlan => {
|
||||||
|
const isPro = isPlanPro(testPlan)
|
||||||
|
const isFree = isPlanFree(testPlan)
|
||||||
|
const tier = getPlanTier(testPlan)
|
||||||
|
console.log(` - ${testPlan.name} ($${testPlan.price}): isPro=${isPro}, isFree=${isFree}, tier=${tier}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('\n🎉 Subscription system test completed successfully!')
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Test failed:', error)
|
||||||
|
throw error
|
||||||
|
} finally {
|
||||||
|
await prisma.$disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 运行测试
|
||||||
|
if (require.main === module) {
|
||||||
|
testSubscriptionSystem()
|
||||||
|
.then(() => {
|
||||||
|
console.log('✅ All tests passed!')
|
||||||
|
process.exit(0)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('❌ Tests failed:', error)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export { testSubscriptionSystem }
|
215
src/app/api/admin/subscription-plans/route.ts
Normal file
215
src/app/api/admin/subscription-plans/route.ts
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { createServerClient } from '@supabase/ssr'
|
||||||
|
import { cookies } from 'next/headers'
|
||||||
|
import { prisma } from '@/lib/prisma'
|
||||||
|
import { SubscriptionService } from '@/lib/subscription-service'
|
||||||
|
|
||||||
|
// 验证管理员权限
|
||||||
|
async function verifyAdmin() {
|
||||||
|
const cookieStore = await cookies()
|
||||||
|
const supabase = createServerClient(
|
||||||
|
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||||
|
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||||
|
{
|
||||||
|
cookies: {
|
||||||
|
get(name: string) {
|
||||||
|
return cookieStore.get(name)?.value
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const { data: { user }, error: authError } = await supabase.auth.getUser()
|
||||||
|
|
||||||
|
if (authError || !user) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const userData = await prisma.user.findUnique({
|
||||||
|
where: { id: user.id },
|
||||||
|
select: { isAdmin: true }
|
||||||
|
})
|
||||||
|
|
||||||
|
return userData?.isAdmin ? user : null
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET - 获取所有订阅套餐
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const user = await verifyAdmin()
|
||||||
|
if (!user) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const plans = await SubscriptionService.getAvailablePlans()
|
||||||
|
return NextResponse.json({ plans })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching subscription plans:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Failed to fetch subscription plans' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST - 创建新的订阅套餐
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const user = await verifyAdmin()
|
||||||
|
if (!user) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
displayName,
|
||||||
|
description,
|
||||||
|
price,
|
||||||
|
currency = 'usd',
|
||||||
|
interval = 'month',
|
||||||
|
stripePriceId,
|
||||||
|
isActive = true,
|
||||||
|
sortOrder = 0,
|
||||||
|
features,
|
||||||
|
limits
|
||||||
|
} = await request.json()
|
||||||
|
|
||||||
|
// 验证必填字段
|
||||||
|
if (!id || !name || !displayName || !features || !limits) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Missing required fields' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建套餐
|
||||||
|
const plan = await prisma.subscriptionPlan.create({
|
||||||
|
data: {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
displayName,
|
||||||
|
description,
|
||||||
|
price,
|
||||||
|
currency,
|
||||||
|
interval,
|
||||||
|
stripePriceId,
|
||||||
|
isActive,
|
||||||
|
sortOrder,
|
||||||
|
features,
|
||||||
|
limits
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return NextResponse.json({ plan })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating subscription plan:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Failed to create subscription plan' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUT - 更新订阅套餐
|
||||||
|
export async function PUT(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const user = await verifyAdmin()
|
||||||
|
if (!user) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
displayName,
|
||||||
|
description,
|
||||||
|
price,
|
||||||
|
currency,
|
||||||
|
interval,
|
||||||
|
stripePriceId,
|
||||||
|
isActive,
|
||||||
|
sortOrder,
|
||||||
|
features,
|
||||||
|
limits
|
||||||
|
} = await request.json()
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Plan ID is required' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新套餐
|
||||||
|
const plan = await prisma.subscriptionPlan.update({
|
||||||
|
where: { id },
|
||||||
|
data: {
|
||||||
|
...(name && { name }),
|
||||||
|
...(displayName && { displayName }),
|
||||||
|
...(description !== undefined && { description }),
|
||||||
|
...(price !== undefined && { price }),
|
||||||
|
...(currency && { currency }),
|
||||||
|
...(interval && { interval }),
|
||||||
|
...(stripePriceId !== undefined && { stripePriceId }),
|
||||||
|
...(isActive !== undefined && { isActive }),
|
||||||
|
...(sortOrder !== undefined && { sortOrder }),
|
||||||
|
...(features && { features }),
|
||||||
|
...(limits && { limits })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return NextResponse.json({ plan })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating subscription plan:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Failed to update subscription plan' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE - 删除订阅套餐
|
||||||
|
export async function DELETE(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const user = await verifyAdmin()
|
||||||
|
if (!user) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const { searchParams } = new URL(request.url)
|
||||||
|
const planId = searchParams.get('id')
|
||||||
|
|
||||||
|
if (!planId) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Plan ID is required' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否有用户正在使用此套餐
|
||||||
|
const usersCount = await prisma.user.count({
|
||||||
|
where: { subscriptionPlanId: planId }
|
||||||
|
})
|
||||||
|
|
||||||
|
if (usersCount > 0) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Cannot delete plan with active users' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除套餐
|
||||||
|
await prisma.subscriptionPlan.delete({
|
||||||
|
where: { id: planId }
|
||||||
|
})
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting subscription plan:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Failed to delete subscription plan' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
import { prisma } from '@/lib/prisma'
|
import { prisma } from '@/lib/prisma'
|
||||||
import { getVersionsToDelete } from '@/lib/subscription'
|
import { SubscriptionService } from '@/lib/subscription-service'
|
||||||
|
|
||||||
interface RouteParams {
|
interface RouteParams {
|
||||||
params: Promise<{ id: string }>
|
params: Promise<{ id: string }>
|
||||||
@ -153,25 +153,22 @@ export async function PUT(request: NextRequest, { params }: RouteParams) {
|
|||||||
|
|
||||||
// 如果内容有变化,创建新版本并处理版本限制
|
// 如果内容有变化,创建新版本并处理版本限制
|
||||||
if (shouldCreateVersion && content) {
|
if (shouldCreateVersion && content) {
|
||||||
const user = existingPrompt.user
|
// 使用新的订阅服务检查版本限制
|
||||||
const currentVersionCount = existingPrompt.versions.length
|
const versionsToDelete = await SubscriptionService.getVersionsToDelete(userId, id)
|
||||||
|
|
||||||
// 检查是否需要删除旧版本以遵守版本限制
|
|
||||||
const versionsToDelete = getVersionsToDelete(currentVersionCount, user)
|
|
||||||
|
|
||||||
if (versionsToDelete > 0) {
|
if (versionsToDelete > 0) {
|
||||||
// 删除最旧的版本
|
// 删除最旧的版本
|
||||||
const oldestVersions = existingPrompt.versions
|
const oldestVersions = existingPrompt.versions
|
||||||
.slice(-versionsToDelete)
|
.slice(-versionsToDelete)
|
||||||
.map(v => v.id)
|
.map(v => v.id)
|
||||||
|
|
||||||
await prisma.promptVersion.deleteMany({
|
await prisma.promptVersion.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
id: { in: oldestVersions }
|
id: { in: oldestVersions }
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextVersion = (currentVersion?.version || 0) + 1
|
const nextVersion = (currentVersion?.version || 0) + 1
|
||||||
await prisma.promptVersion.create({
|
await prisma.promptVersion.create({
|
||||||
data: {
|
data: {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
import { prisma } from '@/lib/prisma'
|
import { prisma } from '@/lib/prisma'
|
||||||
|
import { SubscriptionService } from '@/lib/subscription-service'
|
||||||
|
|
||||||
interface RouteParams {
|
interface RouteParams {
|
||||||
params: Promise<{ id: string }>
|
params: Promise<{ id: string }>
|
||||||
@ -75,9 +76,35 @@ export async function POST(request: NextRequest, { params }: RouteParams) {
|
|||||||
return NextResponse.json({ error: 'Prompt not found' }, { status: 404 })
|
return NextResponse.json({ error: 'Prompt not found' }, { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查用户是否可以创建新版本
|
||||||
|
const canCreateVersion = await SubscriptionService.canCreateVersion(userId, id)
|
||||||
|
if (!canCreateVersion) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Version limit reached. Please upgrade your plan or delete old versions.' },
|
||||||
|
{ status: 403 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// 获取下一个版本号
|
// 获取下一个版本号
|
||||||
const nextVersion = (prompt.versions[0]?.version || 0) + 1
|
const nextVersion = (prompt.versions[0]?.version || 0) + 1
|
||||||
|
|
||||||
|
// 检查是否需要删除旧版本
|
||||||
|
const versionsToDelete = await SubscriptionService.getVersionsToDelete(userId, id)
|
||||||
|
if (versionsToDelete > 0) {
|
||||||
|
// 获取最旧的版本并删除
|
||||||
|
const oldVersions = await prisma.promptVersion.findMany({
|
||||||
|
where: { promptId: id },
|
||||||
|
orderBy: { version: 'asc' },
|
||||||
|
take: versionsToDelete
|
||||||
|
})
|
||||||
|
|
||||||
|
await prisma.promptVersion.deleteMany({
|
||||||
|
where: {
|
||||||
|
id: { in: oldVersions.map(v => v.id) }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 创建新版本
|
// 创建新版本
|
||||||
const newVersion = await prisma.promptVersion.create({
|
const newVersion = await prisma.promptVersion.create({
|
||||||
data: {
|
data: {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
import { prisma } from '@/lib/prisma'
|
import { prisma } from '@/lib/prisma'
|
||||||
|
import { SubscriptionService } from '@/lib/subscription-service'
|
||||||
|
|
||||||
// GET /api/prompts - 获取用户的 prompts 列表
|
// GET /api/prompts - 获取用户的 prompts 列表
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
@ -122,6 +123,15 @@ export async function POST(request: NextRequest) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查用户是否可以创建新的提示词
|
||||||
|
const canCreate = await SubscriptionService.canCreatePrompt(userId)
|
||||||
|
if (!canCreate) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Prompt limit reached. Please upgrade your plan to create more prompts.' },
|
||||||
|
{ status: 403 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// 创建或获取标签
|
// 创建或获取标签
|
||||||
const tagObjects = []
|
const tagObjects = []
|
||||||
if (tags && tags.length > 0) {
|
if (tags && tags.length > 0) {
|
||||||
|
16
src/app/api/subscription-plans/route.ts
Normal file
16
src/app/api/subscription-plans/route.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
import { SubscriptionService } from '@/lib/subscription-service'
|
||||||
|
|
||||||
|
// GET - 获取所有可用的订阅套餐(公开接口)
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const plans = await SubscriptionService.getAvailablePlans()
|
||||||
|
return NextResponse.json({ plans })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching subscription plans:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Failed to fetch subscription plans' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
import { createServerClient } from '@supabase/ssr'
|
import { createServerClient } from '@supabase/ssr'
|
||||||
import { cookies } from 'next/headers'
|
import { cookies } from 'next/headers'
|
||||||
import { createOrGetStripeCustomer, createSubscriptionSession, STRIPE_CONFIG } from '@/lib/stripe'
|
import { createOrGetStripeCustomer, createSubscriptionSession } from '@/lib/stripe'
|
||||||
import { prisma } from '@/lib/prisma'
|
import { prisma } from '@/lib/prisma'
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
@ -38,8 +38,12 @@ export async function POST(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: 'User not found' }, { status: 404 })
|
return NextResponse.json({ error: 'User not found' }, { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证价格 ID
|
// 验证价格 ID - 检查是否是有效的套餐价格
|
||||||
if (priceId !== STRIPE_CONFIG.products.pro.priceId) {
|
const plan = await prisma.subscriptionPlan.findFirst({
|
||||||
|
where: { stripePriceId: priceId, isActive: true }
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!plan) {
|
||||||
return NextResponse.json({ error: 'Invalid price ID' }, { status: 400 })
|
return NextResponse.json({ error: 'Invalid price ID' }, { status: 400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,11 +2,11 @@ import { NextRequest, NextResponse } from 'next/server'
|
|||||||
import { createServerClient } from '@supabase/ssr'
|
import { createServerClient } from '@supabase/ssr'
|
||||||
import { cookies } from 'next/headers'
|
import { cookies } from 'next/headers'
|
||||||
import {
|
import {
|
||||||
getCustomerSubscriptions,
|
|
||||||
cancelSubscription,
|
cancelSubscription,
|
||||||
reactivateSubscription,
|
reactivateSubscription,
|
||||||
createCustomerPortalSession
|
createCustomerPortalSession
|
||||||
} from '@/lib/stripe'
|
} from '@/lib/stripe'
|
||||||
|
import { SubscriptionService } from '@/lib/subscription-service'
|
||||||
import { prisma } from '@/lib/prisma'
|
import { prisma } from '@/lib/prisma'
|
||||||
|
|
||||||
// GET - 获取用户订阅信息
|
// GET - 获取用户订阅信息
|
||||||
@ -32,39 +32,21 @@ export async function GET() {
|
|||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取用户的 Stripe 客户 ID
|
// 获取用户的订阅状态和权益
|
||||||
const userData = await prisma.user.findUnique({
|
const subscriptionStatus = await SubscriptionService.getUserSubscriptionStatus(user.id)
|
||||||
where: { id: user.id },
|
const permissions = await SubscriptionService.getUserPermissions(user.id)
|
||||||
select: { stripeCustomerId: true, subscribePlan: true }
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!userData?.stripeCustomerId) {
|
// 获取套餐信息
|
||||||
return NextResponse.json({
|
const plan = await SubscriptionService.getPlanById(subscriptionStatus.planId)
|
||||||
plan: 'free',
|
|
||||||
status: 'active',
|
|
||||||
subscriptions: []
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取 Stripe 订阅信息
|
|
||||||
const subscriptions = await getCustomerSubscriptions(userData.stripeCustomerId)
|
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
plan: userData.subscribePlan,
|
subscription: subscriptionStatus,
|
||||||
subscriptions: subscriptions.map(sub => {
|
permissions: permissions,
|
||||||
const subData = sub as unknown as Record<string, unknown>
|
plan: plan ? {
|
||||||
const items = subData.items as { data: Array<{ price: { id: string; unit_amount: number; currency: string } }> }
|
id: plan.id,
|
||||||
return {
|
displayName: plan.displayName,
|
||||||
id: sub.id,
|
price: plan.price
|
||||||
status: sub.status,
|
} : null
|
||||||
currentPeriodStart: subData.current_period_start as number,
|
|
||||||
currentPeriodEnd: subData.current_period_end as number,
|
|
||||||
cancelAtPeriodEnd: subData.cancel_at_period_end as boolean,
|
|
||||||
priceId: items?.data[0]?.price?.id,
|
|
||||||
amount: items?.data[0]?.price?.unit_amount,
|
|
||||||
currency: items?.data[0]?.price?.currency,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -101,14 +83,11 @@ export async function POST(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取用户的 Stripe 客户 ID
|
// 获取用户的订阅状态
|
||||||
const userData = await prisma.user.findUnique({
|
const subscriptionStatus = await SubscriptionService.getUserSubscriptionStatus(user.id)
|
||||||
where: { id: user.id },
|
|
||||||
select: { stripeCustomerId: true }
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!userData?.stripeCustomerId) {
|
if (!subscriptionStatus.stripeSubscriptionId) {
|
||||||
return NextResponse.json({ error: 'No customer found' }, { status: 404 })
|
return NextResponse.json({ error: 'No active subscription found' }, { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
let result
|
let result
|
||||||
@ -122,6 +101,16 @@ export async function POST(request: NextRequest) {
|
|||||||
break
|
break
|
||||||
case 'portal':
|
case 'portal':
|
||||||
try {
|
try {
|
||||||
|
// 获取用户的 Stripe 客户 ID
|
||||||
|
const userData = await prisma.user.findUnique({
|
||||||
|
where: { id: user.id },
|
||||||
|
select: { stripeCustomerId: true }
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!userData?.stripeCustomerId) {
|
||||||
|
return NextResponse.json({ error: 'No customer found' }, { status: 404 })
|
||||||
|
}
|
||||||
|
|
||||||
result = await createCustomerPortalSession(
|
result = await createCustomerPortalSession(
|
||||||
userData.stripeCustomerId,
|
userData.stripeCustomerId,
|
||||||
`${process.env.NEXT_PUBLIC_APP_URL}/subscription`
|
`${process.env.NEXT_PUBLIC_APP_URL}/subscription`
|
||||||
|
24
src/app/api/subscription/pro-price-id/route.ts
Normal file
24
src/app/api/subscription/pro-price-id/route.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
import { SubscriptionService } from '@/lib/subscription-service'
|
||||||
|
|
||||||
|
// GET - 获取 Pro 套餐的 Stripe 价格 ID
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const priceId = await SubscriptionService.getProPriceId()
|
||||||
|
|
||||||
|
if (!priceId) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Pro plan not found or not configured' },
|
||||||
|
{ status: 404 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ priceId })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching Pro price ID:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Failed to fetch Pro price ID' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,7 @@
|
|||||||
import { NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { createServerClient } from '@supabase/ssr'
|
import { createServerClient } from '@supabase/ssr'
|
||||||
import { cookies } from 'next/headers'
|
import { cookies } from 'next/headers'
|
||||||
import { getCustomerSubscriptions, STRIPE_CONFIG } from '@/lib/stripe'
|
import { SubscriptionService } from '@/lib/subscription-service'
|
||||||
import { prisma } from '@/lib/prisma'
|
|
||||||
|
|
||||||
export async function POST() {
|
export async function POST() {
|
||||||
try {
|
try {
|
||||||
@ -26,58 +25,21 @@ export async function POST() {
|
|||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取用户的 Stripe 客户 ID
|
// 获取用户的实时订阅状态
|
||||||
const userData = await prisma.user.findUnique({
|
const subscriptionStatus = await SubscriptionService.getUserSubscriptionStatus(user.id)
|
||||||
where: { id: user.id },
|
|
||||||
select: { stripeCustomerId: true }
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!userData?.stripeCustomerId) {
|
// 更新用户的订阅套餐
|
||||||
return NextResponse.json({ error: 'No Stripe customer found' }, { status: 404 })
|
await SubscriptionService.updateUserSubscriptionPlan(user.id, subscriptionStatus.planId)
|
||||||
}
|
|
||||||
|
|
||||||
// 获取 Stripe 订阅信息
|
// 获取用户权益
|
||||||
const subscriptions = await getCustomerSubscriptions(userData.stripeCustomerId)
|
const permissions = await SubscriptionService.getUserPermissions(user.id)
|
||||||
|
|
||||||
// 查找活跃的订阅
|
|
||||||
const activeSubscription = subscriptions.find(sub => sub.status === 'active')
|
|
||||||
|
|
||||||
let subscribePlan = 'free'
|
|
||||||
let maxVersionLimit = 3
|
|
||||||
let promptLimit = 500
|
|
||||||
|
|
||||||
if (activeSubscription) {
|
console.log(`Synced user ${user.id} subscription to ${subscriptionStatus.planId}`)
|
||||||
const subData = activeSubscription as unknown as Record<string, unknown>
|
|
||||||
const items = subData.items as { data: Array<{ price: { id: string } }> }
|
|
||||||
const priceId = items?.data[0]?.price?.id
|
|
||||||
|
|
||||||
if (priceId === STRIPE_CONFIG.products.pro.priceId) {
|
|
||||||
subscribePlan = 'pro'
|
|
||||||
maxVersionLimit = 10
|
|
||||||
promptLimit = 5000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新用户订阅状态
|
|
||||||
const updatedUser = await prisma.user.update({
|
|
||||||
where: { id: user.id },
|
|
||||||
data: {
|
|
||||||
subscribePlan,
|
|
||||||
maxVersionLimit,
|
|
||||||
promptLimit,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log(`Synced user ${user.id} subscription to ${subscribePlan}`)
|
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
plan: subscribePlan,
|
subscription: subscriptionStatus,
|
||||||
user: {
|
permissions: permissions
|
||||||
subscribePlan: updatedUser.subscribePlan,
|
|
||||||
maxVersionLimit: updatedUser.maxVersionLimit,
|
|
||||||
promptLimit: updatedUser.promptLimit
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
import { prisma } from '@/lib/prisma'
|
import { prisma } from '@/lib/prisma'
|
||||||
import { getMaxVersionLimit } from '@/lib/subscription'
|
import { SubscriptionService } from '@/lib/subscription-service'
|
||||||
|
|
||||||
// GET /api/users/profile - 获取用户配置
|
// GET /api/users/profile - 获取用户配置
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
@ -76,17 +76,25 @@ export async function PUT(request: NextRequest) {
|
|||||||
// 验证版本限制不能超过订阅计划的最大限制
|
// 验证版本限制不能超过订阅计划的最大限制
|
||||||
let finalVersionLimit = versionLimit
|
let finalVersionLimit = versionLimit
|
||||||
if (versionLimit !== undefined) {
|
if (versionLimit !== undefined) {
|
||||||
const maxVersionLimit = getMaxVersionLimit(currentUser.subscribePlan)
|
try {
|
||||||
if (versionLimit > maxVersionLimit) {
|
const permissions = await SubscriptionService.getUserPermissions(userId)
|
||||||
return NextResponse.json(
|
const maxVersionLimit = permissions.maxVersionLimit
|
||||||
{
|
|
||||||
error: 'Version limit exceeds subscription plan maximum',
|
if (versionLimit > maxVersionLimit) {
|
||||||
maxAllowed: maxVersionLimit
|
return NextResponse.json(
|
||||||
},
|
{
|
||||||
{ status: 400 }
|
error: 'Version limit exceeds subscription plan maximum',
|
||||||
)
|
maxAllowed: maxVersionLimit
|
||||||
|
},
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
finalVersionLimit = Math.max(1, Math.min(versionLimit, maxVersionLimit))
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting user permissions:', error)
|
||||||
|
// 如果获取权限失败,使用默认限制
|
||||||
|
finalVersionLimit = Math.max(1, Math.min(versionLimit, 3))
|
||||||
}
|
}
|
||||||
finalVersionLimit = Math.max(1, Math.min(versionLimit, maxVersionLimit))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateData: Record<string, unknown> = {}
|
const updateData: Record<string, unknown> = {}
|
||||||
|
@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'
|
|||||||
import { stripe, STRIPE_CONFIG } from '@/lib/stripe'
|
import { stripe, STRIPE_CONFIG } from '@/lib/stripe'
|
||||||
import { prisma } from '@/lib/prisma'
|
import { prisma } from '@/lib/prisma'
|
||||||
import { headers } from 'next/headers'
|
import { headers } from 'next/headers'
|
||||||
|
import { SubscriptionService } from '@/lib/subscription-service'
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
@ -84,12 +85,6 @@ async function handleSubscriptionUpdate(subscription: Record<string, unknown>) {
|
|||||||
const items = subscription.items as { data: Array<{ price: { id: string } }> }
|
const items = subscription.items as { data: Array<{ price: { id: string } }> }
|
||||||
const priceId = items?.data[0]?.price?.id
|
const priceId = items?.data[0]?.price?.id
|
||||||
|
|
||||||
// 根据价格 ID 确定订阅计划
|
|
||||||
let subscribePlan = 'free'
|
|
||||||
if (priceId === STRIPE_CONFIG.products.pro.priceId) {
|
|
||||||
subscribePlan = 'pro'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查找用户
|
// 查找用户
|
||||||
const user = await prisma.user.findFirst({
|
const user = await prisma.user.findFirst({
|
||||||
where: { stripeCustomerId: customerId }
|
where: { stripeCustomerId: customerId }
|
||||||
@ -100,19 +95,17 @@ async function handleSubscriptionUpdate(subscription: Record<string, unknown>) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新用户订阅状态
|
// 根据订阅状态确定套餐
|
||||||
await prisma.user.update({
|
let planId = 'free'
|
||||||
where: { id: user.id },
|
if (status === 'active' || status === 'trialing') {
|
||||||
data: {
|
// 根据 Stripe 价格 ID 获取套餐 ID
|
||||||
subscribePlan: status === 'active' ? subscribePlan : 'free',
|
planId = await SubscriptionService.getPlanIdByStripePriceId(priceId)
|
||||||
// 根据订阅计划更新限制
|
}
|
||||||
maxVersionLimit: subscribePlan === 'pro' ? 10 : 3,
|
|
||||||
promptLimit: subscribePlan === 'pro' ? 5000 : 500,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
|
// 更新用户订阅套餐
|
||||||
|
await SubscriptionService.updateUserSubscriptionPlan(user.id, planId)
|
||||||
|
|
||||||
|
console.log(`Updated user ${user.id} subscription to plan: ${planId}`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error handling subscription update:', error)
|
console.error('Error handling subscription update:', error)
|
||||||
}
|
}
|
||||||
@ -133,17 +126,9 @@ async function handleSubscriptionDeleted(subscription: Record<string, unknown>)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 将用户降级为免费计划
|
// 将用户降级为免费计划
|
||||||
await prisma.user.update({
|
await SubscriptionService.updateUserSubscriptionPlan(user.id, 'free')
|
||||||
where: { id: user.id },
|
|
||||||
data: {
|
|
||||||
subscribePlan: 'free',
|
|
||||||
maxVersionLimit: 3,
|
|
||||||
promptLimit: 500,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
console.log(`Reset user ${user.id} to free plan`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error handling subscription deletion:', error)
|
console.error('Error handling subscription deletion:', error)
|
||||||
}
|
}
|
||||||
|
@ -6,13 +6,42 @@ import { useUser } from '@/hooks/useUser'
|
|||||||
import { Header } from '@/components/layout/Header'
|
import { Header } from '@/components/layout/Header'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Check, Crown, Star } from 'lucide-react'
|
import { Check, Crown, Star } from 'lucide-react'
|
||||||
import { SUBSCRIPTION_PLANS } from '@/lib/subscription'
|
|
||||||
import { SubscribeButton } from '@/components/subscription/SubscribeButton'
|
import { SubscribeButton } from '@/components/subscription/SubscribeButton'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import type { SubscriptionPlan } from '@prisma/client'
|
||||||
|
import {
|
||||||
|
isPlanPro,
|
||||||
|
isPlanFree,
|
||||||
|
getPlanLimits,
|
||||||
|
getPlanFeatures,
|
||||||
|
formatPlanPrice,
|
||||||
|
getPlanTheme
|
||||||
|
} from '@/lib/subscription-utils'
|
||||||
|
|
||||||
export default function PricingPage() {
|
export default function PricingPage() {
|
||||||
const { user } = useAuth()
|
const { user } = useAuth()
|
||||||
const { userData } = useUser()
|
const { userData } = useUser()
|
||||||
const t = useTranslations('pricing')
|
const t = useTranslations('pricing')
|
||||||
|
const [plans, setPlans] = useState<SubscriptionPlan[]>([])
|
||||||
|
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 || [])
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching plans:', error)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleGetStarted = () => {
|
const handleGetStarted = () => {
|
||||||
if (user) {
|
if (user) {
|
||||||
@ -22,9 +51,25 @@ export default function PricingPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isCurrentPlan = (planKey: string) => {
|
const isCurrentPlan = (planId: string) => {
|
||||||
if (!userData) return false
|
if (!userData) return false
|
||||||
return userData.subscribePlan === planKey
|
return userData.subscriptionPlanId === planId
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background">
|
||||||
|
<Header />
|
||||||
|
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-primary mx-auto"></div>
|
||||||
|
<p className="mt-4 text-muted-foreground">Loading pricing plans...</p>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -44,126 +89,109 @@ export default function PricingPage() {
|
|||||||
|
|
||||||
{/* Pricing Cards */}
|
{/* Pricing Cards */}
|
||||||
<div className="grid md:grid-cols-2 gap-8 max-w-4xl mx-auto">
|
<div className="grid md:grid-cols-2 gap-8 max-w-4xl mx-auto">
|
||||||
{/* Free Plan */}
|
{plans.map((plan) => {
|
||||||
<div className="bg-card p-8 rounded-lg shadow-sm border relative">
|
const limits = getPlanLimits(plan)
|
||||||
{isCurrentPlan('free') && (
|
const features = getPlanFeatures(plan)
|
||||||
<div className="absolute top-4 right-4 bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-300 px-3 py-1 rounded-full text-xs font-semibold">
|
const isFree = isPlanFree(plan)
|
||||||
{t('currentPlan')}
|
const isPro = isPlanPro(plan)
|
||||||
</div>
|
const isCurrent = isCurrentPlan(plan.id)
|
||||||
)}
|
const theme = getPlanTheme(plan)
|
||||||
|
|
||||||
<div className="text-center mb-6">
|
|
||||||
<div className="flex items-center justify-center mb-4">
|
|
||||||
<div className="p-3 rounded-full bg-gradient-to-br from-slate-400 to-gray-500 dark:from-slate-500 dark:to-gray-400">
|
|
||||||
<Star className="w-6 h-6 text-white" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<h3 className="text-2xl font-bold text-card-foreground mb-2">
|
|
||||||
{t('free.title')}
|
|
||||||
</h3>
|
|
||||||
<div className="text-4xl font-bold text-card-foreground mb-2">
|
|
||||||
{t('free.price')}
|
|
||||||
</div>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
{t('free.description')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ul className="space-y-3 mb-8">
|
|
||||||
<li className="flex items-center">
|
|
||||||
<Check className="h-5 w-5 text-green-500 mr-3" />
|
|
||||||
<span className="text-card-foreground">
|
|
||||||
{SUBSCRIPTION_PLANS.free.promptLimit} Prompt Limit
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
<li className="flex items-center">
|
|
||||||
<Check className="h-5 w-5 text-green-500 mr-3" />
|
|
||||||
<span className="text-card-foreground">
|
|
||||||
{SUBSCRIPTION_PLANS.free.maxVersionLimit} Versions per Prompt
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
className="w-full"
|
|
||||||
variant={isCurrentPlan('free') ? 'outline' : 'default'}
|
|
||||||
onClick={handleGetStarted}
|
|
||||||
disabled={isCurrentPlan('free')}
|
|
||||||
>
|
|
||||||
{isCurrentPlan('free') ? t('currentPlan') : t('getStartedFree')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Pro Plan */}
|
return (
|
||||||
<div className="bg-primary p-8 rounded-lg shadow-sm text-primary-foreground relative">
|
<div
|
||||||
<div className="absolute top-4 right-4 bg-primary-foreground text-primary px-3 py-1 rounded-full text-xs font-semibold">
|
key={plan.id}
|
||||||
{t('popular')}
|
className={`bg-card p-8 rounded-lg shadow-sm border relative ${
|
||||||
</div>
|
isPro ? `${theme.borderColor} shadow-lg` : ''
|
||||||
|
}`}
|
||||||
{isCurrentPlan('pro') && (
|
>
|
||||||
<div className="absolute top-4 left-4 bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-300 px-3 py-1 rounded-full text-xs font-semibold">
|
{isCurrent && (
|
||||||
{t('currentPlan')}
|
<div className="absolute top-4 right-4 bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-300 px-3 py-1 rounded-full text-xs font-semibold">
|
||||||
</div>
|
{t('currentPlan')}
|
||||||
)}
|
</div>
|
||||||
|
)}
|
||||||
<div className="text-center mb-6">
|
|
||||||
<div className="flex items-center justify-center mb-4">
|
{isPro && (
|
||||||
<div className="p-3 rounded-full bg-gradient-to-br from-amber-500 to-orange-500 dark:from-amber-400 dark:to-orange-400">
|
<div className="absolute top-4 left-4 bg-primary text-primary-foreground px-3 py-1 rounded-full text-xs font-semibold">
|
||||||
<Crown className="w-6 h-6 text-white" />
|
{t('popular')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="text-center mb-6">
|
||||||
|
<div className="flex items-center justify-center mb-4">
|
||||||
|
<div className={`p-3 rounded-full ${theme.iconGradient}`}>
|
||||||
|
{isPro ? (
|
||||||
|
<Crown className="w-6 h-6 text-white" />
|
||||||
|
) : (
|
||||||
|
<Star className="w-6 h-6 text-white" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-2xl font-bold text-card-foreground mb-2">
|
||||||
|
{plan.displayName}
|
||||||
|
</h3>
|
||||||
|
<div className="text-4xl font-bold text-card-foreground mb-2">
|
||||||
|
{formatPlanPrice(plan, t)}
|
||||||
|
</div>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
{plan.description}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ul className="space-y-3 mb-8">
|
||||||
|
<li className="flex items-center">
|
||||||
|
<Check className="h-5 w-5 text-green-500 mr-3" />
|
||||||
|
<span className="text-card-foreground">
|
||||||
|
{limits.promptLimit} Prompt Limit
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center">
|
||||||
|
<Check className="h-5 w-5 text-green-500 mr-3" />
|
||||||
|
<span className="text-card-foreground">
|
||||||
|
{limits.maxVersionLimit} Versions per Prompt
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
{features.includes('prioritySupport') && (
|
||||||
|
<li className="flex items-center">
|
||||||
|
<Check className="h-5 w-5 text-green-500 mr-3" />
|
||||||
|
<span className="text-card-foreground">Priority Support</span>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{features.includes('advancedAnalytics') && (
|
||||||
|
<li className="flex items-center">
|
||||||
|
<Check className="h-5 w-5 text-green-500 mr-3" />
|
||||||
|
<span className="text-card-foreground">Advanced Analytics</span>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{features.includes('apiAccess') && (
|
||||||
|
<li className="flex items-center">
|
||||||
|
<Check className="h-5 w-5 text-green-500 mr-3" />
|
||||||
|
<span className="text-card-foreground">API Access</span>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{isFree ? (
|
||||||
|
<Button
|
||||||
|
className="w-full"
|
||||||
|
variant={isCurrent ? 'outline' : 'default'}
|
||||||
|
onClick={handleGetStarted}
|
||||||
|
disabled={isCurrent}
|
||||||
|
>
|
||||||
|
{isCurrent ? t('currentPlan') : t('getStartedFree')}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<SubscribeButton
|
||||||
|
priceId={plan.stripePriceId || ''}
|
||||||
|
planName={plan.displayName}
|
||||||
|
className={`w-full ${isPro ? 'bg-primary hover:bg-primary/90' : ''}`}
|
||||||
|
disabled={isCurrent}
|
||||||
|
>
|
||||||
|
{isCurrent ? t('currentPlan') : t('upgradePlan')}
|
||||||
|
</SubscribeButton>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-2xl font-bold mb-2">
|
)
|
||||||
{t('pro.title')}
|
})}
|
||||||
</h3>
|
|
||||||
<div className="text-4xl font-bold mb-2">
|
|
||||||
{t('pro.price')}
|
|
||||||
</div>
|
|
||||||
<p className="text-primary-foreground/80">
|
|
||||||
{t('perMonth')}
|
|
||||||
</p>
|
|
||||||
<p className="text-primary-foreground/80 mt-2">
|
|
||||||
{t('pro.description')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ul className="space-y-3 mb-8">
|
|
||||||
<li className="flex items-center">
|
|
||||||
<Check className="h-5 w-5 text-primary-foreground/80 mr-3" />
|
|
||||||
<span>
|
|
||||||
{SUBSCRIPTION_PLANS.pro.promptLimit} Prompt Limit
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
<li className="flex items-center">
|
|
||||||
<Check className="h-5 w-5 text-primary-foreground/80 mr-3" />
|
|
||||||
<span>
|
|
||||||
{SUBSCRIPTION_PLANS.pro.maxVersionLimit} Versions per Prompt
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
<li className="flex items-center">
|
|
||||||
<Check className="h-5 w-5 text-primary-foreground/80 mr-3" />
|
|
||||||
<span>Priority Support</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
{isCurrentPlan('pro') ? (
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="w-full bg-primary-foreground text-primary"
|
|
||||||
disabled
|
|
||||||
>
|
|
||||||
{t('currentPlan')}
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<SubscribeButton
|
|
||||||
priceId={process.env.NEXT_PUBLIC_STRIPE_PRO_PRICE_ID || ''}
|
|
||||||
planName="Pro"
|
|
||||||
variant="outline"
|
|
||||||
className="w-full bg-primary-foreground text-primary hover:bg-primary-foreground/90"
|
|
||||||
>
|
|
||||||
{t('upgradeToPro')}
|
|
||||||
</SubscribeButton>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Additional Info */}
|
{/* Additional Info */}
|
||||||
|
@ -11,7 +11,7 @@ import { LoadingSpinner } from '@/components/ui/loading-spinner'
|
|||||||
import { Crown, Star, CreditCard, AlertTriangle } from 'lucide-react'
|
import { Crown, Star, CreditCard, AlertTriangle } from 'lucide-react'
|
||||||
|
|
||||||
import { SubscriptionStatus } from '@/components/subscription/SubscriptionStatus'
|
import { SubscriptionStatus } from '@/components/subscription/SubscriptionStatus'
|
||||||
import { SubscribeButton } from '@/components/subscription/SubscribeButton'
|
import { QuickUpgradeButton } from '@/components/subscription/SubscribeButton'
|
||||||
|
|
||||||
interface SubscriptionData {
|
interface SubscriptionData {
|
||||||
plan: string
|
plan: string
|
||||||
@ -239,14 +239,10 @@ export default function SubscriptionPage() {
|
|||||||
<h3 className="font-semibold text-foreground mb-4">Quick Actions</h3>
|
<h3 className="font-semibold text-foreground mb-4">Quick Actions</h3>
|
||||||
<div className="flex flex-col sm:flex-row gap-3">
|
<div className="flex flex-col sm:flex-row gap-3">
|
||||||
{currentPlan === 'free' ? (
|
{currentPlan === 'free' ? (
|
||||||
<SubscribeButton
|
<QuickUpgradeButton className="flex items-center">
|
||||||
priceId={process.env.NEXT_PUBLIC_STRIPE_PRO_PRICE_ID || ''}
|
|
||||||
planName="Pro"
|
|
||||||
className="flex items-center"
|
|
||||||
>
|
|
||||||
<Crown className="w-4 h-4 mr-2" />
|
<Crown className="w-4 h-4 mr-2" />
|
||||||
{t('upgradePlan')}
|
{t('upgradePlan')}
|
||||||
</SubscribeButton>
|
</QuickUpgradeButton>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* 简单的账单信息显示 */}
|
{/* 简单的账单信息显示 */}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useAuth } from '@/hooks/useAuth'
|
import { useAuth } from '@/hooks/useAuth'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { LoadingSpinner } from '@/components/ui/loading-spinner'
|
import { LoadingSpinner } from '@/components/ui/loading-spinner'
|
||||||
@ -94,17 +94,63 @@ export function SubscribeButton({
|
|||||||
// 简化版本,用于快速升级
|
// 简化版本,用于快速升级
|
||||||
interface QuickUpgradeButtonProps {
|
interface QuickUpgradeButtonProps {
|
||||||
className?: string
|
className?: string
|
||||||
|
children?: React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
export function QuickUpgradeButton({ className }: QuickUpgradeButtonProps) {
|
export function QuickUpgradeButton({ className, children }: QuickUpgradeButtonProps) {
|
||||||
|
const [proPriceId, setProPriceId] = useState<string>('')
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [error, setError] = useState<string>('')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchProPriceId = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/subscription/pro-price-id')
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json()
|
||||||
|
if (data.priceId) {
|
||||||
|
setProPriceId(data.priceId)
|
||||||
|
} else {
|
||||||
|
setError('Pro plan not available')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setError('Failed to load Pro plan')
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError('Failed to load Pro plan')
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchProPriceId()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<Button variant="default" className={className} disabled>
|
||||||
|
<LoadingSpinner className="w-4 h-4 mr-2" />
|
||||||
|
Loading...
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error || !proPriceId) {
|
||||||
|
return (
|
||||||
|
<Button variant="outline" className={className} disabled>
|
||||||
|
Pro plan unavailable
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SubscribeButton
|
<SubscribeButton
|
||||||
priceId={process.env.NEXT_PUBLIC_STRIPE_PRO_PRICE_ID || ''}
|
priceId={proPriceId}
|
||||||
planName="Pro"
|
planName="Pro"
|
||||||
className={className}
|
className={className}
|
||||||
variant="default"
|
variant="default"
|
||||||
>
|
>
|
||||||
Upgrade to Pro
|
{children || 'Upgrade to Pro'}
|
||||||
</SubscribeButton>
|
</SubscribeButton>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -5,16 +5,30 @@ import { useUser } from '@/hooks/useUser'
|
|||||||
import { useTranslations } from 'next-intl'
|
import { useTranslations } from 'next-intl'
|
||||||
import { Crown, Star, AlertTriangle, CheckCircle } from 'lucide-react'
|
import { Crown, Star, AlertTriangle, CheckCircle } from 'lucide-react'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
import { getPlanTheme } from '@/lib/subscription-utils'
|
||||||
|
|
||||||
interface SubscriptionData {
|
interface SubscriptionData {
|
||||||
plan: string
|
subscription: {
|
||||||
status: string
|
isActive: boolean
|
||||||
subscriptions: Array<{
|
planId: string
|
||||||
|
stripeSubscriptionId?: string
|
||||||
|
currentPeriodStart?: Date
|
||||||
|
currentPeriodEnd?: Date
|
||||||
|
cancelAtPeriodEnd?: boolean
|
||||||
|
status?: string
|
||||||
|
}
|
||||||
|
permissions: {
|
||||||
|
maxVersionLimit: number
|
||||||
|
promptLimit: number
|
||||||
|
features: string[]
|
||||||
|
canCreatePrompt: boolean
|
||||||
|
canCreateVersion: boolean
|
||||||
|
}
|
||||||
|
plan?: {
|
||||||
id: string
|
id: string
|
||||||
status: string
|
displayName: string
|
||||||
currentPeriodEnd: number
|
price: number
|
||||||
cancelAtPeriodEnd: boolean
|
}
|
||||||
}>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SubscriptionStatusProps {
|
interface SubscriptionStatusProps {
|
||||||
@ -58,22 +72,18 @@ export function SubscriptionStatus({ className, showDetails = true }: Subscripti
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentPlan = userData?.subscribePlan || 'free'
|
const planData = subscriptionData?.plan
|
||||||
const isPro = currentPlan === 'pro'
|
const isPro = planData ? planData.price > 19 : false
|
||||||
const activeSubscription = subscriptionData?.subscriptions?.find(sub => sub.status === 'active')
|
const theme = getPlanTheme(planData)
|
||||||
|
const subscription = subscriptionData?.subscription
|
||||||
|
const permissions = subscriptionData?.permissions
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("bg-card rounded-lg border border-border overflow-hidden", className)}>
|
<div className={cn("bg-card rounded-lg border border-border overflow-hidden", className)}>
|
||||||
<div className={`p-4 border-b border-border ${isPro
|
<div className={`p-4 border-b border-border ${theme.gradient}`}>
|
||||||
? 'bg-gradient-to-r from-amber-50/60 to-orange-50/60 dark:from-amber-950/10 dark:to-orange-950/10'
|
|
||||||
: 'bg-gradient-to-r from-slate-50/60 to-gray-50/60 dark:from-slate-950/5 dark:to-gray-950/5'
|
|
||||||
}`}>
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<div className={`p-2 rounded-full ${isPro
|
<div className={`p-2 rounded-full ${theme.iconGradient}`}>
|
||||||
? 'bg-gradient-to-br from-amber-500 to-orange-500 dark:from-amber-400 dark:to-orange-400'
|
|
||||||
: 'bg-gradient-to-br from-slate-400 to-gray-500 dark:from-slate-500 dark:to-gray-400'
|
|
||||||
}`}>
|
|
||||||
{isPro ? (
|
{isPro ? (
|
||||||
<Crown className="w-5 h-5 text-white" />
|
<Crown className="w-5 h-5 text-white" />
|
||||||
) : (
|
) : (
|
||||||
@ -84,10 +94,7 @@ export function SubscriptionStatus({ className, showDetails = true }: Subscripti
|
|||||||
<h3 className="font-semibold text-foreground">
|
<h3 className="font-semibold text-foreground">
|
||||||
{t('currentPlan')}
|
{t('currentPlan')}
|
||||||
</h3>
|
</h3>
|
||||||
<p className={`text-sm font-medium ${isPro
|
<p className={`text-sm font-medium ${theme.textColor}`}>
|
||||||
? 'text-orange-700 dark:text-orange-300'
|
|
||||||
: 'text-slate-600 dark:text-slate-400'
|
|
||||||
}`}>
|
|
||||||
{isPro ? t('proPlan') : t('freePlan')}
|
{isPro ? t('proPlan') : t('freePlan')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -95,7 +102,7 @@ export function SubscriptionStatus({ className, showDetails = true }: Subscripti
|
|||||||
|
|
||||||
{/* Status indicator */}
|
{/* Status indicator */}
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
{activeSubscription?.cancelAtPeriodEnd ? (
|
{subscription?.cancelAtPeriodEnd ? (
|
||||||
<div className="flex items-center text-orange-600 dark:text-orange-400">
|
<div className="flex items-center text-orange-600 dark:text-orange-400">
|
||||||
<AlertTriangle className="w-4 h-4 mr-1" />
|
<AlertTriangle className="w-4 h-4 mr-1" />
|
||||||
<span className="text-xs font-medium">Canceling</span>
|
<span className="text-xs font-medium">Canceling</span>
|
||||||
@ -116,26 +123,26 @@ export function SubscriptionStatus({ className, showDetails = true }: Subscripti
|
|||||||
<div>
|
<div>
|
||||||
<span className="text-muted-foreground">Prompts:</span>
|
<span className="text-muted-foreground">Prompts:</span>
|
||||||
<span className="ml-2 font-medium">
|
<span className="ml-2 font-medium">
|
||||||
{userData?.promptLimit || (isPro ? '5000' : '500')}
|
{permissions?.promptLimit || (isPro ? '5000' : '500')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-muted-foreground">Versions:</span>
|
<span className="text-muted-foreground">Versions:</span>
|
||||||
<span className="ml-2 font-medium">
|
<span className="ml-2 font-medium">
|
||||||
{userData?.maxVersionLimit || (isPro ? '10' : '3')} per prompt
|
{permissions?.maxVersionLimit || (isPro ? '10' : '3')} per prompt
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{activeSubscription && (
|
{subscription?.currentPeriodEnd && (
|
||||||
<div className="mt-4 pt-4 border-t border-border">
|
<div className="mt-4 pt-4 border-t border-border">
|
||||||
<div className="text-sm">
|
<div className="text-sm">
|
||||||
<span className="text-muted-foreground">{t('nextBilling')}:</span>
|
<span className="text-muted-foreground">{t('nextBilling')}:</span>
|
||||||
<span className="ml-2 font-medium">
|
<span className="ml-2 font-medium">
|
||||||
{new Date(activeSubscription.currentPeriodEnd * 1000).toLocaleDateString()}
|
{new Date(subscription.currentPeriodEnd).toLocaleDateString()}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{activeSubscription.cancelAtPeriodEnd && (
|
{subscription.cancelAtPeriodEnd && (
|
||||||
<div className="mt-2 text-xs text-orange-600 dark:text-orange-400">
|
<div className="mt-2 text-xs text-orange-600 dark:text-orange-400">
|
||||||
Subscription will end on the next billing date
|
Subscription will end on the next billing date
|
||||||
</div>
|
</div>
|
||||||
|
@ -12,9 +12,10 @@ interface UserData {
|
|||||||
language: string
|
language: string
|
||||||
isAdmin: boolean
|
isAdmin: boolean
|
||||||
versionLimit: number
|
versionLimit: number
|
||||||
|
subscriptionPlanId: string
|
||||||
subscribePlan: string
|
subscribePlan: string
|
||||||
maxVersionLimit: number
|
maxVersionLimit: number
|
||||||
promptLimit: number
|
promptLimit?: number // 已弃用,忽略此字段
|
||||||
creditBalance: number
|
creditBalance: number
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
|
@ -213,19 +213,21 @@ export async function consumeCredit(userId: string, amount: number): Promise<boo
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取订阅计划的限制
|
* 获取订阅计划的限制
|
||||||
|
* @deprecated 请使用 SubscriptionService.getUserPermissions() 替代
|
||||||
*/
|
*/
|
||||||
export function getSubscriptionLimits(plan: string) {
|
export function getSubscriptionLimits(plan: string) {
|
||||||
|
// 保留向后兼容性,但建议使用新的订阅服务
|
||||||
switch (plan) {
|
switch (plan) {
|
||||||
case 'pro':
|
case 'pro':
|
||||||
return {
|
return {
|
||||||
promptLimit: 500,
|
promptLimit: 5000, // 修正为正确的 Pro 限制
|
||||||
maxVersionLimit: 10,
|
maxVersionLimit: 10,
|
||||||
monthlyCredit: 20.0
|
monthlyCredit: 20.0
|
||||||
}
|
}
|
||||||
case 'free':
|
case 'free':
|
||||||
default:
|
default:
|
||||||
return {
|
return {
|
||||||
promptLimit: 20,
|
promptLimit: 500, // 修正为正确的免费限制
|
||||||
maxVersionLimit: 3,
|
maxVersionLimit: 3,
|
||||||
monthlyCredit: 0.0 // 免费版没有月度额度
|
monthlyCredit: 0.0 // 免费版没有月度额度
|
||||||
}
|
}
|
||||||
|
@ -15,17 +15,8 @@ export const getStripe = () => {
|
|||||||
return stripePromise
|
return stripePromise
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stripe 产品和价格配置
|
// Stripe 配置
|
||||||
export const STRIPE_CONFIG = {
|
export const STRIPE_CONFIG = {
|
||||||
products: {
|
|
||||||
pro: {
|
|
||||||
priceId: process.env.STRIPE_PRO_PRICE_ID || 'price_dummy', // 需要在 Stripe Dashboard 中创建
|
|
||||||
name: 'Pro Plan',
|
|
||||||
amount: 1990, // $19.90 in cents
|
|
||||||
currency: 'usd',
|
|
||||||
interval: 'month' as const,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET || 'whsec_dummy',
|
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET || 'whsec_dummy',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
280
src/lib/subscription-service.ts
Normal file
280
src/lib/subscription-service.ts
Normal file
@ -0,0 +1,280 @@
|
|||||||
|
import { prisma } from '@/lib/prisma'
|
||||||
|
import { getCustomerSubscriptions } from '@/lib/stripe'
|
||||||
|
import type { SubscriptionPlan } from '@prisma/client'
|
||||||
|
import { isPlanPro } from '@/lib/subscription-utils'
|
||||||
|
|
||||||
|
// 订阅状态类型
|
||||||
|
export interface SubscriptionStatus {
|
||||||
|
isActive: boolean
|
||||||
|
planId: string
|
||||||
|
stripeSubscriptionId?: string
|
||||||
|
currentPeriodStart?: Date
|
||||||
|
currentPeriodEnd?: Date
|
||||||
|
cancelAtPeriodEnd?: boolean
|
||||||
|
status?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户权益类型
|
||||||
|
export interface UserPermissions {
|
||||||
|
maxVersionLimit: number
|
||||||
|
promptLimit: number
|
||||||
|
features: string[]
|
||||||
|
canCreatePrompt: boolean
|
||||||
|
canCreateVersion: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
// 套餐配置类型
|
||||||
|
export interface PlanLimits {
|
||||||
|
maxVersionLimit: number
|
||||||
|
promptLimit: number
|
||||||
|
creditMonthly: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlanFeatures {
|
||||||
|
[key: string]: boolean | string | number
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SubscriptionService {
|
||||||
|
/**
|
||||||
|
* 判断套餐是否为 Pro 级别(价格超过 19)
|
||||||
|
*/
|
||||||
|
static isPlanPro(plan: SubscriptionPlan | null): boolean {
|
||||||
|
return isPlanPro(plan)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据套餐 ID 判断是否为 Pro 级别
|
||||||
|
*/
|
||||||
|
static async isPlanIdPro(planId: string): Promise<boolean> {
|
||||||
|
const plan = await this.getPlanById(planId)
|
||||||
|
return this.isPlanPro(plan)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断用户是否为 Pro 用户
|
||||||
|
*/
|
||||||
|
static async isUserPro(userId: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const subscriptionStatus = await this.getUserSubscriptionStatus(userId)
|
||||||
|
return await this.isPlanIdPro(subscriptionStatus.planId)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking if user is pro:', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取所有可用的订阅套餐
|
||||||
|
*/
|
||||||
|
static async getAvailablePlans(): Promise<SubscriptionPlan[]> {
|
||||||
|
return await prisma.subscriptionPlan.findMany({
|
||||||
|
where: { isActive: true },
|
||||||
|
orderBy: { sortOrder: 'asc' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ID获取套餐
|
||||||
|
*/
|
||||||
|
static async getPlanById(planId: string): Promise<SubscriptionPlan | null> {
|
||||||
|
return await prisma.subscriptionPlan.findUnique({
|
||||||
|
where: { id: planId }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 Pro 套餐(名称为 "pro")
|
||||||
|
*/
|
||||||
|
static async getProPlan(): Promise<SubscriptionPlan | null> {
|
||||||
|
return await prisma.subscriptionPlan.findFirst({
|
||||||
|
where: {
|
||||||
|
name: 'pro',
|
||||||
|
isActive: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 Pro 套餐的 Stripe 价格 ID
|
||||||
|
*/
|
||||||
|
static async getProPriceId(): Promise<string | null> {
|
||||||
|
const proPlan = await this.getProPlan()
|
||||||
|
return proPlan?.stripePriceId || null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户的当前订阅状态(从 Stripe 实时获取)
|
||||||
|
*/
|
||||||
|
static async getUserSubscriptionStatus(userId: string): Promise<SubscriptionStatus> {
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { id: userId },
|
||||||
|
select: {
|
||||||
|
stripeCustomerId: true,
|
||||||
|
subscriptionPlanId: true,
|
||||||
|
subscriptionPlan: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new Error('User not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果 plan 没有 stripePriceId,则直接返回当前套餐有效
|
||||||
|
// 无需从 Stripe 实时获取
|
||||||
|
if (!user.subscriptionPlan?.stripePriceId) {
|
||||||
|
return {
|
||||||
|
isActive: true,
|
||||||
|
planId: user.subscriptionPlanId || 'free'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有匹配到上述规则,则需要从 Stripe 获取实时状态
|
||||||
|
// 如果此时没有 Stripe 客户 ID,则返免费套餐状态
|
||||||
|
if (!user.stripeCustomerId) {
|
||||||
|
return {
|
||||||
|
isActive: true,
|
||||||
|
planId: 'free'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 从 Stripe 获取实时订阅状态
|
||||||
|
const subscriptions = await getCustomerSubscriptions(user.stripeCustomerId)
|
||||||
|
const activeSubscription = subscriptions.find(sub =>
|
||||||
|
sub.status === 'active' || sub.status === 'trialing'
|
||||||
|
)
|
||||||
|
|
||||||
|
if (activeSubscription) {
|
||||||
|
// 根据 Stripe 价格 ID 确定套餐
|
||||||
|
const items = activeSubscription.items?.data || []
|
||||||
|
const priceId = items[0]?.price?.id
|
||||||
|
|
||||||
|
// 查找对应的套餐
|
||||||
|
const plan = await prisma.subscriptionPlan.findFirst({
|
||||||
|
where: { stripePriceId: priceId }
|
||||||
|
})
|
||||||
|
|
||||||
|
const subData = activeSubscription as unknown as Record<string, unknown>
|
||||||
|
|
||||||
|
return {
|
||||||
|
isActive: true,
|
||||||
|
planId: plan?.id || 'free',
|
||||||
|
stripeSubscriptionId: activeSubscription.id,
|
||||||
|
currentPeriodStart: new Date((subData.current_period_start as number) * 1000),
|
||||||
|
currentPeriodEnd: new Date((subData.current_period_end as number) * 1000),
|
||||||
|
cancelAtPeriodEnd: subData.cancel_at_period_end as boolean,
|
||||||
|
status: activeSubscription.status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 没有活跃订阅,返回免费套餐
|
||||||
|
return {
|
||||||
|
isActive: true,
|
||||||
|
planId: 'free'
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching subscription status from Stripe:', error)
|
||||||
|
// Stripe 错误时,返回用户当前套餐状态
|
||||||
|
return {
|
||||||
|
isActive: true,
|
||||||
|
planId: user.subscriptionPlanId || 'free'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户的权益和限制
|
||||||
|
*/
|
||||||
|
static async getUserPermissions(userId: string): Promise<UserPermissions> {
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { id: userId },
|
||||||
|
include: { subscriptionPlan: true }
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new Error('User not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取实时订阅状态
|
||||||
|
const subscriptionStatus = await this.getUserSubscriptionStatus(userId)
|
||||||
|
|
||||||
|
// 获取对应的套餐配置
|
||||||
|
const plan = await this.getPlanById(subscriptionStatus.planId)
|
||||||
|
if (!plan) {
|
||||||
|
throw new Error('Subscription plan not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
const limits = plan.limits as unknown as PlanLimits
|
||||||
|
const features = plan.features as unknown as PlanFeatures
|
||||||
|
|
||||||
|
// 用户自定义的版本限制不能超过套餐限制
|
||||||
|
const maxVersionLimit = Math.min(user.versionLimit, limits.maxVersionLimit)
|
||||||
|
|
||||||
|
// 检查当前使用情况(完全基于套餐限制,忽略用户表中的 promptLimit)
|
||||||
|
const currentPromptCount = await prisma.prompt.count({
|
||||||
|
where: { userId }
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
maxVersionLimit,
|
||||||
|
promptLimit: limits.promptLimit, // 完全从套餐配置获取
|
||||||
|
features: Object.keys(features).filter(key => features[key] === true),
|
||||||
|
canCreatePrompt: currentPromptCount < limits.promptLimit, // 使用套餐限制
|
||||||
|
canCreateVersion: true // 这个需要在具体创建时检查
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查用户是否可以创建新的提示词
|
||||||
|
*/
|
||||||
|
static async canCreatePrompt(userId: string): Promise<boolean> {
|
||||||
|
const permissions = await this.getUserPermissions(userId)
|
||||||
|
return permissions.canCreatePrompt
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查用户是否可以创建新版本
|
||||||
|
*/
|
||||||
|
static async canCreateVersion(userId: string, promptId: string): Promise<boolean> {
|
||||||
|
const permissions = await this.getUserPermissions(userId)
|
||||||
|
|
||||||
|
// 获取当前版本数量
|
||||||
|
const currentVersionCount = await prisma.promptVersion.count({
|
||||||
|
where: { promptId }
|
||||||
|
})
|
||||||
|
|
||||||
|
return currentVersionCount < permissions.maxVersionLimit
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算需要删除的版本数量(当超出限制时)
|
||||||
|
*/
|
||||||
|
static async getVersionsToDelete(userId: string, promptId: string): Promise<number> {
|
||||||
|
const permissions = await this.getUserPermissions(userId)
|
||||||
|
|
||||||
|
const currentVersionCount = await prisma.promptVersion.count({
|
||||||
|
where: { promptId }
|
||||||
|
})
|
||||||
|
|
||||||
|
return Math.max(0, currentVersionCount - permissions.maxVersionLimit + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新用户的订阅套餐(通常在 webhook 中调用)
|
||||||
|
*/
|
||||||
|
static async updateUserSubscriptionPlan(userId: string, planId: string): Promise<void> {
|
||||||
|
await prisma.user.update({
|
||||||
|
where: { id: userId },
|
||||||
|
data: { subscriptionPlanId: planId }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 Stripe 价格 ID 获取套餐 ID
|
||||||
|
*/
|
||||||
|
static async getPlanIdByStripePriceId(stripePriceId: string): Promise<string> {
|
||||||
|
const plan = await prisma.subscriptionPlan.findFirst({
|
||||||
|
where: { stripePriceId }
|
||||||
|
})
|
||||||
|
return plan?.id || 'free'
|
||||||
|
}
|
||||||
|
}
|
144
src/lib/subscription-utils.ts
Normal file
144
src/lib/subscription-utils.ts
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
import type { SubscriptionPlan } from '@prisma/client'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订阅相关的工具函数
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 支持部分套餐数据的类型
|
||||||
|
type PlanLike = { price: number } | SubscriptionPlan | null | undefined
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断套餐是否为 Pro 级别(价格超过 19)
|
||||||
|
* 这是一个纯函数,可以在客户端和服务端使用
|
||||||
|
*/
|
||||||
|
export function isPlanPro(plan: PlanLike): boolean {
|
||||||
|
if (!plan) return false
|
||||||
|
return plan.price > 19
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断套餐是否为免费套餐(价格为 0)
|
||||||
|
*/
|
||||||
|
export function isPlanFree(plan: PlanLike): boolean {
|
||||||
|
if (!plan) return true
|
||||||
|
return plan.price === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取套餐的显示级别
|
||||||
|
*/
|
||||||
|
export function getPlanTier(plan: PlanLike): 'free' | 'pro' | 'enterprise' {
|
||||||
|
if (!plan || plan.price === 0) return 'free'
|
||||||
|
if (plan.price > 50) return 'enterprise' // 为未来的企业版预留
|
||||||
|
if (plan.price > 19) return 'pro'
|
||||||
|
return 'free'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取套餐的图标组件名称
|
||||||
|
*/
|
||||||
|
export function getPlanIcon(plan: PlanLike): 'Star' | 'Crown' | 'Building' {
|
||||||
|
const tier = getPlanTier(plan)
|
||||||
|
switch (tier) {
|
||||||
|
case 'enterprise':
|
||||||
|
return 'Building'
|
||||||
|
case 'pro':
|
||||||
|
return 'Crown'
|
||||||
|
case 'free':
|
||||||
|
default:
|
||||||
|
return 'Star'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取套餐的颜色主题
|
||||||
|
*/
|
||||||
|
export function getPlanTheme(plan: PlanLike): {
|
||||||
|
gradient: string
|
||||||
|
iconGradient: string
|
||||||
|
textColor: string
|
||||||
|
borderColor?: string
|
||||||
|
} {
|
||||||
|
const tier = getPlanTier(plan)
|
||||||
|
|
||||||
|
switch (tier) {
|
||||||
|
case 'enterprise':
|
||||||
|
return {
|
||||||
|
gradient: 'bg-gradient-to-r from-purple-50/60 to-indigo-50/60 dark:from-purple-950/10 dark:to-indigo-950/10',
|
||||||
|
iconGradient: 'bg-gradient-to-br from-purple-500 to-indigo-500 dark:from-purple-400 dark:to-indigo-400',
|
||||||
|
textColor: 'text-purple-700 dark:text-purple-300',
|
||||||
|
borderColor: 'border-purple-200 dark:border-purple-800'
|
||||||
|
}
|
||||||
|
case 'pro':
|
||||||
|
return {
|
||||||
|
gradient: 'bg-gradient-to-r from-amber-50/60 to-orange-50/60 dark:from-amber-950/10 dark:to-orange-950/10',
|
||||||
|
iconGradient: 'bg-gradient-to-br from-amber-500 to-orange-500 dark:from-amber-400 dark:to-orange-400',
|
||||||
|
textColor: 'text-orange-700 dark:text-orange-300',
|
||||||
|
borderColor: 'border-primary'
|
||||||
|
}
|
||||||
|
case 'free':
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
gradient: 'bg-gradient-to-r from-slate-50/60 to-gray-50/60 dark:from-slate-950/5 dark:to-gray-950/5',
|
||||||
|
iconGradient: 'bg-gradient-to-br from-slate-400 to-gray-500 dark:from-slate-500 dark:to-gray-400',
|
||||||
|
textColor: 'text-slate-600 dark:text-slate-400'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化套餐价格显示
|
||||||
|
*/
|
||||||
|
export function formatPlanPrice(plan: PlanLike & { interval?: string }, t?: (key: string) => string): string {
|
||||||
|
if (!plan || plan.price === 0) {
|
||||||
|
return t?.('free.price') || 'Free'
|
||||||
|
}
|
||||||
|
|
||||||
|
const price = `$${plan.price}`
|
||||||
|
const planWithInterval = plan as { interval?: string }
|
||||||
|
const interval = planWithInterval.interval === 'year' ? '/year' : '/month'
|
||||||
|
|
||||||
|
return `${price}${interval}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取套餐限制的类型安全访问器
|
||||||
|
*/
|
||||||
|
export function getPlanLimits(plan: SubscriptionPlan | null | undefined): {
|
||||||
|
promptLimit: number
|
||||||
|
maxVersionLimit: number
|
||||||
|
creditMonthly: number
|
||||||
|
} {
|
||||||
|
if (!plan) {
|
||||||
|
return {
|
||||||
|
promptLimit: 500,
|
||||||
|
maxVersionLimit: 3,
|
||||||
|
creditMonthly: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const limits = plan.limits as Record<string, unknown>
|
||||||
|
return {
|
||||||
|
promptLimit: (limits?.promptLimit as number) || 500,
|
||||||
|
maxVersionLimit: (limits?.maxVersionLimit as number) || 3,
|
||||||
|
creditMonthly: (limits?.creditMonthly as number) || 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取套餐功能的类型安全访问器
|
||||||
|
*/
|
||||||
|
export function getPlanFeatures(plan: SubscriptionPlan | null | undefined): string[] {
|
||||||
|
if (!plan) return []
|
||||||
|
|
||||||
|
const features = plan.features as Record<string, unknown>
|
||||||
|
return Object.keys(features).filter(key => features[key] === true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查套餐是否包含特定功能
|
||||||
|
*/
|
||||||
|
export function planHasFeature(plan: SubscriptionPlan | null | undefined, feature: string): boolean {
|
||||||
|
const features = getPlanFeatures(plan)
|
||||||
|
return features.includes(feature)
|
||||||
|
}
|
@ -1,31 +1,37 @@
|
|||||||
// 订阅计划配置
|
// 兼容性:重新导出新的订阅服务
|
||||||
|
export { SubscriptionService } from './subscription-service'
|
||||||
|
|
||||||
|
// 订阅计划配置(已弃用,保留用于向后兼容)
|
||||||
|
// @deprecated 请使用 SubscriptionService.getAvailablePlans() 替代
|
||||||
export const SUBSCRIPTION_PLANS = {
|
export const SUBSCRIPTION_PLANS = {
|
||||||
free: {
|
free: {
|
||||||
name: 'Free',
|
name: 'Free',
|
||||||
maxVersionLimit: 3,
|
maxVersionLimit: 3,
|
||||||
promptLimit: 500, // 放宽限制到500
|
promptLimit: 500,
|
||||||
price: 0,
|
price: 0,
|
||||||
features: [
|
features: [
|
||||||
'promptLimit', // 500 Prompt Limit
|
'promptLimit',
|
||||||
'versionsPerPrompt' // 3 Versions per Prompt
|
'versionsPerPrompt'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
pro: {
|
pro: {
|
||||||
name: 'Pro',
|
name: 'Pro',
|
||||||
maxVersionLimit: 10,
|
maxVersionLimit: 10,
|
||||||
promptLimit: 5000, // Pro版本5000个提示词限制
|
promptLimit: 5000,
|
||||||
price: 19.9,
|
price: 19.9,
|
||||||
features: [
|
features: [
|
||||||
'promptLimit', // 5000 Prompt Limit
|
'promptLimit',
|
||||||
'versionsPerPrompt', // 10 Versions per Prompt
|
'versionsPerPrompt',
|
||||||
'prioritySupport' // Priority Support
|
'prioritySupport'
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type SubscriptionPlan = keyof typeof SUBSCRIPTION_PLANS
|
export type SubscriptionPlan = keyof typeof SUBSCRIPTION_PLANS
|
||||||
|
|
||||||
// 获取用户的版本限制
|
// 兼容性函数(已弃用,保留用于向后兼容)
|
||||||
|
|
||||||
|
// @deprecated 请使用 SubscriptionService.getUserPermissions() 替代
|
||||||
export function getUserVersionLimit(user: {
|
export function getUserVersionLimit(user: {
|
||||||
versionLimit: number
|
versionLimit: number
|
||||||
subscribePlan: string
|
subscribePlan: string
|
||||||
@ -33,25 +39,23 @@ export function getUserVersionLimit(user: {
|
|||||||
}): number {
|
}): number {
|
||||||
const plan = user.subscribePlan as SubscriptionPlan
|
const plan = user.subscribePlan as SubscriptionPlan
|
||||||
const planConfig = SUBSCRIPTION_PLANS[plan]
|
const planConfig = SUBSCRIPTION_PLANS[plan]
|
||||||
|
|
||||||
if (!planConfig) {
|
if (!planConfig) {
|
||||||
// 如果订阅计划无效,使用免费计划的限制
|
|
||||||
return Math.min(user.versionLimit, SUBSCRIPTION_PLANS.free.maxVersionLimit)
|
return Math.min(user.versionLimit, SUBSCRIPTION_PLANS.free.maxVersionLimit)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 用户设置的限制不能超过订阅计划的最大限制
|
|
||||||
return Math.min(user.versionLimit, planConfig.maxVersionLimit)
|
return Math.min(user.versionLimit, planConfig.maxVersionLimit)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取用户的最大版本限制(基于订阅)
|
// @deprecated 请使用 SubscriptionService.getPlanById() 替代
|
||||||
export function getMaxVersionLimit(subscribePlan: string): number {
|
export function getMaxVersionLimit(subscribePlan: string): number {
|
||||||
const plan = subscribePlan as SubscriptionPlan
|
const plan = subscribePlan as SubscriptionPlan
|
||||||
const planConfig = SUBSCRIPTION_PLANS[plan]
|
const planConfig = SUBSCRIPTION_PLANS[plan]
|
||||||
|
|
||||||
return planConfig?.maxVersionLimit || SUBSCRIPTION_PLANS.free.maxVersionLimit
|
return planConfig?.maxVersionLimit || SUBSCRIPTION_PLANS.free.maxVersionLimit
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查用户是否可以创建新版本
|
// @deprecated 请使用 SubscriptionService.canCreateVersion() 替代
|
||||||
export function canCreateNewVersion(
|
export function canCreateNewVersion(
|
||||||
currentVersionCount: number,
|
currentVersionCount: number,
|
||||||
user: {
|
user: {
|
||||||
@ -64,7 +68,7 @@ export function canCreateNewVersion(
|
|||||||
return currentVersionCount < versionLimit
|
return currentVersionCount < versionLimit
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算需要删除的版本数量
|
// @deprecated 请使用 SubscriptionService.getVersionsToDelete() 替代
|
||||||
export function getVersionsToDelete(
|
export function getVersionsToDelete(
|
||||||
currentVersionCount: number,
|
currentVersionCount: number,
|
||||||
user: {
|
user: {
|
||||||
@ -74,32 +78,19 @@ export function getVersionsToDelete(
|
|||||||
}
|
}
|
||||||
): number {
|
): number {
|
||||||
const versionLimit = getUserVersionLimit(user)
|
const versionLimit = getUserVersionLimit(user)
|
||||||
return Math.max(0, currentVersionCount - versionLimit + 1) // +1 因为要创建新版本
|
return Math.max(0, currentVersionCount - versionLimit + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取用户的提示词限制
|
// @deprecated 请使用 SubscriptionService.getUserPermissions() 替代
|
||||||
export function getPromptLimit(subscribePlan: string): number {
|
export function getPromptLimit(subscribePlan: string): number {
|
||||||
const plan = subscribePlan as SubscriptionPlan
|
const plan = subscribePlan as SubscriptionPlan
|
||||||
const planConfig = SUBSCRIPTION_PLANS[plan]
|
const planConfig = SUBSCRIPTION_PLANS[plan]
|
||||||
|
|
||||||
return planConfig?.promptLimit || SUBSCRIPTION_PLANS.free.promptLimit
|
return planConfig?.promptLimit || SUBSCRIPTION_PLANS.free.promptLimit
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查用户是否可以创建新提示词
|
// @deprecated 请使用 SubscriptionService.canCreatePrompt() 替代
|
||||||
export async function canCreateNewPrompt(userId: string): Promise<boolean> {
|
export async function canCreateNewPrompt(userId: string): Promise<boolean> {
|
||||||
const { prisma } = await import('@/lib/prisma')
|
const { SubscriptionService } = await import('./subscription-service')
|
||||||
|
return await SubscriptionService.canCreatePrompt(userId)
|
||||||
const user = await prisma.user.findUnique({
|
|
||||||
where: { id: userId },
|
|
||||||
select: { subscribePlan: true }
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!user) return false
|
|
||||||
|
|
||||||
const promptLimit = getPromptLimit(user.subscribePlan)
|
|
||||||
const currentPromptCount = await prisma.prompt.count({
|
|
||||||
where: { userId }
|
|
||||||
})
|
|
||||||
|
|
||||||
return currentPromptCount < promptLimit
|
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user