PayrollManagementSystem/backend/alembic/versions/22a3b4c5d6e7_authorization.py
liuwenxin 1461a8d113 feat(后端): 添加授权体系、系统监控与试用期计提
按 2026-11-02 需求补齐计划交付后端能力:
- 角色授权:权限目录化,接口统一准入校验,未登记默认拒绝;
  管理员判断改为按数据库角色重新判定,防止权限码持有者绕过
- 系统监控:单进程采集资源、请求与异常指标,分钟级聚合,
  阈值配置化;敏感配置增加 repr 保护,Redis 密码 URL 转义
- 试用期计提:按回款日判定并写入快照,年度结清与调整单联动;
  人员模型补试用期、离职日期与工资版本历史字段
- 计提模板:公式骨架受控,页面维护名称与参数绑定
- T+ 采集:自动化配置表与进度查询

内置角色禁停用保护对齐模块注释;核算规则版本提为常量并
补充批量核算失败堆栈日志。

影响范围:后端全部模块、数据库迁移 V22-V28
验证:uv run ruff check 通过;922 个单元测试全部通过;
集成测试因本机 MySQL 数据卷被其他容器锁定暂未运行
2026-11-03 11:20:55 +08:00

48 lines
17 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.

"""V22 完整角色及个人授权;目录与模板固化于此版本。"""
from alembic import op
import sqlalchemy as sa
revision: str = "22a3b4c5d6e7"
down_revision: str | None = "21f2a3b4c5d6"
branch_labels = None
depends_on = None
DDL = ["CREATE TABLE authorization_settings (\n\tid BIGINT UNSIGNED NOT NULL COMMENT '单例主键ID' AUTO_INCREMENT, \n\tmaker_user_id BIGINT UNSIGNED COMMENT '现任工资制作人账号ID', \n\trevision INTEGER NOT NULL COMMENT '全局授权版本' DEFAULT '1', \n\tupdated_by BIGINT UNSIGNED COMMENT '最近修改人ID', \n\tcreate_time DATETIME NOT NULL COMMENT '创建时间' DEFAULT CURRENT_TIMESTAMP, \n\tupdate_time DATETIME NOT NULL COMMENT '更新时间' DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, \n\tCONSTRAINT pk_authorization_settings PRIMARY KEY (id), \n\tCONSTRAINT fk_authorization_settings_maker_user_id FOREIGN KEY(maker_user_id) REFERENCES users (id) ON DELETE RESTRICT, \n\tCONSTRAINT fk_authorization_settings_updated_by FOREIGN KEY(updated_by) REFERENCES users (id) ON DELETE SET NULL\n)ENGINE=InnoDB CHARSET=utf8mb4 COMMENT='授权设置表(单例与并发修改锁)' COLLATE utf8mb4_general_ci;", "CREATE TABLE user_permission_overrides (\n\tid BIGINT UNSIGNED NOT NULL COMMENT '主键ID' AUTO_INCREMENT, \n\tuser_id BIGINT UNSIGNED NOT NULL COMMENT '系统用户ID', \n\tpermission_id BIGINT UNSIGNED NOT NULL COMMENT '权限ID', \n\teffect VARCHAR(8) NOT NULL COMMENT '个人覆盖:ALLOW允许 DENY禁止', \n\tcreate_time DATETIME NOT NULL COMMENT '创建时间' DEFAULT CURRENT_TIMESTAMP, \n\tupdate_time DATETIME NOT NULL COMMENT '更新时间' DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, \n\tCONSTRAINT pk_user_permission_overrides PRIMARY KEY (id), \n\tCONSTRAINT uk_user_permission_overrides_user_permission UNIQUE (user_id, permission_id), \n\tCONSTRAINT fk_user_permission_overrides_user_id FOREIGN KEY(user_id) REFERENCES users (id) ON DELETE CASCADE, \n\tCONSTRAINT fk_user_permission_overrides_permission_id FOREIGN KEY(permission_id) REFERENCES permissions (id) ON DELETE RESTRICT\n)ENGINE=InnoDB CHARSET=utf8mb4 COMMENT='用户权限覆盖表(个人禁止优先)' COLLATE utf8mb4_general_ci;", 'CREATE INDEX idx_user_permission_overrides_permission_id ON user_permission_overrides (permission_id);']
PERMISSIONS = [('dashboard:view', '首页:查看', 1, None, 1), ('system:users:view', '用户管理:查看', 1, None, 2), ('system:users:create', '用户管理:新增', 2, 'system:users:view', 3), ('system:users:update', '用户管理:编辑', 2, 'system:users:view', 4), ('system:users:delete', '用户管理:删除', 2, 'system:users:view', 5), ('system:users:status', '用户管理:变更状态', 2, 'system:users:view', 6), ('system:users:password', '用户管理:重置密码', 2, 'system:users:view', 7), ('system:roles:view', '角色管理:查看', 1, None, 8), ('system:roles:create', '角色管理:新增', 2, 'system:roles:view', 9), ('system:roles:update', '角色管理:编辑', 2, 'system:roles:view', 10), ('system:roles:delete', '角色管理:删除', 2, 'system:roles:view', 11), ('system:roles:status', '角色管理:变更状态', 2, 'system:roles:view', 12), ('system:authorization:view', '权限与工资制作人:查看', 1, None, 13), ('system:authorization:update', '权限与工资制作人:编辑', 2, 'system:authorization:view', 14), ('system:business_roles:view', '岗位角色:查看', 1, None, 15), ('system:business_roles:create', '岗位角色:新增', 2, 'system:business_roles:view', 16), ('system:business_roles:update', '岗位角色:编辑', 2, 'system:business_roles:view', 17), ('system:business_roles:delete', '岗位角色:删除', 2, 'system:business_roles:view', 18), ('system:business_roles:status', '岗位角色:变更状态', 2, 'system:business_roles:view', 19), ('system:params:view', '计提参数:查看', 1, None, 20), ('system:params:update', '计提参数:编辑', 2, 'system:params:view', 21), ('system:params:withdraw', '计提参数:撤回参数', 2, 'system:params:view', 22), ('system:params:export', '计提参数:导出', 2, 'system:params:view', 23), ('system:params:preview', '计提参数:预览', 2, 'system:params:view', 24), ('system:settings:view', '公司信息:查看', 1, None, 25), ('system:settings:update', '公司信息:编辑', 2, 'system:settings:view', 26), ('system:logs:view', '日志查询:查看', 1, None, 27), ('base:personnel:view', '人员管理:查看', 1, None, 28), ('base:personnel:create', '人员管理:新增', 2, 'base:personnel:view', 29), ('base:personnel:update', '人员管理:编辑', 2, 'base:personnel:view', 30), ('base:personnel:delete', '人员管理:删除', 2, 'base:personnel:view', 31), ('base:personnel:status', '人员管理:变更状态', 2, 'base:personnel:view', 32), ('base:personnel:regions', '人员管理:维护区域', 2, 'base:personnel:view', 33), ('base:personnel:plans', '人员管理:维护计划', 2, 'base:personnel:view', 34), ('base:products:view', '产品管理:查看', 1, None, 35), ('base:products:create', '产品管理:新增', 2, 'base:products:view', 36), ('base:products:update', '产品管理:编辑', 2, 'base:products:view', 37), ('base:products:delete', '产品管理:删除', 2, 'base:products:view', 38), ('base:products:status', '产品管理:变更状态', 2, 'base:products:view', 39), ('base:customers:view', '客户管理:查看', 1, None, 40), ('base:customers:create', '客户管理:新增', 2, 'base:customers:view', 41), ('base:customers:update', '客户管理:编辑', 2, 'base:customers:view', 42), ('base:customers:delete', '客户管理:删除', 2, 'base:customers:view', 43), ('base:customers:rating', '客户管理:客户评级', 2, 'base:customers:view', 44), ('base:customers:ownership', '客户管理:变更归属', 2, 'base:customers:view', 45), ('business:businesses:view', '业务管理:查看', 1, None, 46), ('business:businesses:create', '业务管理:新增', 2, 'business:businesses:view', 47), ('business:businesses:update', '业务管理:编辑', 2, 'business:businesses:view', 48), ('business:businesses:delete', '业务管理:删除', 2, 'business:businesses:view', 49), ('business:businesses:status', '业务管理:变更状态', 2, 'business:businesses:view', 50), ('business:businesses:ownership', '业务管理:变更归属', 2, 'business:businesses:view', 51), ('business:shipments:view', '发货管理:查看', 1, None, 52), ('business:shipments:create', '发货管理:新增', 2, 'business:shipments:view', 53), ('business:shipments:update', '发货管理:编辑', 2, 'business:shipments:view', 54), ('business:shipments:delete', '发货管理:删除', 2, 'business:shipments:view', 55), ('business:receipts:view', '回款管理:查看', 1, None, 56), ('business:receipts:create', '回款管理:新增', 2, 'business:receipts:view', 57), ('business:receipts:update', '回款管理:编辑', 2, 'business:receipts:view', 58), ('business:receipts:delete', '回款管理:删除', 2, 'business:receipts:view', 59), ('business:ratings:view', '评级标准:查看', 1, None, 60), ('business:ratings:create', '评级标准:新增', 2, 'business:ratings:view', 61), ('business:ratings:update', '评级标准:编辑', 2, 'business:ratings:view', 62), ('business:ratings:delete', '评级标准:删除', 2, 'business:ratings:view', 63), ('tplus:view', '采集与导入:查看', 1, None, 64), ('tplus:collect', '采集与导入:创建采集', 2, 'tplus:view', 65), ('tplus:retry', '采集与导入:重试采集', 2, 'tplus:view', 66), ('tplus:preview', '采集与导入:预览', 2, 'tplus:view', 67), ('tplus:import', '采集与导入:执行导入', 2, 'tplus:view', 68), ('tplus:facts', '采集与导入:维护补充资料', 2, 'tplus:view', 69), ('tplus:export', '采集与导入:导出', 2, 'tplus:view', 70), ('payroll:view', '正式核算与分配:查看', 1, None, 71), ('payroll:prepare', '正式核算与分配:准备核算资料', 2, 'payroll:view', 72), ('payroll:calculate', '正式核算与分配:核算与重算', 2, 'payroll:view', 73), ('payroll:allocation', '正式核算与分配:修改工资分配', 2, 'payroll:view', 74), ('settlements:view', '月度工资:查看', 1, None, 75), ('settlements:generate', '月度工资:生成工资', 2, 'settlements:view', 76), ('settlements:submit', '月度工资:提交审核', 2, 'settlements:view', 77), ('adjustments:view', '工资调整:查看', 1, None, 78), ('adjustments:create', '工资调整:新增', 2, 'adjustments:view', 79), ('approvals:view', '审核记录:查看', 1, None, 80), ('approvals:finance', '审核记录:财务复核', 2, 'approvals:view', 81), ('approvals:leader', '审核记录:领导审核', 2, 'approvals:view', 82), ('reports:business:view', '业务统计:查看', 1, None, 83), ('reports:business:export', '业务统计:导出', 2, 'reports:business:view', 84), ('reports:personal:view', '个人明细:查看', 1, None, 85), ('reports:personal:export', '个人明细:导出', 2, 'reports:personal:view', 86), ('reports:department:view', '部门汇总:查看', 1, None, 87), ('reports:department:export', '部门汇总:导出', 2, 'reports:department:view', 88), ('reports:payments:view', '回款统计:查看', 1, None, 89), ('reports:payments:export', '回款统计:导出', 2, 'reports:payments:view', 90)]
DEFAULTS = {'admin': ['adjustments:create', 'adjustments:view', 'approvals:finance', 'approvals:leader', 'approvals:view', 'base:customers:create', 'base:customers:delete', 'base:customers:ownership', 'base:customers:rating', 'base:customers:update', 'base:customers:view', 'base:personnel:create', 'base:personnel:delete', 'base:personnel:plans', 'base:personnel:regions', 'base:personnel:status', 'base:personnel:update', 'base:personnel:view', 'base:products:create', 'base:products:delete', 'base:products:status', 'base:products:update', 'base:products:view', 'business:businesses:create', 'business:businesses:delete', 'business:businesses:ownership', 'business:businesses:status', 'business:businesses:update', 'business:businesses:view', 'business:ratings:create', 'business:ratings:delete', 'business:ratings:update', 'business:ratings:view', 'business:receipts:create', 'business:receipts:delete', 'business:receipts:update', 'business:receipts:view', 'business:shipments:create', 'business:shipments:delete', 'business:shipments:update', 'business:shipments:view', 'dashboard:view', 'payroll:allocation', 'payroll:calculate', 'payroll:prepare', 'payroll:view', 'reports:business:export', 'reports:business:view', 'reports:department:export', 'reports:department:view', 'reports:payments:export', 'reports:payments:view', 'reports:personal:export', 'reports:personal:view', 'settlements:generate', 'settlements:submit', 'settlements:view', 'system:authorization:update', 'system:authorization:view', 'system:business_roles:create', 'system:business_roles:delete', 'system:business_roles:status', 'system:business_roles:update', 'system:business_roles:view', 'system:logs:view', 'system:params:export', 'system:params:preview', 'system:params:update', 'system:params:view', 'system:params:withdraw', 'system:roles:create', 'system:roles:delete', 'system:roles:status', 'system:roles:update', 'system:roles:view', 'system:settings:update', 'system:settings:view', 'system:users:create', 'system:users:delete', 'system:users:password', 'system:users:status', 'system:users:update', 'system:users:view', 'tplus:collect', 'tplus:export', 'tplus:facts', 'tplus:import', 'tplus:preview', 'tplus:retry', 'tplus:view'], 'user': ['base:customers:view', 'base:personnel:view', 'base:products:view', 'business:businesses:view', 'business:ratings:view', 'business:receipts:view', 'business:shipments:view'], 'marketing_ops': ['base:customers:view', 'base:personnel:view', 'base:products:view', 'business:businesses:view', 'business:ratings:view', 'business:receipts:view', 'business:shipments:view'], 'finance': ['adjustments:view', 'approvals:finance', 'approvals:view', 'base:customers:view', 'base:personnel:view', 'base:products:view', 'business:businesses:view', 'business:ratings:view', 'business:receipts:view', 'business:shipments:view', 'dashboard:view', 'payroll:view', 'reports:business:export', 'reports:business:view', 'reports:department:export', 'reports:department:view', 'reports:payments:export', 'reports:payments:view', 'reports:personal:export', 'reports:personal:view', 'settlements:view', 'system:params:preview', 'system:params:view'], 'leader': ['adjustments:view', 'approvals:leader', 'approvals:view', 'base:customers:view', 'base:personnel:view', 'base:products:view', 'business:businesses:view', 'business:ratings:view', 'business:receipts:view', 'business:shipments:view', 'dashboard:view', 'payroll:view', 'reports:business:export', 'reports:business:view', 'reports:department:export', 'reports:department:view', 'reports:payments:export', 'reports:payments:view', 'reports:personal:export', 'reports:personal:view', 'settlements:view', 'system:params:preview', 'system:params:view'], 'payroll_maker': ['adjustments:create', 'adjustments:view', 'approvals:view', 'base:customers:create', 'base:customers:delete', 'base:customers:ownership', 'base:customers:rating', 'base:customers:update', 'base:customers:view', 'base:personnel:create', 'base:personnel:delete', 'base:personnel:plans', 'base:personnel:regions', 'base:personnel:status', 'base:personnel:update', 'base:personnel:view', 'base:products:create', 'base:products:delete', 'base:products:status', 'base:products:update', 'base:products:view', 'business:businesses:create', 'business:businesses:delete', 'business:businesses:ownership', 'business:businesses:status', 'business:businesses:update', 'business:businesses:view', 'business:ratings:create', 'business:ratings:delete', 'business:ratings:update', 'business:ratings:view', 'business:receipts:create', 'business:receipts:delete', 'business:receipts:update', 'business:receipts:view', 'business:shipments:create', 'business:shipments:delete', 'business:shipments:update', 'business:shipments:view', 'payroll:allocation', 'payroll:calculate', 'payroll:prepare', 'payroll:view', 'reports:business:view', 'reports:department:view', 'reports:payments:view', 'reports:personal:view', 'settlements:generate', 'settlements:submit', 'settlements:view', 'tplus:collect', 'tplus:export', 'tplus:facts', 'tplus:import', 'tplus:preview', 'tplus:retry', 'tplus:view']}
def upgrade():
bind=op.get_bind()
for sql in DDL:
op.execute(sql)
for code,name,kind,parent,order in PERMISSIONS:
bind.execute(sa.text("INSERT INTO permissions (perm_code,perm_name,perm_type,sort_order,status) VALUES (:code,:name,:kind,:order,1)"),dict(code=code,name=name,kind=kind,order=order))
for code,_,_,parent,_ in PERMISSIONS:
if parent:
bind.execute(sa.text("UPDATE permissions p JOIN permissions parent ON parent.perm_code=:parent SET p.parent_id=parent.id WHERE p.perm_code=:code"),dict(parent=parent,code=code))
bind.execute(sa.text("INSERT INTO roles (role_code,role_name,description,sort_order,status) VALUES ('payroll_maker','工资制作人','指定制作人还需在权限设置中绑定,角色本身不能绕过月份锁',5,1)"))
for role,codes in DEFAULTS.items():
for code in codes:
bind.execute(sa.text("INSERT INTO role_permissions (role_id,permission_id) SELECT r.id,p.id FROM roles r JOIN permissions p ON p.perm_code=:code WHERE r.role_code=:role"),dict(code=code,role=role))
from app.core.config import get_settings
maker=get_settings().payroll_maker_user_id
if maker is not None and not bind.execute(sa.text("SELECT id FROM users WHERE id=:id AND status=1"),dict(id=maker)).scalar():
raise RuntimeError("旧工资制作人账号无效,请修正配置后重试迁移")
bind.execute(sa.text("INSERT INTO authorization_settings (id,maker_user_id,revision) VALUES (1,:maker,1)"),dict(maker=maker))
if maker is not None:
bind.execute(sa.text("INSERT INTO user_roles (user_id,role_id) SELECT :maker,id FROM roles WHERE role_code='payroll_maker'"),dict(maker=maker))
def downgrade():
bind=op.get_bind()
if bind.execute(sa.text("SELECT revision FROM authorization_settings WHERE id=1")).scalar()!=1 or bind.execute(sa.text("SELECT COUNT(*) FROM user_permission_overrides")).scalar():
raise RuntimeError("已发生授权修改,拒绝删除授权依据;请保留数据并回退应用版本")
op.drop_table('user_permission_overrides')
op.drop_table('authorization_settings')
codes=[row[0] for row in PERMISSIONS]
for code in codes:
bind.execute(sa.text("DELETE rp FROM role_permissions rp JOIN permissions p ON p.id=rp.permission_id WHERE p.perm_code=:code"),dict(code=code))
bind.execute(sa.text("DELETE ur FROM user_roles ur JOIN roles r ON r.id=ur.role_id WHERE r.role_code='payroll_maker'"))
bind.execute(sa.text("DELETE FROM roles WHERE role_code='payroll_maker'"))
for code in reversed(codes):
bind.execute(sa.text("DELETE FROM permissions WHERE perm_code=:code"),dict(code=code))