增加银行流水和定时任务程序

This commit is contained in:
2026-08-14 14:35:25 +08:00
parent 148b89c3ca
commit 7c3ef21762
9 changed files with 616 additions and 94 deletions
+25 -57
View File
@@ -1,10 +1,19 @@
"""定时任务执行器。
支持两种任务类型:
- external_api: 调用外部 HTTP 接口
- internal_method: 动态导入并执行内部函数
支持内部方法执行类型,供 Celery Beat 定时调用。
任务在 Celery Worker 中执行,通过 run_async 桥接异步操作。
配置 JSON 格式(示例):
{
"module": "app.services.bank.sync_service",
"function": "sync_bank_transactions",
"url": "http://...",
"api_key": "...",
"acct_no": "...",
"start_date": "2026-08-01",
"end_date": "2026-08-14"
}
其中 module 和 function 指定要调用的函数,其余字段作为 kwargs 传给该函数。
"""
import json
@@ -12,7 +21,6 @@ import logging
import time
from datetime import datetime, timezone
import httpx
from sqlalchemy import select
from app.models.base import async_session
@@ -40,45 +48,14 @@ def _update_task_status(task_id: str, status: str, error_msg: str | None = None)
run_async(_do())
def _execute_external_api(config: str | None) -> dict:
"""执行外部 API 调用。"""
cfg = json.loads(config or "{}")
url = cfg.get("url", "").strip()
method = cfg.get("method", "GET").upper()
headers = cfg.get("headers") or {}
payload = cfg.get("payload")
timeout_val = float(cfg.get("timeout", 30))
if not url:
raise ValueError("外部接口地址 (url) 未配置")
start = time.monotonic()
try:
with httpx.Client(timeout=httpx.Timeout(timeout_val, connect=10.0)) as client:
if method == "GET":
resp = client.get(url, headers=headers, params=payload)
elif method == "DELETE":
resp = client.delete(url, headers=headers, params=payload)
else:
resp = client.request(method, url, headers=headers, json=payload)
duration_ms = int((time.monotonic() - start) * 1000)
resp.raise_for_status()
return {
"status_code": resp.status_code,
"duration_ms": duration_ms,
"body": resp.text[:2000],
}
except httpx.HTTPError as e:
duration_ms = int((time.monotonic() - start) * 1000)
raise RuntimeError(f"外部接口请求失败: {e} (耗时 {duration_ms}ms)") from e
def _execute_internal_method(config: str | None) -> dict:
"""执行内部方法调用。"""
"""执行内部方法调用。
配置 JSON 中 module 和 function 指定要调用的函数,其余字段作为 kwargs 传入。
"""
cfg = json.loads(config or "{}")
module_path = cfg.get("module", "").strip()
function_name = cfg.get("function", "").strip()
args = cfg.get("args") or []
module_path = cfg.pop("module", "").strip()
function_name = cfg.pop("function", "").strip()
if not module_path or not function_name:
raise ValueError("内部方法需要指定 module 和 function")
@@ -90,8 +67,9 @@ def _execute_internal_method(config: str | None) -> dict:
if func is None or not callable(func):
raise ValueError(f"模块 {module_path} 中不存在可调用函数 {function_name}")
# 剩余字段作为 kwargs 传给函数
start = time.monotonic()
result = func(*args)
result = func(**cfg)
duration_ms = int((time.monotonic() - start) * 1000)
return {
"duration_ms": duration_ms,
@@ -101,10 +79,7 @@ def _execute_internal_method(config: str | None) -> dict:
@celery_app.task(name="execute_scheduled_task", bind=True, ignore_result=True) # type: ignore[call-arg]
def execute_scheduled_task(self, task_id: str):
"""执行定时任务(Celery 任务入口)。
通过 run_async 桥接到异步上下文读取任务配置并执行。
"""
"""执行定时任务(Celery 任务入口)。"""
async def _run():
async with async_session() as db:
@@ -118,21 +93,14 @@ def execute_scheduled_task(self, task_id: str):
return
task_config = task.config
task_type = task.task_type
try:
if task_type == "external_api":
exec_result = _execute_external_api(task_config)
elif task_type == "internal_method":
exec_result = _execute_internal_method(task_config)
else:
raise ValueError(f"未知的任务类型: {task_type}")
exec_result = _execute_internal_method(task_config)
_update_task_status(task_id, "success")
logger.info("定时任务执行成功: %s (%s) -> %s", task_id, task_type, exec_result)
logger.info("定时任务执行成功: %s -> %s", task_id, exec_result)
except Exception as e:
error_msg = str(e)
_update_task_status(task_id, "error", error_msg)
logger.exception("定时任务执行失败: %s (%s)", task_id, task_type)
logger.exception("定时任务执行失败: %s", task_id)
run_async(_run())