feat(admin-v2): add billing management page
- Plan cards with feature comparison and pricing - Usage progress bars with quota visualization - Alipay/WeChat Pay method selection modal - Payment status polling with auto-refresh on success - Navigation + route registration Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,7 @@ import {
|
|||||||
MoonOutlined,
|
MoonOutlined,
|
||||||
ApiOutlined,
|
ApiOutlined,
|
||||||
BookOutlined,
|
BookOutlined,
|
||||||
|
CrownOutlined,
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import { Avatar, Dropdown, Tooltip, Drawer } from 'antd'
|
import { Avatar, Dropdown, Tooltip, Drawer } from 'antd'
|
||||||
import { useAuthStore } from '@/stores/authStore'
|
import { useAuthStore } from '@/stores/authStore'
|
||||||
@@ -44,6 +45,7 @@ const navItems: NavItem[] = [
|
|||||||
{ path: '/usage', name: '用量统计', icon: <BarChartOutlined />, permission: 'admin:full', group: '运维' },
|
{ path: '/usage', name: '用量统计', icon: <BarChartOutlined />, permission: 'admin:full', group: '运维' },
|
||||||
{ path: '/relay', name: '中转任务', icon: <SwapOutlined />, permission: 'relay:use', group: '运维' },
|
{ path: '/relay', name: '中转任务', icon: <SwapOutlined />, permission: 'relay:use', group: '运维' },
|
||||||
{ path: '/knowledge', name: '知识库', icon: <BookOutlined />, permission: 'knowledge:read', group: '资源管理' },
|
{ path: '/knowledge', name: '知识库', icon: <BookOutlined />, permission: 'knowledge:read', group: '资源管理' },
|
||||||
|
{ path: '/billing', name: '计费管理', icon: <CrownOutlined />, permission: 'billing:read', group: '核心' },
|
||||||
{ path: '/logs', name: '操作日志', icon: <FileTextOutlined />, permission: 'admin:full', group: '运维' },
|
{ path: '/logs', name: '操作日志', icon: <FileTextOutlined />, permission: 'admin:full', group: '运维' },
|
||||||
{ path: '/config', name: '系统配置', icon: <SettingOutlined />, permission: 'config:read', group: '系统' },
|
{ path: '/config', name: '系统配置', icon: <SettingOutlined />, permission: 'config:read', group: '系统' },
|
||||||
{ path: '/prompts', name: '提示词管理', icon: <MessageOutlined />, permission: 'prompt:read', group: '系统' },
|
{ path: '/prompts', name: '提示词管理', icon: <MessageOutlined />, permission: 'prompt:read', group: '系统' },
|
||||||
@@ -207,6 +209,7 @@ const breadcrumbMap: Record<string, string> = {
|
|||||||
'/usage': '用量统计',
|
'/usage': '用量统计',
|
||||||
'/relay': '中转任务',
|
'/relay': '中转任务',
|
||||||
'/knowledge': '知识库',
|
'/knowledge': '知识库',
|
||||||
|
'/billing': '计费管理',
|
||||||
'/config': '系统配置',
|
'/config': '系统配置',
|
||||||
'/prompts': '提示词管理',
|
'/prompts': '提示词管理',
|
||||||
'/logs': '操作日志',
|
'/logs': '操作日志',
|
||||||
|
|||||||
352
admin-v2/src/pages/Billing.tsx
Normal file
352
admin-v2/src/pages/Billing.tsx
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
// ============================================================
|
||||||
|
// 计费管理 — 计划/订阅/用量/支付
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
|
import {
|
||||||
|
Button, message, Tag, Modal, Card, Row, Col, Statistic, Typography,
|
||||||
|
Progress, Space, Radio, Spin, Empty, Divider,
|
||||||
|
} from 'antd'
|
||||||
|
import {
|
||||||
|
CrownOutlined, CheckCircleOutlined, ThunderboltOutlined,
|
||||||
|
RocketOutlined, TeamOutlined, AlipayCircleOutlined,
|
||||||
|
WechatOutlined, LoadingOutlined,
|
||||||
|
} from '@ant-design/icons'
|
||||||
|
import { PageHeader } from '@/components/PageHeader'
|
||||||
|
import { ErrorState } from '@/components/ErrorState'
|
||||||
|
import { billingService } from '@/services/billing'
|
||||||
|
import type { BillingPlan, SubscriptionInfo, PaymentResult } from '@/services/billing'
|
||||||
|
|
||||||
|
const { Text, Title } = Typography
|
||||||
|
|
||||||
|
// === 计划卡片 ===
|
||||||
|
|
||||||
|
const planIcons: Record<string, React.ReactNode> = {
|
||||||
|
free: <RocketOutlined style={{ fontSize: 24 }} />,
|
||||||
|
pro: <ThunderboltOutlined style={{ fontSize: 24 }} />,
|
||||||
|
team: <TeamOutlined style={{ fontSize: 24 }} />,
|
||||||
|
}
|
||||||
|
|
||||||
|
const planColors: Record<string, string> = {
|
||||||
|
free: '#8c8c8c',
|
||||||
|
pro: '#863bff',
|
||||||
|
team: '#47bfff',
|
||||||
|
}
|
||||||
|
|
||||||
|
function PlanCard({
|
||||||
|
plan,
|
||||||
|
isCurrent,
|
||||||
|
onSelect,
|
||||||
|
}: {
|
||||||
|
plan: BillingPlan
|
||||||
|
isCurrent: boolean
|
||||||
|
onSelect: (plan: BillingPlan) => void
|
||||||
|
}) {
|
||||||
|
const color = planColors[plan.name] || '#666'
|
||||||
|
const limits = plan.limits as Record<string, unknown> | undefined
|
||||||
|
const maxRelay = (limits?.max_relay_requests_monthly as number) ?? '∞'
|
||||||
|
const maxHand = (limits?.max_hand_executions_monthly as number) ?? '∞'
|
||||||
|
const maxPipeline = (limits?.max_pipeline_runs_monthly as number) ?? '∞'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
className={`relative overflow-hidden transition-all duration-200 hover:shadow-lg ${
|
||||||
|
isCurrent ? 'ring-2 ring-offset-2' : ''
|
||||||
|
}`}
|
||||||
|
style={isCurrent ? { borderColor: color, '--tw-ring-color': color } as React.CSSProperties : {}}
|
||||||
|
>
|
||||||
|
{isCurrent && (
|
||||||
|
<div
|
||||||
|
className="absolute top-0 right-0 px-3 py-1 text-xs font-medium text-white rounded-bl-lg"
|
||||||
|
style={{ background: color }}
|
||||||
|
>
|
||||||
|
当前计划
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="text-center mb-4">
|
||||||
|
<div style={{ color }} className="mb-2">
|
||||||
|
{planIcons[plan.name] || <CrownOutlined style={{ fontSize: 24 }} />}
|
||||||
|
</div>
|
||||||
|
<Title level={4} style={{ margin: 0 }}>{plan.display_name}</Title>
|
||||||
|
{plan.description && (
|
||||||
|
<Text type="secondary" className="text-sm">{plan.description}</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center mb-4">
|
||||||
|
<span className="text-3xl font-bold" style={{ color }}>
|
||||||
|
¥{plan.price_cents === 0 ? '0' : (plan.price_cents / 100).toFixed(0)}
|
||||||
|
</span>
|
||||||
|
<Text type="secondary"> /{plan.interval === 'month' ? '月' : '年'}</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2 text-sm">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<CheckCircleOutlined style={{ color: '#52c41a' }} />
|
||||||
|
<span>中转请求: {maxRelay === Infinity ? '无限' : `${maxRelay} 次/月`}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<CheckCircleOutlined style={{ color: '#52c41a' }} />
|
||||||
|
<span>Hand 执行: {maxHand === Infinity ? '无限' : `${maxHand} 次/月`}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<CheckCircleOutlined style={{ color: '#52c41a' }} />
|
||||||
|
<span>Pipeline 运行: {maxPipeline === Infinity ? '无限' : `${maxPipeline} 次/月`}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<CheckCircleOutlined style={{ color: '#52c41a' }} />
|
||||||
|
<span>知识库: {plan.name === 'free' ? '基础' : '高级'}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<CheckCircleOutlined style={{ color: '#52c41a' }} />
|
||||||
|
<span>优先级队列: {plan.name === 'team' ? '最高' : plan.name === 'pro' ? '高' : '标准'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
block
|
||||||
|
type={isCurrent ? 'default' : 'primary'}
|
||||||
|
disabled={isCurrent}
|
||||||
|
onClick={() => onSelect(plan)}
|
||||||
|
style={!isCurrent ? { background: color, borderColor: color } : {}}
|
||||||
|
>
|
||||||
|
{isCurrent ? '当前计划' : '升级'}
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// === 用量进度条 ===
|
||||||
|
|
||||||
|
function UsageBar({ label, current, max }: { label: string; current: number; max: number | null }) {
|
||||||
|
const pct = max ? Math.min((current / max) * 100, 100) : 0
|
||||||
|
const displayMax = max ? max.toLocaleString() : '∞'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-3">
|
||||||
|
<div className="flex justify-between text-xs text-neutral-500 dark:text-neutral-400 mb-1">
|
||||||
|
<span>{label}</span>
|
||||||
|
<span>{current.toLocaleString()} / {displayMax}</span>
|
||||||
|
</div>
|
||||||
|
<Progress
|
||||||
|
percent={pct}
|
||||||
|
showInfo={false}
|
||||||
|
strokeColor={pct >= 90 ? '#ff4d4f' : pct >= 70 ? '#faad14' : '#863bff'}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// === 主页面 ===
|
||||||
|
|
||||||
|
export default function Billing() {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const [payModalOpen, setPayModalOpen] = useState(false)
|
||||||
|
const [selectedPlan, setSelectedPlan] = useState<BillingPlan | null>(null)
|
||||||
|
const [payMethod, setPayMethod] = useState<'alipay' | 'wechat'>('alipay')
|
||||||
|
const [payResult, setPayResult] = useState<PaymentResult | null>(null)
|
||||||
|
const [pollingPayment, setPollingPayment] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const { data: plans = [], isLoading: plansLoading, error: plansError, refetch } = useQuery({
|
||||||
|
queryKey: ['billing-plans'],
|
||||||
|
queryFn: ({ signal }) => billingService.listPlans(signal),
|
||||||
|
})
|
||||||
|
|
||||||
|
const { data: subInfo, isLoading: subLoading } = useQuery({
|
||||||
|
queryKey: ['billing-subscription'],
|
||||||
|
queryFn: ({ signal }) => billingService.getSubscription(signal),
|
||||||
|
})
|
||||||
|
|
||||||
|
// 支付状态轮询
|
||||||
|
const { data: paymentStatus } = useQuery({
|
||||||
|
queryKey: ['payment-status', pollingPayment],
|
||||||
|
queryFn: ({ signal }) => billingService.getPaymentStatus(pollingPayment!, signal),
|
||||||
|
enabled: !!pollingPayment,
|
||||||
|
refetchInterval: pollingPayment ? 3000 : false,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 支付成功后刷新
|
||||||
|
if (paymentStatus?.status === 'succeeded' && pollingPayment) {
|
||||||
|
setPollingPayment(null)
|
||||||
|
setPayModalOpen(false)
|
||||||
|
setPayResult(null)
|
||||||
|
message.success('支付成功!计划已更新')
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['billing-subscription'] })
|
||||||
|
}
|
||||||
|
|
||||||
|
const createPaymentMutation = useMutation({
|
||||||
|
mutationFn: (data: { plan_id: string; payment_method: 'alipay' | 'wechat' }) =>
|
||||||
|
billingService.createPayment(data),
|
||||||
|
onSuccess: (result) => {
|
||||||
|
setPayResult(result)
|
||||||
|
setPollingPayment(result.payment_id)
|
||||||
|
// 打开支付链接
|
||||||
|
window.open(result.pay_url, '_blank', 'width=480,height=640')
|
||||||
|
},
|
||||||
|
onError: (err: Error) => message.error(err.message || '创建支付失败'),
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleSelectPlan = (plan: BillingPlan) => {
|
||||||
|
if (plan.price_cents === 0) return
|
||||||
|
setSelectedPlan(plan)
|
||||||
|
setPayResult(null)
|
||||||
|
setPayModalOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleConfirmPay = () => {
|
||||||
|
if (!selectedPlan) return
|
||||||
|
createPaymentMutation.mutate({
|
||||||
|
plan_id: selectedPlan.id,
|
||||||
|
payment_method: payMethod,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plansError) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHeader title="计费管理" description="管理订阅计划和用量配额" />
|
||||||
|
<ErrorState message={(plansError as Error).message} onRetry={() => refetch()} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentPlanName = subInfo?.plan?.name || 'free'
|
||||||
|
const usage = subInfo?.usage
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<PageHeader title="计费管理" description="管理订阅计划和用量配额" />
|
||||||
|
|
||||||
|
{/* 当前计划 + 用量 */}
|
||||||
|
{subInfo && usage && (
|
||||||
|
<Card className="mb-6" title={<span className="text-sm font-semibold">当前用量</span>}>
|
||||||
|
<Row gutter={[24, 16]}>
|
||||||
|
<Col xs={24} md={8}>
|
||||||
|
<UsageBar
|
||||||
|
label="中转请求"
|
||||||
|
current={usage.relay_requests}
|
||||||
|
max={usage.max_relay_requests}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} md={8}>
|
||||||
|
<UsageBar
|
||||||
|
label="Hand 执行"
|
||||||
|
current={usage.hand_executions}
|
||||||
|
max={usage.max_hand_executions}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} md={8}>
|
||||||
|
<UsageBar
|
||||||
|
label="Pipeline 运行"
|
||||||
|
current={usage.pipeline_runs}
|
||||||
|
max={usage.max_pipeline_runs}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
{subInfo.subscription && (
|
||||||
|
<div className="mt-4 text-xs text-neutral-400">
|
||||||
|
订阅周期: {new Date(subInfo.subscription.current_period_start).toLocaleDateString()} — {new Date(subInfo.subscription.current_period_end).toLocaleDateString()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 计划选择 */}
|
||||||
|
<Title level={5} className="mb-4">选择计划</Title>
|
||||||
|
|
||||||
|
{plansLoading ? (
|
||||||
|
<div className="flex justify-center py-8"><Spin /></div>
|
||||||
|
) : (
|
||||||
|
<Row gutter={[16, 16]}>
|
||||||
|
{plans.map((plan) => (
|
||||||
|
<Col key={plan.id} xs={24} sm={12} lg={8}>
|
||||||
|
<PlanCard
|
||||||
|
plan={plan}
|
||||||
|
isCurrent={plan.name === currentPlanName}
|
||||||
|
onSelect={handleSelectPlan}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 支付弹窗 */}
|
||||||
|
<Modal
|
||||||
|
title={selectedPlan ? `升级到 ${selectedPlan.display_name}` : '支付'}
|
||||||
|
open={payModalOpen}
|
||||||
|
onCancel={() => {
|
||||||
|
setPayModalOpen(false)
|
||||||
|
setPollingPayment(null)
|
||||||
|
setPayResult(null)
|
||||||
|
}}
|
||||||
|
footer={payResult ? null : undefined}
|
||||||
|
onOk={handleConfirmPay}
|
||||||
|
okText={createPaymentMutation.isPending ? '处理中...' : '确认支付'}
|
||||||
|
confirmLoading={createPaymentMutation.isPending}
|
||||||
|
>
|
||||||
|
{payResult ? (
|
||||||
|
<div className="text-center py-4">
|
||||||
|
<LoadingOutlined style={{ fontSize: 32, color: '#863bff' }} className="mb-4" />
|
||||||
|
<Title level={5}>等待支付确认...</Title>
|
||||||
|
<Text type="secondary">
|
||||||
|
支付窗口已打开,请在新窗口完成支付。
|
||||||
|
<br />
|
||||||
|
支付金额: ¥{(payResult.amount_cents / 100).toFixed(2)}
|
||||||
|
</Text>
|
||||||
|
<div className="mt-4">
|
||||||
|
<Button onClick={() => { setPollingPayment(null); setPayModalOpen(false); setPayResult(null) }}>
|
||||||
|
关闭
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
{selectedPlan && (
|
||||||
|
<div className="text-center mb-6">
|
||||||
|
<div className="text-2xl font-bold" style={{ color: planColors[selectedPlan.name] || '#666' }}>
|
||||||
|
¥{(selectedPlan.price_cents / 100).toFixed(0)}
|
||||||
|
</div>
|
||||||
|
<Text type="secondary">/{selectedPlan.interval === 'month' ? '月' : '年'}</Text>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Title level={5} className="text-center mb-4">选择支付方式</Title>
|
||||||
|
|
||||||
|
<Radio.Group
|
||||||
|
value={payMethod}
|
||||||
|
onChange={(e) => setPayMethod(e.target.value)}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<Space direction="vertical" className="w-full" size={12}>
|
||||||
|
<Radio value="alipay" className="w-full">
|
||||||
|
<div className="flex items-center gap-3 p-3 border rounded-lg w-full hover:border-blue-400 transition-colors">
|
||||||
|
<AlipayCircleOutlined style={{ fontSize: 28, color: '#1677ff' }} />
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">支付宝</div>
|
||||||
|
<div className="text-xs text-neutral-400">推荐个人用户</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Radio>
|
||||||
|
<Radio value="wechat" className="w-full">
|
||||||
|
<div className="flex items-center gap-3 p-3 border rounded-lg w-full hover:border-green-400 transition-colors">
|
||||||
|
<WechatOutlined style={{ fontSize: 28, color: '#07c160' }} />
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">微信支付</div>
|
||||||
|
<div className="text-xs text-neutral-400">扫码支付</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Radio>
|
||||||
|
</Space>
|
||||||
|
</Radio.Group>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -27,6 +27,7 @@ export const router = createBrowserRouter([
|
|||||||
{ path: 'agent-templates', lazy: () => import('@/pages/AgentTemplates').then((m) => ({ Component: m.default })) },
|
{ path: 'agent-templates', lazy: () => import('@/pages/AgentTemplates').then((m) => ({ Component: m.default })) },
|
||||||
{ path: 'api-keys', lazy: () => import('@/pages/ModelServices').then((m) => ({ Component: m.default })) },
|
{ path: 'api-keys', lazy: () => import('@/pages/ModelServices').then((m) => ({ Component: m.default })) },
|
||||||
{ path: 'usage', lazy: () => import('@/pages/Usage').then((m) => ({ Component: m.default })) },
|
{ path: 'usage', lazy: () => import('@/pages/Usage').then((m) => ({ Component: m.default })) },
|
||||||
|
{ path: 'billing', lazy: () => import('@/pages/Billing').then((m) => ({ Component: m.default })) },
|
||||||
{ path: 'relay', lazy: () => import('@/pages/Relay').then((m) => ({ Component: m.default })) },
|
{ path: 'relay', lazy: () => import('@/pages/Relay').then((m) => ({ Component: m.default })) },
|
||||||
{ path: 'knowledge', lazy: () => import('@/pages/Knowledge').then((m) => ({ Component: m.default })) },
|
{ path: 'knowledge', lazy: () => import('@/pages/Knowledge').then((m) => ({ Component: m.default })) },
|
||||||
{ path: 'config', lazy: () => import('@/pages/Config').then((m) => ({ Component: m.default })) },
|
{ path: 'config', lazy: () => import('@/pages/Config').then((m) => ({ Component: m.default })) },
|
||||||
|
|||||||
101
admin-v2/src/services/billing.ts
Normal file
101
admin-v2/src/services/billing.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import request, { withSignal } from './request'
|
||||||
|
|
||||||
|
// === Types ===
|
||||||
|
|
||||||
|
export interface BillingPlan {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
display_name: string
|
||||||
|
description: string | null
|
||||||
|
price_cents: number
|
||||||
|
currency: string
|
||||||
|
interval: string
|
||||||
|
features: Record<string, unknown>
|
||||||
|
limits: Record<string, unknown>
|
||||||
|
is_default: boolean
|
||||||
|
sort_order: number
|
||||||
|
status: string
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Subscription {
|
||||||
|
id: string
|
||||||
|
account_id: string
|
||||||
|
plan_id: string
|
||||||
|
status: string
|
||||||
|
current_period_start: string
|
||||||
|
current_period_end: string
|
||||||
|
trial_end: string | null
|
||||||
|
canceled_at: string | null
|
||||||
|
cancel_at_period_end: boolean
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UsageQuota {
|
||||||
|
id: string
|
||||||
|
account_id: string
|
||||||
|
period_start: string
|
||||||
|
period_end: string
|
||||||
|
input_tokens: number
|
||||||
|
output_tokens: number
|
||||||
|
relay_requests: number
|
||||||
|
hand_executions: number
|
||||||
|
pipeline_runs: number
|
||||||
|
max_input_tokens: number | null
|
||||||
|
max_output_tokens: number | null
|
||||||
|
max_relay_requests: number | null
|
||||||
|
max_hand_executions: number | null
|
||||||
|
max_pipeline_runs: number | null
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubscriptionInfo {
|
||||||
|
plan: BillingPlan
|
||||||
|
subscription: Subscription | null
|
||||||
|
usage: UsageQuota
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaymentResult {
|
||||||
|
payment_id: string
|
||||||
|
trade_no: string
|
||||||
|
pay_url: string
|
||||||
|
amount_cents: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaymentStatus {
|
||||||
|
id: string
|
||||||
|
method: string
|
||||||
|
amount_cents: number
|
||||||
|
currency: string
|
||||||
|
status: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Service ===
|
||||||
|
|
||||||
|
export const billingService = {
|
||||||
|
listPlans: (signal?: AbortSignal) =>
|
||||||
|
request.get<BillingPlan[]>('/billing/plans', withSignal({}, signal))
|
||||||
|
.then((r) => r.data),
|
||||||
|
|
||||||
|
getPlan: (id: string, signal?: AbortSignal) =>
|
||||||
|
request.get<BillingPlan>(`/billing/plans/${id}`, withSignal({}, signal))
|
||||||
|
.then((r) => r.data),
|
||||||
|
|
||||||
|
getSubscription: (signal?: AbortSignal) =>
|
||||||
|
request.get<SubscriptionInfo>('/billing/subscription', withSignal({}, signal))
|
||||||
|
.then((r) => r.data),
|
||||||
|
|
||||||
|
getUsage: (signal?: AbortSignal) =>
|
||||||
|
request.get<UsageQuota>('/billing/usage', withSignal({}, signal))
|
||||||
|
.then((r) => r.data),
|
||||||
|
|
||||||
|
createPayment: (data: { plan_id: string; payment_method: 'alipay' | 'wechat' }) =>
|
||||||
|
request.post<PaymentResult>('/billing/payments', data).then((r) => r.data),
|
||||||
|
|
||||||
|
getPaymentStatus: (id: string, signal?: AbortSignal) =>
|
||||||
|
request.get<PaymentStatus>(`/billing/payments/${id}`, withSignal({}, signal))
|
||||||
|
.then((r) => r.data),
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user