- 日报/周报/月报改为结构化卡片推送(column_set布局) - 新增 report:daily/weekly/monthly 权限到角色管理 - 产出统计只算中期制作阶段动画秒数 - 效率之星改为跨项目加权通过率 - AI点评补充风险数据源 - 禁用多余admin账号,股东角色加报告权限 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
456 lines
19 KiB
Python
456 lines
19 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", "新增提交", "内容提交"),
|
||
("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", "查看团队效率", "结算与效率"),
|
||
# 报告推送
|
||
("report:daily", "触发/查看日报", "报告推送"),
|
||
("report:weekly", "触发/查看周报(含成本)", "报告推送"),
|
||
("report:monthly", "触发/查看月报(含成本+盈亏)", "报告推送"),
|
||
]
|
||
|
||
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",
|
||
"report:daily", "report:weekly", "report:monthly",
|
||
],
|
||
},
|
||
"组长": {
|
||
"description": "管理本组提交和查看成本",
|
||
"permissions": [
|
||
"project:view",
|
||
"submission:view", "submission:create",
|
||
"cost_ai:view", "cost_ai:create",
|
||
"efficiency:view",
|
||
"report:daily",
|
||
],
|
||
},
|
||
"成员": {
|
||
"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=内置角色不可删
|
||
exempt_submission = 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 = {"策划案", "大纲/梗概", "概念设计图", "测试片"}
|