Files
shutiaozhao127-boop cfdeeb5e41 chore: 初始化项目结构并添加基础配置文件
添加后端API和前端项目的初始目录结构,包括应用层模块、工具模块和模型定义
配置前后端的.dockerignore文件以排除开发环境文件
添加前端样式文件,包括字体、Tailwind CSS和主题配置
初始化Python虚拟环境中的类型标记文件
2026-04-01 16:55:24 +08:00

291 lines
9.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import logging
from datetime import datetime, timezone, date
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
from app.middleware.auth import get_current_user
from app.models.report import Report, DataItem
from app.models.user import User
from app.schemas.report import (
ReportVerifyRequest,
ReportResponse,
ReportListItem,
DataItemSchema,
)
from app.services.storage import save_upload_file
from app.services.ocr import ocr_recognize
from app.utils.helpers import parse_object_id, mask_name
logger = logging.getLogger(__name__)
router = APIRouter()
ALLOWED_CONTENT_TYPES = {"image/jpeg", "image/png", "image/webp"}
MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB
@router.post("/upload")
async def upload_report_image(
file: UploadFile = File(...),
user: User = Depends(get_current_user),
):
"""上传报告图片并进行 OCR 识别"""
# Validate file type
if file.content_type not in ALLOWED_CONTENT_TYPES:
raise HTTPException(
status_code=400,
detail=f"不支持的文件类型: {file.content_type},请上传 JPG/PNG/WebP 图片",
)
# Validate file size (read content and check)
content = await file.read()
if len(content) > MAX_FILE_SIZE:
raise HTTPException(status_code=400, detail="文件大小不能超过 10MB")
# Reset file position for storage
await file.seek(0)
url = await save_upload_file(file, sub_dir="reports")
# OCR recognition
try:
ocr_result = await ocr_recognize(url)
except FileNotFoundError:
raise HTTPException(status_code=500, detail="文件保存异常,请重试")
except RuntimeError as e:
# OCR 服务已捕获并转换为友好消息
logger.warning("OCR 识别失败: %s", e)
raise HTTPException(status_code=503, detail=str(e))
except Exception as e:
logger.exception("OCR 识别失败")
raise HTTPException(
status_code=500,
detail="AI识别服务暂时不可用,请稍后重试",
)
# 检查是否为孕期相关报告
if not ocr_result.get("is_pregnancy_report", True):
reject_reason = ocr_result.get("reject_reason", "")
raise HTTPException(
status_code=422,
detail=f"请上传孕期检查报告。{reject_reason}",
)
# 根据报告日期 + 末次经期推算检查时的孕周
report_date_str = ocr_result.get("report_date")
pregnancy_week_at_report = user.pregnancy_week # 默认用用户当前孕周
if report_date_str and user.last_period_date:
try:
report_dt = date.fromisoformat(report_date_str)
lmp = date.fromisoformat(user.last_period_date) if isinstance(user.last_period_date, str) else user.last_period_date
delta_days = (report_dt - lmp).days
if 0 < delta_days < 300:
pregnancy_week_at_report = delta_days // 7
except (ValueError, TypeError):
pass
# Create report with OCR data
data_items = [DataItem(**item) for item in ocr_result.get("data_items", [])]
report = Report(
user_id=user.id,
report_type=ocr_result.get("report_type", ""),
pregnancy_week=pregnancy_week_at_report,
report_date=report_date_str,
image_urls=[url],
data_items=data_items,
patient_name=ocr_result.get("patient_name"),
status="pending",
)
await report.insert()
return {
"report_id": str(report.id),
"image_url": url,
"ocr_result": {
"report_type": report.report_type,
"data_items": [item.model_dump() for item in data_items],
"patient_name": mask_name(report.patient_name),
},
}
@router.post("/{report_id}/add-image")
async def add_image_to_report(
report_id: str,
file: UploadFile = File(...),
user: User = Depends(get_current_user),
):
"""向已有报告追加图片并进行 OCR 识别,合并数据"""
oid = parse_object_id(report_id)
report = await Report.get(oid)
if not report or report.user_id != user.id:
raise HTTPException(status_code=404, detail="报告不存在")
if file.content_type not in ALLOWED_CONTENT_TYPES:
raise HTTPException(
status_code=400,
detail=f"不支持的文件类型: {file.content_type},请上传 JPG/PNG/WebP 图片",
)
content = await file.read()
if len(content) > MAX_FILE_SIZE:
raise HTTPException(status_code=400, detail="文件大小不能超过 10MB")
await file.seek(0)
url = await save_upload_file(file, sub_dir="reports")
# OCR recognition
try:
ocr_result = await ocr_recognize(url)
except FileNotFoundError:
raise HTTPException(status_code=500, detail="文件保存异常,请重试")
except RuntimeError as e:
logger.warning("OCR 识别失败: %s", e)
raise HTTPException(status_code=503, detail=str(e))
except Exception:
logger.exception("OCR 识别失败")
raise HTTPException(status_code=500, detail="AI识别服务暂时不可用,请稍后重试")
# 检查是否为孕期相关报告
if not ocr_result.get("is_pregnancy_report", True):
reject_reason = ocr_result.get("reject_reason", "")
raise HTTPException(
status_code=422,
detail=f"请上传孕期检查报告。{reject_reason}",
)
# Merge new data into existing report
new_items = [DataItem(**item) for item in ocr_result.get("data_items", [])]
report.image_urls.append(url)
report.data_items.extend(new_items)
# Update report_type if current report has multiple types
new_type = ocr_result.get("report_type", "")
if new_type and new_type != report.report_type:
report.report_type = f"{report.report_type}+{new_type}" if report.report_type else new_type
report.updated_at = datetime.now(timezone.utc)
await report.save()
return {
"report_id": str(report.id),
"image_url": url,
"image_count": len(report.image_urls),
"ocr_result": {
"report_type": new_type,
"data_items": [item.model_dump() for item in new_items],
"patient_name": mask_name(ocr_result.get("patient_name")),
},
"total_items": len(report.data_items),
}
@router.put("/{report_id}/verify", response_model=ReportResponse)
async def verify_report(
report_id: str,
req: ReportVerifyRequest,
user: User = Depends(get_current_user),
):
"""数据校对确认"""
oid = parse_object_id(report_id)
report = await Report.get(oid)
if not report or report.user_id != user.id:
raise HTTPException(status_code=404, detail="报告不存在")
report.data_items = [DataItem(**item.model_dump()) for item in req.data_items]
report.symptoms = req.symptoms
report.notes = req.notes
if req.report_type:
report.report_type = req.report_type
report.status = "verified"
# Calculate overall status
statuses = [item.status for item in report.data_items]
if any(s in ("high", "low") for s in statuses):
abnormal_count = sum(1 for s in statuses if s != "normal")
report.overall_status = "warning" if abnormal_count >= 3 else "attention"
else:
report.overall_status = "normal"
report.updated_at = datetime.now(timezone.utc)
await report.save()
return _report_to_response(report)
@router.get("/list", response_model=list[ReportListItem])
async def list_reports(
limit: int = 50,
user: User = Depends(get_current_user),
):
"""获取报告列表"""
reports = await Report.find(
Report.user_id == user.id
).sort("-created_at").limit(limit).to_list()
return [
ReportListItem(
id=str(r.id),
report_type=r.report_type,
pregnancy_week=r.pregnancy_week,
overall_status=r.overall_status,
summary=_generate_summary(r),
item_count=len(r.data_items),
created_at=r.created_at,
)
for r in reports
]
@router.get("/{report_id}", response_model=ReportResponse)
async def get_report(report_id: str, user: User = Depends(get_current_user)):
"""获取报告详情"""
oid = parse_object_id(report_id)
report = await Report.get(oid)
if not report or report.user_id != user.id:
raise HTTPException(status_code=404, detail="报告不存在")
return _report_to_response(report)
def _generate_summary(report: Report) -> str:
"""从报告指标中生成有意义的摘要文本"""
if not report.data_items:
return "暂无指标数据"
# 优先展示异常指标
abnormal = [
item for item in report.data_items if item.status in ("high", "low")
]
# 再补充正常的关键指标
normal = [item for item in report.data_items if item.status == "normal"]
parts = []
# 先添加异常指标(最多2个)
for item in abnormal[:2]:
flag = "↑偏高" if item.status == "high" else "↓偏低"
val = f"{item.name}{item.value}{item.unit}({flag})" if item.unit else f"{item.name}{item.value}({flag})"
parts.append(val)
# 再补充正常指标,总共最多3个
for item in normal[:3 - len(parts)]:
val = f"{item.name}{item.value}{item.unit}" if item.unit else f"{item.name}{item.value}"
parts.append(val)
if not parts:
return f"共{len(report.data_items)}项指标"
return ",".join(parts)
def _report_to_response(report: Report) -> ReportResponse:
return ReportResponse(
id=str(report.id),
report_type=report.report_type,
pregnancy_week=report.pregnancy_week,
image_urls=report.image_urls,
data_items=[DataItemSchema(**item.model_dump()) for item in report.data_items],
symptoms=report.symptoms,
notes=report.notes,
status=report.status,
overall_status=report.overall_status,
created_at=report.created_at,
)