chore: 初始化项目结构并添加基础配置文件
添加后端API和前端项目的初始目录结构,包括应用层模块、工具模块和模型定义 配置前后端的.dockerignore文件以排除开发环境文件 添加前端样式文件,包括字体、Tailwind CSS和主题配置 初始化Python虚拟环境中的类型标记文件
This commit is contained in:
9
后端-孕期报告解读-api/app/routers/__init__.py
Normal file
9
后端-孕期报告解读-api/app/routers/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from app.routers.auth import router as auth_router
|
||||
from app.routers.user import router as user_router
|
||||
from app.routers.report import router as report_router
|
||||
from app.routers.analysis import router as analysis_router
|
||||
from app.routers.chat import router as chat_router
|
||||
from app.routers.health_records import router as health_records_router
|
||||
from app.routers.calendar import router as calendar_router
|
||||
from app.routers.doctor import router as doctor_router
|
||||
from app.routers.share import router as share_router
|
||||
147
后端-孕期报告解读-api/app/routers/analysis.py
Normal file
147
后端-孕期报告解读-api/app/routers/analysis.py
Normal file
@@ -0,0 +1,147 @@
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from app.middleware.auth import get_current_user
|
||||
from app.models.analysis import (
|
||||
Analysis,
|
||||
CategoryInterpretation,
|
||||
IndicatorInterpretation,
|
||||
ActionItem,
|
||||
)
|
||||
from app.models.report import Report
|
||||
from app.models.user import User
|
||||
from app.services.ai_service import analyze_report
|
||||
from app.routers.calendar import link_report_to_checkup
|
||||
from app.utils.helpers import parse_object_id
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/{report_id}/generate")
|
||||
async def generate_analysis(
|
||||
report_id: str, user: User = Depends(get_current_user)
|
||||
):
|
||||
"""生成 AI 解读"""
|
||||
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="报告不存在")
|
||||
|
||||
# Check if analysis already exists
|
||||
existing = await Analysis.find_one(Analysis.report_id == report.id)
|
||||
if existing:
|
||||
return _analysis_to_dict(existing)
|
||||
|
||||
# Call Claude API
|
||||
try:
|
||||
result = await analyze_report(report, user)
|
||||
except Exception:
|
||||
logger.exception("AI 解读失败 report_id=%s", report_id)
|
||||
raise HTTPException(status_code=502, detail="AI 解读服务暂时不可用,请稍后重试")
|
||||
|
||||
# Save analysis
|
||||
categories = []
|
||||
for cat in result.get("categories", []):
|
||||
indicators = [
|
||||
IndicatorInterpretation(**ind) for ind in cat.get("indicators", [])
|
||||
]
|
||||
categories.append(
|
||||
CategoryInterpretation(
|
||||
category=cat.get("category", ""),
|
||||
status=cat.get("status", "normal"),
|
||||
indicators=indicators,
|
||||
)
|
||||
)
|
||||
|
||||
action_items = [ActionItem(**a) for a in result.get("action_items", [])]
|
||||
|
||||
analysis = Analysis(
|
||||
report_id=report.id,
|
||||
user_id=user.id,
|
||||
overall_status=result.get("overall_status", "normal"),
|
||||
risk_level=result.get("risk_level", "low"),
|
||||
overall_summary=result.get("overall_summary", ""),
|
||||
emotional_summary=result.get("emotional_summary", ""),
|
||||
categories=categories,
|
||||
action_items=action_items,
|
||||
tips=result.get("tips", []),
|
||||
)
|
||||
await analysis.insert()
|
||||
|
||||
# Update report status
|
||||
report.status = "analyzed"
|
||||
report.overall_status = analysis.overall_status
|
||||
report.updated_at = datetime.now(timezone.utc)
|
||||
await report.save()
|
||||
|
||||
# Auto-link report to the nearest calendar checkup
|
||||
if report.pregnancy_week:
|
||||
try:
|
||||
await link_report_to_checkup(user.id, str(report.id), report.pregnancy_week)
|
||||
except Exception:
|
||||
logger.warning("自动关联日历失败 report_id=%s", report_id)
|
||||
|
||||
return _analysis_to_dict(analysis)
|
||||
|
||||
|
||||
@router.get("/{report_id}")
|
||||
async def get_analysis(report_id: str, user: User = Depends(get_current_user)):
|
||||
"""获取报告的 AI 解读结果"""
|
||||
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="报告不存在")
|
||||
|
||||
analysis = await Analysis.find_one(Analysis.report_id == report.id)
|
||||
if not analysis:
|
||||
raise HTTPException(status_code=404, detail="暂无解读结果,请先生成")
|
||||
|
||||
return _analysis_to_dict(analysis)
|
||||
|
||||
|
||||
def _analysis_to_dict(analysis: Analysis) -> dict:
|
||||
return {
|
||||
"id": str(analysis.id),
|
||||
"report_id": str(analysis.report_id),
|
||||
"overall_status": analysis.overall_status,
|
||||
"risk_level": analysis.risk_level,
|
||||
"overall_summary": analysis.overall_summary,
|
||||
"emotional_summary": analysis.emotional_summary,
|
||||
"categories": [
|
||||
{
|
||||
"category": cat.category,
|
||||
"status": cat.status,
|
||||
"indicators": [
|
||||
{
|
||||
"name": ind.name,
|
||||
"nickname": ind.nickname,
|
||||
"value": ind.value,
|
||||
"unit": "",
|
||||
"reference_range": "",
|
||||
"status": ind.status,
|
||||
"what_is_it": ind.what_is_it,
|
||||
"baby_impact": ind.impact,
|
||||
"suggestion": ind.suggestion,
|
||||
"follow_up": ind.recheck_reminder or "",
|
||||
}
|
||||
for ind in cat.indicators
|
||||
],
|
||||
}
|
||||
for cat in analysis.categories
|
||||
],
|
||||
"action_items": [
|
||||
{
|
||||
"priority": a.priority,
|
||||
"title": a.content,
|
||||
"description": a.category,
|
||||
"timeframe": "",
|
||||
}
|
||||
for a in analysis.action_items
|
||||
],
|
||||
"tips": analysis.tips,
|
||||
"created_at": analysis.created_at.isoformat(),
|
||||
}
|
||||
87
后端-孕期报告解读-api/app/routers/auth.py
Normal file
87
后端-孕期报告解读-api/app/routers/auth.py
Normal file
@@ -0,0 +1,87 @@
|
||||
import uuid
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pymongo.errors import DuplicateKeyError
|
||||
|
||||
from app.models.user import User
|
||||
from app.schemas.auth import WechatLoginRequest, PhoneLoginRequest, TokenResponse
|
||||
from app.middleware.auth import create_access_token
|
||||
from app.services.wechat import code_to_session
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/demo-login", response_model=TokenResponse)
|
||||
async def demo_login():
|
||||
"""Web 体验用的演示登录,每次创建独立的临时用户"""
|
||||
demo_openid = f"demo_{uuid.uuid4().hex[:12]}"
|
||||
user = User(
|
||||
openid=demo_openid,
|
||||
name="体验用户",
|
||||
)
|
||||
await user.insert()
|
||||
|
||||
token = create_access_token(str(user.id))
|
||||
return TokenResponse(
|
||||
access_token=token,
|
||||
user_id=str(user.id),
|
||||
is_new_user=True,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/wechat-login", response_model=TokenResponse)
|
||||
async def wechat_login(req: WechatLoginRequest):
|
||||
"""微信小程序登录"""
|
||||
try:
|
||||
wechat_data = await code_to_session(req.code)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
openid = wechat_data["openid"]
|
||||
user = await User.find_one(User.openid == openid)
|
||||
is_new = user is None
|
||||
|
||||
if not user:
|
||||
user = User(
|
||||
openid=openid,
|
||||
session_key=wechat_data.get("session_key"),
|
||||
union_id=wechat_data.get("unionid"),
|
||||
)
|
||||
await user.insert()
|
||||
else:
|
||||
user.session_key = wechat_data.get("session_key")
|
||||
await user.save()
|
||||
|
||||
token = create_access_token(str(user.id))
|
||||
return TokenResponse(
|
||||
access_token=token,
|
||||
user_id=str(user.id),
|
||||
is_new_user=is_new,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/phone-login", response_model=TokenResponse)
|
||||
async def phone_login(req: PhoneLoginRequest):
|
||||
"""手机号登录(简化版,生产环境需接入短信验证)"""
|
||||
# TODO: 接入真实短信验证服务
|
||||
if req.sms_code != "123456":
|
||||
raise HTTPException(status_code=400, detail="验证码错误")
|
||||
|
||||
user = await User.find_one(User.phone == req.phone)
|
||||
is_new = user is None
|
||||
|
||||
if not user:
|
||||
try:
|
||||
user = User(openid=f"phone_{req.phone}", phone=req.phone)
|
||||
await user.insert()
|
||||
except DuplicateKeyError:
|
||||
# 并发竞态:另一个请求已创建该用户,回退查找
|
||||
user = await User.find_one(User.phone == req.phone)
|
||||
is_new = False
|
||||
|
||||
token = create_access_token(str(user.id))
|
||||
return TokenResponse(
|
||||
access_token=token,
|
||||
user_id=str(user.id),
|
||||
is_new_user=is_new,
|
||||
)
|
||||
101
后端-孕期报告解读-api/app/routers/calendar.py
Normal file
101
后端-孕期报告解读-api/app/routers/calendar.py
Normal file
@@ -0,0 +1,101 @@
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from app.middleware.auth import get_current_user
|
||||
from app.models.checkup import CheckupPlan, CheckupItem
|
||||
from app.models.user import User
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# 标准产检计划
|
||||
STANDARD_CHECKUPS = [
|
||||
CheckupItem(title="首次产检", week=6, items=["B超确认宫内孕", "血常规", "尿常规", "肝功能", "肾功能"], preparations=["空腹", "带身份证和医保卡"], notes="确认怀孕和预产期", importance="high"),
|
||||
CheckupItem(title="NT检查", week=12, items=["NT超声筛查", "血清学筛查", "血压", "体重"], preparations=["不需要空腹", "需憋尿"], notes="筛查唐氏综合征风险", importance="high"),
|
||||
CheckupItem(title="唐氏筛查", week=16, items=["唐氏筛查抽血", "血压", "体重", "宫高腹围"], preparations=["空腹抽血"], notes="评估胎儿染色体异常风险", importance="high"),
|
||||
CheckupItem(title="大排畸检查", week=22, items=["四维彩超", "血压", "体重", "宫高腹围"], preparations=["不需要空腹", "提前预约"], notes="全面筛查胎儿结构畸形", importance="high"),
|
||||
CheckupItem(title="糖耐量检查", week=24, items=["OGTT糖耐量试验", "血压", "体重"], preparations=["空腹8-14小时", "检查前3天正常饮食"], notes="筛查妊娠期糖尿病", importance="high"),
|
||||
CheckupItem(title="常规产检", week=28, items=["血常规", "尿常规", "血压", "体重", "胎心监护"], preparations=["空腹"], notes="进入孕晚期,增加产检频率", importance="medium"),
|
||||
CheckupItem(title="常规产检", week=30, items=["B超评估胎儿发育", "血压", "体重", "胎心监护"], preparations=[], notes="评估胎儿生长发育", importance="medium"),
|
||||
CheckupItem(title="常规产检", week=32, items=["血常规", "尿常规", "血压", "体重", "胎心监护", "B超"], preparations=["空腹"], notes="观察胎位", importance="medium"),
|
||||
CheckupItem(title="GBS筛查", week=36, items=["B族链球菌筛查", "血压", "体重", "胎心监护", "内检"], preparations=[], notes="预防新生儿感染", importance="high"),
|
||||
CheckupItem(title="每周产检", week=37, items=["胎心监护", "血压", "体重", "宫高腹围"], preparations=[], notes="每周一次,准备待产", importance="medium"),
|
||||
CheckupItem(title="每周产检", week=38, items=["胎心监护", "B超评估", "血压", "体重"], preparations=[], notes="评估分娩时机", importance="medium"),
|
||||
CheckupItem(title="每周产检", week=39, items=["胎心监护", "血压", "体重", "宫颈评估"], preparations=[], notes="关注产兆", importance="high"),
|
||||
CheckupItem(title="预产期产检", week=40, items=["胎心监护", "B超", "血压", "体重", "催产评估"], preparations=[], notes="超过预产期需密切监护", importance="high"),
|
||||
]
|
||||
|
||||
|
||||
@router.get("/plan")
|
||||
async def get_checkup_plan(user: User = Depends(get_current_user)):
|
||||
"""获取产检计划"""
|
||||
plan = await CheckupPlan.find_one(CheckupPlan.user_id == user.id)
|
||||
|
||||
if not plan:
|
||||
# Generate default plan
|
||||
plan = CheckupPlan(user_id=user.id, checkups=STANDARD_CHECKUPS)
|
||||
await plan.insert()
|
||||
|
||||
# Mark completed checkups based on current week
|
||||
current_week = user.pregnancy_week or 0
|
||||
checkups = []
|
||||
for c in plan.checkups:
|
||||
status = c.status
|
||||
if c.week < current_week and status == "upcoming":
|
||||
status = "missed"
|
||||
checkups.append({
|
||||
**c.model_dump(),
|
||||
"status": status,
|
||||
})
|
||||
|
||||
# Find next checkup
|
||||
next_checkup = None
|
||||
for c in checkups:
|
||||
if c["status"] == "upcoming":
|
||||
next_checkup = c
|
||||
break
|
||||
|
||||
return {
|
||||
"checkups": checkups,
|
||||
"next_checkup": next_checkup,
|
||||
"current_week": current_week,
|
||||
}
|
||||
|
||||
|
||||
@router.put("/plan/{week}/complete")
|
||||
async def complete_checkup(week: int, user: User = Depends(get_current_user)):
|
||||
"""标记产检已完成"""
|
||||
plan = await CheckupPlan.find_one(CheckupPlan.user_id == user.id)
|
||||
if not plan:
|
||||
raise HTTPException(status_code=404, detail="产检计划不存在")
|
||||
|
||||
for checkup in plan.checkups:
|
||||
if checkup.week == week:
|
||||
checkup.status = "completed"
|
||||
break
|
||||
|
||||
plan.updated_at = datetime.now(timezone.utc)
|
||||
await plan.save()
|
||||
return {"message": "已标记完成"}
|
||||
|
||||
|
||||
async def link_report_to_checkup(user_id, report_id: str, pregnancy_week: int):
|
||||
"""将报告自动关联到最近的产检(±2周窗口),并标记该产检为已完成"""
|
||||
plan = await CheckupPlan.find_one(CheckupPlan.user_id == user_id)
|
||||
if not plan:
|
||||
return
|
||||
|
||||
best = None
|
||||
best_diff = 999
|
||||
for c in plan.checkups:
|
||||
diff = abs(c.week - pregnancy_week)
|
||||
if diff <= 2 and diff < best_diff:
|
||||
best = c
|
||||
best_diff = diff
|
||||
|
||||
if best:
|
||||
if report_id not in best.report_ids:
|
||||
best.report_ids.append(report_id)
|
||||
best.status = "completed"
|
||||
plan.updated_at = datetime.now(timezone.utc)
|
||||
await plan.save()
|
||||
98
后端-孕期报告解读-api/app/routers/chat.py
Normal file
98
后端-孕期报告解读-api/app/routers/chat.py
Normal file
@@ -0,0 +1,98 @@
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from app.middleware.auth import get_current_user
|
||||
from app.models.chat import ChatSession, ChatMessage
|
||||
from app.models.report import Report
|
||||
from app.models.user import User
|
||||
from app.schemas.chat import ChatMessageRequest, ChatSessionResponse, ChatMessageResponse
|
||||
from app.services.ai_service import chat_with_ai
|
||||
from app.utils.helpers import parse_object_id
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/{report_id}/message")
|
||||
async def send_message(
|
||||
report_id: str,
|
||||
req: ChatMessageRequest,
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""发送消息并获取 AI 回复"""
|
||||
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="报告不存在")
|
||||
|
||||
# Get or create chat session
|
||||
session = await ChatSession.find_one(
|
||||
ChatSession.user_id == user.id,
|
||||
ChatSession.report_id == report.id,
|
||||
)
|
||||
if not session:
|
||||
session = ChatSession(user_id=user.id, report_id=report.id)
|
||||
await session.insert()
|
||||
|
||||
# Add user message
|
||||
user_msg = ChatMessage(role="user", content=req.content)
|
||||
session.messages.append(user_msg)
|
||||
|
||||
# Get AI response
|
||||
history = [{"role": m.role, "content": m.content} for m in session.messages[:-1]]
|
||||
try:
|
||||
ai_response = await chat_with_ai(report, user, history, req.content)
|
||||
except Exception:
|
||||
logger.exception("AI 对话失败 report_id=%s", report_id)
|
||||
raise HTTPException(status_code=502, detail="AI 对话服务暂时不可用,请稍后重试")
|
||||
|
||||
# Add AI message
|
||||
ai_msg = ChatMessage(role="assistant", content=ai_response)
|
||||
session.messages.append(ai_msg)
|
||||
session.updated_at = datetime.now(timezone.utc)
|
||||
await session.save()
|
||||
|
||||
return {
|
||||
"user_message": {"role": "user", "content": req.content, "timestamp": user_msg.timestamp.isoformat()},
|
||||
"ai_message": {"role": "assistant", "content": ai_response, "timestamp": ai_msg.timestamp.isoformat()},
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{report_id}/history")
|
||||
async def get_chat_history(
|
||||
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="报告不存在")
|
||||
|
||||
session = await ChatSession.find_one(
|
||||
ChatSession.user_id == user.id,
|
||||
ChatSession.report_id == report.id,
|
||||
)
|
||||
|
||||
if not session:
|
||||
return {"messages": [], "quick_questions": _get_quick_questions()}
|
||||
|
||||
return {
|
||||
"messages": [
|
||||
{"role": m.role, "content": m.content, "timestamp": m.timestamp.isoformat()}
|
||||
for m in session.messages
|
||||
],
|
||||
"quick_questions": _get_quick_questions(),
|
||||
}
|
||||
|
||||
|
||||
def _get_quick_questions() -> list[str]:
|
||||
return [
|
||||
"这个指标偏高会影响宝宝吗?",
|
||||
"我需要注意什么饮食?",
|
||||
"什么时候需要复查?",
|
||||
"这个结果严重吗?",
|
||||
"有什么改善建议?",
|
||||
]
|
||||
96
后端-孕期报告解读-api/app/routers/doctor.py
Normal file
96
后端-孕期报告解读-api/app/routers/doctor.py
Normal file
@@ -0,0 +1,96 @@
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from app.middleware.auth import get_current_user
|
||||
from app.models.report import Report
|
||||
from app.models.user import User
|
||||
from app.services.ai_service import generate_doctor_questions
|
||||
from app.utils.helpers import parse_object_id
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# 预设医生列表
|
||||
DOCTORS = [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "王丽华",
|
||||
"title": "主任医师",
|
||||
"hospital": "北京协和医院",
|
||||
"specialty": "高危妊娠、产前诊断",
|
||||
"experience": "25年",
|
||||
"rating": 4.9,
|
||||
"review_count": 1280,
|
||||
"consult_count": 5600,
|
||||
"response_time": "30分钟内",
|
||||
"price": {"text": 50, "video": 200, "urgent": 100},
|
||||
"tags": ["高危妊娠", "产前诊断", "遗传咨询"],
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"name": "张美琳",
|
||||
"title": "副主任医师",
|
||||
"hospital": "上海第一妇婴保健院",
|
||||
"specialty": "孕期营养、妊娠期糖尿病",
|
||||
"experience": "18年",
|
||||
"rating": 4.8,
|
||||
"review_count": 960,
|
||||
"consult_count": 3800,
|
||||
"response_time": "1小时内",
|
||||
"price": {"text": 40, "video": 180, "urgent": 80},
|
||||
"tags": ["孕期营养", "妊娠糖尿病", "体重管理"],
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"name": "刘晓芳",
|
||||
"title": "主任医师",
|
||||
"hospital": "广州市妇女儿童医疗中心",
|
||||
"specialty": "超声诊断、胎儿医学",
|
||||
"experience": "20年",
|
||||
"rating": 4.9,
|
||||
"review_count": 1100,
|
||||
"consult_count": 4200,
|
||||
"response_time": "2小时内",
|
||||
"price": {"text": 45, "video": 190, "urgent": 90},
|
||||
"tags": ["超声诊断", "胎儿医学", "产前筛查"],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@router.get("/questions/{report_id}")
|
||||
async def get_doctor_questions(
|
||||
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="报告不存在")
|
||||
|
||||
try:
|
||||
result = await generate_doctor_questions(report, user)
|
||||
except Exception:
|
||||
logger.exception("问诊清单生成失败 report_id=%s", report_id)
|
||||
raise HTTPException(status_code=502, detail="AI 服务暂时不可用,请稍后重试")
|
||||
|
||||
return {
|
||||
"report_id": report_id,
|
||||
"questions": result.get("questions", []),
|
||||
}
|
||||
|
||||
|
||||
@router.get("/experts")
|
||||
async def list_experts():
|
||||
"""获取专家医生列表"""
|
||||
return {"doctors": DOCTORS}
|
||||
|
||||
|
||||
@router.get("/experts/{doctor_id}")
|
||||
async def get_expert_detail(doctor_id: str):
|
||||
"""获取医生详情"""
|
||||
for doc in DOCTORS:
|
||||
if doc["id"] == doctor_id:
|
||||
return doc
|
||||
raise HTTPException(status_code=404, detail="医生不存在")
|
||||
258
后端-孕期报告解读-api/app/routers/health_records.py
Normal file
258
后端-孕期报告解读-api/app/routers/health_records.py
Normal file
@@ -0,0 +1,258 @@
|
||||
import re
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from beanie import PydanticObjectId
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from app.middleware.auth import get_current_user
|
||||
from app.models.report import Report
|
||||
from app.models.user import User
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _parse_numeric_value(value) -> Optional[float]:
|
||||
value_str = str(value).strip() if value is not None else ""
|
||||
if not value_str:
|
||||
return None
|
||||
match = re.search(r'(\d+\.?\d*)', value_str)
|
||||
if not match:
|
||||
return None
|
||||
try:
|
||||
return float(match.group(1))
|
||||
except (ValueError, AttributeError):
|
||||
return None
|
||||
|
||||
|
||||
def _parse_reference_range(reference_range: str) -> Tuple[Optional[float], Optional[float]]:
|
||||
if not reference_range:
|
||||
return None, None
|
||||
match = re.search(r'(\d+\.?\d*)\s*[-~]\s*(\d+\.?\d*)', reference_range)
|
||||
if not match:
|
||||
return None, None
|
||||
return float(match.group(1)), float(match.group(2))
|
||||
|
||||
|
||||
@router.get("/overview")
|
||||
async def get_health_overview(user: User = Depends(get_current_user)):
|
||||
"""健康档案概览"""
|
||||
reports = await Report.find(Report.user_id == user.id).sort("-created_at").to_list()
|
||||
total = len(reports)
|
||||
normal = sum(1 for r in reports if r.overall_status == "normal")
|
||||
attention = sum(1 for r in reports if r.overall_status == "attention")
|
||||
warning = sum(1 for r in reports if r.overall_status == "warning")
|
||||
|
||||
recent = [
|
||||
{
|
||||
"id": str(r.id),
|
||||
"report_type": r.report_type,
|
||||
"overall_status": r.overall_status,
|
||||
"created_at": r.created_at.isoformat(),
|
||||
}
|
||||
for r in reports[:5]
|
||||
]
|
||||
|
||||
return {
|
||||
"stats": {
|
||||
"total": total,
|
||||
"normal": normal,
|
||||
"attention": attention,
|
||||
"warning": warning,
|
||||
},
|
||||
"recent_reports": recent,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/growth-curve")
|
||||
async def get_growth_curve(user: User = Depends(get_current_user)):
|
||||
"""获取胎儿生长曲线数据(从B超报告提取)"""
|
||||
reports = await Report.find(
|
||||
Report.user_id == user.id,
|
||||
Report.report_type == "B超",
|
||||
).sort("pregnancy_week").to_list()
|
||||
|
||||
data_points = []
|
||||
for r in reports:
|
||||
if not r.pregnancy_week:
|
||||
continue
|
||||
point = {"week": r.pregnancy_week}
|
||||
for item in r.data_items:
|
||||
name_lower = item.name.lower()
|
||||
numeric_value = _parse_numeric_value(item.value)
|
||||
if numeric_value is None:
|
||||
continue
|
||||
|
||||
# 匹配指标名称
|
||||
if "双顶径" in item.name or "bpd" in name_lower:
|
||||
point["bpd"] = numeric_value
|
||||
elif "腹围" in item.name or "ac" in name_lower:
|
||||
point["ac"] = numeric_value
|
||||
elif "股骨长" in item.name or "fl" in name_lower:
|
||||
point["fl"] = numeric_value
|
||||
|
||||
# 只有包含至少一个生长指标的数据点才添加
|
||||
if any(k in point for k in ["bpd", "ac", "fl"]):
|
||||
data_points.append(point)
|
||||
|
||||
return {"data_points": data_points}
|
||||
|
||||
|
||||
@router.get("/timeline")
|
||||
async def get_timeline(user: User = Depends(get_current_user)):
|
||||
"""报告时间轴"""
|
||||
reports = await Report.find(Report.user_id == user.id).sort("-created_at").to_list()
|
||||
|
||||
timeline = []
|
||||
for r in reports:
|
||||
week = r.pregnancy_week or 0
|
||||
if week <= 12:
|
||||
period = "early"
|
||||
elif week <= 28:
|
||||
period = "mid"
|
||||
else:
|
||||
period = "late"
|
||||
|
||||
timeline.append({
|
||||
"id": str(r.id),
|
||||
"report_type": r.report_type,
|
||||
"date": r.created_at.isoformat(),
|
||||
"status": r.overall_status,
|
||||
"summary": f"{len(r.data_items)}项指标",
|
||||
"period": period,
|
||||
"week": week,
|
||||
})
|
||||
|
||||
return {"reports": timeline}
|
||||
|
||||
|
||||
@router.get("/indicator-trends")
|
||||
async def get_indicator_trends(user: User = Depends(get_current_user)):
|
||||
"""获取跨报告指标趋势数据(同名数值指标出现2+次的)"""
|
||||
reports = await Report.find(
|
||||
Report.user_id == user.id,
|
||||
).sort("pregnancy_week", "created_at").to_list()
|
||||
|
||||
# 收集所有指标:indicator_name -> list of {week, value, unit, ref_range, status, date, report_type}
|
||||
indicator_map: dict[str, list[dict]] = {}
|
||||
|
||||
for r in reports:
|
||||
week = r.pregnancy_week or 0
|
||||
date_str = r.created_at.isoformat() if r.created_at else ""
|
||||
for item in r.data_items:
|
||||
value_str = str(item.value).strip() if item.value else ""
|
||||
if not value_str:
|
||||
continue
|
||||
match = re.search(r'(\d+\.?\d*)', value_str)
|
||||
if not match:
|
||||
continue
|
||||
try:
|
||||
numeric_value = float(match.group(1))
|
||||
except (ValueError, AttributeError):
|
||||
continue
|
||||
|
||||
name = item.name.strip()
|
||||
if name not in indicator_map:
|
||||
indicator_map[name] = []
|
||||
indicator_map[name].append({
|
||||
"week": week,
|
||||
"value": numeric_value,
|
||||
"unit": item.unit or "",
|
||||
"reference_range": item.reference_range or "",
|
||||
"status": item.status or "normal",
|
||||
"date": date_str,
|
||||
"report_type": r.report_type,
|
||||
})
|
||||
|
||||
# 保留所有有数据的指标(含仅1次的,用于综合分析)
|
||||
indicators = []
|
||||
for name, points in indicator_map.items():
|
||||
|
||||
# 解析参考范围(取第一个非空的)
|
||||
ref_lower = None
|
||||
ref_upper = None
|
||||
for p in points:
|
||||
if p["reference_range"]:
|
||||
# 尝试解析 "3.5-5.0" 或 "3.5~5.0" 格式
|
||||
ref_match = re.search(r'(\d+\.?\d*)\s*[-~]\s*(\d+\.?\d*)', p["reference_range"])
|
||||
if ref_match:
|
||||
ref_lower = float(ref_match.group(1))
|
||||
ref_upper = float(ref_match.group(2))
|
||||
break
|
||||
|
||||
indicators.append({
|
||||
"name": name,
|
||||
"unit": points[0]["unit"],
|
||||
"ref_lower": ref_lower,
|
||||
"ref_upper": ref_upper,
|
||||
"points": [{"week": p["week"], "value": p["value"], "date": p["date"],
|
||||
"status": p["status"], "report_type": p["report_type"]} for p in points],
|
||||
})
|
||||
|
||||
return {"indicators": indicators}
|
||||
|
||||
|
||||
@router.get("/report-type-trends")
|
||||
async def get_report_type_trends(user: User = Depends(get_current_user)):
|
||||
"""按报告类型分组的指标趋势(同类型报告中出现2+次的数值指标)"""
|
||||
reports = await Report.find(
|
||||
Report.user_id == user.id,
|
||||
).sort("pregnancy_week", "created_at").to_list()
|
||||
|
||||
# report_type -> indicator_name -> list of data points
|
||||
type_map: dict[str, dict[str, list[dict]]] = {}
|
||||
|
||||
for r in reports:
|
||||
rt = r.report_type or "其他"
|
||||
week = r.pregnancy_week or 0
|
||||
date_str = r.created_at.isoformat() if r.created_at else ""
|
||||
|
||||
if rt not in type_map:
|
||||
type_map[rt] = {}
|
||||
|
||||
for item in r.data_items:
|
||||
numeric_value = _parse_numeric_value(item.value)
|
||||
if numeric_value is None:
|
||||
continue
|
||||
|
||||
name = item.name.strip()
|
||||
if name not in type_map[rt]:
|
||||
type_map[rt][name] = []
|
||||
type_map[rt][name].append({
|
||||
"week": week,
|
||||
"value": numeric_value,
|
||||
"unit": item.unit or "",
|
||||
"reference_range": item.reference_range or "",
|
||||
"status": item.status or "normal",
|
||||
"date": date_str,
|
||||
})
|
||||
|
||||
# 组装结果:保留所有有数据的指标(含仅1个数据点的)
|
||||
result = []
|
||||
for rt, ind_map in type_map.items():
|
||||
indicators = []
|
||||
for name, points in ind_map.items():
|
||||
ref_lower, ref_upper = None, None
|
||||
for p in points:
|
||||
ref_lower, ref_upper = _parse_reference_range(p["reference_range"])
|
||||
if ref_lower is not None:
|
||||
break
|
||||
indicators.append({
|
||||
"name": name,
|
||||
"unit": points[0]["unit"],
|
||||
"ref_lower": ref_lower,
|
||||
"ref_upper": ref_upper,
|
||||
"points": [{"week": p["week"], "value": p["value"], "date": p["date"],
|
||||
"status": p["status"]} for p in points],
|
||||
})
|
||||
if indicators:
|
||||
# 按数据点数量降序,让最有数据的指标排前面
|
||||
indicators.sort(key=lambda x: len(x["points"]), reverse=True)
|
||||
result.append({
|
||||
"report_type": rt,
|
||||
"report_count": sum(1 for r in reports if (r.report_type or "其他") == rt),
|
||||
"indicators": indicators,
|
||||
})
|
||||
|
||||
# 按报告份数降序
|
||||
result.sort(key=lambda x: x["report_count"], reverse=True)
|
||||
return {"report_types": result}
|
||||
290
后端-孕期报告解读-api/app/routers/report.py
Normal file
290
后端-孕期报告解读-api/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,
|
||||
)
|
||||
70
后端-孕期报告解读-api/app/routers/share.py
Normal file
70
后端-孕期报告解读-api/app/routers/share.py
Normal file
@@ -0,0 +1,70 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.middleware.auth import get_current_user
|
||||
from app.models.analysis import Analysis
|
||||
from app.models.report import Report
|
||||
from app.models.user import User
|
||||
from app.utils.helpers import parse_object_id, mask_name
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class ShareOptions(BaseModel):
|
||||
privacy_mode: bool = True # 隐私模式,脱敏个人信息
|
||||
include_details: bool = False # 是否包含详细数据
|
||||
|
||||
|
||||
@router.post("/{report_id}/generate")
|
||||
async def generate_share_data(
|
||||
report_id: str,
|
||||
options: ShareOptions = ShareOptions(privacy_mode=True, include_details=True),
|
||||
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="报告不存在")
|
||||
|
||||
analysis = await Analysis.find_one(Analysis.report_id == report.id)
|
||||
if not analysis:
|
||||
raise HTTPException(status_code=404, detail="暂无解读结果")
|
||||
|
||||
# Build share data
|
||||
share_data = {
|
||||
"report_type": report.report_type,
|
||||
"pregnancy_week": report.pregnancy_week,
|
||||
"overall_status": analysis.overall_status,
|
||||
"risk_level": analysis.risk_level,
|
||||
"overall_summary": analysis.overall_summary,
|
||||
"emotional_summary": analysis.emotional_summary,
|
||||
"action_items": [a.model_dump() for a in analysis.action_items],
|
||||
"tips": analysis.tips,
|
||||
"categories": [
|
||||
{
|
||||
"category": cat.category,
|
||||
"status": cat.status,
|
||||
"indicators": [
|
||||
{
|
||||
"name": ind.name,
|
||||
"nickname": ind.nickname,
|
||||
"value": ind.value,
|
||||
"status": ind.status,
|
||||
"what_is_it": ind.what_is_it,
|
||||
"impact": ind.impact,
|
||||
"suggestion": ind.suggestion,
|
||||
}
|
||||
for ind in cat.indicators
|
||||
],
|
||||
}
|
||||
for cat in analysis.categories
|
||||
],
|
||||
}
|
||||
|
||||
if not options.privacy_mode:
|
||||
share_data["user_name"] = user.name
|
||||
else:
|
||||
share_data["user_name"] = mask_name(user.name)
|
||||
|
||||
return {"share_data": share_data}
|
||||
84
后端-孕期报告解读-api/app/routers/user.py
Normal file
84
后端-孕期报告解读-api/app/routers/user.py
Normal file
@@ -0,0 +1,84 @@
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from app.middleware.auth import get_current_user
|
||||
from app.models.user import User
|
||||
from app.schemas.user import (
|
||||
OnboardingRequest,
|
||||
UserProfileUpdate,
|
||||
UserProfileResponse,
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _user_to_response(user: User) -> UserProfileResponse:
|
||||
return UserProfileResponse(
|
||||
id=str(user.id),
|
||||
name=user.name,
|
||||
phone=user.phone,
|
||||
age=user.age,
|
||||
height=user.height,
|
||||
pre_pregnancy_weight=user.pre_pregnancy_weight,
|
||||
current_weight=user.current_weight,
|
||||
blood_type=user.blood_type,
|
||||
allergies=user.allergies,
|
||||
medical_history=user.medical_history,
|
||||
last_period_date=user.last_period_date,
|
||||
due_date=user.due_date,
|
||||
pregnancy_week=user.pregnancy_week,
|
||||
pregnancy_day=user.pregnancy_day,
|
||||
multiple_pregnancy=user.multiple_pregnancy,
|
||||
pregnancy_count=user.pregnancy_count,
|
||||
emergency_contact=user.emergency_contact.model_dump() if user.emergency_contact else None,
|
||||
is_onboarded=user.is_onboarded,
|
||||
avatar_url=user.avatar_url,
|
||||
created_at=user.created_at,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/profile", response_model=UserProfileResponse)
|
||||
async def get_profile(user: User = Depends(get_current_user)):
|
||||
"""获取用户信息"""
|
||||
return _user_to_response(user)
|
||||
|
||||
|
||||
@router.post("/onboarding", response_model=UserProfileResponse)
|
||||
async def complete_onboarding(
|
||||
req: OnboardingRequest, user: User = Depends(get_current_user)
|
||||
):
|
||||
"""完成用户画像设置"""
|
||||
user.name = req.name or user.name
|
||||
user.age = req.age
|
||||
user.height = req.height
|
||||
user.pre_pregnancy_weight = req.pre_pregnancy_weight
|
||||
user.current_weight = req.current_weight
|
||||
user.blood_type = req.blood_type
|
||||
user.allergies = req.allergies
|
||||
user.medical_history = req.medical_history
|
||||
user.last_period_date = req.last_period_date
|
||||
user.due_date = req.due_date
|
||||
user.pregnancy_week = req.pregnancy_week
|
||||
user.pregnancy_day = req.pregnancy_day
|
||||
user.multiple_pregnancy = req.multiple_pregnancy
|
||||
user.pregnancy_count = req.pregnancy_count
|
||||
if req.emergency_contact:
|
||||
user.emergency_contact = req.emergency_contact
|
||||
user.is_onboarded = True
|
||||
user.updated_at = datetime.now(timezone.utc)
|
||||
await user.save()
|
||||
return _user_to_response(user)
|
||||
|
||||
|
||||
@router.put("/profile", response_model=UserProfileResponse)
|
||||
async def update_profile(
|
||||
req: UserProfileUpdate, user: User = Depends(get_current_user)
|
||||
):
|
||||
"""更新用户信息"""
|
||||
update_data = req.model_dump(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(user, field, value)
|
||||
user.updated_at = datetime.now(timezone.utc)
|
||||
await user.save()
|
||||
return _user_to_response(user)
|
||||
Reference in New Issue
Block a user