airlabs-manage/backend/routers/submissions.py
seaislee1209 087d4e1a6b
All checks were successful
Build and Deploy Backend / build-and-deploy (push) Successful in 5m44s
Build and Deploy Web / build-and-deploy (push) Successful in 3m24s
feat: 新增「修改」「QC」工作类型 + EP集数追踪
- 工作类型新增「修改」(返工)和「QC」(质量审核),支持组长多角色工作记录
- 提交表单支持按集数(EP)归类,项目详情展示各集进度
- 热力图增加修改(橙色)、QC(绿色)颜色区分和图例
- 效率算法优化:产出按小时计算,损耗统计更精准
- 数据库自动迁移:episode_number字段 + work_type ENUM扩展

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 23:18:47 +08:00

225 lines
8.7 KiB
Python

"""内容提交路由"""
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 project_id:
pass # 项目内提交不做用户过滤
elif 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("", response_model=SubmissionOut)
def create_submission(
req: SubmissionCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
# 校验项目存在
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)
# 自动关联里程碑:根据 content_type 匹配同名里程碑
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}天),请填写延期原因"
)
sub = Submission(
user_id=current_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=req.hours_spent,
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=req.episode_number,
)
db.add(sub)
db.commit()
db.refresh(sub)
return submission_to_out(sub)
@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_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)
# 保存新数据
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
]