From bd1db1efd861b36e1b6cbf4696e144e761c44989 Mon Sep 17 00:00:00 2001 From: erio Date: Sat, 14 Mar 2026 05:06:36 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A5=97=E9=A4=90=E5=88=86=E7=BB=84?= =?UTF-8?q?=E6=B8=85=E7=90=86=20+=20=E7=BB=AD=E8=B4=B9=E5=BB=B6=E6=9C=9F?= =?UTF-8?q?=20+=20UI=E7=BB=9F=E4=B8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Schema: groupId 改为 nullable,新增迁移 - GET 套餐列表自动检测并清除 Sub2API 中已删除的分组绑定 - PUT 保存时校验分组存在性,已删除则自动解绑并返回 409 - 续费逻辑:同分组有活跃订阅时从到期日计算天数再 createAndRedeem - 提取 PlanInfoDisplay 共享组件,SubscriptionConfirm 复用 - 默认模型统一到 /v1/messages badge 内 - 前端编辑表单适配 nullable groupId,未绑定时禁用保存 --- .../migration.sql | 2 + prisma/schema.prisma | 2 +- src/app/admin/subscriptions/page.tsx | 46 ++++-- .../admin/subscription-plans/[id]/route.ts | 25 ++- src/app/api/admin/subscription-plans/route.ts | 30 +++- src/app/api/subscription-plans/route.ts | 2 + src/components/SubscriptionConfirm.tsx | 147 +----------------- src/components/SubscriptionPlanCard.tsx | 43 +++-- src/lib/order/service.ts | 50 +++++- 9 files changed, 163 insertions(+), 184 deletions(-) create mode 100644 prisma/migrations/20260314200000_nullable_group_id/migration.sql diff --git a/prisma/migrations/20260314200000_nullable_group_id/migration.sql b/prisma/migrations/20260314200000_nullable_group_id/migration.sql new file mode 100644 index 0000000..52c21f7 --- /dev/null +++ b/prisma/migrations/20260314200000_nullable_group_id/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable: make group_id nullable on subscription_plans +ALTER TABLE "subscription_plans" ALTER COLUMN "group_id" DROP NOT NULL; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index a9248b2..8f44a33 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -108,7 +108,7 @@ model Channel { // ── 订阅套餐配置 ── model SubscriptionPlan { id String @id @default(cuid()) - groupId Int @unique @map("group_id") + groupId Int? @unique @map("group_id") name String description String? @db.Text price Decimal @db.Decimal(10, 2) diff --git a/src/app/admin/subscriptions/page.tsx b/src/app/admin/subscriptions/page.tsx index 72fadff..a8f0602 100644 --- a/src/app/admin/subscriptions/page.tsx +++ b/src/app/admin/subscriptions/page.tsx @@ -17,7 +17,7 @@ interface SubscriptionPlan { validDays: number; validityUnit: 'day' | 'week' | 'month'; features: string[]; - groupId: string; + groupId: string | null; groupName: string | null; sortOrder: number; enabled: boolean; @@ -427,7 +427,7 @@ function SubscriptionsContent() { const openEdit = (plan: SubscriptionPlan) => { setEditingPlan(plan); - setFormGroupId(plan.groupId); + setFormGroupId(plan.groupId ?? ''); setFormName(plan.name); setFormDescription(plan.description ?? ''); setFormPrice(String(plan.price)); @@ -448,11 +448,11 @@ function SubscriptionsContent() { /* --- save plan (snake_case for backend) --- */ const handleSave = async () => { - if (!formName.trim() || !formPrice) return; + if (!formName.trim() || !formPrice || !formGroupId) return; setSaving(true); setError(''); const body = { - group_id: formGroupId ? Number(formGroupId) : undefined, + group_id: Number(formGroupId), name: formName.trim(), description: formDescription.trim() || null, price: parseFloat(formPrice), @@ -485,7 +485,9 @@ function SubscriptionsContent() { closeModal(); fetchPlans(); } catch (e) { + // 分组被删除等错误:刷新列表使前端状态同步 setError(e instanceof Error ? e.message : t.saveFailed); + fetchPlans(); } finally { setSaving(false); } @@ -617,7 +619,9 @@ function SubscriptionsContent() { /* available groups for the form: only subscription type, exclude already used */ const subscriptionGroups = groups.filter((g) => g.subscription_type === 'subscription'); - const usedGroupIds = new Set(plans.filter((p) => p.id !== editingPlan?.id).map((p) => p.groupId)); + const usedGroupIds = new Set( + plans.filter((p) => p.id !== editingPlan?.id && p.groupId != null).map((p) => p.groupId!), + ); const availableGroups = subscriptionGroups.filter((g) => !usedGroupIds.has(String(g.id))); /* group id → name map (all groups, for subscription display) */ @@ -850,10 +854,18 @@ function SubscriptionsContent() { {t.colGroup}
- {plan.groupId} - {plan.groupName && ( - - ({plan.groupName}) + {plan.groupId ? ( + <> + {plan.groupId} + {plan.groupName && ( + + ({plan.groupName}) + + )} + + ) : ( + + {locale === 'en' ? 'Unbound' : '未绑定'} )}
@@ -1221,12 +1233,14 @@ function SubscriptionsContent() { {g.name} ({g.id}) ))} - {/* If editing, ensure the current group is always visible */} - {editingPlan && !availableGroups.some((g) => String(g.id) === editingPlan.groupId) && ( - - )} + {/* If editing, ensure the current group is always visible (only if still bound) */} + {editingPlan && + editingPlan.groupId && + !availableGroups.some((g) => String(g.id) === editingPlan.groupId) && ( + + )} @@ -1462,7 +1476,7 @@ function SubscriptionsContent() {