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() {