from fastapi import APIRouter, Depends, HTTPException, Query, Request from sqlalchemy import or_ from sqlalchemy.orm import Session from typing import Optional from database import get_db from models.resource import Resource, ResourceFavorite from models.user import User from schemas.resource import ResourceCreate, ResourceOut, ResourceUpdate from services.auth import get_current_user, get_optional_current_user from routers.notification import push_notification from services.audit import log_action router = APIRouter(prefix="/api/resources", tags=["资源库"]) def _sync_favorite_count(res: Resource, db: Session) -> None: res.likes = db.query(ResourceFavorite).filter(ResourceFavorite.resource_id == res.id).count() def _serialize_resource(res: Resource, current_user: User | None, db: Session) -> Resource: is_favorited = False if current_user: is_favorited = db.query(ResourceFavorite).filter( ResourceFavorite.user_id == current_user.id, ResourceFavorite.resource_id == res.id, ).first() is not None setattr(res, "is_favorited", is_favorited) setattr(res, "author", res.author) return res def _apply_sort(query, sort: str | None): sort = (sort or "latest").lower() if sort == "popular": return query.order_by(Resource.views.desc(), Resource.created_at.desc()) if sort in {"downloads", "download"}: return query.order_by(Resource.downloads.desc(), Resource.created_at.desc()) if sort in {"likes", "like"}: return query.order_by(Resource.likes.desc(), Resource.created_at.desc()) return query.order_by(Resource.created_at.desc()) @router.post("/", response_model=ResourceOut, status_code=201) def create_resource(data: ResourceCreate, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): res = Resource(user_id=current_user.id, **data.model_dump()) db.add(res) db.commit() db.refresh(res) return _serialize_resource(res, current_user, db) @router.post("/publish", response_model=ResourceOut, status_code=201) def publish_resource(request: Request, data: ResourceCreate, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): query = db.query(Resource).filter(Resource.user_id == current_user.id) if data.content_ref: query = query.filter(Resource.content_ref == data.content_ref) else: query = query.filter(Resource.title == data.title, Resource.resource_type == data.resource_type) res = query.first() is_new = res is None if res: for key, value in data.model_dump().items(): setattr(res, key, value) res.status = "active" else: res = Resource(user_id=current_user.id, status="active", **data.model_dump()) db.add(res) db.commit() db.refresh(res) log_action(db, action="publish_resource", user=current_user, request=request, target_type="resource", target_id=res.id, detail=f"{'新建' if is_new else '更新'}资源《{res.title}》({res.resource_type})") return _serialize_resource(res, current_user, db) @router.get("/", response_model=list[ResourceOut]) def list_resources( resource_type: Optional[str] = None, subject: Optional[str] = None, keyword: Optional[str] = None, sort: Optional[str] = Query("latest"), skip: int = Query(0, ge=0), limit: int = Query(20, ge=1, le=100), current_user: User | None = Depends(get_optional_current_user), db: Session = Depends(get_db), ): query = db.query(Resource).filter(Resource.status == "active") if resource_type: query = query.filter(Resource.resource_type == resource_type) if subject: query = query.filter(Resource.subject == subject) if keyword: query = query.filter(or_( Resource.title.contains(keyword), Resource.description.contains(keyword), Resource.subject.contains(keyword), Resource.grade.contains(keyword), )) if current_user: query = query.filter((Resource.user_id == current_user.id) | (Resource.is_public == 1)) else: query = query.filter(Resource.is_public == 1) resources = _apply_sort(query, sort).offset(skip).limit(limit).all() return [_serialize_resource(res, current_user, db) for res in resources] @router.put("/{resource_id}", response_model=ResourceOut) def update_resource( resource_id: int, data: ResourceUpdate, current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): res = db.query(Resource).filter(Resource.id == resource_id, Resource.user_id == current_user.id).first() if not res: raise HTTPException(status_code=404, detail="资源不存在") for key, value in data.model_dump(exclude_unset=True).items(): setattr(res, key, value) db.commit() db.refresh(res) return _serialize_resource(res, current_user, db) @router.get("/favorites/me", response_model=list[ResourceOut]) def list_favorite_resources( resource_type: Optional[str] = None, subject: Optional[str] = None, keyword: Optional[str] = None, sort: Optional[str] = Query("latest"), skip: int = Query(0, ge=0), limit: int = Query(20, ge=1, le=100), current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): query = ( db.query(Resource) .join(ResourceFavorite, ResourceFavorite.resource_id == Resource.id) .filter(ResourceFavorite.user_id == current_user.id, Resource.status == "active") ) if resource_type: query = query.filter(Resource.resource_type == resource_type) if subject: query = query.filter(Resource.subject == subject) if keyword: query = query.filter(or_( Resource.title.contains(keyword), Resource.description.contains(keyword), Resource.subject.contains(keyword), Resource.grade.contains(keyword), )) order = ResourceFavorite.created_at.desc() if (sort or "latest") == "latest" else None resources = (query.order_by(order) if order is not None else _apply_sort(query, sort)).offset(skip).limit(limit).all() return [_serialize_resource(res, current_user, db) for res in resources] @router.get("/mine", response_model=list[ResourceOut]) def list_my_resources( resource_type: Optional[str] = None, subject: Optional[str] = None, keyword: Optional[str] = None, sort: Optional[str] = Query("latest"), skip: int = Query(0, ge=0), limit: int = Query(20, ge=1, le=100), current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): query = db.query(Resource).filter(Resource.user_id == current_user.id, Resource.status == "active") if resource_type: query = query.filter(Resource.resource_type == resource_type) if subject: query = query.filter(Resource.subject == subject) if keyword: query = query.filter(or_( Resource.title.contains(keyword), Resource.description.contains(keyword), Resource.subject.contains(keyword), Resource.grade.contains(keyword), )) resources = (_apply_sort(query, sort) if (sort or "latest") != "latest" else query.order_by(Resource.updated_at.desc())).offset(skip).limit(limit).all() return [_serialize_resource(res, current_user, db) for res in resources] @router.get("/{resource_id}", response_model=ResourceOut) def get_resource( resource_id: int, current_user: User | None = Depends(get_optional_current_user), db: Session = Depends(get_db), ): res = db.query(Resource).filter(Resource.id == resource_id).first() if not res or res.status == "archived": raise HTTPException(status_code=404, detail="资源不存在") if not res.is_public and (not current_user or res.user_id != current_user.id): raise HTTPException(status_code=403, detail="无权访问该资源") res.views = (res.views or 0) + 1 db.commit() db.refresh(res) return _serialize_resource(res, current_user, db) @router.post("/{resource_id}/download", response_model=dict) def download_resource( resource_id: int, current_user: User | None = Depends(get_optional_current_user), db: Session = Depends(get_db), ): res = db.query(Resource).filter(Resource.id == resource_id, Resource.status == "active").first() if not res: raise HTTPException(status_code=404, detail="资源不存在") if not res.is_public and (not current_user or res.user_id != current_user.id): raise HTTPException(status_code=403, detail="无权访问该资源") res.downloads = (res.downloads or 0) + 1 db.commit() return {"success": True, "downloads": res.downloads, "file_url": res.file_url} @router.post("/{resource_id}/like", response_model=dict) def like_resource( resource_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): res = db.query(Resource).filter(Resource.id == resource_id, Resource.status == "active").first() if not res: raise HTTPException(status_code=404, detail="资源不存在") if not res.is_public and res.user_id != current_user.id: raise HTTPException(status_code=403, detail="无权访问该资源") favorite = db.query(ResourceFavorite).filter( ResourceFavorite.user_id == current_user.id, ResourceFavorite.resource_id == resource_id, ).first() if not favorite: db.add(ResourceFavorite(user_id=current_user.id, resource_id=resource_id)) db.flush() push_notification(db, user_id=res.user_id, actor_id=current_user.id, ntype="like", title=f"{current_user.name} 收藏了你的资源", content=res.title[:60], link=f"/resources/{res.id}") _sync_favorite_count(res, db) db.commit() return {"success": True, "likes": res.likes, "is_favorited": True} @router.delete("/{resource_id}/like", response_model=dict) def unlike_resource( resource_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): res = db.query(Resource).filter(Resource.id == resource_id, Resource.status == "active").first() if not res: raise HTTPException(status_code=404, detail="资源不存在") if not res.is_public and res.user_id != current_user.id: raise HTTPException(status_code=403, detail="无权访问该资源") favorite = db.query(ResourceFavorite).filter( ResourceFavorite.user_id == current_user.id, ResourceFavorite.resource_id == resource_id, ).first() if favorite: db.delete(favorite) db.flush() _sync_favorite_count(res, db) db.commit() return {"success": True, "likes": res.likes, "is_favorited": False} @router.delete("/{resource_id}", status_code=204) def delete_resource(resource_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): res = db.query(Resource).filter(Resource.id == resource_id, Resource.user_id == current_user.id).first() if not res: raise HTTPException(status_code=404, detail="资源不存在") res.status = "archived" db.commit()