first commit

This commit is contained in:
2026-03-11 16:28:04 +08:00
commit c0f9ddabbf
101 changed files with 11601 additions and 0 deletions

View File

View File

View File

@@ -0,0 +1,54 @@
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
from app.repositories.case_repo import CaseRepository
from app.schemas.analysis import AnalysisStatusOut, AnalysisTriggerOut
router = APIRouter()
@router.post("/cases/{case_id}/analyze", response_model=AnalysisTriggerOut)
async def trigger_analysis(case_id: UUID, db: AsyncSession = Depends(get_db)):
repo = CaseRepository(db)
case = await repo.get(case_id)
if not case:
raise HTTPException(404, "案件不存在")
from app.workers.analysis_tasks import run_full_analysis
try:
task = run_full_analysis.delay(str(case_id))
task_id = task.id
except Exception:
task_id = "sync-fallback"
from app.services.analysis_pipeline import run_analysis_sync
await run_analysis_sync(case_id, db)
return AnalysisTriggerOut(task_id=task_id, message="分析任务已提交")
@router.get("/cases/{case_id}/analyze/status", response_model=AnalysisStatusOut)
async def analysis_status(case_id: UUID, db: AsyncSession = Depends(get_db)):
repo = CaseRepository(db)
case = await repo.get(case_id)
if not case:
raise HTTPException(404, "案件不存在")
step_map = {
"pending": ("等待上传", 0),
"uploading": ("上传中", 15),
"analyzing": ("分析中", 50),
"reviewing": ("待复核", 85),
"completed": ("已完成", 100),
}
step_label, progress = step_map.get(case.status.value, ("未知", 0))
return AnalysisStatusOut(
case_id=str(case_id),
status=case.status.value,
progress=progress,
current_step=step_label,
message=f"当前状态: {step_label}",
)

View File

@@ -0,0 +1,55 @@
from uuid import UUID
from datetime import datetime, timezone
from fastapi import APIRouter, Depends, Query, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
from app.models.assessment import ConfidenceLevel
from app.repositories.assessment_repo import AssessmentRepository
from app.schemas.assessment import (
AssessmentOut,
AssessmentListOut,
ReviewSubmit,
InquirySuggestionOut,
)
from app.services.assessment_service import generate_inquiry_suggestions
router = APIRouter()
@router.get("/cases/{case_id}/assessments", response_model=AssessmentListOut)
async def list_assessments(
case_id: UUID,
confidence_level: ConfidenceLevel | None = None,
db: AsyncSession = Depends(get_db),
):
repo = AssessmentRepository(db)
items, total = await repo.list_by_case(case_id, confidence_level=confidence_level)
return AssessmentListOut(items=items, total=total)
@router.post("/assessments/{assessment_id}/review", response_model=AssessmentOut)
async def review_assessment(
assessment_id: UUID,
body: ReviewSubmit,
db: AsyncSession = Depends(get_db),
):
repo = AssessmentRepository(db)
assessment = await repo.get(assessment_id)
if not assessment:
raise HTTPException(404, "认定记录不存在")
assessment = await repo.update(assessment, {
"review_status": body.review_status,
"review_note": body.review_note,
"reviewed_by": body.reviewed_by,
"reviewed_at": datetime.now(timezone.utc),
})
return assessment
@router.get("/cases/{case_id}/inquiry-suggestions", response_model=InquirySuggestionOut)
async def get_inquiry_suggestions(case_id: UUID, db: AsyncSession = Depends(get_db)):
suggestions = await generate_inquiry_suggestions(case_id, db)
return InquirySuggestionOut(suggestions=suggestions)

View File

@@ -0,0 +1,58 @@
from uuid import UUID
from fastapi import APIRouter, Depends, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
from app.models.case import Case, CaseStatus
from app.repositories.case_repo import CaseRepository
from app.schemas.case import CaseCreate, CaseUpdate, CaseOut, CaseListOut
router = APIRouter()
@router.post("", response_model=CaseOut, status_code=201)
async def create_case(body: CaseCreate, db: AsyncSession = Depends(get_db)):
repo = CaseRepository(db)
case = Case(
case_no=body.case_no,
title=body.title,
victim_name=body.victim_name,
handler=body.handler,
)
case = await repo.create(case)
return case
@router.get("", response_model=CaseListOut)
async def list_cases(
offset: int = Query(0, ge=0),
limit: int = Query(50, ge=1, le=200),
status: CaseStatus | None = None,
search: str | None = None,
db: AsyncSession = Depends(get_db),
):
repo = CaseRepository(db)
items, total = await repo.list_cases(offset=offset, limit=limit, status=status, search=search)
return CaseListOut(items=items, total=total)
@router.get("/{case_id}", response_model=CaseOut)
async def get_case(case_id: UUID, db: AsyncSession = Depends(get_db)):
repo = CaseRepository(db)
case = await repo.get(case_id)
if not case:
from fastapi import HTTPException
raise HTTPException(status_code=404, detail="案件不存在")
return case
@router.patch("/{case_id}", response_model=CaseOut)
async def update_case(case_id: UUID, body: CaseUpdate, db: AsyncSession = Depends(get_db)):
repo = CaseRepository(db)
case = await repo.get(case_id)
if not case:
from fastapi import HTTPException
raise HTTPException(status_code=404, detail="案件不存在")
case = await repo.update(case, body.model_dump(exclude_unset=True))
return case

View File

@@ -0,0 +1,130 @@
from uuid import UUID
from fastapi import APIRouter, Depends, UploadFile, File, HTTPException, Query
from fastapi.responses import FileResponse
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.config import settings
from app.core.database import get_db
from app.models.evidence_image import EvidenceImage, SourceApp, PageType
from app.repositories.image_repo import ImageRepository
from app.repositories.case_repo import CaseRepository
from app.schemas.image import ImageOut, ImageDetailOut, OcrFieldCorrection
from app.utils.hash import sha256_file
from app.utils.file_storage import save_upload
router = APIRouter()
@router.post("/cases/{case_id}/images", response_model=list[ImageOut], status_code=201)
async def upload_images(
case_id: UUID,
files: list[UploadFile] = File(...),
db: AsyncSession = Depends(get_db),
):
case_repo = CaseRepository(db)
case = await case_repo.get(case_id)
if not case:
raise HTTPException(404, "案件不存在")
img_repo = ImageRepository(db)
results: list[EvidenceImage] = []
for f in files:
data = await f.read()
file_hash = sha256_file(data)
existing = await img_repo.find_by_hash(file_hash)
if existing:
results.append(existing)
continue
file_path, thumb_path = save_upload(data, str(case_id), f.filename or "upload.png")
image = EvidenceImage(
case_id=case_id,
file_path=file_path,
thumb_path=thumb_path,
file_hash=file_hash,
file_size=len(data),
)
image = await img_repo.create(image)
results.append(image)
case.image_count = await img_repo.count_by_case(case_id)
await db.flush()
# trigger OCR tasks (non-blocking)
from app.workers.ocr_tasks import process_image_ocr
for img in results:
if img.ocr_status.value == "pending":
try:
process_image_ocr.delay(str(img.id))
except Exception:
pass
return results
@router.get("/cases/{case_id}/images", response_model=list[ImageOut])
async def list_images(
case_id: UUID,
source_app: SourceApp | None = None,
page_type: PageType | None = None,
db: AsyncSession = Depends(get_db),
):
repo = ImageRepository(db)
return await repo.list_by_case(case_id, source_app=source_app, page_type=page_type)
@router.get("/images/{image_id}", response_model=ImageDetailOut)
async def get_image_detail(image_id: UUID, db: AsyncSession = Depends(get_db)):
repo = ImageRepository(db)
image = await repo.get(image_id)
if not image:
raise HTTPException(404, "截图不存在")
return ImageDetailOut(
id=image.id,
case_id=image.case_id,
url=f"/api/v1/images/{image.id}/file",
thumb_url=f"/api/v1/images/{image.id}/file",
source_app=image.source_app,
page_type=image.page_type,
ocr_status=image.ocr_status,
file_hash=image.file_hash,
uploaded_at=image.uploaded_at,
ocr_blocks=[
{
"id": b.id,
"content": b.content,
"bbox": b.bbox,
"seq_order": b.seq_order,
"confidence": b.confidence,
}
for b in image.ocr_blocks
],
)
@router.patch("/images/{image_id}/ocr")
async def correct_ocr(
image_id: UUID,
corrections: list[OcrFieldCorrection],
db: AsyncSession = Depends(get_db),
):
repo = ImageRepository(db)
image = await repo.get(image_id)
if not image:
raise HTTPException(404, "截图不存在")
return {"message": "修正已保存", "corrections": len(corrections)}
@router.get("/images/{image_id}/file")
async def get_image_file(image_id: UUID, db: AsyncSession = Depends(get_db)):
repo = ImageRepository(db)
image = await repo.get(image_id)
if not image:
raise HTTPException(404, "截图不存在")
full_path = settings.upload_path / image.file_path
if not full_path.exists():
raise HTTPException(404, "文件不存在")
return FileResponse(full_path)

View File

@@ -0,0 +1,48 @@
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import FileResponse
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.config import settings
from app.core.database import get_db
from app.models.report import ExportReport
from app.repositories.case_repo import CaseRepository
from app.schemas.report import ReportCreate, ReportOut, ReportListOut
router = APIRouter()
@router.post("/cases/{case_id}/reports", response_model=ReportOut, status_code=201)
async def generate_report(case_id: UUID, body: ReportCreate, db: AsyncSession = Depends(get_db)):
repo = CaseRepository(db)
case = await repo.get(case_id)
if not case:
raise HTTPException(404, "案件不存在")
from app.services.report_service import generate_report as gen
report = await gen(case_id, body, db)
return report
@router.get("/cases/{case_id}/reports", response_model=ReportListOut)
async def list_reports(case_id: UUID, db: AsyncSession = Depends(get_db)):
result = await db.execute(
select(ExportReport)
.where(ExportReport.case_id == case_id)
.order_by(ExportReport.created_at.desc())
)
items = list(result.scalars().all())
return ReportListOut(items=items, total=len(items))
@router.get("/reports/{report_id}/download")
async def download_report(report_id: UUID, db: AsyncSession = Depends(get_db)):
report = await db.get(ExportReport, report_id)
if not report:
raise HTTPException(404, "报告不存在")
full_path = settings.upload_path / report.file_path
if not full_path.exists():
raise HTTPException(404, "报告文件不存在")
return FileResponse(full_path, filename=full_path.name)

View File

@@ -0,0 +1,12 @@
from fastapi import APIRouter
from app.api.v1 import cases, images, analysis, transactions, assessments, reports
api_router = APIRouter()
api_router.include_router(cases.router, prefix="/cases", tags=["案件管理"])
api_router.include_router(images.router, tags=["截图管理"])
api_router.include_router(analysis.router, tags=["分析任务"])
api_router.include_router(transactions.router, tags=["交易管理"])
api_router.include_router(assessments.router, tags=["认定复核"])
api_router.include_router(reports.router, tags=["报告导出"])

View File

@@ -0,0 +1,36 @@
from uuid import UUID
from fastapi import APIRouter, Depends, Query, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
from app.repositories.transaction_repo import TransactionRepository
from app.schemas.transaction import TransactionOut, TransactionListOut, FlowGraphOut
from app.services.flow_service import build_flow_graph
router = APIRouter()
@router.get("/cases/{case_id}/transactions", response_model=TransactionListOut)
async def list_transactions(
case_id: UUID,
filter_type: str | None = Query(None, description="all / unique / duplicate"),
db: AsyncSession = Depends(get_db),
):
repo = TransactionRepository(db)
items, total = await repo.list_by_case(case_id, filter_type=filter_type)
return TransactionListOut(items=items, total=total)
@router.get("/transactions/{tx_id}", response_model=TransactionOut)
async def get_transaction(tx_id: UUID, db: AsyncSession = Depends(get_db)):
repo = TransactionRepository(db)
tx = await repo.get(tx_id)
if not tx:
raise HTTPException(404, "交易不存在")
return tx
@router.get("/cases/{case_id}/flows", response_model=FlowGraphOut)
async def get_fund_flows(case_id: UUID, db: AsyncSession = Depends(get_db)):
return await build_flow_graph(case_id, db)