seaislee1209 e4ff7763b5 feat: 加权效率算法 + 代人提交/删除权限 + 角色权限同步优化
- 效率算法重构:加权效率=(制作-修改)/工时,新增通过率、熟练度等级
- 团队效率表格展示制作/修改/通过率/日均净产出/熟练度等级
- 新增 submission:proxy 和 submission:delete 权限,支持代人提交和删除记录
- 角色权限同步改为双向对齐(增+减),主管去除财务权限,组长去除创建项目
- 提交列表UI:项目列固定宽度、操作列居中对齐、编辑弹窗内置删除按钮

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 22:12:17 +08:00

433 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 = "策划案"
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)
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": 1},
{"name": "音效", "phase": "后期", "sort_order": 2},
{"name": "修补镜头", "phase": "后期", "sort_order": 3},
{"name": "剪辑", "phase": "后期", "sort_order": 4},
{"name": "杂项", "phase": "后期", "sort_order": 5},
]
# 内容类型 → 阶段映射(用于自动设置阶段和关联里程碑)
CONTENT_PHASE_MAP = {
"策划案": "前期", "剧本": "前期", "分镜": "前期",
"人设图": "前期", "场景图": "前期",
"动画制作": "制作",
"配音": "后期", "音效": "后期", "修补镜头": "后期", "剪辑": "后期",
}