From 6721a1cc6e74a40a0c1f623e6ca66f61abb4495f Mon Sep 17 00:00:00 2001 From: iven Date: Tue, 14 Apr 2026 19:06:58 +0800 Subject: [PATCH] =?UTF-8?q?fix(admin):=20=E8=A1=8C=E4=B8=9A=E9=80=89?= =?UTF-8?q?=E6=8B=A9500=E4=BF=AE=E5=A4=8D=20+=20=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=91=98=E5=88=87=E6=8D=A2=E8=AE=A2=E9=98=85=E8=AE=A1=E5=88=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fix(industry): list_industries SQL参数编号错位 — count查询和items查询 共用WHERE子句但参数从$3开始,sqlx bind按$1/$2顺序绑定导致500 - feat(billing): 新增 PUT /admin/accounts/:id/subscription 端点 (super_admin) 验证目标计划 → 取消当前订阅 → 创建新订阅(30天) → 同步配额 - feat(admin-v2): Accounts.tsx 编辑弹窗新增「订阅计划」选择区 显示所有活跃计划,保存时调用admin switch plan API --- admin-v2/src/pages/Accounts.tsx | 44 +++++++++++- admin-v2/src/services/billing.ts | 5 ++ crates/zclaw-saas/src/billing/handlers.rs | 36 ++++++++++ crates/zclaw-saas/src/billing/mod.rs | 8 ++- crates/zclaw-saas/src/billing/service.rs | 87 +++++++++++++++++++++++ crates/zclaw-saas/src/billing/types.rs | 6 ++ crates/zclaw-saas/src/industry/service.rs | 44 +++++++++--- crates/zclaw-saas/src/main.rs | 1 + 8 files changed, 218 insertions(+), 13 deletions(-) diff --git a/admin-v2/src/pages/Accounts.tsx b/admin-v2/src/pages/Accounts.tsx index 480ed96..9ce0081 100644 --- a/admin-v2/src/pages/Accounts.tsx +++ b/admin-v2/src/pages/Accounts.tsx @@ -9,6 +9,7 @@ import type { ProColumns } from '@ant-design/pro-components' import { ProTable } from '@ant-design/pro-components' import { accountService } from '@/services/accounts' import { industryService } from '@/services/industries' +import { billingService } from '@/services/billing' import { PageHeader } from '@/components/PageHeader' import type { AccountPublic } from '@/types' @@ -70,6 +71,12 @@ export default function Accounts() { } }, [accountIndustries, editingId, form]) + // 获取所有活跃计划(用于管理员切换) + const { data: plansData } = useQuery({ + queryKey: ['billing-plans'], + queryFn: ({ signal }) => billingService.listPlans(signal), + }) + const updateMutation = useMutation({ mutationFn: ({ id, data }: { id: string; data: Partial }) => accountService.update(id, data), @@ -101,6 +108,14 @@ export default function Accounts() { onError: (err: Error) => message.error(err.message || '行业授权更新失败'), }) + // 管理员切换用户计划 + const switchPlanMutation = useMutation({ + mutationFn: ({ accountId, planId }: { accountId: string; planId: string }) => + billingService.adminSwitchPlan(accountId, planId), + onSuccess: () => message.success('计划切换成功'), + onError: (err: Error) => message.error(err.message || '计划切换失败'), + }) + const columns: ProColumns[] = [ { title: '用户名', dataIndex: 'username', width: 120, tooltip: '搜索用户名、邮箱或显示名' }, { title: '显示名', dataIndex: 'display_name', width: 120, hideInSearch: true }, @@ -186,7 +201,7 @@ export default function Accounts() { try { // 更新基础信息 - const { industry_ids, ...accountData } = values + const { industry_ids, plan_id, ...accountData } = values await updateMutation.mutateAsync({ id: editingId, data: accountData }) // 更新行业授权(如果变更了) @@ -201,6 +216,11 @@ export default function Accounts() { queryClient.invalidateQueries({ queryKey: ['account-industries'] }) } + // 切换订阅计划(如果选择了新计划) + if (plan_id) { + await switchPlanMutation.mutateAsync({ accountId: editingId, planId: plan_id }) + } + handleClose() } catch { // Errors handled by mutation onError callbacks @@ -218,6 +238,11 @@ export default function Accounts() { label: `${item.icon} ${item.name}`, })) + const planOptions = (plansData || []).map((plan) => ({ + value: plan.id, + label: `${plan.display_name} (¥${(plan.price_cents / 100).toFixed(0)}/月)`, + })) + return (
@@ -256,7 +281,7 @@ export default function Accounts() { open={modalOpen} onOk={handleSave} onCancel={handleClose} - confirmLoading={updateMutation.isPending || setIndustriesMutation.isPending} + confirmLoading={updateMutation.isPending || setIndustriesMutation.isPending || switchPlanMutation.isPending} width={560} >
@@ -280,6 +305,21 @@ export default function Accounts() { ]} /> + 订阅计划 + + +