feat: add teacher help cards

This commit is contained in:
2026-07-31 15:53:47 +08:00
parent 79094d0832
commit d8fde93b69
15 changed files with 467 additions and 4 deletions

View File

@@ -0,0 +1,123 @@
from __future__ import annotations
from datetime import UTC, datetime
from fastapi import HTTPException, status
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.models.chat import ChatSession, TopicSession
from app.models.growth import TeacherHelpCard, TopicSummary
from app.models.user import User
from app.services.entitlement_service import EntitlementService
from app.services.growth_profile_service import GrowthProfileService, topic_summary_dict
class HelpCardService:
@staticmethod
def generate_for_session(db: Session, *, user: User, session: ChatSession) -> TeacherHelpCard:
entitlement = EntitlementService.active_entitlement(db, user)
if not entitlement.allow_help_card:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="当前权益暂不支持生成老师求助卡")
topic = _latest_topic(db, user=user, session=session)
if topic is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="当前会话还没有可生成求助卡的主题")
summary = GrowthProfileService.generate_topic_summary(db, user=user, topic=topic)
content = _render_help_card(user=user, topic=topic, summary=summary)
card = TeacherHelpCard(
user_id=user.id,
topic_session_id=topic.id,
summary_id=summary.id,
content=content,
source="topic_summary",
)
topic.help_card_generated = 1
db.add_all([topic, card])
db.commit()
db.refresh(card)
return card
@staticmethod
def list_user_cards(db: Session, *, user: User, limit: int = 20) -> list[TeacherHelpCard]:
return list(
db.scalars(
select(TeacherHelpCard)
.where(TeacherHelpCard.user_id == user.id)
.order_by(TeacherHelpCard.created_at.desc(), TeacherHelpCard.id.desc())
.limit(limit)
)
)
@staticmethod
def mark_copied(db: Session, *, user: User, card_id: int) -> TeacherHelpCard:
card = db.scalar(select(TeacherHelpCard).where(TeacherHelpCard.id == card_id, TeacherHelpCard.user_id == user.id))
if card is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="求助卡不存在")
card.copied = 1
card.copied_at = _now()
db.add(card)
db.commit()
db.refresh(card)
return card
def help_card_dict(card: TeacherHelpCard) -> dict:
return {
"id": card.id,
"userId": card.user_id,
"topicSessionId": card.topic_session_id,
"summaryId": card.summary_id,
"content": card.content,
"source": card.source,
"copied": bool(card.copied),
"copiedAt": card.copied_at,
"createdAt": card.created_at,
}
def _latest_topic(db: Session, *, user: User, session: ChatSession) -> TopicSession | None:
return db.scalar(
select(TopicSession)
.where(TopicSession.user_id == user.id, TopicSession.chat_session_id == session.id)
.order_by(TopicSession.updated_at.desc(), TopicSession.id.desc())
.limit(1)
)
def _render_help_card(*, user: User, topic: TopicSession, summary: TopicSummary) -> str:
data = topic_summary_dict(summary)
return (
"【给老师的求助卡】\n"
"说明:这是我根据本次 AI 对话整理出的求助信息,请老师帮我确认方向。"
"我会按实际情况自行删改后再发送。\n\n"
f"学员:{user.name or user.nickname or user.phone}\n"
f"主题:{topic.title}\n"
f"主题时间:{_format_time(topic.started_at)} - {_format_time(topic.ended_at) if topic.ended_at else '进行中'}\n\n"
"1. 我遇到的问题\n"
f"{topic.core_question or '(请补充)'}\n\n"
"2. AI 已经帮我梳理出的重点\n"
f"{data.get('summary') or '(暂无摘要)'}\n\n"
"3. 我现在最明显的情绪 / 身体感受\n"
f"情绪:{data.get('emotions') or '(请补充)'}\n"
f"身体感受:{data.get('bodyFeelings') or '(请补充)'}\n\n"
"4. 我已经尝试过或被建议的功课\n"
f"{data.get('recommendedHomework') or '(请补充)'}\n\n"
"5. 我仍然卡住的地方\n"
f"{data.get('beliefs') or data.get('nextObservation') or '(请补充)'}\n\n"
"6. 我想请老师确认的问题\n"
"(请把最想确认的一两个问题写在这里)\n\n"
"备注:这张卡片不会自动发送给老师,也不代表已经转人工处理。"
)
def _format_time(value: datetime | None) -> str:
if value is None:
return "未知"
return value.strftime("%Y-%m-%d %H:%M")
def _now() -> datetime:
return datetime.now(UTC).replace(tzinfo=None)