seaislee1209 dc42306c24
All checks were successful
Build and Deploy Backend / build-and-deploy (push) Successful in 1m22s
Build and Deploy Web / build-and-deploy (push) Successful in 55s
feat: V2.1 三阶段损耗前端增强 + 路由修复 + 演示数据重写
- 仪表盘双色进度条(超100%蓝红分段)、工时损耗展示、chart tooltip增强
- 修复 Submissions.vue 延期原因字段始终显示的Bug
- 修复 SPA catch-all 路由拦截 API 请求(去尾部斜杠)
- seed_demo.py 重写:5项目/4类型/32里程碑/124提交,真实时间线
- 三阶段损耗计算(前期工时/制作秒数/后期工时)
- ContentType 扩展为11种,里程碑增强(预估天数/开始日期/超期检测)
- 更新 PRD 和项目总结文档

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 18:32:07 +08:00

425 lines
17 KiB
Python

"""数据库模型 —— 所有表定义"""
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", "查看仪表盘", "仪表盘"),
# 项目管理
("project:view", "查看项目", "项目管理"),
("project:create", "创建项目", "项目管理"),
("project:edit", "编辑项目", "项目管理"),
("project:delete", "删除项目", "项目管理"),
("project:complete", "确认完成项目", "项目管理"),
("project:view_contract", "查看合同金额", "项目管理"),
# 内容提交
("submission:view", "查看提交记录", "内容提交"),
("submission:create", "新增提交", "内容提交"),
# 成本管理 —— 按类型细分
("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: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": "管理项目和成本,不可管理用户和角色",
"permissions": [
"dashboard:view",
"project:view", "project:create", "project:edit", "project:complete",
"submission:view", "submission:create",
"cost_ai:view", "cost_ai:create", "cost_ai:delete",
"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",
"settlement:view", "efficiency:view",
],
},
"组长": {
"description": "管理本组提交和查看成本",
"permissions": [
"project:view", "project:create",
"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 = "方案"
class ContentType(str, enum.Enum):
# 前期
PLANNING = "策划案"
SCRIPT = "剧本"
STORYBOARD = "分镜"
CHARACTER_DESIGN = "人设图"
SCENE_DESIGN = "场景图"
# 制作
ANIMATION = "动画制作"
# 后期
DUBBING = "配音"
SOUND_EFFECTS = "音效"
SHOT_REPAIR = "修补镜头"
EDITING = "剪辑"
# 通用
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)
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": 1},
{"name": "音效", "phase": "后期", "sort_order": 2},
{"name": "修补镜头", "phase": "后期", "sort_order": 3},
{"name": "剪辑", "phase": "后期", "sort_order": 4},
{"name": "杂项", "phase": "后期", "sort_order": 5},
]
# 内容类型 → 阶段映射(用于自动设置阶段和关联里程碑)
CONTENT_PHASE_MAP = {
"策划案": "前期", "剧本": "前期", "分镜": "前期",
"人设图": "前期", "场景图": "前期",
"动画制作": "制作",
"配音": "后期", "音效": "后期", "修补镜头": "后期", "剪辑": "后期",
}