""" 字段映射 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": "规则已删除"}