feat: 后端 CRUD 完成(episodes + yearly_targets + users 管理 API)

This commit is contained in:
simonkoson
2026-05-15 12:17:46 +08:00
parent fab15505bd
commit 3dc1a1cf3c
10 changed files with 497 additions and 4 deletions
+82
View File
@@ -0,0 +1,82 @@
"""
节目期次 API — 增改查(责编和管理员可操作)
"""
from fastapi import APIRouter, Depends, HTTPException, status
from sqlmodel import Session, select
from app.core.deps import get_current_user, require_role
from app.db.session import get_session
from app.models.user import User, UserRole
from app.schemas.episode import EpisodeCreate, EpisodeResponse, EpisodeUpdate
router = APIRouter(prefix="/api/episodes", tags=["节目期次"])
@router.get("", response_model=list[EpisodeResponse])
def list_episodes(
limit: int = 50,
offset: int = 0,
session: Session = Depends(get_session),
current_user: User = Depends(require_role(UserRole.zhipianren, UserRole.zebian)),
):
"""获取节目期次列表(仅管理员/责编)。按播出日期倒序。"""
statement = select(Episode).order_by(Episode.air_date.desc()).offset(offset).limit(limit)
episodes = session.exec(statement).all()
return episodes
@router.get("/{episode_id}", response_model=EpisodeResponse)
def get_episode(
episode_id: int,
session: Session = Depends(get_session),
current_user: User = Depends(require_role(UserRole.zhipianren, UserRole.zebian)),
):
"""根据 ID 获取单期节目(仅管理员/责编)。"""
episode = session.get(Episode, episode_id)
if episode is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="期次不存在")
return episode
@router.post("", response_model=EpisodeResponse, status_code=status.HTTP_201_CREATED)
def create_episode(
body: EpisodeCreate,
session: Session = Depends(get_session),
current_user: User = Depends(require_role(UserRole.zhipianren, UserRole.zebian)),
):
"""新增一期节目(仅管理员/责编)。"""
# 检查期次号唯一
existing = session.exec(
select(Episode).where(Episode.episode_number == body.episode_number)
).first()
if existing:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="期次号已存在")
episode = Episode.model_validate(body.model_dump())
session.add(episode)
session.commit()
session.refresh(episode)
return episode
@router.patch("/{episode_id}", response_model=EpisodeResponse)
def update_episode(
episode_id: int,
body: EpisodeUpdate,
session: Session = Depends(get_session),
current_user: User = Depends(require_role(UserRole.zhipianren, UserRole.zebian)),
):
"""更新一期节目信息(仅管理员/责编)。"""
episode = session.get(Episode, episode_id)
if episode is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="期次不存在")
update_data = body.model_dump(exclude_unset=True)
for field, value in update_data.items():
setattr(episode, field, value)
session.add(episode)
session.commit()
session.refresh(episode)
return episode
+106
View File
@@ -0,0 +1,106 @@
"""
用户管理 API — 管理员 CRUD(仅 zhipianren 可操作)
"""
from fastapi import APIRouter, Depends, HTTPException, status
from sqlmodel import Session, select
from app.core.deps import get_current_user, require_role
from app.core.security import hash_password, verify_password
from app.db.session import get_session
from app.models.user import User, UserRole
from app.schemas.user_admin import UserCreate, UserResponse, UserUpdate
router = APIRouter(prefix="/api/users", tags=["用户管理"])
@router.get("", response_model=list[UserResponse])
def list_users(
session: Session = Depends(get_session),
current_user: User = Depends(require_role(UserRole.zhipianren)),
):
"""获取所有用户列表(仅管理员)。"""
statement = select(User).order_by(User.id)
users = session.exec(statement).all()
return users
@router.get("/{user_id}", response_model=UserResponse)
def get_user(
user_id: int,
session: Session = Depends(get_session),
current_user: User = Depends(require_role(UserRole.zhipianren)),
):
"""根据 ID 获取单个用户(仅管理员)。"""
user = session.get(User, user_id)
if user is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在")
return user
@router.post("", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
def create_user(
body: UserCreate,
session: Session = Depends(get_session),
current_user: User = Depends(require_role(UserRole.zhipianren)),
):
"""创建新用户(仅管理员)。"""
# 检查用户名唯一
existing = session.exec(select(User).where(User.username == body.username)).first()
if existing:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="用户名已存在")
user = User(
username=body.username,
display_name=body.display_name,
password_hash=hash_password(body.password),
role=body.role,
profile_text=body.profile_text,
)
session.add(user)
session.commit()
session.refresh(user)
return user
@router.patch("/{user_id}", response_model=UserResponse)
def update_user(
user_id: int,
body: UserUpdate,
session: Session = Depends(get_session),
current_user: User = Depends(require_role(UserRole.zhipianren)),
):
"""更新用户信息(仅管理员)。可更新 display_name/role/profile_text/is_active。"""
user = session.get(User, user_id)
if user is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在")
update_data = body.model_dump(exclude_unset=True)
for field, value in update_data.items():
setattr(user, field, value)
session.add(user)
session.commit()
session.refresh(user)
return user
@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
def deactivate_user(
user_id: int,
session: Session = Depends(get_session),
current_user: User = Depends(require_role(UserRole.zhipianren)),
):
"""停用用户(软删除,仅管理员)。"""
user = session.get(User, user_id)
if user is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在")
# 不能停用自己的账号
if user.id == current_user.id:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="不能停用自己的账号")
user.is_active = False
session.add(user)
session.commit()
return None
+96
View File
@@ -0,0 +1,96 @@
"""
年度收视目标 API — 增改查
业务规则(.clinerules 5.2 只增不改):
- yearly_targets.year 有 UNIQUE 约束
- INSERT 时 year 冲突 → 直接报错,阻止静默覆盖
- 禁止 ON CONFLICT UPDATE
"""
from fastapi import APIRouter, Depends, HTTPException, status
from sqlmodel import Session, select
from app.core.deps import get_current_user, require_role
from app.db.session import get_session
from app.models.user import User, UserRole
from app.schemas.yearly_target import YearlyTargetCreate, YearlyTargetResponse, YearlyTargetUpdate
router = APIRouter(prefix="/api/yearly_targets", tags=["年度目标"])
@router.get("", response_model=list[YearlyTargetResponse])
def list_targets(
session: Session = Depends(get_session),
current_user: User = Depends(require_role(UserRole.zhipianren, UserRole.zebian)),
):
"""获取所有年度目标(仅管理员/责编)。"""
statement = select(YearlyTarget).order_by(YearlyTarget.year.desc())
targets = session.exec(statement).all()
return targets
@router.get("/{target_id}", response_model=YearlyTargetResponse)
def get_target(
target_id: int,
session: Session = Depends(get_session),
current_user: User = Depends(require_role(UserRole.zhipianren, UserRole.zebian)),
):
"""根据 ID 获取单个年度目标(仅管理员/责编)。"""
target = session.get(YearlyTarget, target_id)
if target is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="目标不存在")
return target
@router.post("", response_model=YearlyTargetResponse, status_code=status.HTTP_201_CREATED)
def create_target(
body: YearlyTargetCreate,
session: Session = Depends(get_session),
current_user: User = Depends(require_role(UserRole.zhipianren, UserRole.zebian)),
):
"""新增年度目标(仅管理员/责编)。
注意:year 有 UNIQUE 约束,重复年份会直接报错。
这是预期行为——历史年份目标不可覆盖。
"""
# 先查是否已存在(让报错更友好,而不是数据库层直接崩溃)
existing = session.exec(select(YearlyTarget).where(YearlyTarget.year == body.year)).first()
if existing:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail=f"{body.year} 年目标已存在,历史年份不可覆盖。请新增其他年份。",
)
target = YearlyTarget.model_validate(body.model_dump())
session.add(target)
session.commit()
session.refresh(target)
return target
@router.patch("/{target_id}", response_model=YearlyTargetResponse)
def update_target(
target_id: int,
body: YearlyTargetUpdate,
session: Session = Depends(get_session),
current_user: User = Depends(require_role(UserRole.zhipianren, UserRole.zebian)),
):
"""更新年度目标(仅管理员/责编)。
注意:此接口仅用于修正错字等小修,year 字段不可修改。
"""
target = session.get(YearlyTarget, target_id)
if target is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="目标不存在")
update_data = body.model_dump(exclude_unset=True)
# 禁止修改 year 字段
update_data.pop("year", None)
for field, value in update_data.items():
setattr(target, field, value)
session.add(target)
session.commit()
session.refresh(target)
return target