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 数据库迁移配置 - 添加初始化示例数据脚本 - 更新项目文档
141 lines
4.5 KiB
Python
141 lines
4.5 KiB
Python
"""
|
|
对账执行 API
|
|
|
|
执行对账任务
|
|
"""
|
|
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
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.services.reconciliation.engine import run_reconciliation
|
|
|
|
|
|
router = APIRouter(prefix="/api/reconciliation", tags=["对账执行"])
|
|
|
|
|
|
@router.post("/execute/{task_id}")
|
|
async def execute_reconciliation(
|
|
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="无权访问该任务")
|
|
|
|
if task.status not in ["MAPPING_COMPLETED", "DATA_UPLOADED"]:
|
|
raise HTTPException(status_code=400, detail="任务状态不允许执行对账")
|
|
|
|
try:
|
|
# 获取上传的数据
|
|
salary_data = task.salary_data or []
|
|
social_security_data = task.social_security_data or []
|
|
tax_data = task.tax_data or []
|
|
bank_data = task.bank_data if hasattr(task, 'bank_data') else None
|
|
|
|
# 执行对账
|
|
result = await run_reconciliation(
|
|
db=db,
|
|
company_id=company_id,
|
|
task_id=task_id,
|
|
period=task.period,
|
|
salary_records=salary_data,
|
|
social_security_records=social_security_data,
|
|
tax_records=tax_data,
|
|
bank_records=bank_data,
|
|
enable_bank_rules=bool(bank_data),
|
|
)
|
|
|
|
return {
|
|
"success": True,
|
|
"task_id": task_id,
|
|
"result": {
|
|
"total_employees": result.total_employees,
|
|
"matched_employees": result.matched_employees,
|
|
"exception_count": result.exception_count,
|
|
"exceptions_by_type": result.exceptions_by_type,
|
|
"exceptions_by_severity": result.exceptions_by_severity,
|
|
"execution_time_ms": result.execution_time_ms,
|
|
"completed_at": result.completed_at,
|
|
},
|
|
}
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"对账执行失败: {str(e)}")
|
|
|
|
|
|
@router.get("/result/{task_id}")
|
|
async def get_reconciliation_result(
|
|
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="无权访问该任务")
|
|
|
|
# 获取异常统计
|
|
from app.services.exception_service import ExceptionService
|
|
service = ExceptionService(db)
|
|
summary = await service.get_exception_summary(task_id=task_id, company_id=company_id)
|
|
|
|
return {
|
|
"task_id": task_id,
|
|
"period": task.period,
|
|
"total_employees": task.total_employees,
|
|
"matched_employees": task.matched_count,
|
|
"exception_count": task.exception_count,
|
|
"exceptions_by_type": summary.get("by_type", {}),
|
|
"exceptions_by_severity": summary.get("by_severity", {}),
|
|
"exceptions_by_status": summary.get("by_status", {}),
|
|
"completed_at": task.updated_at.isoformat() if task.updated_at else None,
|
|
}
|
|
|
|
|
|
@router.post("/retry/{task_id}")
|
|
async def retry_reconciliation(
|
|
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="无权访问该任务")
|
|
|
|
# 重置任务状态
|
|
task.status = "PROCESSING"
|
|
task.matched_count = 0
|
|
task.exception_count = 0
|
|
await db.commit()
|
|
|
|
# 执行对账
|
|
return await execute_reconciliation(task_id, db, company_id) |