1、后台菜单增加定时任务和银行交易
2、增加后台配置银行账户和请求网址
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
from app.admin_api.bank.routes import router
|
||||
|
||||
__all__ = ["router"]
|
||||
@@ -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 "已禁用"}
|
||||
Reference in New Issue
Block a user