作者:呱牛
发布日期:2026年3月27日
标签:FastAPI、绩效考核、指标计算、规则引擎、Python
🔥 今日亮点
2026年3月27日 - 全指标计算引擎完成 & 系统优化
- ✅ 完成全指标计算引擎:C101-C403全部指标计算功能上线
- ✅ 修复字段映射问题:解决C301、C401-C403的字段名不一致问题
- ✅ 前端进度条优化:添加动态进度条,提升用户体验
- ✅ 超时问题解决:优化前后端交互,解决长时间计算超时问题
- ✅ 完成21个指标实现:C101-C107、C201-C214、C301、C401-C403全部上线
📋 文章目录
🎯 全指标计算完成
1.1 指标体系全景图
绩效考核指标体系(已完成)
├── 需求工作类 (C1XX) ✅
│ ├── C101: 需求流程管理 ✅
│ ├── C102: 需求分析工作 ✅
│ ├── C103: 需求评审管理 ✅
│ ├── C105: 文档归档管理 ✅
│ ├── C106: 外包面试管理 ✅
│ └── C107: 外包信息维护 ✅
├── 项目工作类 (C2XX) ✅
│ ├── C201: 项目管理1 ✅
│ ├── C202: 项目管理2 ✅
│ ├── C203: 项目管理3 ✅
│ ├── C204: 项目管理4 ✅
│ ├── C205: 项目管理5 ✅
│ ├── C206: 项目管理6 ✅
│ ├── C207: 项目管理7 ✅
│ ├── C208: 项目管理8 ✅
│ ├── C209: 项目管理9 ✅
│ ├── C210: 项目管理10 ✅
│ ├── C211: 项目管理11 ✅
│ ├── C212: 项目管理12 ✅
│ ├── C213: 项目管理13 ✅
│ └── C214: 项目管理14 ✅
├── 运维工作类 (C3XX) ✅
│ └── C301: 运维值班管理 ✅
└── 管理工作类 (C4XX) ✅
├── C401: 电话受理日常运维工作 ✅
├── C402: 日常运维、配合三方联调等工作 ✅
└── C403: 生产问题单上线修复 ✅
1.2 计算引擎统一架构
┌─────────────────────────────────────────────────────────────────────┐
│ 指标得分计算引擎架构 │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ 触发层:Excel文件上传并解析成功 │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ POST /api/v1/*_pa_excel_upload_record/upload │ │
│ │ - 上传Excel文件 │ │
│ │ - 解析数据并入库 │ │
│ │ - 触发得分计算 │ │
│ └───────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 计算层:批量计算所有指标得分 │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ async def calculate_all_scores(): │ │
│ │ # 需求工作类 │ │
│ │ await C101Service.calculate_scores(...) │ │
│ │ await C102Service.calculate_scores(...) │ │
│ │ ... │ │
│ │ # 项目工作类 │ │
│ │ await C201Service.calculate_scores(...) │ │
│ │ ... │ │
│ │ # 运维工作类 │ │
│ │ await C301Service.calculate_scores(...) │ │
│ │ # 管理工作类 │ │
│ │ await C401Service.calculate_scores(...) │ │
│ │ await C402Service.calculate_scores(...) │ │
│ │ await C403Service.calculate_scores(...) │ │
│ └───────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 数据层:更新指标得分到数据库 │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ UPDATE *_pa_kpi_cXXX │ │
│ │ SET indicator_score = ? │ │
│ │ WHERE id = ? │ │
│ └───────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
🐛 问题排查与修复
2.1 字段映射问题
问题描述
在开发C301、C401、C402、C403指标计算时,发现得分计算失败。经过排查,发现这些表的model中没有judgment_condition字段,而是使用indicator_status字段。
问题分析
问题根源:
1. C301、C401、C402、C403表的字段命名不一致
2. 其他指标表使用 judgment_condition 字段
3. 这几个表使用 indicator_status 字段
4. 计算代码中使用 record.judgment_condition 导致获取不到值
解决方案
# 修改前(错误)
judgment_condition = record.judgment_condition
# 修改后(正确)
judgment_condition = record.indicator_status
完整修复代码
# *_pa_kpi_c401/service.py
@classmethod
async def calculate_scores_*_pa_kpi_c401_service(
cls, auth: AuthSchema, assessment_period: str = None, data_date = None
) -> dict:
"""
计算C401指标得分
"""
# 构建搜索条件
search_dict = {}
if assessment_period:
search_dict['assessment_period'] = assessment_period
if data_date:
search_dict['data_date'] = data_date
# 获取C401指标记录
c401_records = await YurdmcPaKpiC401CRUD(auth).list_*_pa_kpi_c401_crud(search=search_dict)
update_count = 0
for record in c401_records:
# 关键修复:使用indicator_status字段
judgment_condition = record.indicator_status
if not judgment_condition:
continue
# 查找对应的评分规则
score_crud = *PaPerfIndicatorScoreCRUD(auth)
score_search_dict = {
'indicator_level1_code': 'C401',
'judgment_condition': judgment_condition
}
score_rules = await score_crud.list_*_pa_perf_indicator_score_crud(search=score_search_dict)
if score_rules:
rule = score_rules[0]
if rule.indicator_score:
try:
# 根据unit类型计算
unit = rule.unit or '个'
if unit == '个':
indicator_score = Decimal(str(rule.indicator_score)) * Decimal('1')
elif unit == '天':
work_days = getattr(record, 'work_days', 0)
indicator_score = Decimal(str(rule.indicator_score)) * Decimal(str(work_days or 0))
else:
indicator_score = Decimal(str(rule.indicator_score)) * Decimal('1')
# 更新得分
update_data = {'indicator_score': float(indicator_score)}
await *PaKpiC401CRUD(auth).update_*_pa_kpi_c401_crud(
id=record.id,
data=update_data
)
update_count += 1
except Exception as e:
log.error(f"计算C401指标得分失败,记录ID: {record.id}, 错误: {str(e)}")
log.info(f"C401指标得分计算完成,成功更新 {update_count} 条记录")
return {
'success_count': update_count,
'total_count': len(c401_records)
}
2.2 数据清洗问题排查
排查过程
步骤1: 检查数据库是否有数据
执行SQL: SELECT COUNT(*) FROM *_pa_kpi_c401 WHERE assessment_period = '202603'
结果: 有数据,说明数据插入正常
步骤2: 检查字段值是否正确
执行SQL: SELECT DISTINCT indicator_status FROM yurdmc_pa_kpi_c401
结果: 字段值正确,如"已解决"、"解决中"等
步骤3: 检查评分规则配置
执行SQL: SELECT * FROM *_pa_perf_indicator_score WHERE indicator_level1_code = 'C401'
结果: 配置正确,分值和单位都正确
步骤4: 检查计算逻辑
发现: 代码使用record.judgment_condition,但表中没有这个字段
修复: 改为使用record.indicator_status
🎨 前端体验优化
3.1 动态进度条实现
需求背景
上传考核期次文件并计算得分需要较长时间(约10-15秒),用户在等待过程中看不到进度,体验不佳。
实现方案
<template>
<el-dialog
v-model="progressVisible"
title="正在计算得分"
width="500px"
:close-on-click-modal="false"
:close-on-press-escape="false"
:show-close="false"
>
<el-progress
:percentage="progressPercentage"
:status="progressStatus"
:stroke-width="20"
/>
<p style="text-align: center; margin-top: 20px;">
{{ progressText }}
</p>
</el-dialog>
</template>
<script setup>
import { ref } from 'vue'
const progressVisible = ref(false)
const progressPercentage = ref(0)
const progressStatus = ref('')
const progressText = ref('正在上传文件...')
const simulateProgress = () => {
progressVisible.value = true
progressPercentage.value = 0
progressStatus.value = ''
const stages = [
{ percentage: 10, text: '正在上传文件...' },
{ percentage: 30, text: '正在解析Excel数据...' },
{ percentage: 50, text: '正在插入数据库...' },
{ percentage: 70, text: '正在计算指标得分...' },
{ percentage: 90, text: '正在更新计算状态...' },
]
let currentStage = 0
const timer = setInterval(() => {
if (currentStage < stages.length) {
progressPercentage.value = stages[currentStage].percentage
progressText.value = stages[currentStage].text
currentStage++
}
}, 1000)
return timer
}
const handleUpload = async () => {
const timer = simulateProgress()
try {
const response = await uploadApi(formData)
// 完成
clearInterval(timer)
progressPercentage.value = 100
progressStatus.value = 'success'
progressText.value = '计算完成!'
setTimeout(() => {
progressVisible.value = false
ElMessage.success('上传并计算成功')
}, 1000)
} catch (error) {
clearInterval(timer)
progressVisible.value = false
ElMessage.error('上传失败:' + error.message)
}
}
</script>
3.2 按钮状态动态变化
需求描述
在操作列表下增加"计算考核期次得分"按钮:
- 默认状态:未计算(红色)
- 计算完成:已计算(蓝色)
实现代码
<template>
<el-table-column label="操作" width="300" fixed="right">
<template #default="{ row }">
<el-button
type="primary"
size="small"
@click="handleEdit(row)"
>
编辑
</el-button>
<el-button
:type="row.calculation_status === '已生成' ? 'primary' : 'danger'"
size="small"
@click="handleCalculate(row)"
:disabled="row.calculation_status === '已生成'"
>
{{ row.calculation_status === '已生成' ? '已计算' : '计算得分' }}
</el-button>
<el-button
type="danger"
size="small"
@click="handleDelete(row)"
>
删除
</el-button>
</template>
</el-table-column>
</template>
<script setup>
const handleCalculate = async (row) => {
try {
await ElMessageBox.confirm(
'确定要计算该考核期次的得分吗?',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
)
const response = await calculateScoresApi(row.id)
ElMessage.success('计算完成')
// 刷新列表
fetchList()
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('计算失败:' + error.message)
}
}
}
</script>
3.3 超时问题解决
问题分析
问题:前端请求超时,但后端实际已完成处理
原因:
1. 计算所有指标得分需要10-15秒
2. 前端默认超时时间为5秒
3. 后端处理完成,但前端已经超时报错
解决方案
import axios from 'axios'
const service = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 30000, // 修改为30秒
headers: {
'Content-Type': 'application/json;charset=UTF-8',
},
})
export default service
🔧 系统稳定性优化
4.1 日志系统优化
问题描述
错误信息:
PermissionError: [WinError 32] 另一个程序正在使用此文件,进程无法访问。
原因:
Windows多进程环境下,loguru日志文件轮转时会出现权限错误
解决方案
# backend/app/core/logger.py
from loguru import logger
def setup_logging() -> None:
"""配置日志系统"""
# 配置常规日志文件
handler_id = logger.add(
str(log_dir / "info.log"),
format=log_format,
level="INFO",
rotation="00:00", # 每天午夜轮转
retention=30, # 日志保留天数
compression="gz",
encoding="utf-8",
enqueue=True, # 关键修复:多进程安全,避免Windows文件权限错误
)
# 配置错误日志文件
handler_id = logger.add(
str(log_dir / "error.log"),
format=log_format,
level="ERROR",
rotation="00:00",
retention=30,
compression="gz",
encoding="utf-8",
backtrace=True,
diagnose=True,
enqueue=True, # 关键修复:多进程安全
)
技术说明
enqueue=True 参数的作用:
1. 日志消息会被放入队列
2. 由单独的线程负责写入文件
3. 避免多进程同时写入导致的权限冲突
4. 提高日志写入性能
5. 确保日志不丢失
4.2 数据清洗工具优化
系统保留字段配置
class DataCleaningUtil:
"""
数据清洗工具类
"""
# 系统保留字段列表,这些字段不参与业务数据空值检查
SYSTEM_RESERVED_FIELDS = [
'id', 'uuid', 'status', 'description',
'created_time', 'updated_time', 'created_id', 'updated_id',
'staff_no', 'real_name', 'position',
'assessment_period', 'data_date',
'indicator_score' # 指标得分字段,允许为空
]
清洗逻辑
def clean_data(self, table_name: str, data: dict) -> tuple[bool, str]:
"""
清洗单条数据
规则:
1. 系统保留字段可以为空
2. 业务字段不能为空
3. 如果业务字段为空,则过滤该数据
"""
empty_fields = []
for field_name, field_value in data.items():
# 跳过系统保留字段
if field_name in self.SYSTEM_RESERVED_FIELDS:
continue
# 检查业务字段是否为空
if self.is_empty_value(field_value):
empty_fields.append(field_name)
# 如果存在空字段,则过滤该数据
if empty_fields:
reason = f"业务字段存在空值: {', '.join(empty_fields)}"
self._log_filtered_record(table_name, data, reason)
return False, reason
return True, ""
📊 完整指标体系
5.1 指标分类与计算规则
| 指标编码 | 指标名称 | 判断条件字段 | 计算单位 | 分值范围 |
|---|---|---|---|---|
| C101 | 需求流程管理 | judgment_condition | 个 | 5-20分 |
| C102 | 需求分析工作 | judgment_condition | 天 | 10分/天 |
| C103 | 需求评审管理 | judgment_condition | 个 | 5-10分 |
| C105 | 文档归档管理 | judgment_condition | 个 | 5分 |
| C106 | 外包面试管理 | judgment_condition | 个 | 2分 |
| C107 | 外包信息维护 | judgment_condition | 个 | 1分 |
| C201-C214 | 项目管理 | judgment_condition | 天 | 5-10分/天 |
| C301 | 运维值班管理 | indicator_status | 天 | 8分/天 |
| C401 | 电话受理日常运维 | indicator_status | 天 | 8分/天 |
| C402 | 日常运维配合 | indicator_status | 天 | 5-8分/天 |
| C403 | 生产问题单修复 | indicator_status | 个 | 8分 |
5.2 数据库配置示例
C401评分规则配置
INSERT INTO *_pa_perf_indicator_score
(indicator_category, indicator_level1_code, indicator_level1_name, judgment_condition, indicator_score, unit)
VALUES
('系统运维', 'C401', '电话受理日常运维工作', '已解决', 8.00, '天'),
('系统运维', 'C401', '电话受理日常运维工作', '解决中', 8.00, '天'),
('系统运维', 'C401', '电话受理日常运维工作', '转需求问题单', 8.00, '天');
C402评分规则配置
INSERT INTO *_pa_perf_indicator_score
(indicator_category, indicator_level1_code, indicator_level1_name, judgment_condition, indicator_score, unit)
VALUES
('系统运维', 'C402', '日常运维、配合三方联调等工作', '已解决', 8.00, '天'),
('系统运维', 'C402', '日常运维、配合三方联调等工作', '解决中', 5.00, '天'),
('系统运维', 'C402', '日常运维、配合三方联调等工作', '转需求问题单', 5.00, '天');
🚀 性能优化实践
6.1 批量计算优化
异步并发计算
async def calculate_all_scores(cls, auth, record):
"""
批量计算所有指标得分
优化策略:
1. 异步执行,不阻塞主线程
2. 按类别分组计算
3. 记录每个指标的计算结果
"""
results = {}
# 需求工作类指标
results['c101'] = await C101Service.calculate_scores(auth, record.assessment_period, record.data_date)
results['c102'] = await C102Service.calculate_scores(auth, record.assessment_period, record.data_date)
results['c103'] = await C103Service.calculate_scores(auth, record.assessment_period, record.data_date)
results['c105'] = await C105Service.calculate_scores(auth, record.assessment_period, record.data_date)
results['c106'] = await C106Service.calculate_scores(auth, record.assessment_period, record.data_date)
results['c107'] = await C107Service.calculate_scores(auth, record.assessment_period, record.data_date)
# 项目工作类指标
for i in range(201, 215):
code = f'c{i}'
service = getattr(cls, f'_{code}_service')
results[code] = await service.calculate_scores(auth, record.assessment_period, record.data_date)
# 运维工作类指标
results['c301'] = await C301Service.calculate_scores(auth, record.assessment_period, record.data_date)
# 管理工作类指标
results['c401'] = await C401Service.calculate_scores(auth, record.assessment_period, record.data_date)
results['c402'] = await C402Service.calculate_scores(auth, record.assessment_period, record.data_date)
results['c403'] = await C403Service.calculate_scores(auth, record.assessment_period, record.data_date)
# 更新计算状态
await CRUD(auth).update_crud(id=record.id, data={'calculation_status': '已生成'})
return results
6.2 数据库查询优化
索引优化
-- 为考核期次和数据时点添加复合索引
ALTER TABLE yurdmc_pa_kpi_cXXX
ADD INDEX ix_assessment_period_data_date (assessment_period, data_date);
-- 为判断条件添加索引
ALTER TABLE *_pa_kpi_cXXX
ADD INDEX ix_judgment_condition (judgment_condition);
-- 为评分规则表添加复合索引
ALTER TABLE *_pa_perf_indicator_score
ADD INDEX ix_indicator_condition (indicator_level1_code, judgment_condition);
查询优化
# 使用索引优化查询
async def calculate_scores_service(cls, auth, assessment_period, data_date):
# 构建搜索条件(使用索引字段)
search_dict = {
'assessment_period': assessment_period,
'data_date': data_date
}
# 查询数据(会使用复合索引)
records = await CRUD(auth).list_crud(search=search_dict)
# 批量查询评分规则(减少数据库访问次数)
score_rules_cache = {}
for record in records:
condition = record.judgment_condition or record.indicator_status
cache_key = f"{record.__tablename__}_{condition}"
if cache_key not in score_rules_cache:
rules = await ScoreCRUD(auth).list_crud(
search={
'indicator_level1_code': record.__tablename__.split('_')[-1].upper(),
'judgment_condition': condition
}
)
score_rules_cache[cache_key] = rules[0] if rules else None
🔍 常见问题
Q1: 为什么C301、C401-C403使用不同的字段名?
A: 这是数据库设计时的命名不一致导致的。其他指标表使用judgment_condition字段,而这几个表使用indicator_status字段。为了兼容这种情况,我们在代码中做了适配:
# 根据表名判断使用哪个字段
if table_name in ['_pa_kpi_c301', '*_pa_kpi_c401', '*_pa_kpi_c402', 'yurdmc_pa_kpi_c403']:
judgment_condition = record.indicator_status
else:
judgment_condition = record.judgment_condition
Q2: 如何快速定位数据插入失败的原因?
A: 按以下步骤排查:
-
查看数据清洗日志
- 位置:
backend/logs/数据清洗日志_YYYYMMDD_HHMMSS.log - 查看是否有数据被过滤及过滤原因
- 位置:
-
查看后端日志
- 搜索关键字:
表 *_pa_kpi_cXXX - 查看解析数量和清洗数量
- 搜索关键字:
-
检查Excel数据
- 确认字段映射是否正确
- 确认数据是否完整
Q3: 如何添加新的指标计算?
A: 按以下步骤操作:
-
添加数据库字段
ALTER TABLE *_pa_kpi_cXXX ADD COLUMN indicator_score DECIMAL(5,2) NULL COMMENT '指标得分'; -
更新Model层
indicator_score: Mapped[float | None] = mapped_column(Numeric(5, 2), nullable=True, comment='指标得分') -
更新Schema层
indicator_score: float | None = Field(default=None, description='指标得分') -
实现计算服务
- 参考
*_pa_kpi_c401/service.py的实现
- 参考
-
集成到主流程
- 在
*_pa_excel_upload_record/service.py中调用
- 在
Q4: 前端超时如何解决?
A: 修改前端请求超时配置:
const service = axios.create({
timeout: 30000, // 设置为30秒
})
Q5: 日志文件权限错误如何解决?
A: 在loguru配置中添加enqueue=True参数:
logger.add(
str(log_dir / "error.log"),
enqueue=True, # 多进程安全
)
📈 更新日志
v1.3.0 (2026-03-27)
新增功能:
- ✅ 完成C201-C214、C301、C401-C403指标得分计算
- ✅ 前端动态进度条显示
- ✅ 按钮状态动态变化(未计算/已计算)
问题修复:
- 🐛 修复C301、C401-C403字段映射问题
- 🐛 修复前端请求超时问题
性能优化:
- ⚡ 优化数据库查询索引
- ⚡ 优化批量计算性能
- ⚡ 优化数据清洗逻辑
文档更新:
- 📝 完善指标计算开发手册
- 📝 添加常见问题FAQ
- 📝 添加性能优化指南
v1.2.0 (2026-03-26)
新增功能:
- ✅ 指标得分计算引擎
- ✅ 支持多种计算单位(个、天)
- ✅ 动态规则配置
已实现指标:
- C101、C102、C103、C105、C106、C107
🎯 下一步计划
短期目标(1-2周)
-
性能优化
- 实现真正的异步并发计算
- 添加Redis缓存层
- 优化数据库批量操作
-
功能增强
- 支持指标得分汇总统计
- 支持得分趋势分析
- 支持异常数据预警
-
用户体验
- 优化前端加载速度
- 添加数据导出功能
- 完善错误提示信息
中期目标(1个月)
-
智能化
- 自动识别异常数据
- 智能推荐评分规则
-
可视化
- 开发数据可视化大屏
- 支持多维度数据分析