seaislee1209 f07126e0ca
All checks were successful
Build and Deploy Backend / build-and-deploy (push) Successful in 2m4s
Build and Deploy Web / build-and-deploy (push) Successful in 1m13s
feat: 内容提交表单全面优化 — 阶段重命名+内容类型扩展+集数条件必填
- 阶段重命名: "制作" → "中期",全前后端同步
- 新增前期内容类型: 大纲/梗概、概念设计图、测试片
- 配音改名为AI配音(零迁移,DB存英文NAME)
- 集数按内容类型条件显示: 项目级隐藏,集数级必填
- 工作类型全阶段统一: 制作/修改/测试/QC
- 前期内容隐藏产出时长字段
- 必填校验: 描述+投入时长+集数(条件)
- 今日工时进度条(8h目标)+提交后提醒
- 效率计算只统计中期阶段
- EP集数进度查询优化(N+1→批量GROUP BY)
- 提交列表增加按提交人筛选

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 17:34:11 +08:00

449 lines
18 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 sqlalchemy import (
Column, Integer, String, Float, Date, DateTime, Text,
ForeignKey, Enum as SAEnum, JSON
)
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from database import Base
import enum
# ──────────────────────────── 权限标识符定义 ────────────────────────────
ALL_PERMISSIONS = [
# 仪表盘
("dashboard:view", "查看仪表盘(基础)", "仪表盘"),
("dashboard:view_cost", "查看成本模块", "仪表盘"),
("dashboard:view_waste", "查看损耗模块", "仪表盘"),
("dashboard:view_profit", "查看盈亏模块", "仪表盘"),
("dashboard:view_risk", "查看风险预警", "仪表盘"),
# 项目管理
("project:view", "查看项目", "项目管理"),
("project:create", "创建项目", "项目管理"),
("project:edit", "编辑项目", "项目管理"),
("project:delete", "删除项目", "项目管理"),
("project:complete", "确认完成项目", "项目管理"),
("project:view_contract", "查看合同金额", "项目管理"),
# 内容提交
("submission:view", "查看提交记录", "内容提交"),
("submission:create", "新增提交", "内容提交"),
("submission:proxy", "代人提交", "内容提交"),
("submission:delete", "删除提交记录", "内容提交"),
# 成本管理 —— 按类型细分
("cost_ai:view", "查看AI工具成本", "成本管理"),
("cost_ai:create", "录入AI工具成本", "成本管理"),
("cost_ai:delete", "删除AI工具成本", "成本管理"),
("cost_outsource:view", "查看外包成本", "成本管理"),
("cost_outsource:create", "录入外包成本", "成本管理"),
("cost_outsource:delete", "删除外包成本", "成本管理"),
("cost_overhead:view", "查看固定开支", "成本管理"),
("cost_overhead:create", "录入固定开支", "成本管理"),
("cost_overhead:delete", "删除固定开支", "成本管理"),
("cost_labor:view", "查看人力调整", "成本管理"),
("cost_labor:create", "录入人力调整", "成本管理"),
# 用户与角色
("user:view", "查看用户列表", "用户与角色"),
("user:view_cost", "查看成员薪资成本", "用户与角色"),
("user:manage", "管理用户", "用户与角色"),
("role:manage", "管理角色", "用户与角色"),
# 结算与效率
("settlement:view", "查看结算报告", "结算与效率"),
("efficiency:view", "查看团队效率", "结算与效率"),
]
PERMISSION_KEYS = [p[0] for p in ALL_PERMISSIONS]
# 成本查看权限集合(用于判断是否有任一成本查看权限)
COST_VIEW_PERMS = ["cost_ai:view", "cost_outsource:view", "cost_overhead:view", "cost_labor:view"]
# 旧权限 → 新权限映射(用于数据库迁移)
COST_PERM_MIGRATION = {
"cost:view": ["cost_ai:view", "cost_outsource:view", "cost_overhead:view", "cost_labor:view"],
"cost:create": ["cost_ai:create", "cost_outsource:create", "cost_overhead:create", "cost_labor:create"],
"cost:delete": ["cost_ai:delete", "cost_outsource:delete", "cost_overhead:delete"],
}
# 内置角色定义
BUILTIN_ROLES = {
"超级管理员": {
"description": "系统最高权限,拥有全部功能",
"permissions": PERMISSION_KEYS[:], # 全部
},
"主管": {
"description": "管理项目和提交可查看AI工具与外包成本",
"permissions": [
"dashboard:view", "dashboard:view_waste", "dashboard:view_risk",
"project:view", "project:create", "project:edit", "project:complete",
"submission:view", "submission:create", "submission:proxy", "submission:delete",
"cost_ai:view", "cost_ai:create", "cost_ai:delete",
"cost_outsource:view", "cost_outsource:create", "cost_outsource:delete",
"user:view",
"efficiency:view",
],
},
"组长": {
"description": "管理本组提交和查看成本",
"permissions": [
"project:view",
"submission:view", "submission:create",
"cost_ai:view", "cost_ai:create",
"efficiency:view",
],
},
"成员": {
"description": "提交内容和查看项目",
"permissions": [
"project:view",
"submission:view", "submission:create",
],
},
}
# ──────────────────────────── 枚举定义 ────────────────────────────
class ProjectType(str, enum.Enum):
CLIENT_FORMAL = "客户正式项目"
CLIENT_TEST = "客户测试项目"
INTERNAL_ORIGINAL = "内部原创项目"
INTERNAL_TEST = "内部测试项目"
class ProjectStatus(str, enum.Enum):
IN_PROGRESS = "制作中"
COMPLETED = "已完成"
ABANDONED = "废弃"
class PhaseGroup(str, enum.Enum):
PRE = "前期"
PRODUCTION = "中期"
POST = "后期"
class WorkType(str, enum.Enum):
PRODUCTION = "制作"
TEST = "测试"
PLAN = "方案"
REVISION = "修改"
QC = "QC"
class ContentType(str, enum.Enum):
# 前期(项目级)
PLANNING = "策划案"
SYNOPSIS = "大纲/梗概"
CONCEPT_DESIGN = "概念设计图"
TEST_FOOTAGE = "测试片"
# 前期(集数级)
SCRIPT = "剧本"
STORYBOARD = "分镜"
CHARACTER_DESIGN = "人设图"
SCENE_DESIGN = "场景图"
PROP_DESIGN = "道具图"
# 中期
ANIMATION = "动画制作"
# 后期
DUBBING = "AI配音"
SOUND_EFFECTS = "音效"
SHOT_REPAIR = "修补镜头"
EDITING = "剪辑"
MUSIC = "音乐/BGM"
SUBTITLE = "字幕"
# 通用
OTHER = "其他"
class SubmitTo(str, enum.Enum):
LEADER = "组长"
PRODUCER = "制片"
INTERNAL = "内部"
EXTERNAL = "外部"
class SubscriptionPeriod(str, enum.Enum):
MONTHLY = ""
YEARLY = ""
class CostAllocationType(str, enum.Enum):
PROJECT = "指定项目"
TEAM = "内容组整体"
MANUAL = "手动分摊"
class OutsourceType(str, enum.Enum):
ANIMATION = "动画"
EDITING = "剪辑"
FULL_EPISODE = "整集"
class OverheadCostType(str, enum.Enum):
OFFICE_RENT = "办公室租金"
UTILITIES = "水电费"
# ──────────────────────────── 角色 ────────────────────────────
class Role(Base):
__tablename__ = "roles"
id = Column(Integer, primary_key=True, index=True)
name = Column(String(50), unique=True, nullable=False)
description = Column(String(200), nullable=True)
permissions = Column(JSON, nullable=False, default=[]) # 权限标识符列表
is_system = Column(Integer, nullable=False, default=0) # 1=内置角色不可删
created_at = Column(DateTime, server_default=func.now())
users = relationship("User", back_populates="role_ref")
def has_permission(self, perm: str) -> bool:
return perm in (self.permissions or [])
# ──────────────────────────── 用户 ────────────────────────────
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String(50), unique=True, nullable=False, index=True)
password_hash = Column(String(255), nullable=False)
name = Column(String(50), nullable=False)
phase_group = Column(SAEnum(PhaseGroup), nullable=False)
role_id = Column(Integer, ForeignKey("roles.id"), nullable=False)
monthly_salary = Column(Float, nullable=False, default=0)
bonus = Column(Float, nullable=False, default=0)
social_insurance = Column(Float, nullable=False, default=0)
is_active = Column(Integer, nullable=False, default=1)
created_at = Column(DateTime, server_default=func.now())
# 关系
role_ref = relationship("Role", back_populates="users")
submissions = relationship("Submission", back_populates="user")
led_projects = relationship("Project", back_populates="leader")
@property
def role_name(self):
return self.role_ref.name if self.role_ref else ""
@property
def permissions(self):
return self.role_ref.permissions if self.role_ref else []
def has_permission(self, perm: str) -> bool:
return self.role_ref.has_permission(perm) if self.role_ref else False
@property
def monthly_total_cost(self):
return (self.monthly_salary or 0) + (self.bonus or 0) + (self.social_insurance or 0)
@property
def daily_cost(self):
from config import WORKING_DAYS_PER_MONTH
return round(self.monthly_total_cost / WORKING_DAYS_PER_MONTH, 2) if self.monthly_total_cost else 0
# ──────────────────────────── 项目 ────────────────────────────
class Project(Base):
__tablename__ = "projects"
id = Column(Integer, primary_key=True, index=True)
name = Column(String(100), nullable=False)
project_type = Column(SAEnum(ProjectType), nullable=False)
status = Column(SAEnum(ProjectStatus), nullable=False, default=ProjectStatus.IN_PROGRESS)
leader_id = Column(Integer, ForeignKey("users.id"), nullable=True)
current_phase = Column(SAEnum(PhaseGroup), nullable=False, default=PhaseGroup.PRE)
episode_duration_minutes = Column(Float, nullable=False)
episode_count = Column(Integer, nullable=False)
estimated_completion_date = Column(Date, nullable=True)
actual_completion_date = Column(Date, nullable=True)
contract_amount = Column(Float, nullable=True)
created_at = Column(DateTime, server_default=func.now())
leader = relationship("User", back_populates="led_projects")
submissions = relationship("Submission", back_populates="project")
outsource_costs = relationship("OutsourceCost", back_populates="project")
ai_tool_allocations = relationship("AIToolCostAllocation", back_populates="project")
milestones = relationship("ProjectMilestone", back_populates="project", cascade="all, delete-orphan")
@property
def target_total_seconds(self):
return int(self.episode_duration_minutes * 60 * self.episode_count)
# ──────────────────────────── 内容提交 ────────────────────────────
class Submission(Base):
__tablename__ = "submissions"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
project_id = Column(Integer, ForeignKey("projects.id"), nullable=False)
project_phase = Column(SAEnum(PhaseGroup), nullable=False)
work_type = Column(SAEnum(WorkType), nullable=False)
content_type = Column(SAEnum(ContentType), nullable=False)
duration_minutes = Column(Float, nullable=True, default=0)
duration_seconds = Column(Float, nullable=True, default=0)
total_seconds = Column(Float, nullable=False, default=0)
hours_spent = Column(Float, nullable=True)
submit_to = Column(SAEnum(SubmitTo), nullable=False)
description = Column(Text, nullable=True)
submit_date = Column(Date, nullable=False)
milestone_id = Column(Integer, ForeignKey("project_milestones.id"), nullable=True)
delay_reason = Column(Text, nullable=True)
episode_number = Column(Integer, nullable=True) # EP集数: 1=EP01, 2=EP02...
created_at = Column(DateTime, server_default=func.now())
user = relationship("User", back_populates="submissions")
project = relationship("Project", back_populates="submissions")
milestone = relationship("ProjectMilestone")
history = relationship("SubmissionHistory", back_populates="submission")
# ──────────────────────────── AI 工具成本 ────────────────────────────
class AIToolCost(Base):
__tablename__ = "ai_tool_costs"
id = Column(Integer, primary_key=True, index=True)
tool_name = Column(String(100), nullable=False)
subscription_period = Column(SAEnum(SubscriptionPeriod), nullable=False)
amount = Column(Float, nullable=False)
allocation_type = Column(SAEnum(CostAllocationType), nullable=False)
project_id = Column(Integer, ForeignKey("projects.id"), nullable=True)
recorded_by = Column(Integer, ForeignKey("users.id"), nullable=False)
record_date = Column(Date, nullable=False)
created_at = Column(DateTime, server_default=func.now())
allocations = relationship("AIToolCostAllocation", back_populates="ai_tool_cost")
class AIToolCostAllocation(Base):
__tablename__ = "ai_tool_cost_allocations"
id = Column(Integer, primary_key=True, index=True)
ai_tool_cost_id = Column(Integer, ForeignKey("ai_tool_costs.id"), nullable=False)
project_id = Column(Integer, ForeignKey("projects.id"), nullable=False)
percentage = Column(Float, nullable=False)
ai_tool_cost = relationship("AIToolCost", back_populates="allocations")
project = relationship("Project", back_populates="ai_tool_allocations")
# ──────────────────────────── 外包成本 ────────────────────────────
class OutsourceCost(Base):
__tablename__ = "outsource_costs"
id = Column(Integer, primary_key=True, index=True)
project_id = Column(Integer, ForeignKey("projects.id"), nullable=False)
outsource_type = Column(SAEnum(OutsourceType), nullable=False)
episode_start = Column(Integer, nullable=True)
episode_end = Column(Integer, nullable=True)
amount = Column(Float, nullable=False)
recorded_by = Column(Integer, ForeignKey("users.id"), nullable=False)
record_date = Column(Date, nullable=False)
created_at = Column(DateTime, server_default=func.now())
project = relationship("Project", back_populates="outsource_costs")
# ──────────────────────────── 人力成本手动调整 ────────────────────────────
class CostOverride(Base):
__tablename__ = "cost_overrides"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
date = Column(Date, nullable=False)
project_id = Column(Integer, ForeignKey("projects.id"), nullable=False)
override_amount = Column(Float, nullable=False)
adjusted_by = Column(Integer, ForeignKey("users.id"), nullable=False)
reason = Column(Text, nullable=True)
created_at = Column(DateTime, server_default=func.now())
# ──────────────────────────── 提交历史版本 ────────────────────────────
class SubmissionHistory(Base):
__tablename__ = "submission_history"
id = Column(Integer, primary_key=True, index=True)
submission_id = Column(Integer, ForeignKey("submissions.id"), nullable=False)
changed_by = Column(Integer, ForeignKey("users.id"), nullable=False)
change_reason = Column(Text, nullable=False)
old_data = Column(JSON, nullable=False)
new_data = Column(JSON, nullable=False)
created_at = Column(DateTime, server_default=func.now())
submission = relationship("Submission", back_populates="history")
# ──────────────────────────── 固定开支 ────────────────────────────
class OverheadCost(Base):
__tablename__ = "overhead_costs"
id = Column(Integer, primary_key=True, index=True)
cost_type = Column(SAEnum(OverheadCostType), nullable=False)
amount = Column(Float, nullable=False)
record_month = Column(String(7), nullable=False)
note = Column(Text, nullable=True)
recorded_by = Column(Integer, ForeignKey("users.id"), nullable=False)
created_at = Column(DateTime, server_default=func.now())
# ──────────────────────────── 项目里程碑 ────────────────────────────
class ProjectMilestone(Base):
__tablename__ = "project_milestones"
id = Column(Integer, primary_key=True, index=True)
project_id = Column(Integer, ForeignKey("projects.id"), nullable=False)
name = Column(String(100), nullable=False)
phase = Column(SAEnum(PhaseGroup), nullable=False)
is_completed = Column(Integer, nullable=False, default=0) # 0/1
completed_at = Column(DateTime, nullable=True)
sort_order = Column(Integer, nullable=False, default=0)
estimated_days = Column(Integer, nullable=True) # 预估工作日
start_date = Column(Date, nullable=True) # 开始日期
project = relationship("Project", back_populates="milestones")
# 默认里程碑模板
DEFAULT_MILESTONES = [
# 前期
{"name": "策划案", "phase": "前期", "sort_order": 1},
{"name": "剧本", "phase": "前期", "sort_order": 2},
{"name": "分镜", "phase": "前期", "sort_order": 3},
{"name": "人设图", "phase": "前期", "sort_order": 4},
{"name": "场景图", "phase": "前期", "sort_order": 5},
{"name": "道具图", "phase": "前期", "sort_order": 6},
# 后期
{"name": "配音", "phase": "后期", "sort_order": 1},
{"name": "AI配音", "phase": "后期", "sort_order": 2},
{"name": "音效", "phase": "后期", "sort_order": 3},
{"name": "修补镜头", "phase": "后期", "sort_order": 4},
{"name": "剪辑", "phase": "后期", "sort_order": 5},
{"name": "音乐/BGM", "phase": "后期", "sort_order": 6},
{"name": "字幕", "phase": "后期", "sort_order": 7},
{"name": "杂项", "phase": "后期", "sort_order": 8},
]
# 内容类型 → 阶段映射(用于自动设置阶段和关联里程碑)
CONTENT_PHASE_MAP = {
"策划案": "前期", "大纲/梗概": "前期", "概念设计图": "前期", "测试片": "前期",
"剧本": "前期", "分镜": "前期",
"人设图": "前期", "场景图": "前期", "道具图": "前期",
"动画制作": "中期",
"AI配音": "后期", "音效": "后期",
"修补镜头": "后期", "剪辑": "后期", "音乐/BGM": "后期", "字幕": "后期",
}
# 项目级内容类型(不需要选集数)
PROJECT_LEVEL_TYPES = {"策划案", "大纲/梗概", "概念设计图", "测试片"}