From 7dcedf974d88f11d15e57f3774f25c9f9f14aa0a Mon Sep 17 00:00:00 2001 From: wwwwwwwww <526125649@qq.com> Date: Thu, 13 Aug 2026 15:12:16 +0800 Subject: [PATCH] =?UTF-8?q?1=E3=80=81=E5=90=8E=E5=8F=B0=E8=8F=9C=E5=8D=95?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=AE=9A=E6=97=B6=E4=BB=BB=E5=8A=A1=E5=92=8C?= =?UTF-8?q?=E9=93=B6=E8=A1=8C=E4=BA=A4=E6=98=93=202=E3=80=81=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E5=90=8E=E5=8F=B0=E9=85=8D=E7=BD=AE=E9=93=B6=E8=A1=8C?= =?UTF-8?q?=E8=B4=A6=E6=88=B7=E5=92=8C=E8=AF=B7=E6=B1=82=E7=BD=91=E5=9D=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- video-gen-admin/src/App.tsx | 4 + video-gen-admin/src/api/index.ts | 97 ++++++ .../src/pages/AdminBankTransactions.tsx | 250 ++++++++++++++ .../src/pages/AdminScheduledTasks.tsx | 311 ++++++++++++++++++ video-gen-admin/src/pages/AdminSettings.tsx | 202 +++++++++++- video-gen-admin/src/types/index.ts | 31 ++ .../versions/20260813_银行账户与定时任务.py | 53 +++ video-gen-api/app/admin_api/bank/__init__.py | 3 + video-gen-api/app/admin_api/bank/routes.py | 173 ++++++++++ .../app/admin_api/scheduled_tasks/__init__.py | 3 + .../app/admin_api/scheduled_tasks/routes.py | 170 ++++++++++ video-gen-api/app/api/admin/__init__.py | 4 + video-gen-api/app/config.py | 4 + video-gen-api/app/models/__init__.py | 3 + video-gen-api/app/models/bank_account.py | 18 + video-gen-api/app/models/scheduled_task.py | 23 ++ video-gen-api/app/services/bank/__init__.py | 4 + video-gen-api/app/services/bank/client.py | 76 +++++ .../app/services/bank/file_logger.py | 50 +++ video-gen-api/app/services/bank/service.py | 47 +++ video-gen-api/app/tasks/celery_app.py | 80 +++++ video-gen-api/app/tasks/scheduled_tasks.py | 138 ++++++++ 22 files changed, 1741 insertions(+), 3 deletions(-) create mode 100644 video-gen-admin/src/pages/AdminBankTransactions.tsx create mode 100644 video-gen-admin/src/pages/AdminScheduledTasks.tsx create mode 100644 video-gen-api/alembic/versions/20260813_银行账户与定时任务.py create mode 100644 video-gen-api/app/admin_api/bank/__init__.py create mode 100644 video-gen-api/app/admin_api/bank/routes.py create mode 100644 video-gen-api/app/admin_api/scheduled_tasks/__init__.py create mode 100644 video-gen-api/app/admin_api/scheduled_tasks/routes.py create mode 100644 video-gen-api/app/models/bank_account.py create mode 100644 video-gen-api/app/models/scheduled_task.py create mode 100644 video-gen-api/app/services/bank/__init__.py create mode 100644 video-gen-api/app/services/bank/client.py create mode 100644 video-gen-api/app/services/bank/file_logger.py create mode 100644 video-gen-api/app/services/bank/service.py create mode 100644 video-gen-api/app/tasks/scheduled_tasks.py diff --git a/video-gen-admin/src/App.tsx b/video-gen-admin/src/App.tsx index 59222f02..e91dde54 100644 --- a/video-gen-admin/src/App.tsx +++ b/video-gen-admin/src/App.tsx @@ -45,6 +45,8 @@ import AdminApiKeys from './pages/AdminApiKeys'; import AdminApiModelPricings from './pages/AdminApiModelPricings'; import AdminApiUsage from './pages/AdminApiUsage'; import AdminInvoices from './pages/AdminInvoices'; +import AdminBankTransactions from './pages/AdminBankTransactions'; +import AdminScheduledTasks from './pages/AdminScheduledTasks'; import { useAdminStore } from './store'; @@ -112,6 +114,8 @@ const App = () => { } /> } /> } /> + } /> + } /> } /> } /> } /> diff --git a/video-gen-admin/src/api/index.ts b/video-gen-admin/src/api/index.ts index 269f89d7..0a764ade 100644 --- a/video-gen-admin/src/api/index.ts +++ b/video-gen-admin/src/api/index.ts @@ -18,6 +18,7 @@ import type { PrivatePortraitConfig, PrivatePortraitProjectListOut, PrivatePortraitAssetListOut, AdminUploadFileResult, AdminUploadResourceType, AdminUploadScene, VideoUpscaleConfigOut, VideoUpscaleConfigSavePayload, CreditProduct, + BankAccount, ScheduledTask, } from '../types'; import type { @@ -216,6 +217,102 @@ export async function toggleUserStatus(userId: string, isActive: boolean): Promi await api.put(`/admin/users/${userId}/status`, { is_active: isActive }); } +export async function queryBankTransactions(params: { + accountId: string; + startDate: string; + endDate: string; + dcFlag?: number; + page?: number; + pageSize?: number; +}): Promise<{ items: any[]; total: number }> { + const qs = new URLSearchParams(); + qs.set('account_id', params.accountId); + qs.set('start_date', params.startDate); + qs.set('end_date', params.endDate); + if (params.dcFlag !== undefined) qs.set('dc_flag', String(params.dcFlag)); + if (params.page) qs.set('page', String(params.page)); + if (params.pageSize) qs.set('page_size', String(params.pageSize)); + return api.get(`/admin/bank/transactions?${qs.toString()}`); +} + +// ── Bank Account Management ──────────────────────────────── + +export async function listBankAccounts(): Promise<{ items: BankAccount[] }> { + return api.get('/admin/bank/accounts'); +} + +export async function createBankAccount(payload: { + account_name: string; + bank_name: string; + account_no: string; + is_active?: boolean; + is_default?: boolean; + description?: string; +}): Promise<{ id: string; message: string }> { + return api.post('/admin/bank/accounts', payload); +} + +export async function updateBankAccount( + accountId: string, + payload: Partial<{ + account_name: string; + bank_name: string; + account_no: string; + is_active: boolean; + is_default: boolean; + description: string; + }>, +): Promise<{ message: string }> { + return api.put(`/admin/bank/accounts/${accountId}`, payload); +} + +export async function deleteBankAccount(accountId: string): Promise<{ message: string }> { + await api.delete(`/admin/bank/accounts/${accountId}`); + return { message: '删除成功' }; +} + +// ── Scheduled Tasks ──────────────────────────────────────── + +export async function listScheduledTasks(): Promise<{ items: ScheduledTask[] }> { + return api.get('/admin/scheduled-tasks'); +} + +export async function createScheduledTask(payload: { + name: string; + task_type: 'external_api' | 'internal_method'; + schedule: string; + config?: Record | string; + is_active?: boolean; +}): Promise<{ id: string; message: string }> { + return api.post('/admin/scheduled-tasks', payload); +} + +export async function updateScheduledTask( + taskId: string, + payload: Partial<{ + name: string; + task_type: 'external_api' | 'internal_method'; + schedule: string; + config: Record | string; + is_active: boolean; + }>, +): Promise<{ message: string }> { + return api.put(`/admin/scheduled-tasks/${taskId}`, payload); +} + +export async function deleteScheduledTask(taskId: string): Promise<{ message: string }> { + await api.delete(`/admin/scheduled-tasks/${taskId}`); + return { message: '删除成功' }; +} + +export async function runScheduledTask(taskId: string): Promise<{ message: string }> { + return api.post(`/admin/scheduled-tasks/${taskId}/run`, {}); +} + +export async function toggleScheduledTask(taskId: string): Promise<{ is_active: boolean; message: string }> { + return api.post(`/admin/scheduled-tasks/${taskId}/toggle`, {}); +} + export async function updateSingleDeviceLoginOverride(userId: string, override: boolean | null): Promise { await api.put(`/admin/users/${userId}/single-device-login-override`, { override }); } diff --git a/video-gen-admin/src/pages/AdminBankTransactions.tsx b/video-gen-admin/src/pages/AdminBankTransactions.tsx new file mode 100644 index 00000000..0fe0be0f --- /dev/null +++ b/video-gen-admin/src/pages/AdminBankTransactions.tsx @@ -0,0 +1,250 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { Table, Button, Space, Typography, message, Card, DatePicker, Select } from 'antd'; +import { BankOutlined, SearchOutlined } from '@ant-design/icons'; +import { queryBankTransactions, listBankAccounts } from '../api'; +import { formatDate } from '../utils/formatDate'; +import type { BankAccount } from '../types'; + +const { RangePicker } = DatePicker; + +interface TransactionRecord { + counterAcctNo?: string; + cnterName?: string; + cnterBankName?: string; + acctNo?: string; + transAmt?: number | string; + dcFlag?: number; + dcFlagLabel?: string; + transTimeStr?: string; + digestCode?: string; + remark?: string; + balance?: number | string; + tellerSeqno?: string; + transferId?: string; +} + +const AdminBankTransactions: React.FC = () => { + const [data, setData] = useState([]); + const [total, setTotal] = useState(0); + const [loading, setLoading] = useState(false); + const [page, setPage] = useState(1); + const [pageSize, setPageSize] = useState(20); + + // 银行账户 + const [accounts, setAccounts] = useState([]); + const [selectedAccountId, setSelectedAccountId] = useState(undefined); + + // 搜索条件 + const [dateRange, setDateRange] = useState<[string | null, string | null]>([null, null]); + const [dcFlag, setDcFlag] = useState(undefined); + + useEffect(() => { + loadAccounts(); + }, []); + + const loadAccounts = async () => { + try { + const res = await listBankAccounts(); + setAccounts(res.items || []); + } catch (err: any) { + message.error(err?.message || '加载银行账户失败'); + } + }; + + const fetchData = useCallback(async () => { + if (!selectedAccountId || !dateRange[0] || !dateRange[1]) return; + setLoading(true); + try { + const res = await queryBankTransactions({ + accountId: selectedAccountId, + startDate: dateRange[0], + endDate: dateRange[1], + dcFlag, + page, + pageSize, + }); + setData(res.items || []); + setTotal(res.total || 0); + } catch (err: any) { + message.error(err?.message || '查询失败'); + } finally { + setLoading(false); + } + }, [selectedAccountId, dateRange, dcFlag, page, pageSize]); + + useEffect(() => { + if (selectedAccountId && dateRange[0] && dateRange[1]) { + fetchData(); + } + }, [fetchData]); + + const handleSearch = () => { + if (!selectedAccountId) { + message.warning('请选择银行账户'); + return; + } + if (!dateRange[0] || !dateRange[1]) { + message.warning('请选择交易时间区间'); + return; + } + setPage(1); + fetchData(); + }; + + const selectedAccount = accounts.find(a => a.id === selectedAccountId); + + const columns = [ + { + title: '收款账号', + dataIndex: 'counterAcctNo', + width: 180, + render: (v: string) => v || '-', + }, + { + title: '收款户名', + dataIndex: 'cnterName', + width: 120, + render: (v: string) => v || '-', + }, + { + title: '收款开户行', + dataIndex: 'cnterBankName', + width: 150, + render: (v: string) => v || '-', + }, + { + title: '我方付款账号', + dataIndex: 'acctNo', + width: 180, + render: (v: string) => v || '-', + }, + { + title: '打款金额', + dataIndex: 'transAmt', + width: 120, + align: 'right' as const, + render: (v: number | string) => v != null ? Number(v).toFixed(2) : '-', + }, + { + title: '借贷方向', + dataIndex: 'dcFlagLabel', + width: 100, + render: (v: string, r: TransactionRecord) => v || (r.dcFlag === 0 ? '借/出金' : r.dcFlag === 1 ? '贷/入金' : '-'), + }, + { + title: '打款时间', + dataIndex: 'transTimeStr', + width: 160, + render: (v: string) => v ? formatDate(v) : '-', + }, + { + title: '摘要', + dataIndex: 'digestCode', + width: 100, + render: (v: string) => v || '-', + }, + { + title: '用途/备注', + dataIndex: 'remark', + width: 150, + render: (v: string) => v || '-', + }, + { + title: '账户余额', + dataIndex: 'balance', + width: 120, + align: 'right' as const, + render: (v: number | string) => v != null ? Number(v).toFixed(2) : '-', + }, + { + title: '流水号', + dataIndex: 'tellerSeqno', + width: 150, + render: (v: string) => v || '-', + }, + ]; + + return ( +
+ +
+
+ +
+
+ 银行交易查询 + 查询银行账户交易流水记录 +
+
+ + + + 借/出金 + 贷/入金 + + + + + {selectedAccount && ( +
+ 当前查询账户:{selectedAccount.bank_name} | {selectedAccount.account_no} | {selectedAccount.account_name} +
+ )} +
+ + + `${r.tellerSeqno || ''}-${r.transferId || ''}-${i}`} + columns={columns} + dataSource={data} + loading={loading} + scroll={{ x: 1400 }} + pagination={{ + current: page, + pageSize, + total, + showSizeChanger: true, + showTotal: (t) => `共 ${t} 条`, + onChange: (p, ps) => { + setPage(p); + setPageSize(ps); + }, + }} + /> + + + ); +}; + +export default AdminBankTransactions; diff --git a/video-gen-admin/src/pages/AdminScheduledTasks.tsx b/video-gen-admin/src/pages/AdminScheduledTasks.tsx new file mode 100644 index 00000000..ff39e1aa --- /dev/null +++ b/video-gen-admin/src/pages/AdminScheduledTasks.tsx @@ -0,0 +1,311 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { + Table, Button, Space, Typography, message, Card, Modal, Form, Input, Select, Switch, Tag, Popconfirm, Tabs, +} from 'antd'; +import { + ClockCircleOutlined, PlusOutlined, EditOutlined, DeleteOutlined, PlayCircleOutlined, StopOutlined, CheckCircleOutlined, CloseCircleOutlined, +} from '@ant-design/icons'; +import { + listScheduledTasks, + createScheduledTask, + updateScheduledTask, + deleteScheduledTask, + runScheduledTask, + toggleScheduledTask, +} from '../api'; +import type { ScheduledTask } from '../types'; + +const { TextArea } = Input; + +const SCHEDULE_TYPE_OPTIONS = [ + { value: 'external_api', label: '外部接口调用' }, + { value: 'internal_method', label: '内部方法执行' }, +]; + +const TASK_TYPE_LABELS: Record = { + external_api: '外部接口', + internal_method: '内部方法', +}; + +const STATUS_LABELS: Record = { + success: { label: '成功', color: 'green' }, + error: { label: '失败', color: 'red' }, +}; + +const AdminScheduledTasks: React.FC = () => { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(false); + const [modal, setModal] = useState(false); + const [editing, setEditing] = useState(null); + const [form] = Form.useForm(); + const [saving, setSaving] = useState(false); + const [activeTab, setActiveTab] = useState<'basic' | 'config'>('basic'); + + const fetchData = useCallback(async () => { + setLoading(true); + try { + const res = await listScheduledTasks(); + setData(res.items || []); + } catch (err: any) { + message.error(err?.message || '加载失败'); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + const openModal = (task?: ScheduledTask) => { + if (task) { + setEditing(task); + let configStr = ''; + if (task.config) { + try { + configStr = typeof task.config === 'string' ? JSON.stringify(JSON.parse(task.config), null, 2) : JSON.stringify(task.config, null, 2); + } catch { + configStr = task.config; + } + } + form.setFieldsValue({ + name: task.name, + task_type: task.task_type, + schedule: task.schedule, + config: configStr, + is_active: task.is_active, + }); + } else { + setEditing(null); + form.resetFields(); + form.setFieldsValue({ is_active: true, task_type: 'external_api' }); + } + setActiveTab('basic'); + setModal(true); + }; + + const handleSave = async () => { + try { + const values = await form.validateFields(); + let configVal = values.config; + if (configVal && typeof configVal === 'string') { + try { + configVal = JSON.stringify(JSON.parse(configVal)); + } catch { + message.warning('配置 JSON 格式不合法,将按原样保存'); + } + } + setSaving(true); + if (editing) { + await updateScheduledTask(editing.id, { ...values, config: configVal }); + message.success('更新成功'); + } else { + await createScheduledTask({ ...values, config: configVal }); + message.success('创建成功'); + } + setModal(false); + await fetchData(); + } catch (err: any) { + message.error(err?.message || '保存失败'); + } finally { + setSaving(false); + } + }; + + const handleDelete = async (taskId: string) => { + try { + await deleteScheduledTask(taskId); + message.success('删除成功'); + await fetchData(); + } catch (err: any) { + message.error(err?.message || '删除失败'); + } + }; + + const handleRun = async (taskId: string) => { + try { + await runScheduledTask(taskId); + message.success('任务已提交执行'); + setTimeout(fetchData, 1500); + } catch (err: any) { + message.error(err?.message || '执行失败'); + } + }; + + const handleToggle = async (task: ScheduledTask) => { + try { + const res = await toggleScheduledTask(task.id); + message.success(res.message); + await fetchData(); + } catch (err: any) { + message.error(err?.message || '操作失败'); + } + }; + + const taskType = Form.useWatch('task_type', form); + + const columns = [ + { title: '任务名称', dataIndex: 'name', width: 160, ellipsis: true }, + { + title: '类型', + dataIndex: 'task_type', + width: 100, + render: (v: string) => {TASK_TYPE_LABELS[v] || v}, + }, + { title: '调度表达式', dataIndex: 'schedule', width: 140, render: (v: string) => {v} }, + { + title: '状态', + dataIndex: 'is_active', + width: 80, + render: (v: boolean) => {v ? '启用' : '禁用'}, + }, + { + title: '最后执行', + dataIndex: 'last_run_at', + width: 160, + render: (v: string, r: ScheduledTask) => { + if (!v) return '-'; + const status = r.last_status ? STATUS_LABELS[r.last_status] : null; + return ( + + {v.includes('T') ? v.replace('T', ' ').slice(0, 19) : v} + {status && {status.label}} + + ); + }, + }, + { + title: '操作', + width: 220, + render: (_: any, record: ScheduledTask) => ( + + + + + handleDelete(record.id)} okText="确定" cancelText="取消"> + + + + ), + }, + ]; + + const scheduleHelp = ( +
+
• 纯数字:间隔秒数(如 60 = 每 60 秒执行一次)
+
• Cron 表达式(5 字段):分 时 日 月 周
+
• 示例:* * * * * = 每分钟 | 0 * * * * = 每小时
+
+ ); + + return ( +
+ +
+
+
+ +
+
+ 定时任务管理 + 自定义定时执行外部接口调用或内部方法 +
+
+ +
+
+ + +
`共 ${t} 条` }} + /> + + + {/* 新增/编辑弹窗 */} + setModal(false)} + confirmLoading={saving} + okText="保存" + cancelText="取消" + destroyOnClose + width={600} + > +
+ setActiveTab(k as 'basic' | 'config')} + items={[ + { + key: 'basic', + label: '基本设置', + children: ( + <> + + + + + + + + + + + ), + }, + { + key: 'config', + label: '任务配置', + children: ( + +