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 数据库迁移配置
- 添加初始化示例数据脚本
- 更新项目文档
This commit is contained in:
freedakgmail
2026-07-07 09:04:47 +08:00
parent 8487f6eadf
commit 33b4c734aa
117 changed files with 22070 additions and 295 deletions
+288
View File
@@ -0,0 +1,288 @@
"""
字段映射 API 端点
提供字段识别、映射确认和规则管理功能
"""
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
from app.services.auth import AuthService
from app.core.tenant import get_current_company
from app.schemas.mapping import (
FieldMappingResponse,
FieldMappingUpdate,
ConfirmMappingsRequest,
ConfirmMappingsResponse,
SaveAsRuleRequest,
SaveAsRuleResponse,
CompanyRuleResponse,
UpdateRuleStatusRequest,
RecognizeFieldsRequest,
RecognizeFieldsResponse,
TaskMappingOverview,
FileMappingSummary,
)
from app.services.mapping import MappingService
from app.models.user import User
from app.models.company import Company
router = APIRouter(prefix="/mappings", tags=["字段映射"])
def get_mapping_service(db: AsyncSession = Depends(get_db)) -> MappingService:
return MappingService(db)
@router.post("/recognize", response_model=RecognizeFieldsResponse)
async def recognize_fields(
request: RecognizeFieldsRequest,
company_id: int = Depends(get_current_company),
current_user: User = Depends(AuthService.get_current_user),
service: MappingService = Depends(get_mapping_service),
):
"""
触发 AI 字段识别
- 读取已上传文件的表头和样例数据
- 调用 AI 识别服务
- 保存映射建议
- 返回映射结果
"""
try:
mappings = await service.recognize_and_save(
company_id=company_id,
file_id=request.file_id,
file_type=request.file_type
)
# 统计置信度分布
high = sum(1 for m in mappings if m.confidence > 0.9)
medium = sum(1 for m in mappings if 0.7 <= m.confidence <= 0.9)
low = sum(1 for m in mappings if m.confidence < 0.7)
return RecognizeFieldsResponse(
file_id=request.file_id,
mappings=[FieldMappingResponse.model_validate(m) for m in mappings],
high_confidence_count=high,
medium_confidence_count=medium,
low_confidence_count=low,
needs_review=low > 0 or medium > 0
)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=f"字段识别失败: {str(e)}")
@router.get("/task/{task_id}", response_model=TaskMappingOverview)
async def get_task_mappings(
task_id: int,
company_id: int = Depends(get_current_company),
current_user: User = Depends(AuthService.get_current_user),
service: MappingService = Depends(get_mapping_service),
):
"""
获取任务的字段映射概览
- 按文件分组返回所有映射
- 包含置信度统计
"""
try:
grouped_mappings = await service.get_mappings_by_task(task_id, company_id)
files = []
total_fields = 0
confirmed_fields = 0
for file_id, mappings in grouped_mappings.items():
file = mappings[0].file if mappings else None
high = sum(1 for m in mappings if m.confidence > 0.9)
medium = sum(1 for m in mappings if 0.7 <= m.confidence <= 0.9)
low = sum(1 for m in mappings if m.confidence < 0.7)
confirmed = sum(1 for m in mappings if m.confirmed)
files.append(FileMappingSummary(
file_id=file_id,
file_type=file.file_type if file else "",
file_name=file.original_filename if file else "",
total_fields=len(mappings),
confirmed_fields=confirmed,
high_confidence=high,
medium_confidence=medium,
low_confidence=low,
mappings=[FieldMappingResponse.model_validate(m) for m in mappings]
))
total_fields += len(mappings)
confirmed_fields += confirmed
needs_review = any(
f.medium_confidence > 0 or f.low_confidence > 0
for f in files
)
return TaskMappingOverview(
task_id=task_id,
total_files=len(files),
total_fields=total_fields,
confirmed_fields=confirmed_fields,
needs_review=needs_review,
files=files
)
except Exception as e:
raise HTTPException(status_code=500, detail=f"获取映射失败: {str(e)}")
@router.get("/{mapping_id}", response_model=FieldMappingResponse)
async def get_mapping(
mapping_id: int,
current_user: User = Depends(AuthService.get_current_user),
service: MappingService = Depends(get_mapping_service),
):
"""获取单个字段映射"""
# 这里需要添加 company_id 检查
mapping = await service.db.get(mapping_id)
if not mapping:
raise HTTPException(status_code=404, detail="映射不存在")
return FieldMappingResponse.model_validate(mapping)
@router.put("/{mapping_id}", response_model=FieldMappingResponse)
async def update_mapping(
mapping_id: int,
update: FieldMappingUpdate,
current_user: User = Depends(AuthService.get_current_user),
service: MappingService = Depends(get_mapping_service),
):
"""
修改字段映射
- 修改标准字段
- 标记跳过
"""
mapping = await service.update_mapping(
mapping_id=mapping_id,
standard_field=update.standard_field,
is_skipped=update.is_skipped
)
if not mapping:
raise HTTPException(status_code=404, detail="映射不存在")
return FieldMappingResponse.model_validate(mapping)
@router.post("/confirm", response_model=ConfirmMappingsResponse)
async def confirm_mappings(
request: ConfirmMappingsRequest,
current_user: User = Depends(AuthService.get_current_user),
service: MappingService = Depends(get_mapping_service),
):
"""
批量确认字段映射
- 确认后映射将用于数据处理
- 不会自动保存为规则
"""
confirmed = await service.confirm_mappings(
mapping_ids=request.mapping_ids,
user_id=current_user.id
)
return ConfirmMappingsResponse(
confirmed_count=len(confirmed),
mappings=[FieldMappingResponse.model_validate(m) for m in confirmed]
)
@router.post("/{mapping_id}/save-as-rule", response_model=SaveAsRuleResponse)
async def save_mapping_as_rule(
mapping_id: int,
current_user: User = Depends(AuthService.get_current_user),
service: MappingService = Depends(get_mapping_service),
):
"""
将字段映射保存为企业规则
- 下次上传同类文件时自动应用规则
- 可在规则管理页面查看和管理
"""
from app.models.field_mapping import FieldMapping
# 获取映射
mapping = await service.db.get(FieldMapping, mapping_id)
if not mapping:
raise HTTPException(status_code=404, detail="映射不存在")
# 保存为规则
rule = await service.save_as_rule(mapping, current_user.id)
return SaveAsRuleResponse(
rule_id=rule.id,
source_field=mapping.source_field,
target_field=mapping.standard_field,
message=f"规则已保存:'{mapping.source_field}' -> '{mapping.standard_field}'"
)
# ===== 规则管理 API =====
@router.get("/rules/list", response_model=List[CompanyRuleResponse])
async def list_rules(
rule_type: Optional[str] = Query(None, description="规则类型"),
company_id: int = Depends(get_current_company),
current_user: User = Depends(AuthService.get_current_user),
service: MappingService = Depends(get_mapping_service),
):
"""获取企业的规则列表"""
rules = await service.get_company_rules(company_id, rule_type)
return [CompanyRuleResponse.model_validate(r) for r in rules]
@router.patch("/rules/{rule_id}/status", response_model=CompanyRuleResponse)
async def update_rule_status(
rule_id: int,
request: UpdateRuleStatusRequest,
company_id: int = Depends(get_current_company),
current_user: User = Depends(AuthService.get_current_user),
service: MappingService = Depends(get_mapping_service),
):
"""更新规则状态(启用/停用)"""
rule = await service.update_rule_status(rule_id, request.status)
if not rule:
raise HTTPException(status_code=404, detail="规则不存在")
if rule.company_id != company_id:
raise HTTPException(status_code=403, detail="无权操作此规则")
return CompanyRuleResponse.model_validate(rule)
@router.delete("/rules/{rule_id}")
async def delete_rule(
rule_id: int,
company_id: int = Depends(get_current_company),
current_user: User = Depends(AuthService.get_current_user),
service: MappingService = Depends(get_mapping_service),
):
"""删除规则"""
from app.models.company_rule import CompanyRule
rule = await service.db.get(CompanyRule, rule_id)
if not rule:
raise HTTPException(status_code=404, detail="规则不存在")
if rule.company_id != company_id:
raise HTTPException(status_code=403, detail="无权操作此规则")
success = await service.delete_rule(rule_id)
if not success:
raise HTTPException(status_code=500, detail="删除失败")
return {"message": "规则已删除"}