"""群发消息路由。 端点: - 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 import time 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() 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 status = await worker.get_status(batch_id) logger.info( "send/batch: ✓ batch_id=%s targets=%d (总耗时 %.0fms)", batch_id, len(req.targets), (time.perf_counter() - t_total) * 1000, ) 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 不存在 """ logger.debug("send/batch/status: 查询 batch_id=%s", batch_id) 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 不存在或已完成 """ logger.info("send/batch/cancel: 收到请求 batch_id=%s", batch_id) worker = _require_batch_worker() 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 if not cancelled: logger.warning( "send/batch/cancel: 失败 NOT_FOUND: batch_id 不存在或已完成 (batch_id=%s)", batch_id, ) raise BridgeError( code="NOT_FOUND", message=f"batch_id={batch_id} 不存在或已完成", http_status=404, ) logger.info("send/batch/cancel: ✓ batch_id=%s", batch_id) 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 越界 """ logger.debug("send/batches: 列表查询 limit=%d", limit) 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}