- 用户管理页面权限拆分:user:view可查看列表,user:manage控制新增/编辑,user:view_cost控制薪资列显示 - 成本管理新增"人力调整"Tab,支持查看和录入CostOverride记录 - 仪表盘新增4个子权限(成本/损耗/盈亏/风险预警),管理员可按角色灵活配置可见模块 - 修复组长进入成本管理页面弹出"权限不足"提示的问题 - 修复主管无法访问用户管理页面的路由权限问题 - 提交页面延期原因字段仅在里程碑超期时显示 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
430 lines
18 KiB
Python
430 lines
18 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", "查看仪表盘(基础)", "仪表盘"),
|
|
("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", "新增提交", "内容提交"),
|
|
# 成本管理 —— 按类型细分
|
|
("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": "管理项目和成本,不可管理用户和角色",
|
|
"permissions": [
|
|
"dashboard:view", "dashboard:view_cost", "dashboard:view_waste", "dashboard:view_profit", "dashboard:view_risk",
|
|
"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 = {
|
|
"策划案": "前期", "剧本": "前期", "分镜": "前期",
|
|
"人设图": "前期", "场景图": "前期",
|
|
"动画制作": "制作",
|
|
"配音": "后期", "音效": "后期", "修补镜头": "后期", "剪辑": "后期",
|
|
}
|