add backend source code

This commit is contained in:
2026-02-28 15:06:52 +08:00
parent 1bc330e20c
commit 2c37aa9064
67 changed files with 11654 additions and 0 deletions

68
backend/alembic/env.py Normal file
View File

@@ -0,0 +1,68 @@
"""
Alembic环境配置
"""
from logging.config import fileConfig
from sqlalchemy import pool
from sqlalchemy.engine import Connection
from sqlalchemy.ext.asyncio import async_engine_from_config
from alembic import context
from app.core.config import settings
from app.models.models import Base
config = context.config
# 使用 settings 中的数据库 URL
config.set_main_option("sqlalchemy.url", settings.DATABASE_URL)
if config.config_file_name is not None:
fileConfig(config.config_file_name)
target_metadata = Base.metadata
def run_migrations_offline() -> None:
"""离线模式运行迁移"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def do_run_migrations(connection: Connection) -> None:
context.configure(connection=connection, target_metadata=target_metadata)
with context.begin_transaction():
context.run_migrations()
async def run_async_migrations() -> None:
"""异步模式运行迁移"""
connectable = async_engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
async with connectable.connect() as connection:
await connection.run_sync(do_run_migrations)
await connectable.dispose()
def run_migrations_online() -> None:
"""在线模式运行迁移"""
import asyncio
asyncio.run(run_async_migrations())
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

View File

@@ -0,0 +1,182 @@
"""${message}
Revision ID: initial
Revises:
Create Date: 2024-01-01 00:00:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'initial'
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# 创建科室表
op.create_table(
'departments',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('name', sa.String(length=100), nullable=False, comment='科室名称'),
sa.Column('code', sa.String(length=20), nullable=False, comment='科室编码'),
sa.Column('dept_type', sa.String(length=50), nullable=False, comment='科室类型'),
sa.Column('parent_id', sa.Integer(), nullable=True, comment='上级科室'),
sa.Column('level', sa.Integer(), nullable=True, comment='层级'),
sa.Column('sort_order', sa.Integer(), nullable=True, comment='排序'),
sa.Column('is_active', sa.Boolean(), nullable=True, comment='是否启用'),
sa.Column('description', sa.Text(), nullable=True, comment='描述'),
sa.Column('created_at', sa.DateTime(), nullable=True, comment='创建时间'),
sa.Column('updated_at', sa.DateTime(), nullable=True, comment='更新时间'),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('code')
)
op.create_index('idx_dept_type', 'departments', ['dept_type'])
op.create_index('idx_dept_parent', 'departments', ['parent_id'])
# 创建员工表
op.create_table(
'staff',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('employee_id', sa.String(length=20), nullable=False, comment='工号'),
sa.Column('name', sa.String(length=50), nullable=False, comment='姓名'),
sa.Column('department_id', sa.Integer(), nullable=False, comment='所属科室'),
sa.Column('position', sa.String(length=50), nullable=False, comment='职位'),
sa.Column('title', sa.String(length=50), nullable=True, comment='职称'),
sa.Column('phone', sa.String(length=20), nullable=True, comment='联系电话'),
sa.Column('email', sa.String(length=100), nullable=True, comment='邮箱'),
sa.Column('base_salary', sa.Numeric(precision=10, scale=2), nullable=True, comment='基本工资'),
sa.Column('performance_ratio', sa.Numeric(precision=5, scale=2), nullable=True, comment='绩效系数'),
sa.Column('status', sa.String(length=50), nullable=True, comment='状态'),
sa.Column('hire_date', sa.DateTime(), nullable=True, comment='入职日期'),
sa.Column('created_at', sa.DateTime(), nullable=True, comment='创建时间'),
sa.Column('updated_at', sa.DateTime(), nullable=True, comment='更新时间'),
sa.ForeignKeyConstraint(['department_id'], ['departments.id']),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('employee_id')
)
op.create_index('idx_staff_dept', 'staff', ['department_id'])
op.create_index('idx_staff_status', 'staff', ['status'])
# 创建指标表
op.create_table(
'indicators',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('name', sa.String(length=100), nullable=False, comment='指标名称'),
sa.Column('code', sa.String(length=20), nullable=False, comment='指标编码'),
sa.Column('indicator_type', sa.String(length=50), nullable=False, comment='指标类型'),
sa.Column('weight', sa.Numeric(precision=5, scale=2), nullable=True, comment='权重'),
sa.Column('max_score', sa.Numeric(precision=5, scale=2), nullable=True, comment='最高分值'),
sa.Column('target_value', sa.Numeric(precision=10, scale=2), nullable=True, comment='目标值'),
sa.Column('unit', sa.String(length=20), nullable=True, comment='计量单位'),
sa.Column('calculation_method', sa.Text(), nullable=True, comment='计算方法'),
sa.Column('description', sa.Text(), nullable=True, comment='描述'),
sa.Column('is_active', sa.Boolean(), nullable=True, comment='是否启用'),
sa.Column('created_at', sa.DateTime(), nullable=True, comment='创建时间'),
sa.Column('updated_at', sa.DateTime(), nullable=True, comment='更新时间'),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('code')
)
op.create_index('idx_indicator_type', 'indicators', ['indicator_type'])
# 创建考核记录表
op.create_table(
'assessments',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('staff_id', sa.Integer(), nullable=False, comment='员工ID'),
sa.Column('period_year', sa.Integer(), nullable=False, comment='考核年度'),
sa.Column('period_month', sa.Integer(), nullable=False, comment='考核月份'),
sa.Column('period_type', sa.String(length=20), nullable=True, comment='考核周期类型'),
sa.Column('total_score', sa.Numeric(precision=5, scale=2), nullable=True, comment='总分'),
sa.Column('weighted_score', sa.Numeric(precision=5, scale=2), nullable=True, comment='加权得分'),
sa.Column('status', sa.String(length=50), nullable=True, comment='状态'),
sa.Column('assessor_id', sa.Integer(), nullable=True, comment='考核人'),
sa.Column('reviewer_id', sa.Integer(), nullable=True, comment='审核人'),
sa.Column('submit_time', sa.DateTime(), nullable=True, comment='提交时间'),
sa.Column('review_time', sa.DateTime(), nullable=True, comment='审核时间'),
sa.Column('remark', sa.Text(), nullable=True, comment='备注'),
sa.Column('created_at', sa.DateTime(), nullable=True, comment='创建时间'),
sa.Column('updated_at', sa.DateTime(), nullable=True, comment='更新时间'),
sa.ForeignKeyConstraint(['staff_id'], ['staff.id']),
sa.ForeignKeyConstraint(['assessor_id'], ['staff.id']),
sa.ForeignKeyConstraint(['reviewer_id'], ['staff.id']),
sa.PrimaryKeyConstraint('id')
)
op.create_index('idx_assessment_staff', 'assessments', ['staff_id'])
op.create_index('idx_assessment_period', 'assessments', ['period_year', 'period_month'])
op.create_index('idx_assessment_status', 'assessments', ['status'])
# 创建考核明细表
op.create_table(
'assessment_details',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('assessment_id', sa.Integer(), nullable=False, comment='考核记录ID'),
sa.Column('indicator_id', sa.Integer(), nullable=False, comment='指标ID'),
sa.Column('actual_value', sa.Numeric(precision=10, scale=2), nullable=True, comment='实际值'),
sa.Column('score', sa.Numeric(precision=5, scale=2), nullable=True, comment='得分'),
sa.Column('evidence', sa.Text(), nullable=True, comment='佐证材料'),
sa.Column('remark', sa.Text(), nullable=True, comment='备注'),
sa.Column('created_at', sa.DateTime(), nullable=True, comment='创建时间'),
sa.Column('updated_at', sa.DateTime(), nullable=True, comment='更新时间'),
sa.ForeignKeyConstraint(['assessment_id'], ['assessments.id']),
sa.ForeignKeyConstraint(['indicator_id'], ['indicators.id']),
sa.PrimaryKeyConstraint('id')
)
op.create_index('idx_detail_assessment', 'assessment_details', ['assessment_id'])
op.create_index('idx_detail_indicator', 'assessment_details', ['indicator_id'])
# 创建工资记录表
op.create_table(
'salary_records',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('staff_id', sa.Integer(), nullable=False, comment='员工ID'),
sa.Column('period_year', sa.Integer(), nullable=False, comment='年度'),
sa.Column('period_month', sa.Integer(), nullable=False, comment='月份'),
sa.Column('base_salary', sa.Numeric(precision=10, scale=2), nullable=True, comment='基本工资'),
sa.Column('performance_score', sa.Numeric(precision=5, scale=2), nullable=True, comment='绩效得分'),
sa.Column('performance_bonus', sa.Numeric(precision=10, scale=2), nullable=True, comment='绩效奖金'),
sa.Column('deduction', sa.Numeric(precision=10, scale=2), nullable=True, comment='扣款'),
sa.Column('allowance', sa.Numeric(precision=10, scale=2), nullable=True, comment='补贴'),
sa.Column('total_salary', sa.Numeric(precision=10, scale=2), nullable=True, comment='应发工资'),
sa.Column('status', sa.String(length=50), nullable=True, comment='状态'),
sa.Column('remark', sa.Text(), nullable=True, comment='备注'),
sa.Column('created_at', sa.DateTime(), nullable=True, comment='创建时间'),
sa.Column('updated_at', sa.DateTime(), nullable=True, comment='更新时间'),
sa.ForeignKeyConstraint(['staff_id'], ['staff.id']),
sa.PrimaryKeyConstraint('id')
)
op.create_index('idx_salary_staff', 'salary_records', ['staff_id'])
op.create_index('idx_salary_period', 'salary_records', ['period_year', 'period_month'])
# 创建用户表
op.create_table(
'users',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('username', sa.String(length=50), nullable=False, comment='用户名'),
sa.Column('password_hash', sa.String(length=255), nullable=False, comment='密码哈希'),
sa.Column('staff_id', sa.Integer(), nullable=True, comment='关联员工'),
sa.Column('role', sa.String(length=20), nullable=True, comment='角色'),
sa.Column('is_active', sa.Boolean(), nullable=True, comment='是否启用'),
sa.Column('last_login', sa.DateTime(), nullable=True, comment='最后登录'),
sa.Column('created_at', sa.DateTime(), nullable=True, comment='创建时间'),
sa.Column('updated_at', sa.DateTime(), nullable=True, comment='更新时间'),
sa.ForeignKeyConstraint(['staff_id'], ['staff.id']),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('username')
)
op.create_index('idx_user_username', 'users', ['username'])
def downgrade() -> None:
op.drop_table('users')
op.drop_table('salary_records')
op.drop_table('assessment_details')
op.drop_table('assessments')
op.drop_table('indicators')
op.drop_table('staff')
op.drop_table('departments')

View File

@@ -0,0 +1,95 @@
"""添加指标模板表
Revision ID: 002_template
Revises: initial
Create Date: 2024-01-02 00:00:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '002_template'
down_revision: Union[str, None] = 'initial'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# 创建指标模板表
op.create_table(
'indicator_templates',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('template_name', sa.String(length=200), nullable=False, comment='模板名称'),
sa.Column('template_code', sa.String(length=50), nullable=False, comment='模板编码'),
sa.Column('template_type', sa.String(length=30), nullable=False, comment='模板类型'),
sa.Column('description', sa.Text(), nullable=True, comment='模板描述'),
sa.Column('dimension_weights', sa.Text(), nullable=True, comment='维度权重 (JSON)'),
sa.Column('assessment_cycle', sa.String(length=20), nullable=True, comment='考核周期'),
sa.Column('is_active', sa.Boolean(), nullable=True, comment='是否启用'),
sa.Column('created_at', sa.DateTime(), nullable=True, comment='创建时间'),
sa.Column('updated_at', sa.DateTime(), nullable=True, comment='更新时间'),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('template_code')
)
op.create_index('idx_template_type', 'indicator_templates', ['template_type'])
op.create_index('idx_template_active', 'indicator_templates', ['is_active'])
# 创建模板指标关联表
op.create_table(
'template_indicators',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('template_id', sa.Integer(), nullable=False, comment='模板 ID'),
sa.Column('indicator_id', sa.Integer(), nullable=False, comment='指标 ID'),
sa.Column('category', sa.String(length=100), nullable=True, comment='指标分类'),
sa.Column('target_value', sa.Numeric(precision=10, scale=2), nullable=True, comment='目标值'),
sa.Column('target_unit', sa.String(length=50), nullable=True, comment='目标值单位'),
sa.Column('weight', sa.Numeric(precision=5, scale=2), nullable=True, comment='权重'),
sa.Column('scoring_method', sa.String(length=50), nullable=True, comment='评分方法'),
sa.Column('scoring_params', sa.Text(), nullable=True, comment='评分参数 (JSON)'),
sa.Column('sort_order', sa.Integer(), nullable=True, comment='排序'),
sa.Column('remark', sa.Text(), nullable=True, comment='备注'),
sa.Column('created_at', sa.DateTime(), nullable=True, comment='创建时间'),
sa.Column('updated_at', sa.DateTime(), nullable=True, comment='更新时间'),
sa.ForeignKeyConstraint(['template_id'], ['indicator_templates.id']),
sa.ForeignKeyConstraint(['indicator_id'], ['indicators.id']),
sa.PrimaryKeyConstraint('id')
)
op.create_index('idx_ti_template', 'template_indicators', ['template_id'])
op.create_index('idx_ti_indicator', 'template_indicators', ['indicator_id'])
op.create_index('idx_ti_unique', 'template_indicators', ['template_id', 'indicator_id'], unique=True)
# 为指标表添加 BSC 维度字段(如果不存在)
# 注意:在 PostgreSQL 中,如果字段已存在会报错,需要检查
conn = op.get_bind()
inspector = sa.inspect(conn)
columns = [col['name'] for col in inspector.get_columns('indicators')]
if 'bs_dimension' not in columns:
op.add_column('indicators', sa.Column('bs_dimension', sa.String(length=30), nullable=True, comment='平衡计分卡维度'))
if 'target_unit' not in columns:
op.add_column('indicators', sa.Column('target_unit', sa.String(length=50), nullable=True, comment='目标值单位'))
if 'assessment_method' not in columns:
op.add_column('indicators', sa.Column('assessment_method', sa.Text(), nullable=True, comment='考核方法'))
if 'deduction_standard' not in columns:
op.add_column('indicators', sa.Column('deduction_standard', sa.Text(), nullable=True, comment='扣分标准'))
if 'data_source' not in columns:
op.add_column('indicators', sa.Column('data_source', sa.String(length=100), nullable=True, comment='数据来源'))
if 'applicable_dept_types' not in columns:
op.add_column('indicators', sa.Column('applicable_dept_types', sa.Text(), nullable=True, comment='适用科室类型'))
if 'is_veto' not in columns:
op.add_column('indicators', sa.Column('is_veto', sa.Boolean(), nullable=True, default=False, comment='是否一票否决指标'))
def downgrade() -> None:
op.drop_table('template_indicators')
op.drop_table('indicator_templates')

View File