Files
s2f/backend/app/api/tasks.py
T
freedakgmail 33b4c734aa feat: 完善前后端核心功能模块
后端:
- 新增认证(auth)、任务(tasks)、映射(mappings)、对账(reconciliation)、异常(exceptions)、导出(exports) API
- 新增核心模块: database, security, permissions, tenant, exceptions, error_handlers
- 新增数据模型: user, company, reconciliation_task, field_mapping, uploaded_file 等
- 新增服务层: ai_recognizer, file_parser, file_storage, mapping, reconciliation 等
- 添加数据库迁移脚本

前端:
- 新增登录页面和仪表盘页面
- 新增任务列表、任务详情、字段映射页面
- 新增异常处理页面和规则设置页面
- 新增 API 代理路由 /api/[...path]
- 新增 UI 组件库 (button, card, dialog, input, table 等)
- 新增 auth 组件 (ProtectedRoute, PermissionGate)
- 新增 layout 组件 (Header, Sidebar)
- 新增 mapping 组件 (FieldMappingTable, AISuggestionPanel)
- 新增 API 客户端和 hooks (useAsync, useToast, usePermission 等)
- 新增状态管理 (auth-store, company-store, ui-store)
- 集成 Tailwind CSS 和 shadcn/ui 组件库

其他:
- 添加 Alembic 数据库迁移配置
- 添加初始化示例数据脚本
- 更新项目文档
2026-07-07 09:04:47 +08:00

182 lines
5.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=List[Dict[str, Any]])
async def list_tasks(
period: Optional[str] = Query(None, description="筛选月份,格式: 2026-04"),
status: Optional[str] = Query(None, description="筛选状态"),
skip: int = Query(0, ge=0, description="跳过记录数"),
limit: int = Query(50, ge=1, le=100, description="返回记录数"),
db: AsyncSession = Depends(get_db),
company_id: Optional[int] = Depends(get_current_company_id),
) -> List[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)
# 按创建时间倒序
query = query.order_by(ReconciliationTask.created_at.desc())
# 分页
query = query.offset(skip).limit(limit)
result = await db.execute(query)
tasks = result.scalars().all()
return [
{
"id": task.id,
"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,
}
for task in tasks
]
@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,
"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,
"error_message": task.error_message,
}