作者:呱牛
发布日期:2026年3月29日
标签:FastAPI、绩效考核
🔥 今日亮点
2026年3月29日 - 指标计算完成 & 分类汇总得分
- ✅ 实现分类汇总得分计算:需求、项目、自主研发、运维工作汇总
- ✅ 解决Cxxx得分计算错误:修复字段解析索引错误,移除按指标编号去重
- ✅ 修复员工信息插入问题:处理重复记录,避免事务关闭
- ✅ 优化数据处理逻辑:支持NULL值处理,确保无数据时按0存入
- ✅ 完善指标配置:扩展到Cxxx指标,支持灵活配置
📋 文章目录
🎯 Cxxx指标计算完成
1.1 功能需求
扩展指标计算功能,支持Cxxx等多个指标的计算:
- 统一使用通用处理方法处理不同指标
- 确保所有指标得分正确计算和汇总
- 实现分类汇总得分计算
1.2 实现步骤
步骤1:指标配置
# 指标配置列表
indicator_configs = [
{'table': '*_pa_kpi_c102', 'field': 'demand_score_c102'},
{'table': '*_pa_kpi_c103', 'field': 'demand_score_c103'},
{'table': '*_pa_kpi_c105', 'field': 'demand_score_c105'},
{'table': '*_pa_kpi_c106', 'field': 'demand_score_c106'},
...
{'table': '*_pa_kpi_c401', 'field': 'ops_score_c401'},
{'table': '*_pa_kpi_c402', 'field': 'ops_score_c402'},
{'table': '*_pa_kpi_c403', 'field': 'ops_score_c403'},
]
步骤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,
staff_infos: list
) -> 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 - 日志目录
- staff_infos: list - 员工信息列表
"""
from datetime import datetime
from collections import defaultdict
from sqlalchemy import select
# 动态导入模型
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 = []
column_names = []
# 基础字段(所有指标表都有)
base_fields = ['staff_no', 'real_name', 'position', 'assessment_period', 'data_date']
for field in base_fields:
if hasattr(model, field):
select_columns.append(getattr(model, field))
column_names.append(field)
# 检查可选字段是否存在
optional_fields = ['indicator_no', 'indicator_content', 'work_days', 'outsourcer_name', 'judgment_condition', 'indicator_score']
for field in optional_fields:
if hasattr(model, field):
select_columns.append(getattr(model, field))
column_names.append(field)
# 构建查询语句
stmt = select(*select_columns).where(
model.assessment_period == assessment_period,
model.data_date == data_date
)
# 执行查询
result = await auth.db.execute(stmt)
data = result.fetchall()
# 写入数据到日志文件
current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
log_path = LOG_DIR / f"{table_name}数据{datetime.now().strftime('%Y%m%d%H%M%S')}.log"
# 按工号聚合得分(累加所有记录的得分)
staff_scores = defaultdict(float)
from decimal import Decimal
# 初始化所有员工的得分为0
for staff in staff_infos:
staff_no = staff[0]
staff_scores[staff_no] = 0.0
with open(log_path, 'w', encoding='utf-8') as f:
f.write(f"查询时间: {current_time}\n")
f.write(f"考核期次: {assessment_period}\n")
f.write(f"数据时点: {data_date}\n")
f.write(f"查询到{table_name}指标数据数量: {len(data)}\n\n")
f.write("=" * 80 + "\n")
f.write(f"{table_name}指标数据:\n")
f.write("=" * 80 + "\n")
for idx, row in enumerate(data, 1):
# 使用column_names将行数据映射为字典
row_dict = dict(zip(column_names, row))
# 解析行数据
staff_no = row_dict.get('staff_no')
real_name = row_dict.get('real_name')
position = row_dict.get('position')
ass_period = row_dict.get('assessment_period')
data_date_val = row_dict.get('data_date')
# 处理可选字段
indicator_no = row_dict.get('indicator_no')
indicator_content = row_dict.get('indicator_content')
outsourcer_name = row_dict.get('outsourcer_name')
judgment_condition = row_dict.get('judgment_condition')
indicator_score = row_dict.get('indicator_score')
# 写入日志
f.write(f"记录 {idx}:\n")
f.write(f" 工号: {staff_no}\n")
f.write(f" 姓名: {real_name}\n")
f.write(f" 岗位: {position}\n")
f.write(f" 考核期次: {ass_period}\n")
f.write(f" 数据时点: {data_date_val}\n")
if indicator_no:
f.write(f" 指标编号: {indicator_no}\n")
if indicator_content:
f.write(f" 指标内容: {indicator_content}\n")
if outsourcer_name:
f.write(f" 外包姓名: {outsourcer_name}\n")
if judgment_condition:
f.write(f" 判定条件: {judgment_condition}\n")
if indicator_score:
f.write(f" 指标得分: {indicator_score}\n")
f.write("-" * 80 + "\n")
# 聚合得分(累加所有记录的得分,不按指标编号去重)
if indicator_score:
# 将Decimal类型转换为float类型
score_value = float(indicator_score) if isinstance(indicator_score, Decimal) else float(indicator_score)
staff_scores[staff_no] += score_value
# 执行UPDATE语句
from sqlalchemy import text
update_count = 0
for staff_no, total_score in staff_scores.items():
update_sql = f"""
UPDATE `yurdmc_pa_performance_assessment`
SET `{field_name}` = {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} 的{field_name}得分失败: {str(e)}")
# 使用flush而不是commit,让上下文管理器自动处理事务提交
await auth.db.flush()
log.info(f"{table_name}指标更新完成,共更新 {update_count} 条记录")
# 将UPDATE SQL写入日志文件
with open(log_path, 'a', encoding='utf-8') as f:
f.write("\n" + "=" * 80 + "\n")
f.write(f"{table_name}指标汇总得分与UPDATE SQL:\n")
f.write("=" * 80 + "\n")
for staff_no, total_score in staff_scores.items():
f.write(f"工号: {staff_no}, 汇总得分: {total_score:.2f}\n")
f.write("\n" + "=" * 80 + "\n")
f.write("UPDATE SQL语句:\n")
f.write("=" * 80 + "\n")
for staff_no, total_score in staff_scores.items():
update_sql = f"""
UPDATE `yurdmc_pa_performance_assessment`
SET `{field_name}` = {total_score:.2f},
`updated_time` = NOW()
WHERE `staff_no` = '{staff_no}'
AND `assessment_period` = '{assessment_period}'
AND `data_timepoint` = '{data_timepoint_date}';
"""
f.write(update_sql + "\n")
log.info(f"{table_name}指标汇总完成,共汇总 {len(staff_scores)} 个员工的得分")
步骤3:分类汇总得分计算
@classmethod
async def _calculate_category_totals(
cls,
auth: AuthSchema,
assessment_period: str,
data_timepoint_date: str,
staff_infos: list
) -> None:
"""
计算分类汇总得分
参数:
- auth: AuthSchema - 认证信息
- assessment_period: str - 考核期次
- data_timepoint_date: str - 数据时点(date格式)
- staff_infos: list - 员工信息列表
"""
from sqlalchemy import select, text
from ..yurdmc_pa_performance_assessment.model import YurdmcPaPerformanceAssessmentModel
# 获取所有员工记录
stmt = select(
*PaPerformanceAssessmentModel.id,
*PaPerformanceAssessmentModel.staff_no,
*PaPerformanceAssessmentModel.demand_score_c101,
...
*PaPerformanceAssessmentModel.self_dev_score_c301,
*PaPerformanceAssessmentModel.ops_score_c401,
*PaPerformanceAssessmentModel.ops_score_c402,
*PaPerformanceAssessmentModel.ops_score_c403
).where(
*PaPerformanceAssessmentModel.assessment_period == assessment_period,
*PaPerformanceAssessmentModel.data_timepoint == data_timepoint_date
)
result = await auth.db.execute(stmt)
records = result.fetchall()
# 计算分类汇总得分
update_count = 0
for record in records:
(record_id, staff_no,
demand_c101, demand_c102, demand_c103, demand_c105, demand_c106, demand_c107,
project_c201, project_c202, project_c203, project_c204, project_c205, project_c206,
project_c207, project_c208, project_c209, project_c210, project_c211, project_c212,
project_c213, project_c214,
self_dev_c301,
ops_c401, ops_c402, ops_c403) = record
# 1. 需求工作汇总得分
demand_total = (demand_c101 or 0) + (demand_c102 or 0) + (demand_c103 or 0) + \
(demand_c105 or 0) + (demand_c106 or 0) + (demand_c107 or 0)
# 2. 项目工作汇总得分
project_total = (project_c201 or 0) + (project_c202 or 0) + (project_c203 or 0) + \
(project_c204 or 0) + (project_c205 or 0) + (project_c206 or 0) + \
(project_c207 or 0) + (project_c208 or 0) + (project_c209 or 0) + \
(project_c210 or 0) + (project_c211 or 0) + (project_c212 or 0) + \
(project_c213 or 0) + (project_c214 or 0)
# 3. 自主研发汇总得分
self_dev_total = self_dev_c301 or 0
# 4. 运维工作汇总得分
ops_total = (ops_c401 or 0) + (ops_c402 or 0) + (ops_c403 or 0)
# 5. 质量工作汇总得分、安全工作汇总得分、OKR目标汇总得分(默认0)
quality_total = 0
security_total = 0
okr_total = 0
# 6. 汇总得分
total_score = demand_total + project_total + self_dev_total + \
ops_total + quality_total + security_total + okr_total
# 更新数据库
update_sql = f"""
UPDATE `*_pa_performance_assessment`
SET `demand_total_score` = {demand_total:.2f},
`project_total_score` = {project_total:.2f},
`self_dev_total_score` = {self_dev_total:.2f},
`ops_total_score` = {ops_total:.2f},
`quality_total_score` = {quality_total:.2f},
`security_total_score` = {security_total:.2f},
`okr_total_score` = {okr_total:.2f},
`total_score` = {total_score:.2f},
`updated_time` = NOW()
WHERE `id` = {record_id};
"""
try:
await auth.db.execute(text(update_sql))
update_count += 1
except Exception as e:
log.error(f"更新员工 {staff_no} 的分类汇总得分失败: {str(e)}")
# 使用flush而不是commit,让上下文管理器自动处理事务提交
await auth.db.flush()
log.info(f"分类汇总得分计算完成,共更新 {update_count} 条记录")
步骤4:调用分类汇总方法
# 处理其他指标
for config in indicator_configs:
await cls._process_indicator_data(
auth=auth,
table_name=config['table'],
field_name=config['field'],
assessment_period=assessment_period,
data_date=data_date,
data_timepoint_date=data_timepoint_date,
LOG_DIR=LOG_DIR,
staff_infos=staff_infos
)
# 记录指标处理结果
indicator_key = config['table'].split('_')[-1] # 提取指标代码,如c102
indicator_results[f'{indicator_key}_result'] = {'success_count': 1, 'message': '计算完成'}
# 计算分类汇总得分
await cls._calculate_category_totals(
auth=auth,
assessment_period=assessment_period,
data_timepoint_date=data_timepoint_date,
staff_infos=staff_infos
)
# 更新计算状态
update_data = {
'calculation_status': '已生成'
}
await YurdmcPaExcelUploadRecordCRUD(auth).update_yurdmc_pa_excel_upload_record_crud(id=id, data=update_data)
🐛 问题排查与修复
2.1 C102指标得分计算错误
问题描述
- xx:预期得分26,实际计算23
- xx:预期得分686,实际计算500
问题分析
问题根源:
1. 字段解析错误:C102模型有work_days字段,但查询时未包含
2. 按指标编号去重:相同指标编号的不同判定条件得分被过滤
3. 固定索引解析:当模型字段顺序变化时导致解析错误
解决方案
# 1. 动态检查字段是否存在,包括work_days
optional_fields = ['indicator_no', 'indicator_content', 'work_days', 'outsourcer_name', 'judgment_condition', 'indicator_score']
for field in optional_fields:
if hasattr(model, field):
select_columns.append(getattr(model, field))
column_names.append(field)
# 2. 使用字典映射方式解析字段值
row_dict = dict(zip(column_names, row))
staff_no = row_dict.get('staff_no')
indicator_score = row_dict.get('indicator_score')
# 3. 移除按指标编号去重,直接累加所有记录的得分
if indicator_score:
score_value = float(indicator_score) if isinstance(indicator_score, Decimal) else float(indicator_score)
staff_scores[staff_no] += score_value
修复效果
- xx:10 + 3 + 13 = 26(正确)
- xx:50 + 30 + 20 + 20 + 60 + 70 + 40 + 110 + 96 + 100 + 90 = 686(正确)
2.2 员工信息插入问题
问题描述
插入员工 xx 到绩效考核结果表失败: 创建失败: (asyncmy.errors.IntegrityError) (1062, "Duplicate entry '2985' for key 'yurdmc_pa_performance_assessment.uk_staff_no'")
问题分析
- 员工记录已存在,插入时触发唯一键约束
- 第一个员工插入失败后,事务被关闭,导致后续所有员工插入都失败
解决方案
# 检查记录是否已存在
from sqlalchemy import select
from ..*_pa_performance_assessment.model import YurdmcPaPerformanceAssessmentModel
check_stmt = select(*PaPerformanceAssessmentModel.id).where(
*PaPerformanceAssessmentModel.staff_no == staff_no,
*PaPerformanceAssessmentModel.assessment_period == assessment_period,
*PaPerformanceAssessmentModel.data_timepoint == (datetime.strptime(data_timepoint, '%Y-%m-%d').date() if isinstance(data_timepoint, str) else data_timepoint)
)
check_result = await auth.db.execute(check_stmt)
existing_record = check_result.scalar_one_or_none()
if existing_record:
log.info(f"员工 {staff_no} - {real_name} 已存在于绩效考核结果表中,跳过插入")
continue
2.3 无数据时得分处理
问题描述
当指标没有数据时,得分应该按0存入,而不是NULL。
解决方案
# 初始化所有员工的得分为0
for staff in staff_infos:
staff_no = staff[0]
staff_scores[staff_no] = 0.0
# 处理NULL值
score_value = float(indicator_score) if isinstance(indicator_score, Decimal) else float(indicator_score)
# 分类汇总时处理NULL值
demand_total = (demand_c101 or 0) + (demand_c102 or 0) + (demand_c103 or 0) + \
(demand_c105 or 0) + (demand_c106 or 0) + (demand_c107 or 0)
🔧 代码优化
3.1 通用指标处理方法
优化前
# 为每个指标编写单独的处理逻辑
def process_c101():
# C101处理逻辑
pass
def process_c102():
# C102处理逻辑
pass
# 重复代码多,维护成本高
优化后
# 通用处理方法,支持任意指标
@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,
staff_infos: list
) -> None:
# 动态导入模型
# 动态构建查询
# 统一处理逻辑
pass
# 配置化管理指标
indicator_configs = [
{'table': 'yurdmc_pa_kpi_c102', 'field': 'demand_score_c102'},
{'table': 'yurdmc_pa_kpi_c103', 'field': 'demand_score_c103'},
# 其他指标...
]
3.2 字段解析优化
优化前
# 固定索引解析,容易出错
staff_no = row[0]
indicator_no = row[5]
indicator_score = row[8]
# 当字段顺序变化时,解析错误
优化后
# 字典映射方式解析,不依赖字段顺序
row_dict = dict(zip(column_names, row))
staff_no = row_dict.get('staff_no')
indicator_no = row_dict.get('indicator_no')
indicator_score = row_dict.get('indicator_score')
# 更加健壮,支持不同表结构
3.3 去重逻辑优化
优化前
# 按指标编号去重,可能过滤有效得分
if indicator_no not in staff_indicators[staff_no]:
staff_scores[staff_no] += score_value
staff_indicators[staff_no].add(indicator_no)
# 相同指标编号但不同判定条件的得分被过滤
优化后
# 直接累加所有记录的得分,不移除重复
if indicator_score:
score_value = float(indicator_score) if isinstance(indicator_score, Decimal) else float(indicator_score)
staff_scores[staff_no] += score_value
# 确保所有得分都被正确计算
📊 完整流程
4.1 指标计算与分类汇总流程图
开始
↓
点击"计算考核期次得分"按钮
↓
查询员工基本信息(*_user_staff_info)
↓
写入员工信息日志
↓
查询C101指标数据(*_pa_kpi_c101)
↓
写入C101指标数据日志
↓
按工号聚合C101得分
↓
插入员工信息到绩效考核结果表
↓
执行UPDATE语句更新C101得分
↓
循环处理C102-C403指标
↓
↓ 动态导入指标模型
↓ 动态构建查询
↓ 执行查询获取数据
↓ 按工号聚合得分
↓ 执行UPDATE语句更新得分
↓
计算分类汇总得分
↓
↓ 需求工作汇总得分
↓ 项目工作汇总得分
↓ 自主研发汇总得分
↓ 运维工作汇总得分
↓ 汇总总得分
↓
更新计算状态为"已生成"
↓
返回计算结果
↓
结束
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, C102, C103, C105, C106, C107 │ │
│ │ C201-C214, C301, C401-C403 │ │
│ │ staff_no, indicator_score, assessment_period, data_date │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 绩效考核结果表:*_pa_performance_assessment │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ 员工基本信息 + 指标得分 │ │
│ │ demand_score_c101-c107 = 汇总得分 │ │
│ │ project_score_c201-c214 = 汇总得分 │ │
│ │ self_dev_score_c301 = 汇总得分 │ │
│ │ ops_score_c401-c403 = 汇总得分 │ │
│ │ demand_total_score = 需求工作汇总 │ │
│ │ project_total_score = 项目工作汇总 │ │
│ │ self_dev_total_score = 自主研发汇总 │ │
│ │ ops_total_score = 运维工作汇总 │ │
│ │ total_score = 总得分 │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
🚀 技术要点
5.1 动态模型导入
# 动态导入模型
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
优势:
- 支持任意指标表,无需为每个指标编写单独的处理逻辑
- 提高代码的通用性和可维护性
- 便于扩展新的指标类型
5.2 动态字段检查
# 动态检查字段是否存在
base_fields = ['staff_no', 'real_name', 'position', 'assessment_period', 'data_date']
for field in base_fields:
if hasattr(model, field):
select_columns.append(getattr(model, field))
column_names.append(field)
# 检查可选字段
optional_fields = ['indicator_no', 'indicator_content', 'work_days', 'outsourcer_name', 'judgment_condition', 'indicator_score']
for field in optional_fields:
if hasattr(model, field):
select_columns.append(getattr(model, field))
column_names.append(field)
优势:
- 支持不同结构的指标表
- 避免字段不存在导致的错误
- 提高代码的健壮性
5.3 字典映射解析
# 使用字典映射方式解析行数据
row_dict = dict(zip(column_names, row))
# 解析字段值
staff_no = row_dict.get('staff_no')
real_name = row_dict.get('real_name')
indicator_score = row_dict.get('indicator_score')
优势:
- 不依赖字段顺序,更加健壮
- 代码可读性更高
- 便于处理不同表结构
5.4 分类汇总计算
# 计算需求工作汇总得分
demand_total = (demand_c101 or 0) + (demand_c102 or 0) + (demand_c103 or 0) + \
(demand_c105 or 0) + (demand_c106 or 0) + (demand_c107 or 0)
# 计算项目工作汇总得分
project_total = (project_c201 or 0) + (project_c202 or 0) + (project_c203 or 0) + \
(project_c204 or 0) + (project_c205 or 0) + (project_c206 or 0) + \
(project_c207 or 0) + (project_c208 or 0) + (project_c209 or 0) + \
(project_c210 or 0) + (project_c211 or 0) + (project_c212 or 0) + \
(project_c213 or 0) + (project_c214 or 0)
# 计算总得分
total_score = demand_total + project_total + self_dev_total + \
ops_total + quality_total + security_total + okr_total
优势:
- 清晰的分类逻辑
- 支持NULL值处理
- 易于理解和维护
📈 待实现功能
6.1 质量和安全指标
6.2 OKR目标指标
6.3 性能优化
6.4 功能完善
- 数据验证和异常处理
- 单元测试和集成测试
- 用户界面优化
- 导出报表功能

编辑