1、后台菜单增加定时任务和银行交易

2、增加后台配置银行账户和请求网址
This commit is contained in:
2026-08-13 15:12:16 +08:00
parent 33a5cc4f94
commit 7dcedf974d
22 changed files with 1741 additions and 3 deletions
@@ -0,0 +1,3 @@
from app.admin_api.bank.routes import router
__all__ = ["router"]
+173
View File
@@ -0,0 +1,173 @@
"""银行账户管理与交易查询后台路由。"""
from fastapi import APIRouter, Depends, HTTPException, Path, Query
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.dependencies import get_admin_user, get_db
from app.models.bank_account import BankAccount
from app.models.user import User
from app.services.bank.service import query_transactions_with_log
from app.utils.id_gen import generate_id
router = APIRouter(prefix="/admin/bank", tags=["admin-bank"])
# ============================================================
# 银行账户 CRUD
# ============================================================
@router.get("/accounts")
async def list_accounts(
admin: User = Depends(get_admin_user),
db: AsyncSession = Depends(get_db),
):
"""列出所有银行账户。"""
result = await db.execute(select(BankAccount).order_by(BankAccount.is_default.desc(), BankAccount.created_at.desc()))
accounts = result.scalars().all()
return {"items": [
{
"id": a.id,
"account_name": a.account_name,
"bank_name": a.bank_name,
"account_no": a.account_no,
"is_active": a.is_active,
"is_default": a.is_default,
"description": a.description,
"created_at": a.created_at.isoformat() if a.created_at else None,
"updated_at": a.updated_at.isoformat() if a.updated_at else None,
}
for a in accounts
]}
@router.post("/accounts")
async def create_account(
body: dict,
admin: User = Depends(get_admin_user),
db: AsyncSession = Depends(get_db),
):
"""新增银行账户。"""
account_name = (body.get("account_name") or "").strip()
bank_name = (body.get("bank_name") or "").strip()
account_no = (body.get("account_no") or "").strip()
if not account_name or not bank_name or not account_no:
raise HTTPException(status_code=400, detail="账户名称、开户银行、银行账号不能为空")
# 检查账号唯一性
existing = await db.execute(select(BankAccount).where(BankAccount.account_no == account_no))
if existing.scalar_one_or_none():
raise HTTPException(status_code=409, detail="该银行账号已存在")
is_default = bool(body.get("is_default", False))
# 如果设为默认,取消其他默认
if is_default:
await db.execute(
BankAccount.__table__.update().where(BankAccount.is_default.is_(True)).values(is_default=False)
)
account = BankAccount(
id=generate_id(),
account_name=account_name,
bank_name=bank_name,
account_no=account_no,
is_active=bool(body.get("is_active", True)),
is_default=is_default,
description=body.get("description"),
)
db.add(account)
await db.commit()
return {"id": account.id, "message": "创建成功"}
@router.put("/accounts/{account_id}")
async def update_account(
account_id: str = Path(..., description="账户 ID"),
body: dict = ...,
admin: User = Depends(get_admin_user),
db: AsyncSession = Depends(get_db),
):
"""编辑银行账户。"""
result = await db.execute(select(BankAccount).where(BankAccount.id == account_id))
account = result.scalar_one_or_none()
if account is None:
raise HTTPException(status_code=404, detail="账户不存在")
if "account_name" in body:
account.account_name = str(body["account_name"]).strip()
if "bank_name" in body:
account.bank_name = str(body["bank_name"]).strip()
if "account_no" in body:
new_no = str(body["account_no"]).strip()
if new_no != account.account_no:
existing = await db.execute(select(BankAccount).where(BankAccount.account_no == new_no))
if existing.scalar_one_or_none():
raise HTTPException(status_code=409, detail="该银行账号已存在")
account.account_no = new_no
if "is_active" in body:
account.is_active = bool(body["is_active"])
if "description" in body:
account.description = body.get("description")
if body.get("is_default"):
await db.execute(
BankAccount.__table__.update()
.where(BankAccount.is_default.is_(True))
.where(BankAccount.id != account_id)
.values(is_default=False)
)
account.is_default = True
await db.commit()
return {"message": "更新成功"}
@router.delete("/accounts/{account_id}")
async def delete_account(
account_id: str = Path(..., description="账户 ID"),
admin: User = Depends(get_admin_user),
db: AsyncSession = Depends(get_db),
):
"""删除银行账户。"""
result = await db.execute(select(BankAccount).where(BankAccount.id == account_id))
account = result.scalar_one_or_none()
if account is None:
raise HTTPException(status_code=404, detail="账户不存在")
await db.delete(account)
await db.commit()
return {"message": "删除成功"}
# ============================================================
# 银行交易查询
# ============================================================
@router.get("/transactions")
async def list_transactions(
account_id: str = Query(..., description="银行账户 ID"),
start_date: str = Query(..., description="开始日期 (YYYY-MM-DD)"),
end_date: str = Query(..., description="结束日期 (YYYY-MM-DD)"),
dc_flag: int | None = Query(None, description="借贷方向: 0-借/出金, 1-贷/入金, 不传返回全部"),
page: int = Query(1, ge=1, description="页码"),
page_size: int = Query(20, ge=1, le=100, description="每页数量"),
admin: User = Depends(get_admin_user),
db: AsyncSession = Depends(get_db),
):
"""查询银行交易流水(带接口请求记录到文件日志)。"""
result = await db.execute(select(BankAccount).where(BankAccount.id == account_id))
account = result.scalar_one_or_none()
if account is None:
raise HTTPException(status_code=404, detail="银行账户不存在")
data = await query_transactions_with_log(
admin.id,
acct_no=account.account_no,
start_date=start_date,
end_date=end_date,
dc_flag=dc_flag,
page=page,
page_size=page_size,
)
return data
@@ -0,0 +1,3 @@
from app.admin_api.scheduled_tasks.routes import router
__all__ = ["router"]
@@ -0,0 +1,170 @@
"""定时任务管理后台路由。"""
import json
from fastapi import APIRouter, Depends, HTTPException, Path
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.dependencies import get_admin_user, get_db
from app.models.scheduled_task import ScheduledTask
from app.models.user import User
from app.utils.id_gen import generate_id
router = APIRouter(prefix="/admin/scheduled-tasks", tags=["admin-scheduled-tasks"])
def _task_to_dict(task: ScheduledTask) -> dict:
return {
"id": task.id,
"name": task.name,
"task_type": task.task_type,
"schedule": task.schedule,
"config": task.config,
"is_active": task.is_active,
"last_run_at": task.last_run_at,
"last_status": task.last_status,
"last_error": task.last_error,
"created_by": task.created_by,
"created_at": task.created_at.isoformat() if task.created_at else None,
"updated_at": task.updated_at.isoformat() if task.updated_at else None,
}
@router.get("")
async def list_tasks(
admin: User = Depends(get_admin_user),
db: AsyncSession = Depends(get_db),
):
"""列出所有定时任务。"""
result = await db.execute(select(ScheduledTask).order_by(ScheduledTask.created_at.desc()))
tasks = result.scalars().all()
return {"items": [_task_to_dict(t) for t in tasks]}
@router.post("")
async def create_task(
body: dict,
admin: User = Depends(get_admin_user),
db: AsyncSession = Depends(get_db),
):
"""创建定时任务。"""
name = (body.get("name") or "").strip()
task_type = (body.get("task_type") or "").strip()
schedule = (body.get("schedule") or "").strip()
if not name or not task_type or not schedule:
raise HTTPException(status_code=400, detail="任务名称、类型、调度表达式不能为空")
if task_type not in ("external_api", "internal_method"):
raise HTTPException(status_code=400, detail="任务类型必须为 external_api 或 internal_method")
config = body.get("config")
if isinstance(config, dict):
config = json.dumps(config, ensure_ascii=False)
elif isinstance(config, str):
# 验证 JSON 合法性
try:
json.loads(config)
except json.JSONDecodeError:
raise HTTPException(status_code=400, detail="config 不是合法的 JSON")
task = ScheduledTask(
id=generate_id(),
name=name,
task_type=task_type,
schedule=schedule,
config=config,
is_active=bool(body.get("is_active", True)),
created_by=admin.id,
)
db.add(task)
await db.commit()
return {"id": task.id, "message": "创建成功"}
@router.put("/{task_id}")
async def update_task(
task_id: str = Path(..., description="任务 ID"),
body: dict = ...,
admin: User = Depends(get_admin_user),
db: AsyncSession = Depends(get_db),
):
"""更新定时任务。"""
result = await db.execute(select(ScheduledTask).where(ScheduledTask.id == task_id))
task = result.scalar_one_or_none()
if task is None:
raise HTTPException(status_code=404, detail="任务不存在")
if "name" in body:
task.name = str(body["name"]).strip()
if "task_type" in body:
t = body["task_type"]
if t not in ("external_api", "internal_method"):
raise HTTPException(status_code=400, detail="任务类型必须为 external_api 或 internal_method")
task.task_type = t
if "schedule" in body:
task.schedule = str(body["schedule"]).strip()
if "is_active" in body:
task.is_active = bool(body["is_active"])
if "config" in body:
config = body["config"]
if isinstance(config, dict):
config = json.dumps(config, ensure_ascii=False)
elif isinstance(config, str):
try:
json.loads(config)
except json.JSONDecodeError:
raise HTTPException(status_code=400, detail="config 不是合法的 JSON")
task.config = config
await db.commit()
return {"message": "更新成功"}
@router.delete("/{task_id}")
async def delete_task(
task_id: str = Path(..., description="任务 ID"),
admin: User = Depends(get_admin_user),
db: AsyncSession = Depends(get_db),
):
"""删除定时任务。"""
result = await db.execute(select(ScheduledTask).where(ScheduledTask.id == task_id))
task = result.scalar_one_or_none()
if task is None:
raise HTTPException(status_code=404, detail="任务不存在")
await db.delete(task)
await db.commit()
return {"message": "删除成功"}
@router.post("/{task_id}/run")
async def run_task(
task_id: str = Path(..., description="任务 ID"),
admin: User = Depends(get_admin_user),
db: AsyncSession = Depends(get_db),
):
"""手动执行一次定时任务。"""
result = await db.execute(select(ScheduledTask).where(ScheduledTask.id == task_id))
task = result.scalar_one_or_none()
if task is None:
raise HTTPException(status_code=404, detail="任务不存在")
from app.tasks.scheduled_tasks import execute_scheduled_task
execute_scheduled_task.apply_async(args=[task_id])
return {"message": "任务已提交执行"}
@router.post("/{task_id}/toggle")
async def toggle_task(
task_id: str = Path(..., description="任务 ID"),
admin: User = Depends(get_admin_user),
db: AsyncSession = Depends(get_db),
):
"""启用/禁用定时任务。"""
result = await db.execute(select(ScheduledTask).where(ScheduledTask.id == task_id))
task = result.scalar_one_or_none()
if task is None:
raise HTTPException(status_code=404, detail="任务不存在")
task.is_active = not task.is_active
await db.commit()
return {"is_active": task.is_active, "message": "已启用" if task.is_active else "已禁用"}