feat: 初始化孕育无忧全栈项目基础代码结构
完成项目初始搭建,包含后端FastAPI+MongoDB服务、前端React+Vite项目的完整基础结构,添加各类配置文件与依赖清单,涵盖核心业务初始框架。
This commit is contained in:
290
backend/app/routers/report.py
Normal file
290
backend/app/routers/report.py
Normal file
@@ -0,0 +1,290 @@
|
||||
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,
|
||||
)
|
||||
Reference in New Issue
Block a user