From 4a8974e387d5cdcc03f319d80cff38cedb51e6fa Mon Sep 17 00:00:00 2001
From: shihao <3127647737@qq.com>
Date: Mon, 27 Apr 2026 18:04:00 +0800
Subject: [PATCH] Add admin settings UI and provider deletes
---
backend/app/modules/providers/router.py | 17 +
backend/app/modules/providers/service.py | 73 ++-
.../admin/(secure)/provider-accounts/page.tsx | 38 +-
.../admin/(secure)/provider-models/page.tsx | 37 +-
.../app/admin/(secure)/system-config/page.tsx | 473 ++++++++++++++++--
frontend-web/src/app/globals.css | 130 ++++-
6 files changed, 714 insertions(+), 54 deletions(-)
diff --git a/backend/app/modules/providers/router.py b/backend/app/modules/providers/router.py
index 8cc3733..bf18988 100644
--- a/backend/app/modules/providers/router.py
+++ b/backend/app/modules/providers/router.py
@@ -38,6 +38,15 @@ def update_provider_account(
return success_response(ProvidersService(db).update_account(account_id, payload))
+@router.delete("/provider-accounts/{account_id}")
+def delete_provider_account(
+ account_id: int,
+ _=Depends(require_admin_permission()),
+ db: Session = Depends(get_db),
+):
+ return success_response(ProvidersService(db).delete_account(account_id))
+
+
@router.get("/provider-models")
def list_provider_models(
_=Depends(require_admin_permission()),
@@ -64,3 +73,11 @@ def update_provider_model(
):
return success_response(ProvidersService(db).update_model(model_id, payload))
+
+@router.delete("/provider-models/{model_id}")
+def delete_provider_model(
+ model_id: int,
+ _=Depends(require_admin_permission()),
+ db: Session = Depends(get_db),
+):
+ return success_response(ProvidersService(db).delete_model(model_id))
diff --git a/backend/app/modules/providers/service.py b/backend/app/modules/providers/service.py
index 35fc156..a06a8d2 100644
--- a/backend/app/modules/providers/service.py
+++ b/backend/app/modules/providers/service.py
@@ -1,7 +1,13 @@
+from sqlalchemy import delete, func, or_, select
from sqlalchemy.orm import Session
-from app.common.errors.app_error import NotFoundAppError
-from app.models.entities import ProviderAccount, ProviderModel
+from app.common.errors.app_error import BusinessAppError, NotFoundAppError
+from app.models.entities import (
+ ProviderAccount,
+ ProviderModel,
+ VideoGenerationTask,
+ VideoModelSupplierBinding,
+)
from app.modules.providers.repository import ProvidersRepository
@@ -50,6 +56,43 @@ class ProvidersService:
self.db.commit()
return self.serialize_account(item)
+ def delete_account(self, account_id: int) -> dict:
+ item = self.repository.get_account(account_id)
+ if not item:
+ raise NotFoundAppError("provider account not found", code=60001)
+
+ model_ids = list(
+ self.db.scalars(
+ select(ProviderModel.id).where(ProviderModel.provider_account_id == account_id)
+ )
+ )
+ task_conditions = [VideoGenerationTask.provider_account_id == account_id]
+ if model_ids:
+ task_conditions.append(VideoGenerationTask.provider_model_id.in_(model_ids))
+ task_count = self.db.scalar(
+ select(func.count())
+ .select_from(VideoGenerationTask)
+ .where(or_(*task_conditions))
+ )
+ if task_count:
+ raise BusinessAppError(
+ "该供应商已有任务记录,不能直接删除,请先停用。",
+ code=60011,
+ )
+
+ if model_ids:
+ self.db.execute(
+ delete(VideoModelSupplierBinding).where(
+ VideoModelSupplierBinding.provider_model_id.in_(model_ids)
+ )
+ )
+ self.db.execute(
+ delete(ProviderModel).where(ProviderModel.provider_account_id == account_id)
+ )
+ self.db.delete(item)
+ self.db.commit()
+ return {"id": account_id, "deleted": True}
+
def list_models(self) -> list[dict]:
accounts = {item.id: item for item in self.repository.list_accounts().all()}
return [self.serialize_model(item, accounts) for item in self.repository.list_models().all()]
@@ -72,6 +115,31 @@ class ProvidersService:
account = self.repository.get_account(item.provider_account_id)
return self.serialize_model(item, {account.id: account} if account else {})
+ def delete_model(self, model_id: int) -> dict:
+ item = self.repository.get_model(model_id)
+ if not item:
+ raise NotFoundAppError("provider model not found", code=60002)
+
+ task_count = self.db.scalar(
+ select(func.count())
+ .select_from(VideoGenerationTask)
+ .where(VideoGenerationTask.provider_model_id == model_id)
+ )
+ if task_count:
+ raise BusinessAppError(
+ "该模型已有任务记录,不能直接删除,请先停用。",
+ code=60012,
+ )
+
+ self.db.execute(
+ delete(VideoModelSupplierBinding).where(
+ VideoModelSupplierBinding.provider_model_id == model_id
+ )
+ )
+ self.db.delete(item)
+ self.db.commit()
+ return {"id": model_id, "deleted": True}
+
@staticmethod
def serialize_account(item: ProviderAccount) -> dict:
return {
@@ -109,4 +177,3 @@ class ProvidersService:
"defaultResolution": item.default_resolution,
"status": item.status,
}
-
diff --git a/frontend-web/src/app/admin/(secure)/provider-accounts/page.tsx b/frontend-web/src/app/admin/(secure)/provider-accounts/page.tsx
index 15460b3..fe964a8 100644
--- a/frontend-web/src/app/admin/(secure)/provider-accounts/page.tsx
+++ b/frontend-web/src/app/admin/(secure)/provider-accounts/page.tsx
@@ -1,10 +1,10 @@
"use client";
import { useMutation, useQuery } from "@tanstack/react-query";
-import { CheckCircle2, PlugZap } from "lucide-react";
+import { CheckCircle2, PlugZap, Trash2 } from "lucide-react";
import { useState } from "react";
-import { api } from "@/lib/api";
+import { api, ApiError } from "@/lib/api";
type ProviderAccountRow = {
id: number;
@@ -74,6 +74,18 @@ export default function ProviderAccountsPage() {
},
});
+ const deleteMutation = useMutation({
+ mutationFn: (accountId: number) =>
+ api.del(`/api/v1/admin/provider-accounts/${accountId}`),
+ async onSuccess() {
+ setFeedback("渠道已删除。");
+ await query.refetch();
+ },
+ onError(error) {
+ setFeedback(error instanceof ApiError ? error.message : "删除失败,请稍后重试。");
+ },
+ });
+
return (
@@ -228,9 +240,25 @@ export default function ProviderAccountsPage() {
{item.providerName}
{item.providerCode}
-
- {item.status === 1 ? "启用" : "停用"}
-
+
+
+ {item.status === 1 ? "启用" : "停用"}
+
+
+
{protocolOptions.find((option) => option.value === item.apiFormat)?.label ?? item.apiFormat}
diff --git a/frontend-web/src/app/admin/(secure)/provider-models/page.tsx b/frontend-web/src/app/admin/(secure)/provider-models/page.tsx
index 08115df..3dedf12 100644
--- a/frontend-web/src/app/admin/(secure)/provider-models/page.tsx
+++ b/frontend-web/src/app/admin/(secure)/provider-models/page.tsx
@@ -1,10 +1,10 @@
"use client";
import { useMutation, useQuery } from "@tanstack/react-query";
-import { Boxes, Plus } from "lucide-react";
+import { Boxes, Plus, Trash2 } from "lucide-react";
import { useMemo, useState } from "react";
-import { api } from "@/lib/api";
+import { api, ApiError } from "@/lib/api";
type ProviderAccountRow = {
id: number;
@@ -100,6 +100,17 @@ export default function ProviderModelsPage() {
},
});
+ const deleteMutation = useMutation({
+ mutationFn: (modelId: number) => api.del(`/api/v1/admin/provider-models/${modelId}`),
+ async onSuccess() {
+ setFeedback("模型已删除。");
+ await modelsQuery.refetch();
+ },
+ onError(error) {
+ setFeedback(error instanceof ApiError ? error.message : "删除失败,请稍后重试。");
+ },
+ });
+
return (
@@ -279,9 +290,25 @@ export default function ProviderModelsPage() {
{item.modelName || item.modelCode}
{item.modelCode}
-
- {item.status === 1 ? "启用" : "停用"}
-
+
+
+ {item.status === 1 ? "启用" : "停用"}
+
+
+
{item.providerName}
diff --git a/frontend-web/src/app/admin/(secure)/system-config/page.tsx b/frontend-web/src/app/admin/(secure)/system-config/page.tsx
index 52bd865..af16a94 100644
--- a/frontend-web/src/app/admin/(secure)/system-config/page.tsx
+++ b/frontend-web/src/app/admin/(secure)/system-config/page.tsx
@@ -1,73 +1,466 @@
"use client";
import { useMutation, useQuery } from "@tanstack/react-query";
-import { useState } from "react";
+import { Save, Settings2, SlidersHorizontal } from "lucide-react";
+import { useMemo, useState } from "react";
-import { api } from "@/lib/api";
+import { api, ApiError } from "@/lib/api";
type ConfigRow = {
configKey: string;
configValue: string;
+ valueType: string;
groupName: string;
+ description: string;
+ isPublic: boolean;
};
-const fieldLabels: Record
= {
- config_key: "配置键",
- config_value: "配置值",
- value_type: "值类型",
- group_name: "配置分组",
- description: "配置说明",
- is_public: "是否公开",
+type EditableConfig = {
+ key: string;
+ label: string;
+ helper: string;
+ group: string;
+ valueType: "string" | "number" | "boolean";
+ control: "text" | "textarea" | "number" | "switch";
+ defaultValue: string;
+ isPublic: boolean;
};
+const groupMeta = [
+ { key: "site", title: "站点设置", subtitle: "前台展示名称和公告文案。" },
+ { key: "reward", title: "奖励设置", subtitle: "注册奖励、邀请奖励的开关和积分。" },
+ { key: "invite", title: "邀请设置", subtitle: "控制邀请码体系是否开放。" },
+ { key: "task", title: "任务设置", subtitle: "视频任务调度和轮询默认值。" },
+];
+
+const editableConfigs: EditableConfig[] = [
+ {
+ key: "site.title",
+ label: "站点名称",
+ helper: "展示在前台导航、标题和登录页。",
+ group: "site",
+ valueType: "string",
+ control: "text",
+ defaultValue: "AIVideo",
+ isPublic: true,
+ },
+ {
+ key: "site.notice",
+ label: "站点公告",
+ helper: "给用户展示的运营提示,可以留空。",
+ group: "site",
+ valueType: "string",
+ control: "textarea",
+ defaultValue: "",
+ isPublic: true,
+ },
+ {
+ key: "reward.signup.enabled",
+ label: "注册奖励",
+ helper: "开启后,新用户注册自动发放积分。",
+ group: "reward",
+ valueType: "boolean",
+ control: "switch",
+ defaultValue: "1",
+ isPublic: false,
+ },
+ {
+ key: "reward.signup.points",
+ label: "注册奖励积分",
+ helper: "新用户注册时发放的积分数量。",
+ group: "reward",
+ valueType: "number",
+ control: "number",
+ defaultValue: "300",
+ isPublic: false,
+ },
+ {
+ key: "reward.invite.enabled",
+ label: "邀请奖励",
+ helper: "开启后,邀请关系成立时发放奖励。",
+ group: "reward",
+ valueType: "boolean",
+ control: "switch",
+ defaultValue: "1",
+ isPublic: false,
+ },
+ {
+ key: "reward.invite.points",
+ label: "邀请奖励积分",
+ helper: "邀请成功后给邀请人的积分数量。",
+ group: "reward",
+ valueType: "number",
+ control: "number",
+ defaultValue: "500",
+ isPublic: false,
+ },
+ {
+ key: "invite.code.enabled",
+ label: "邀请码系统",
+ helper: "关闭后不再展示邀请入口和邀请码能力。",
+ group: "invite",
+ valueType: "boolean",
+ control: "switch",
+ defaultValue: "1",
+ isPublic: false,
+ },
+ {
+ key: "task.default_poll_interval_seconds",
+ label: "任务轮询间隔",
+ helper: "后台轮询供应商任务状态的默认秒数。",
+ group: "task",
+ valueType: "number",
+ control: "number",
+ defaultValue: "5",
+ isPublic: false,
+ },
+];
+
+const editableKeys = new Set(editableConfigs.map((item) => item.key));
+
+function isEnabled(value: string) {
+ return value === "1" || value === "true";
+}
+
export default function SystemConfigPage() {
- const [form, setForm] = useState({
- config_key: "site.notice",
- config_value: "当前为本地联调环境。",
- value_type: "string",
- group_name: "site",
- description: "公告",
- is_public: true,
+ const [draftValues, setDraftValues] = useState>({});
+ const [customForm, setCustomForm] = useState({
+ configKey: "",
+ configValue: "",
+ valueType: "string",
+ groupName: "custom",
+ description: "",
+ isPublic: false,
});
+ const [feedback, setFeedback] = useState("");
+
const query = useQuery({
queryKey: ["system-config"],
queryFn: () => api.get("/api/v1/admin/system-config"),
});
- const mutation = useMutation({
- mutationFn: () => api.put("/api/v1/admin/system-config", form),
- onSuccess: () => query.refetch(),
+
+ const configByKey = useMemo(
+ () => new Map((query.data ?? []).map((item) => [item.configKey, item])),
+ [query.data],
+ );
+
+ const groupedConfigs = useMemo(
+ () =>
+ groupMeta.map((group) => ({
+ ...group,
+ configs: editableConfigs.filter((config) => config.group === group.key),
+ })),
+ [],
+ );
+
+ const otherConfigs = useMemo(
+ () => (query.data ?? []).filter((item) => !editableKeys.has(item.configKey)),
+ [query.data],
+ );
+
+ const saveMutation = useMutation({
+ mutationFn: () =>
+ Promise.all(
+ editableConfigs.map((config) =>
+ api.put("/api/v1/admin/system-config", {
+ config_key: config.key,
+ config_value:
+ draftValues[config.key] ??
+ configByKey.get(config.key)?.configValue ??
+ config.defaultValue,
+ value_type: config.valueType,
+ group_name: config.group,
+ description: config.label,
+ is_public: config.isPublic,
+ }),
+ ),
+ ),
+ async onSuccess() {
+ setFeedback("系统设置已保存。");
+ setDraftValues({});
+ await query.refetch();
+ },
+ onError(error) {
+ setFeedback(error instanceof ApiError ? error.message : "保存失败,请稍后重试。");
+ },
});
+ const customMutation = useMutation({
+ mutationFn: () =>
+ api.put("/api/v1/admin/system-config", {
+ config_key: customForm.configKey.trim(),
+ config_value: customForm.configValue,
+ value_type: customForm.valueType,
+ group_name: customForm.groupName.trim() || "custom",
+ description: customForm.description.trim(),
+ is_public: customForm.isPublic,
+ }),
+ async onSuccess() {
+ setFeedback("高级配置已保存。");
+ setCustomForm((previous) => ({
+ ...previous,
+ configKey: "",
+ configValue: "",
+ description: "",
+ }));
+ await query.refetch();
+ },
+ onError(error) {
+ setFeedback(error instanceof ApiError ? error.message : "保存失败,请稍后重试。");
+ },
+ });
+
+ function getConfigValue(config: EditableConfig) {
+ return (
+ draftValues[config.key] ??
+ configByKey.get(config.key)?.configValue ??
+ config.defaultValue
+ );
+ }
+
+ function renderControl(config: EditableConfig) {
+ const value = getConfigValue(config);
+ if (config.control === "switch") {
+ const enabled = isEnabled(value);
+ return (
+
+ );
+ }
+
+ if (config.control === "textarea") {
+ return (
+