from fastapi import APIRouter, Depends, Query from sqlalchemy import or_ from sqlalchemy.orm import Session from database import get_db from models.animation import Animation from models.classroom import ClassroomActivity from models.community import Post from models.courseware import Courseware from models.exam import Exam from models.exercise import Exercise from models.lesson_plan import LessonPlan from models.material import Material from models.mindmap import MindMap from models.resource import Resource from models.user import User from schemas.search import SearchResponse from services.auth import get_optional_current_user router = APIRouter(prefix="/api/search", tags=["全局搜索"]) def _has_keyword(*values, keyword: str) -> bool: if not keyword: return True haystack = " ".join(str(value or "") for value in values).lower() return keyword.lower() in haystack def _tag_list(value) -> list[str]: if not isinstance(value, list): return [] return [str(item) for item in value if str(item).strip()][:6] def _item( *, id, source: str, source_label: str, title: str, route: str, description: str = "", subject: str = "", grade: str = "", tags=None, action_route: str = "", is_public: bool = False, updated_at=None, ) -> dict: return { "id": id, "source": source, "source_label": source_label, "title": title or "未命名", "description": description or "", "subject": subject or "", "grade": grade or "", "tags": _tag_list(tags), "route": route, "action_route": action_route, "is_public": bool(is_public), "updated_at": updated_at, } def _sort_key(item: dict): return item["updated_at"] or "" def _resource_query(keyword: str, current_user: User | None, db: Session, limit: int) -> list[dict]: query = db.query(Resource).filter(Resource.status == "active") if current_user: query = query.filter((Resource.user_id == current_user.id) | (Resource.is_public == 1)) else: query = query.filter(Resource.is_public == 1) if keyword: query = query.filter(or_( Resource.title.contains(keyword), Resource.description.contains(keyword), Resource.subject.contains(keyword), Resource.grade.contains(keyword), )) return [ _item( id=res.id, source="resource", source_label="资源", title=res.title, description=res.description, subject=res.subject, grade=res.grade, tags=res.tags, route=f"/resources/{res.id}", action_route=f"/resources/{res.id}", is_public=bool(res.is_public), updated_at=res.updated_at, ) for res in query.order_by(Resource.updated_at.desc()).limit(limit).all() ] def _community_query(keyword: str, current_user: User | None, db: Session, limit: int) -> list[dict]: query = db.query(Post) if keyword: query = query.filter(or_(Post.title.contains(keyword), Post.content.contains(keyword))) return [ _item( id=post.id, source="community", source_label="模板社区", title=post.title, description=post.content, tags=post.tags, route=f"/community/{post.id}", action_route=f"/community/{post.id}", is_public=True, updated_at=post.updated_at, ) for post in query.order_by(Post.updated_at.desc()).limit(limit).all() ] def _my_work_queries(keyword: str, current_user: User | None, db: Session, limit: int) -> list[dict]: if not current_user: return [] user_id = current_user.id items = [] coursewares = db.query(Courseware).filter(Courseware.user_id == user_id, Courseware.status != "archived").all() for work in coursewares: if _has_keyword(work.title, work.description, work.subject, work.grade, " ".join(_tag_list(work.tags)), keyword=keyword): items.append(_item( id=work.id, source="courseware", source_label="我的课件", title=work.title, description=work.description, subject=work.subject, grade=work.grade, tags=work.tags, route=f"/courseware/{work.id}", action_route=f"/courseware/{work.id}", updated_at=work.updated_at, )) animations = db.query(Animation).filter(Animation.user_id == user_id).all() for work in animations: if _has_keyword(work.title, work.description, work.anim_type, keyword=keyword): items.append(_item( id=work.id, source="animation", source_label="我的动画", title=work.title, description=work.description, subject=str(work.anim_type or ""), route=f"/animation?open={work.id}", action_route=f"/animation?open={work.id}", updated_at=work.updated_at, )) exercises = db.query(Exercise).filter(Exercise.user_id == user_id).all() for work in exercises: if _has_keyword(work.title, work.subject, " ".join(_tag_list(work.knowledge_points)), keyword=keyword): items.append(_item( id=work.id, source="exercise", source_label="我的练习", title=work.title, subject=work.subject, tags=work.knowledge_points, route=f"/exercise?open={work.id}", action_route=f"/exercise?open={work.id}", updated_at=work.updated_at, )) lesson_plans = db.query(LessonPlan).filter(LessonPlan.user_id == user_id).all() for work in lesson_plans: if _has_keyword(work.title, work.subject, work.grade, " ".join(_tag_list(work.objectives)), keyword=keyword): items.append(_item( id=work.id, source="lesson_plan", source_label="我的教案", title=work.title, subject=work.subject, grade=work.grade, tags=work.objectives, route=f"/lesson-plan?open={work.id}", action_route=f"/lesson-plan?open={work.id}", updated_at=work.updated_at, )) mind_maps = db.query(MindMap).filter(MindMap.user_id == user_id).all() for work in mind_maps: if _has_keyword(work.title, work.subject, keyword=keyword): items.append(_item( id=work.id, source="mindmap", source_label="我的导图", title=work.title, subject=work.subject, route=f"/mindmap?open={work.id}", action_route=f"/mindmap?open={work.id}", updated_at=work.updated_at, )) exams = db.query(Exam).filter(Exam.user_id == user_id).all() for work in exams: if _has_keyword(work.title, work.subject, work.grade, " ".join(_tag_list(work.knowledge_points)), keyword=keyword): items.append(_item( id=work.id, source="exam", source_label="我的试卷", title=work.title, subject=work.subject, grade=work.grade, tags=work.knowledge_points, route=f"/exam?open={work.id}", action_route=f"/exam?open={work.id}", updated_at=work.updated_at, )) items.sort(key=_sort_key, reverse=True) return items[:limit] def _material_query(keyword: str, current_user: User | None, db: Session, limit: int) -> list[dict]: if not current_user: return [] query = db.query(Material).filter(Material.user_id == current_user.id, Material.status == "active") if keyword: query = query.filter(or_( Material.title.contains(keyword), Material.filename.contains(keyword), Material.summary.contains(keyword), Material.subject.contains(keyword), Material.grade.contains(keyword), )) return [ _item( id=item.id, source="material", source_label="素材", title=item.title, description=item.summary, subject=item.subject, grade=item.grade, tags=item.tags, route="/materials", action_route=f"/courseware/create", updated_at=item.updated_at, ) for item in query.order_by(Material.updated_at.desc()).limit(limit).all() ] def _classroom_query(keyword: str, current_user: User | None, db: Session, limit: int) -> list[dict]: if not current_user: return [] activities = db.query(ClassroomActivity).filter(ClassroomActivity.user_id == current_user.id).all() items = [] for activity in activities: config = activity.config or {} if _has_keyword(activity.title, activity.description, config.get("question"), activity.activity_type, keyword=keyword): items.append(_item( id=activity.id, source="classroom", source_label="课堂活动", title=activity.title, description=activity.description or config.get("question") or "", subject=activity.activity_type or "", route=f"/classroom?open={activity.id}", action_route=f"/classroom?open={activity.id}", updated_at=activity.updated_at, )) items.sort(key=_sort_key, reverse=True) return items[:limit] @router.get("", response_model=SearchResponse) def global_search( keyword: str = Query("", max_length=100), limit: int = Query(6, ge=1, le=20), current_user: User | None = Depends(get_optional_current_user), db: Session = Depends(get_db), ): keyword = keyword.strip() resources = _resource_query(keyword, current_user, db, limit) community = _community_query(keyword, current_user, db, limit) works = _my_work_queries(keyword, current_user, db, limit) materials = _material_query(keyword, current_user, db, limit) classroom = _classroom_query(keyword, current_user, db, limit) return { "keyword": keyword, "total": len(resources) + len(community) + len(works) + len(materials) + len(classroom), "resources": resources, "community": community, "works": works, "materials": materials, "classroom": classroom, }