"""内容提交路由""" from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from typing import List, Optional from datetime import date from database import get_db from models import ( User, Submission, SubmissionHistory, Project, ProjectMilestone, PhaseGroup, WorkType, ContentType, SubmitTo, CONTENT_PHASE_MAP ) from datetime import timedelta from schemas import SubmissionCreate, SubmissionUpdate, SubmissionOut from auth import get_current_user, require_permission router = APIRouter(prefix="/api/submissions", tags=["内容提交"]) def submission_to_out(s: Submission) -> SubmissionOut: return SubmissionOut( id=s.id, user_id=s.user_id, user_name=s.user.name if s.user else None, project_id=s.project_id, project_name=s.project.name if s.project else None, project_phase=s.project_phase.value if hasattr(s.project_phase, 'value') else s.project_phase, work_type=s.work_type.value if hasattr(s.work_type, 'value') else s.work_type, content_type=s.content_type.value if hasattr(s.content_type, 'value') else s.content_type, duration_minutes=s.duration_minutes, duration_seconds=s.duration_seconds, total_seconds=s.total_seconds, hours_spent=s.hours_spent, submit_to=s.submit_to.value if hasattr(s.submit_to, 'value') else s.submit_to, description=s.description, submit_date=s.submit_date, milestone_name=s.milestone.name if s.milestone else None, delay_reason=s.delay_reason, episode_number=s.episode_number, created_at=s.created_at, ) @router.get("", response_model=List[SubmissionOut]) def list_submissions( project_id: Optional[int] = Query(None), user_id: Optional[int] = Query(None), start_date: Optional[date] = Query(None), end_date: Optional[date] = Query(None), db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): q = db.query(Submission) # 权限检查:没有 user:view 权限只能看自己的 if not current_user.has_permission("user:view"): q = q.filter(Submission.user_id == current_user.id) elif user_id: # 有权限且指定了用户,过滤该用户 q = q.filter(Submission.user_id == user_id) # 项目筛选 if project_id: q = q.filter(Submission.project_id == project_id) if start_date: q = q.filter(Submission.submit_date >= start_date) if end_date: q = q.filter(Submission.submit_date <= end_date) subs = q.order_by(Submission.submit_date.desc(), Submission.created_at.desc()).all() return [submission_to_out(s) for s in subs] @router.post("") def create_submission( req: SubmissionCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): # 代人提交:有 submission:proxy 权限时可以指定 user_id target_user_id = current_user.id if req.user_id and req.user_id != current_user.id: if not current_user.has_permission("submission:proxy"): raise HTTPException(status_code=403, detail="没有代人提交权限") target_user = db.query(User).filter(User.id == req.user_id).first() if not target_user: raise HTTPException(status_code=404, detail="目标用户不存在") target_user_id = req.user_id # 必填校验 if not req.description or not req.description.strip(): raise HTTPException(status_code=422, detail="请填写描述") if req.hours_spent is None or req.hours_spent <= 0: raise HTTPException(status_code=422, detail="请填写投入时长") # 产出时长校验:前期内容不需要,中期/后期内容必须 > 0 NO_DURATION_TYPES = {'策划案', '大纲/梗概', '概念设计图', '测试片', '剧本', '分镜', '人设图', '场景图', '道具图', '培训', '招聘面试', '内部其他'} content_val = req.content_type.value if hasattr(req.content_type, 'value') else req.content_type if content_val not in NO_DURATION_TYPES: total_secs = (req.duration_minutes or 0) * 60 + (req.duration_seconds or 0) if total_secs <= 0: raise HTTPException(status_code=422, detail="请填写产出时长") # 确定集数列表 from models import PROJECT_LEVEL_TYPES content_val = req.content_type.value if hasattr(req.content_type, 'value') else req.content_type if content_val in PROJECT_LEVEL_TYPES: episode_list = [None] # 项目级,单条无集数 elif req.episode_numbers and len(req.episode_numbers) > 0: episode_list = req.episode_numbers # 批量多集 elif req.episode_number is not None: episode_list = [req.episode_number] # 单集(含0=全集通用) else: raise HTTPException(status_code=422, detail="请选择集数") # 校验项目存在 project = db.query(Project).filter(Project.id == req.project_id).first() if not project: raise HTTPException(status_code=404, detail="项目不存在") # 自动计算总秒数(每集) total_seconds = (req.duration_minutes or 0) * 60 + (req.duration_seconds or 0) # 自动关联里程碑 milestone_id = None content_val = req.content_type if content_val in CONTENT_PHASE_MAP: ms = db.query(ProjectMilestone).filter( ProjectMilestone.project_id == req.project_id, ProjectMilestone.name == content_val, ).first() if ms: milestone_id = ms.id if ms.estimated_days and ms.start_date: expected_end = ms.start_date + timedelta(days=ms.estimated_days) if req.submit_date > expected_end and not req.delay_reason: raise HTTPException( status_code=422, detail=f"里程碑「{ms.name}」已超期(预估{ms.estimated_days}天),请填写延期原因" ) # 投入时长平均分配到每集 ep_count = len(episode_list) hours_per_ep = round(req.hours_spent / ep_count, 2) results = [] for ep in episode_list: sub = Submission( user_id=target_user_id, project_id=req.project_id, project_phase=PhaseGroup(req.project_phase), work_type=WorkType(req.work_type), content_type=ContentType(req.content_type), duration_minutes=req.duration_minutes or 0, duration_seconds=req.duration_seconds or 0, total_seconds=total_seconds, hours_spent=hours_per_ep, submit_to=SubmitTo(req.submit_to), description=req.description, submit_date=req.submit_date, milestone_id=milestone_id, delay_reason=req.delay_reason, episode_number=ep, ) db.add(sub) results.append(sub) db.commit() for s in results: db.refresh(s) if len(results) == 1: return submission_to_out(results[0]) return [submission_to_out(s) for s in results] @router.put("/{submission_id}", response_model=SubmissionOut) def update_submission( submission_id: int, req: SubmissionUpdate, db: Session = Depends(get_db), current_user: User = Depends(require_permission("submission:create")) ): """高权限修改提交记录(需填写原因)""" sub = db.query(Submission).filter(Submission.id == submission_id).first() if not sub: raise HTTPException(status_code=404, detail="提交记录不存在") # 保存旧数据用于历史记录 old_data = { "project_phase": sub.project_phase.value if hasattr(sub.project_phase, 'value') else sub.project_phase, "work_type": sub.work_type.value if hasattr(sub.work_type, 'value') else sub.work_type, "content_type": sub.content_type.value if hasattr(sub.content_type, 'value') else sub.content_type, "duration_minutes": sub.duration_minutes, "duration_seconds": sub.duration_seconds, "total_seconds": sub.total_seconds, "hours_spent": sub.hours_spent, "submit_to": sub.submit_to.value if hasattr(sub.submit_to, 'value') else sub.submit_to, "description": sub.description, "submit_date": str(sub.submit_date), } # 更新字段 if req.project_id is not None: project = db.query(Project).filter(Project.id == req.project_id).first() if not project: raise HTTPException(status_code=404, detail="项目不存在") sub.project_id = req.project_id if req.project_phase is not None: sub.project_phase = PhaseGroup(req.project_phase) if req.work_type is not None: sub.work_type = WorkType(req.work_type) if req.content_type is not None: sub.content_type = ContentType(req.content_type) if req.duration_minutes is not None: sub.duration_minutes = req.duration_minutes if req.duration_seconds is not None: sub.duration_seconds = req.duration_seconds if req.hours_spent is not None: sub.hours_spent = req.hours_spent if req.submit_to is not None: sub.submit_to = SubmitTo(req.submit_to) if req.description is not None: sub.description = req.description if req.submit_date is not None: sub.submit_date = req.submit_date if req.episode_number is not None: sub.episode_number = req.episode_number # 重算总秒数 sub.total_seconds = (sub.duration_minutes or 0) * 60 + (sub.duration_seconds or 0) # 产出时长校验:前期内容不需要,中期/后期内容必须 > 0 NO_DURATION_TYPES = {'策划案', '大纲/梗概', '概念设计图', '测试片', '剧本', '分镜', '人设图', '场景图', '道具图', '培训', '招聘面试', '内部其他'} content_val = sub.content_type.value if hasattr(sub.content_type, 'value') else sub.content_type if content_val not in NO_DURATION_TYPES and sub.total_seconds <= 0: raise HTTPException(status_code=422, detail="请填写产出时长") # 保存新数据 new_data = { "project_phase": sub.project_phase.value if hasattr(sub.project_phase, 'value') else sub.project_phase, "work_type": sub.work_type.value if hasattr(sub.work_type, 'value') else sub.work_type, "content_type": sub.content_type.value if hasattr(sub.content_type, 'value') else sub.content_type, "duration_minutes": sub.duration_minutes, "duration_seconds": sub.duration_seconds, "total_seconds": sub.total_seconds, "hours_spent": sub.hours_spent, "submit_to": sub.submit_to.value if hasattr(sub.submit_to, 'value') else sub.submit_to, "description": sub.description, "submit_date": str(sub.submit_date), } # 写入修改历史 history = SubmissionHistory( submission_id=sub.id, changed_by=current_user.id, change_reason=req.change_reason, old_data=old_data, new_data=new_data, ) db.add(history) db.commit() db.refresh(sub) return submission_to_out(sub) @router.get("/{submission_id}/history") def get_submission_history( submission_id: int, db: Session = Depends(get_db), current_user: User = Depends(require_permission("submission:view")) ): """查看提交的修改历史""" records = db.query(SubmissionHistory).filter( SubmissionHistory.submission_id == submission_id ).order_by(SubmissionHistory.created_at.desc()).all() return [ { "id": r.id, "changed_by": r.changed_by, "change_reason": r.change_reason, "old_data": r.old_data, "new_data": r.new_data, "created_at": r.created_at, } for r in records ] @router.delete("/{submission_id}") def delete_submission( submission_id: int, db: Session = Depends(get_db), current_user: User = Depends(require_permission("submission:delete")) ): """删除提交记录(需要 submission:delete 权限)""" sub = db.query(Submission).filter(Submission.id == submission_id).first() if not sub: raise HTTPException(status_code=404, detail="提交记录不存在") # 同时删除关联的修改历史 db.query(SubmissionHistory).filter( SubmissionHistory.submission_id == submission_id ).delete() db.delete(sub) db.commit() return {"detail": "删除成功"} @router.get("/daily-hours") def get_daily_hours( user_id: Optional[int] = Query(None), target_date: Optional[date] = Query(None, alias="date"), db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """查询指定用户某日已填工时""" from sqlalchemy import func uid = user_id or current_user.id # 非本人查询需要 proxy 权限 if uid != current_user.id and not current_user.has_permission("submission:proxy"): raise HTTPException(status_code=403, detail="无权查看他人工时") d = target_date or date.today() filled = db.query(func.sum(Submission.hours_spent)).filter( Submission.user_id == uid, Submission.submit_date == d, ).scalar() or 0.0 target = 8.0 return { "filled": round(filled, 1), "target": target, "remaining": round(max(target - filled, 0), 1), }