33b4c734aa
后端: - 新增认证(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 数据库迁移配置 - 添加初始化示例数据脚本 - 更新项目文档
182 lines
5.6 KiB
Python
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,
|
|
}
|