feebbb10ac
- 使用公共Pagination组件统一各页面的分页逻辑 - 优化exceptions页面异常列表展示 - 简化settings/rules页面规则管理 - 后端tasks.py增加分页参数支持
202 lines
6.6 KiB
Python
202 lines
6.6 KiB
Python
"""
|
|
对账任务 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,
|
|
}
|