airlabs-manage/backend/routers/submissions.py
seaislee1209 ac350e763b
All checks were successful
Build and Deploy Backend / build-and-deploy (push) Successful in 2m24s
Build and Deploy Web / build-and-deploy (push) Successful in 4m11s
feat: 制作产出/后期产出分离 + 全集通用选项 + 概览卡片布局优化
- 项目详情:拆分"已提交"为"制作产出"(中期)和"后期产出"(后期按类型细分)
- 进度百分比仅计算中期动画产出,EP集数进度只统计中期
- 新增"全集通用"(episode=0)选项,与具体集数互斥
- 概览卡片改为上4下1布局,后期产出独立全宽卡片展示明细

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 21:20:49 +08:00

311 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""内容提交路由"""
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("")
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="请填写投入时长")
# 确定集数列表
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_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
]
@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),
}