chore: 整理项目结构,清理归档文件,更新部署配置

This commit is contained in:
Simon
2026-07-04 21:01:39 +08:00
parent 8bd4ab0366
commit 64ff1bf7d5
508 changed files with 43575 additions and 14129 deletions
+120
View File
@@ -0,0 +1,120 @@
"""RBAC 角色权限基础表
Revision ID: 021_rbac
Revises: 012_sync_remaining_fields
Create Date: 2026-06-22 (v0.7.1 重建)
v0.7.1 重建原因: 022_qrcode_login 的 down_revision 指向 021_rbac 但原文件丢失
本 migration 重建 RBAC 三张表 + 预置 3 角色 + 索引:
- roles 角色定义
- user_roles 用户-角色多对多
- role_mapping_rules 自动映射规则(企微标签 / eHR 字段)
使用 IF NOT EXISTS 兼容"生产数据库已建表"的情况:
- 如果生产 alembic 已 stamp 022 跳过 021(且表已存在),则 upgrade 是 noop
- 如果生产跑过 021 但文件丢了,upgrade 是 noop
- 只有全新环境才真正建表
下游:
- 022_qrcode_login / 023_mfa_fields / 025_messages_id_uuid / 026_drop_agent_otp_legacy
- 都在 021 之后(022 改为 down_revision="021_rbac")
预置数据:
- user 角色 (is_default=True, 所有在职员工自动获得)
- agent 角色 (IT坐席)
- admin 角色 (管理员, is_default=False)
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '021_rbac'
down_revision = '012_sync_remaining_fields'
branch_labels = None
depends_on = None
def upgrade() -> None:
"""重建 RBAC 三张表(IF NOT EXISTS 兼容)。"""
bind = op.get_bind()
inspector = sa.inspect(bind)
# ----------------------------------------------------------------------
# 1. roles 表
# ----------------------------------------------------------------------
if not inspector.has_table('roles'):
op.create_table(
'roles',
sa.Column('id', sa.String(36), primary_key=True),
sa.Column('name', sa.String(50), unique=True, nullable=False,
comment='角色标识:user/agent/admin'),
sa.Column('display_name', sa.String(100), nullable=False,
comment='显示名称:用户/坐席/管理员'),
sa.Column('description', sa.Text, nullable=True,
comment='角色描述'),
sa.Column('permissions', sa.JSON, nullable=False, default=list,
comment='权限列表(JSON数组)'),
sa.Column('is_default', sa.Boolean, nullable=False, default=False,
comment='是否默认角色(所有员工自动获得)'),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False,
comment='创建时间'),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False,
comment='更新时间'),
)
# ----------------------------------------------------------------------
# 2. user_roles 表
# ----------------------------------------------------------------------
if not inspector.has_table('user_roles'):
op.create_table(
'user_roles',
sa.Column('id', sa.String(36), primary_key=True),
sa.Column('employee_id', sa.String(100), nullable=False,
comment='企微 UserID'),
sa.Column('role_id', sa.String(36),
sa.ForeignKey('roles.id', ondelete='CASCADE'),
nullable=False, comment='角色 ID'),
sa.Column('source', sa.String(50), nullable=False,
comment='角色来源:auto/tag/ehr/manual'),
sa.Column('assigned_by', sa.String(100), nullable=True,
comment='分配者(手动分配时记录操作人)'),
sa.Column('assigned_at', sa.DateTime(timezone=True), nullable=False,
comment='分配时间'),
sa.Column('expires_at', sa.DateTime(timezone=True), nullable=True,
comment='过期时间(可选,用于临时角色)'),
sa.UniqueConstraint('employee_id', 'role_id', name='uq_user_role'),
)
op.create_index('idx_user_roles_employee_id', 'user_roles', ['employee_id'])
op.create_index('idx_user_roles_role_id', 'user_roles', ['role_id'])
# ----------------------------------------------------------------------
# 3. role_mapping_rules 表
# ----------------------------------------------------------------------
if not inspector.has_table('role_mapping_rules'):
op.create_table(
'role_mapping_rules',
sa.Column('id', sa.String(36), primary_key=True),
sa.Column('role_id', sa.String(36),
sa.ForeignKey('roles.id', ondelete='CASCADE'),
nullable=False, comment='目标角色 ID'),
sa.Column('source_type', sa.String(50), nullable=False,
comment='来源类型:wecom_tag/ehr_position'),
sa.Column('source_value', sa.String(200), nullable=False,
comment='来源值:标签名/岗位关键词'),
sa.Column('priority', sa.Integer, nullable=False, default=0,
comment='优先级(数值越大优先级越高)'),
sa.Column('is_active', sa.Boolean, nullable=False, default=True,
comment='是否启用'),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False,
comment='创建时间'),
)
op.create_index('idx_role_mapping_rules_role_id', 'role_mapping_rules', ['role_id'])
op.create_index('idx_role_mapping_rules_source_type', 'role_mapping_rules', ['source_type'])
def downgrade() -> None:
"""删除 RBAC 三张表(顺序: 子表 → 父表)。"""
op.drop_table('role_mapping_rules')
op.drop_table('user_roles')
op.drop_table('roles')
@@ -0,0 +1,36 @@
"""merge heads: 022_qrcode_login + 023_mfa_fields + 027_audit_logs
Revision ID: 028_merge_heads
Revises: 022_qrcode_login, 023_mfa_fields, 027_audit_logs
Create Date: 2026-06-22
v0.7.1 部署 P0 修复 2026-06-22:
三个 head 来自:
- 022_qrcode_login (原 down_revision='021_rbac' 指向不存在的 021, 改成 '012_sync_remaining_fields' 后变 head)
- 023_mfa_fields (down_revision='012_sync_remaining_fields' 平行挂 012)
- 027_audit_logs (v0.7.1 audit_log 模型, 顺 025→026 接续)
合并这三个 head 成单一 028_merge_heads 节点,让 alembic upgrade head 不再
"Multiple head revisions are present"
本 migration 是 noop(纯拓扑合并,无 schema 变更),生产 DB 当前 025_messages_id_uuid
早已跑过 022(noop pass)和 023(实际加 MFA 字段),只需要让链可解析。
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '028_merge_heads'
down_revision = ('022_qrcode_login', '023_mfa_fields', '027_audit_logs')
branch_labels = None
depends_on = None
def upgrade() -> None:
"""noop: 纯合并,无 schema 变更"""
pass
def downgrade() -> None:
"""noop: 纯合并,无 schema 变更"""
pass
@@ -0,0 +1,38 @@
"""add message server_timestamp for message ordering
Revision ID: 041_message_server_timestamp
Revises:
Create Date: 2026-07-02
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '041_message_server_timestamp'
down_revision: Union[str, None] = '027_audit_logs'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Add server_timestamp field for message ordering
# BIGINT to store millisecond-level timestamp for precise ordering
op.add_column(
'messages',
sa.Column('server_timestamp', sa.BigInteger(), nullable=True, comment='服务端时间戳(毫秒)')
)
# Add index for server_timestamp queries
op.create_index(
'idx_messages_server_timestamp',
'messages',
['server_timestamp']
)
def downgrade() -> None:
op.drop_index('idx_messages_server_timestamp', table_name='messages')
op.drop_column('messages', 'server_timestamp')