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