企业级软件研发团队绩效考核系统开发(持续更新 Day 5)

0 阅读11分钟

作者:呱牛 

发布日期:2026年3月28日

标签:FastAPI、绩效考核

🔥 今日亮点

2026年3月28日 - C101指标计算完成 & 代码优化

  • 完成C101指标计算功能:实现完整的查询、聚合、更新流程
  • 解决事务管理问题:修复"Can't operate on closed transaction"错误
  • 解决重复计算问题:按指标编号去重,避免得分重复累加
  • 解决类型转换问题:Decimal类型正确转换为float类型
  • 解决字段缺失问题:动态检查字段是否存在,支持不同表结构
  • 代码优化清理:删除冗余日志和注释代码
  • 执行顺序优化:先插入员工信息,再更新指标得分

📋 文章目录


🎯 C101指标计算完成

1.1 功能需求

在上传考核期次页面的操作列表中增加"计算考核期次得分"按钮:

  • 按钮状态与*_pa_excel_upload_record表的calculation_status字段关联
  • 状态默认为"未计算"(红色),计算完成后变为"已计算"(蓝色)

1.2 实现步骤

步骤1:查询员工基本信息

# 从*_user_staff_info表查询在职员工信息
stmt = select(
    *UserStaffInfoModel.staff_no,
    *UserStaffInfoModel.real_name,
    *UserStaffInfoModel.position,
    *UserStaffInfoModel.dept_level1_name,
    *UserStaffInfoModel.dept_level2_name,
    *UserStaffInfoModel.team_name
).where(
    *UserStaffInfoModel.data_status == 1
).order_by(
    *UserStaffInfoModel.id
)

# 执行查询
result = await auth.db.execute(stmt)
staff_infos = result.fetchall()

步骤2:查询C101指标数据

# 从*_pa_kpi_c101表查询指标数据
c101_select_columns = [
    *PaKpiC101Model.staff_no,
    *PaKpiC101Model.real_name,
    *PaKpiC101Model.position,
    *PaKpiC101Model.assessment_period,
    *PaKpiC101Model.data_date,
    *PaKpiC101Model.indicator_no,
    *PaKpiC101Model.indicator_content,
    *PaKpiC101Model.judgment_condition,
    *PaKpiC101Model.indicator_score
]

c101_stmt = select(*c101_select_columns).where(
    *PaKpiC101Model.assessment_period == assessment_period,
    *PaKpiC101Model.data_date == data_date
).order_by(
    *PaKpiC101Model.staff_no,
    *PaKpiC101Model.indicator_no
)

# 执行查询
c101_result = await auth.db.execute(c101_stmt)
c101_data = c101_result.fetchall()

步骤3:按指标编号去重聚合得分

from collections import defaultdict
from decimal import Decimal

staff_scores = defaultdict(float)
staff_indicators = defaultdict(set)  # 记录每个员工已处理的指标编号

for row in c101_data:
    staff_no = row[0]
    indicator_no = row[5]
    indicator_score = row[8]
    
    # 按指标编号去重
    if indicator_score and indicator_no:
        if indicator_no not in staff_indicators[staff_no]:
            # 将Decimal类型转换为float类型
            score_value = float(indicator_score) if isinstance(indicator_score, Decimal) else float(indicator_score)
            staff_scores[staff_no] += score_value
            staff_indicators[staff_no].add(indicator_no)

步骤4:插入员工信息到绩效考核结果表

# 使用CRUD创建员工记录
create_data = *PaPerformanceAssessmentCreateSchema(
    status='0',
    description=None,
    staff_no=staff_no,
    staff_name=real_name,
    position=position,
    dept_level1_name=dept_level1_name,
    dept_level2_name=dept_level2_name,
    team_name=team_name,
    assessment_period=assessment_period,
    data_timepoint=data_timepoint_date
)

await performance_crud.create_yurdmc_pa_performance_assessment_crud(data=create_data)

步骤5:执行UPDATE语句更新得分

from sqlalchemy import text

for staff_no, total_score in staff_scores.items():
    update_sql = f"""
UPDATE `*_pa_performance_assessment`
SET `demand_score_c101` = {total_score:.2f},
    `updated_time` = NOW()
WHERE `staff_no` = '{staff_no}'
  AND `assessment_period` = '{assessment_period}'
  AND `data_timepoint` = '{data_timepoint_date}';
"""
    try:
        await auth.db.execute(text(update_sql))
        update_count += 1
    except Exception as e:
        log.error(f"更新员工 {staff_no} 的C101得分失败: {str(e)}")

# 使用flush而不是commit,让上下文管理器自动处理事务提交
await auth.db.flush()


🐛 问题排查与修复

2.1 事务管理问题

问题描述

InvalidRequestError: Can't operate on closed transaction inside context manager.

问题分析

问题根源:
1. 数据库连接使用了`async with session.begin()`上下文管理器
2. 手动调用`commit()`会导致事务被提前关闭
3. 后续操作无法在已关闭的事务中执行
4. 系统使用上下文管理器自动管理事务生命周期

解决方案

# 错误做法
await auth.db.commit()

# 正确做法
await auth.db.flush()

技术说明

  • flush()将更改同步到数据库,但不提交事务
  • 上下文管理器在请求结束时自动提交事务
  • 避免手动干预事务生命周期

2.2 重复计算问题

问题描述

同一个指标编号在数据库中有多条记录,导致得分被重复累加。

问题数据示例

工号: 1113, 指标编号: 12344334343, 指标得分: 20.00
工号: 1113, 指标编号: 233233232323, 指标得分: 20.00
工号: 1113, 指标编号: 202323232323, 指标得分: 20.00

实际得分应该是:20.00
错误计算结果:60.00(重复累加了3次)

解决方案

# 使用字典记录已处理的指标编号
staff_indicators = defaultdict(set)

if indicator_no not in staff_indicators[staff_no]:
    staff_scores[staff_no] += score_value
    staff_indicators[staff_no].add(indicator_no)

2.3 执行顺序问题

问题描述

先执行UPDATE语句,后插入员工信息,导致UPDATE找不到匹配的记录。

问题分析

错误顺序:
1. 查询C101指标数据
2. 聚合得分
3. 执行UPDATE语句(此时员工记录还不存在)
4. 插入员工信息

正确顺序:
1. 查询C101指标数据
2. 聚合得分
3. 插入员工信息
4. 执行UPDATE语句(此时员工记录已存在)

解决方案

# 先插入员工信息
insert_count = await cls._insert_staff_to_performance_assessment(
    auth=auth,
    staff_infos=staff_infos,
    assessment_period=assessment_period,
    data_timepoint=data_timepoint_date
)

# 再执行UPDATE语句
for staff_no, total_score in staff_scores.items():
    update_sql = f"UPDATE ..."
    await auth.db.execute(text(update_sql))

2.4 类型转换问题

问题描述

TypeError: unsupported operand type(s) for +=: 'float' and 'decimal.Decimal'

问题分析

  • 数据库查询出来的indicator_score字段是Decimal类型
  • 与float类型不兼容,无法直接进行累加操作

解决方案

# 将Decimal类型转换为float类型
score_value = float(indicator_score) if isinstance(indicator_score, Decimal) else float(indicator_score)
staff_scores[staff_no] += score_value

2.5 字段缺失问题

问题描述

AttributeError: type object '*PaKpiC106Model' has no attribute 'indicator_no'

问题分析

  • 不同指标表的字段结构不同
  • 通用处理函数假设所有模型都有相同字段
  • C106模型缺少indicator_no字段

解决方案

# 动态检查字段是否存在
if hasattr(model, 'indicator_no'):
    c101_select_columns.append(model.indicator_no)
if hasattr(model, 'indicator_content'):
    c101_select_columns.append(model.indicator_content)
if hasattr(model, 'judgment_condition'):
    c101_select_columns.append(model.judgment_condition)
if hasattr(model, 'indicator_score'):
    c101_select_columns.append(model.indicator_score)


📊 完整流程

4.1 C101指标得分计算流程图

开始
  ↓
点击"计算考核期次得分"按钮
  ↓
查询员工基本信息(*_user_staff_info)
  ↓
写入员工信息日志
  ↓
查询C101指标数据(*_pa_kpi_c101)
  ↓
写入C101指标数据日志
  ↓
按指标编号去重聚合得分
  ↓
插入员工信息到绩效考核结果表(*_pa_performance_assessment)
  ↓
执行UPDATE语句更新C101得分
  ↓
更新计算状态为"已生成"
  ↓
返回计算结果
  ↓
结束

4.2 数据流转图

┌─────────────────────────────────────────────────────────────┐
│  数据源:Excel文件上传                                    │
│  ┌───────────────────────────────────────────────────────┐ │
│  │  *_pa_excel_upload_record                    │ │
│  │  - assessment_period: 202603                   │ │
│  │  - data_date: 2026-03-23                      │ │
│  │  - calculation_status: 未计算 → 已计算              │ │
│  └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────┐
│  员工信息表:*_user_staff_info                      │
│  ┌───────────────────────────────────────────────────────┐ │
│  │  staff_no, real_name, position, dept_level1_name,  │ │
│  │  dept_level2_name, team_name                     │ │
│  │  data_status = 1 (在职)                          │ │
│  └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────┐
│  指标数据表:*_pa_kpi_c101                       │
│  ┌───────────────────────────────────────────────────────┐ │
│  │  staff_no, indicator_no, indicator_score            │ │
│  │  assessment_period, data_date                      │ │
│  └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────┐
│  绩效考核结果表:*_pa_performance_assessment        │
│  ┌───────────────────────────────────────────────────────┐ │
│  │  员工基本信息 + 指标得分                          │ │
│  │  demand_score_c101 = 汇总得分                    │ │
│  │  assessment_period, data_timepoint                  │ │
│  └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘


🚀 技术要点

5.1 SQLAlchemy异步操作

# 使用AsyncSession进行异步数据库操作
from sqlalchemy.ext.asyncio import AsyncSession

# 异步查询
result = await auth.db.execute(stmt)
data = result.fetchall()

# 异步执行SQL
await auth.db.execute(text(update_sql))

# 使用flush而不是commit
await auth.db.flush()

5.2 上下文管理器

# 数据库依赖注入
async def db_getter() -> AsyncGenerator[AsyncSession, None]:
    async with async_db_session() as session:
        async with session.begin():  # 自动事务管理
            yield session

关键点:

  • 使用async with session.begin()自动管理事务
  • 避免手动调用commit()rollback()
  • 上下文结束时自动提交事务

5.3 动态字段检查

# 检查模型字段是否存在
if hasattr(model, 'indicator_no'):
    select_columns.append(model.indicator_no)
if hasattr(model, 'indicator_content'):
    select_columns.append(model.indicator_content)

# 动态添加排序
if hasattr(model, 'staff_no'):
    stmt = stmt.order_by(model.staff_no)
    if hasattr(model, 'indicator_no'):
        stmt = stmt.order_by(model.indicator_no)

优势:

  • 支持不同结构的指标表
  • 避免字段不存在导致的错误
  • 提高代码的通用性和可维护性

5.4 去重聚合技巧

# 使用defaultdict和set实现去重
from collections import defaultdict

staff_scores = defaultdict(float)
staff_indicators = defaultdict(set)

# 按指标编号去重
if indicator_no not in staff_indicators[staff_no]:
    staff_scores[staff_no] += score_value
    staff_indicators[staff_no].add(indicator_no)


📈 待实现功能

6.1 其他指标处理

  • C102指标得分计算
  • C103指标得分计算
  • C105指标得分计算
  • C106指标得分计算
  • C107指标得分计算
  • C201-C214项目指标得分计算
  • C301自主研发得分计算
  • C401-C403运维指标得分计算
  • C501-C503质量指标得分计算
  • C601-C602安全指标得分计算
  • C701-C703 OKR目标(研发岗)得分计算
  • C801-C803 OKR目标(需求岗)得分计算

6.2 通用指标处理方法

@classmethod
async def _process_indicator_data(
    cls,
    auth: AuthSchema,
    table_name: str,
    field_name: str,
    assessment_period: str,
    data_date: str,
    data_timepoint_date: str,
    LOG_DIR: str
) -> None:
    """
    处理指标数据,生成汇总得分和UPDATE SQL

    参数:
    - auth: AuthSchema - 认证信息
    - table_name: str - 指标表名
    - field_name: str - 目标字段名
    - assessment_period: str - 考核期次
    - data_date: str - 数据时点
    - data_timepoint_date: str - 数据时点(date格式)
    - LOG_DIR: str - 日志目录
    """
    # 动态导入模型
    model_name = f"*PaKpi{table_name.split('_')[-1].title()}Model"
    module_path = f"app.plugin.module_gencode.{table_name}.model"

    try:
        import importlib
        module = importlib.import_module(module_path)
        model = getattr(module, model_name)
    except Exception as e:
        log.error(f"导入 {table_name} 模型失败: {str(e)}")
        return

    # 构建查询,动态检查字段是否存在
    select_columns = [
        model.staff_no,
        model.real_name,
        model.position,
        model.assessment_period,
        model.data_date,
    ]

    # 检查可选字段是否存在
    if hasattr(model, 'indicator_no'):
        select_columns.append(model.indicator_no)
    if hasattr(model, 'indicator_content'):
        select_columns.append(model.indicator_content)
    if hasattr(model, 'judgment_condition'):
        select_columns.append(model.judgment_condition)
    if hasattr(model, 'indicator_score'):
        select_columns.append(model.indicator_score)

    # 执行查询、聚合、更新...


🧪 测试验证

7.1 功能测试

  • 按钮状态正确显示(未计算/已计算)
  • 员工信息正确查询和插入
  • C101指标数据正确查询
  • 按指标编号去重正确聚合
  • UPDATE语句正确执行
  • 数据正确更新到数据库
  • 日志文件正确生成

7.2 异常测试

  • NULL值正确处理(不累加)
  • 事务正确管理(不提前关闭)
  • 类型转换正确处理(Decimal转float)
  • 字段缺失正确处理(动态检查)
  • 执行顺序正确(先插入后更新)

⚠️ 注意事项

8.1 数据一致性

  • 确保assessment_period和data_timepoint字段在所有表中一致
  • 使用相同的格式(assessment_period: '202603', data_timepoint: '2026-03-23')
  • 避免因格式不一致导致UPDATE找不到匹配记录

8.2 性能优化

  • 使用flush()而不是commit()避免事务开销
  • 在循环外部创建CRUD实例,避免重复创建
  • 使用批量操作减少数据库访问次数
  • 合理使用索引提高查询效率

8.3 错误处理

  • 捕获并记录UPDATE失败的情况
  • 继续处理其他员工,不因单个错误而中断
  • 使用详细的错误日志便于排查问题
  • 提供友好的错误提示给用户

8.4 日志管理

  • 日志文件按时间戳命名,避免覆盖
  • 日志内容包含原始数据和汇总结果
  • 日志级别合理使用(info、error)
  • 避免日志文件过大影响性能

📝 今日总结

9.1 完成功能

  1. C101指标得分计算功能完整实现
  2. 员工信息查询和插入功能
  3. UPDATE语句执行功能
  4. 事务管理问题修复
  5. 重复计算问题解决
  6. 类型转换问题解决
  7. 字段缺失问题解决
  8. 执行顺序问题解决
  9. 代码优化和清理

9.2 技术收获

  1. 深入理解SQLAlchemy异步操作和事务管理
  2. 掌握动态字段检查和处理方法
  3. 学会了去重聚合的实现技巧
  4. 提高了代码质量和可维护性
  5. 理解了上下文管理器的工作原理

9.3 下一步计划

  1. 实现其他指标(C102、C103等)的处理逻辑
  2. 完善通用指标处理方法
  3. 添加数据验证和异常处理
  4. 优化性能和用户体验
  5. 完善单元测试和集成测试

​编辑