2026-07-16 16:53:42 +08:00
|
|
|
|
"""群发消息路由。
|
|
|
|
|
|
|
|
|
|
|
|
端点:
|
|
|
|
|
|
- POST /api/send/batch 提交群发任务
|
|
|
|
|
|
- GET /api/send/batch 列出最近 N 条群发任务
|
|
|
|
|
|
- GET /api/send/batch/{batch_id}/status 查询群发进度
|
|
|
|
|
|
- POST /api/send/batch/{batch_id}/cancel 取消群发
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
import logging
|
2026-07-17 18:10:31 +08:00
|
|
|
|
import time
|
2026-07-16 16:53:42 +08:00
|
|
|
|
|
|
|
|
|
|
from fastapi import APIRouter, Request
|
|
|
|
|
|
|
|
|
|
|
|
from woc_bridge.config import _require_batch_worker
|
|
|
|
|
|
from woc_bridge.models import BridgeError, BatchSendRequest
|
|
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger("woc-bridge")
|
|
|
|
|
|
|
|
|
|
|
|
router = APIRouter(prefix="/api/send/batch", tags=["batch-send"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("")
|
|
|
|
|
|
async def submit_batch(req: BatchSendRequest, request: Request):
|
|
|
|
|
|
"""提交群发任务。
|
|
|
|
|
|
|
|
|
|
|
|
请求体:BatchSendRequest(targets / content / client_request_id / abort_on_consecutive_fail)
|
|
|
|
|
|
返回:{"success": True, "batch_id": str, "status": str, "status_query_url": str}
|
|
|
|
|
|
|
|
|
|
|
|
异常:
|
|
|
|
|
|
- 400 INVALID_PARAMS:targets 超过单批上限
|
|
|
|
|
|
- 409 INVALID_PARAMS:batch_id 重复(批级幂等命中)
|
|
|
|
|
|
- 429 RATE_LIMITED:日频次超限 / 批间隔不足
|
|
|
|
|
|
"""
|
|
|
|
|
|
worker = _require_batch_worker()
|
2026-07-17 18:10:31 +08:00
|
|
|
|
t_total = time.perf_counter()
|
|
|
|
|
|
logger.info(
|
|
|
|
|
|
"send/batch: 收到请求 targets=%d content_len=%d client_request_id=%s",
|
|
|
|
|
|
len(req.targets), len(req.content), req.client_request_id or "(none)",
|
|
|
|
|
|
)
|
|
|
|
|
|
try:
|
|
|
|
|
|
batch_id = await worker.submit_batch(req)
|
|
|
|
|
|
except BridgeError as e:
|
|
|
|
|
|
logger.warning(
|
|
|
|
|
|
"send/batch: 提交失败 %s: %s",
|
|
|
|
|
|
e.code, e.message,
|
|
|
|
|
|
)
|
|
|
|
|
|
raise
|
2026-07-16 16:53:42 +08:00
|
|
|
|
status = await worker.get_status(batch_id)
|
2026-07-17 18:10:31 +08:00
|
|
|
|
logger.info(
|
|
|
|
|
|
"send/batch: ✓ batch_id=%s targets=%d (总耗时 %.0fms)",
|
|
|
|
|
|
batch_id, len(req.targets), (time.perf_counter() - t_total) * 1000,
|
|
|
|
|
|
)
|
2026-07-16 16:53:42 +08:00
|
|
|
|
return {
|
|
|
|
|
|
"success": True,
|
|
|
|
|
|
"batch_id": batch_id,
|
|
|
|
|
|
"status": status.status if status is not None else "pending",
|
|
|
|
|
|
"status_query_url": f"/api/send/batch/{batch_id}/status",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/{batch_id}/status")
|
|
|
|
|
|
async def get_batch_status(batch_id: str):
|
|
|
|
|
|
"""查询群发进度。
|
|
|
|
|
|
|
|
|
|
|
|
返回 BatchSendStatus(含 total / processed / success / failed / skipped /
|
|
|
|
|
|
progress / estimated_remaining_sec / items 等字段)。
|
|
|
|
|
|
|
|
|
|
|
|
异常:
|
|
|
|
|
|
- 404 NOT_FOUND:batch_id 不存在
|
|
|
|
|
|
"""
|
2026-07-17 18:10:31 +08:00
|
|
|
|
logger.debug("send/batch/status: 查询 batch_id=%s", batch_id)
|
2026-07-16 16:53:42 +08:00
|
|
|
|
worker = _require_batch_worker()
|
|
|
|
|
|
status = await worker.get_status(batch_id)
|
|
|
|
|
|
if status is None:
|
|
|
|
|
|
raise BridgeError(
|
|
|
|
|
|
code="NOT_FOUND",
|
|
|
|
|
|
message=f"batch_id={batch_id} 不存在",
|
|
|
|
|
|
http_status=404,
|
|
|
|
|
|
)
|
|
|
|
|
|
return status
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/{batch_id}/cancel")
|
|
|
|
|
|
async def cancel_batch(batch_id: str):
|
|
|
|
|
|
"""取消群发。
|
|
|
|
|
|
|
|
|
|
|
|
已处理条目保留结果,未处理条目标记 skipped。
|
|
|
|
|
|
|
|
|
|
|
|
异常:
|
|
|
|
|
|
- 404 NOT_FOUND:batch_id 不存在或已完成
|
|
|
|
|
|
"""
|
2026-07-17 18:10:31 +08:00
|
|
|
|
logger.info("send/batch/cancel: 收到请求 batch_id=%s", batch_id)
|
2026-07-16 16:53:42 +08:00
|
|
|
|
worker = _require_batch_worker()
|
2026-07-17 18:10:31 +08:00
|
|
|
|
try:
|
|
|
|
|
|
cancelled = await worker.cancel_batch(batch_id)
|
|
|
|
|
|
except BridgeError as e:
|
|
|
|
|
|
logger.warning(
|
|
|
|
|
|
"send/batch/cancel: 失败 %s: %s (batch_id=%s)",
|
|
|
|
|
|
e.code, e.message, batch_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
raise
|
2026-07-16 16:53:42 +08:00
|
|
|
|
if not cancelled:
|
2026-07-17 18:10:31 +08:00
|
|
|
|
logger.warning(
|
|
|
|
|
|
"send/batch/cancel: 失败 NOT_FOUND: batch_id 不存在或已完成 (batch_id=%s)",
|
|
|
|
|
|
batch_id,
|
|
|
|
|
|
)
|
2026-07-16 16:53:42 +08:00
|
|
|
|
raise BridgeError(
|
|
|
|
|
|
code="NOT_FOUND",
|
|
|
|
|
|
message=f"batch_id={batch_id} 不存在或已完成",
|
|
|
|
|
|
http_status=404,
|
|
|
|
|
|
)
|
2026-07-17 18:10:31 +08:00
|
|
|
|
logger.info("send/batch/cancel: ✓ batch_id=%s", batch_id)
|
2026-07-16 16:53:42 +08:00
|
|
|
|
return {"success": True, "batch_id": batch_id, "status": "cancelled"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("")
|
|
|
|
|
|
async def list_batches(limit: int = 20):
|
|
|
|
|
|
"""列出最近 N 条群发任务(按提交时间倒序)。
|
|
|
|
|
|
|
|
|
|
|
|
Query 参数:
|
|
|
|
|
|
- limit:返回条数,1-100,默认 20
|
|
|
|
|
|
|
|
|
|
|
|
异常:
|
|
|
|
|
|
- 400 INVALID_PARAMS:limit 越界
|
|
|
|
|
|
"""
|
2026-07-17 18:10:31 +08:00
|
|
|
|
logger.debug("send/batches: 列表查询 limit=%d", limit)
|
2026-07-16 16:53:42 +08:00
|
|
|
|
worker = _require_batch_worker()
|
|
|
|
|
|
if limit < 1 or limit > 100:
|
|
|
|
|
|
raise BridgeError(
|
|
|
|
|
|
code="INVALID_PARAMS",
|
|
|
|
|
|
message="limit 必须在 1-100 之间",
|
|
|
|
|
|
http_status=400,
|
|
|
|
|
|
)
|
|
|
|
|
|
batches = await worker.list_batches(limit)
|
|
|
|
|
|
return {"batches": batches}
|