""" 对账任务 API 管理对账任务的生命周期 """ from typing import Any, Dict, List, Optional from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy import Float from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, func from app.core.database import get_db from app.core.tenant import get_current_company_id from app.models.reconciliation_task import ReconciliationTask from app.models.user import User from app.schemas.user import UserResponse router = APIRouter(prefix="/api/tasks", tags=["对账任务"]) @router.get("/", response_model=Dict[str, Any]) async def list_tasks( period: Optional[str] = Query(None, description="筛选月份,格式: 2026-04"), status: Optional[str] = Query(None, description="筛选状态"), page: int = Query(1, ge=1, description="页码"), page_size: int = Query(10, ge=1, le=100, description="每页数量"), db: AsyncSession = Depends(get_db), company_id: Optional[int] = Depends(get_current_company_id), ) -> Dict[str, Any]: """ 获取对账任务列表 支持按月份和状态筛选,返回分页结构 """ # 构建查询 query = select(ReconciliationTask).where( ReconciliationTask.company_id == company_id ) if period: query = query.where(ReconciliationTask.period == period) if status: query = query.where(ReconciliationTask.status == status) # 计算总数 count_query = select(func.count()).select_from(ReconciliationTask).where( ReconciliationTask.company_id == company_id ) if period: count_query = count_query.where(ReconciliationTask.period == period) if status: count_query = count_query.where(ReconciliationTask.status == status) count_result = await db.execute(count_query) total = count_result.scalar() or 0 # 按创建时间倒序 + 分页 skip = (page - 1) * page_size query = query.order_by(ReconciliationTask.created_at.desc()) query = query.offset(skip).limit(page_size) result = await db.execute(query) tasks = result.scalars().all() return { "items": [ { "id": task.id, "name": f"{task.period} 对账任务", "period": task.period, "status": task.status, "total_employees": task.total_employees, "matched_count": task.matched_count, "exception_count": task.exception_count, "created_at": task.created_at.isoformat() if task.created_at else None, "completed_at": task.updated_at.isoformat() if task.status == "COMPLETED" and task.updated_at else None, } for task in tasks ], "total": total, "page": page, "page_size": page_size, "total_pages": (total + page_size - 1) // page_size if page_size > 0 else 0, } @router.get("/stats", response_model=Dict[str, Any]) async def get_task_stats( db: AsyncSession = Depends(get_db), company_id: int = Depends(get_current_company_id), ) -> Dict[str, Any]: """ 获取任务统计信息 返回本月任务数、待处理异常、已完成、匹配率等统计 """ from datetime import datetime from dateutil.relativedelta import relativedelta # 计算本月时间范围 now = datetime.now() first_day_of_month = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0) next_month = first_day_of_month + relativedelta(months=1) current_period = now.strftime("%Y-%m") # 统计本月任务 from sqlalchemy import case as sql_case query = select( func.count(ReconciliationTask.id).label("total"), func.sum( sql_case( (ReconciliationTask.status == "COMPLETED", 1), else_=0 ) ).label("completed"), func.sum( sql_case( (ReconciliationTask.exception_count > 0, 1), else_=0 ) ).label("with_exceptions"), ).where( ReconciliationTask.company_id == company_id, ReconciliationTask.period == current_period, ) result = await db.execute(query) row = result.one() total_tasks = row.total or 0 completed_tasks = row.completed or 0 # 获取待处理异常数 from app.models.exception_item import ExceptionItem exception_query = select( func.count(ExceptionItem.id) ).where( ExceptionItem.company_id == company_id, ExceptionItem.status == "PENDING", ) exception_result = await db.execute(exception_query) pending_exceptions = exception_result.scalar() or 0 # 计算匹配率 match_rate = 0.0 if total_tasks > 0: match_query = select( func.avg( func.cast(ReconciliationTask.matched_count * 100.0 / func.nullif(ReconciliationTask.total_employees, 0), Float) ) ).where( ReconciliationTask.company_id == company_id, ReconciliationTask.period == current_period, ReconciliationTask.status == "COMPLETED", ) match_result = await db.execute(match_query) match_rate = match_result.scalar() or 0.0 return { "monthly_tasks": total_tasks, "pending_exceptions": pending_exceptions, "completed_tasks": completed_tasks, "match_rate": round(match_rate, 1), "current_period": current_period, } @router.get("/{task_id}", response_model=Dict[str, Any]) async def get_task( task_id: int, db: AsyncSession = Depends(get_db), company_id: int = Depends(get_current_company_id), ) -> Dict[str, Any]: """ 获取任务详情 """ task = await db.get(ReconciliationTask, task_id) if not task: raise HTTPException(status_code=404, detail="任务不存在") if task.company_id != company_id: raise HTTPException(status_code=403, detail="无权访问该任务") return { "id": task.id, "name": f"{task.period} 对账任务", "period": task.period, "status": task.status, "total_employees": task.total_employees, "matched_count": task.matched_count, "exception_count": task.exception_count, "file_ids": task.file_ids, "reconciliation_result": task.reconciliation_result, "created_at": task.created_at.isoformat() if task.created_at else None, "completed_at": task.updated_at.isoformat() if task.status == "COMPLETED" and task.updated_at else None, "error_message": task.error_message, }