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

0 阅读14分钟

作者:呱牛

发布日期: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_condition5-20分
C102需求分析工作judgment_condition10分/天
C103需求评审管理judgment_condition5-10分
C105文档归档管理judgment_condition5分
C106外包面试管理judgment_condition2分
C107外包信息维护judgment_condition1分
C201-C214项目管理judgment_condition5-10分/天
C301运维值班管理indicator_status8分/天
C401电话受理日常运维indicator_status8分/天
C402日常运维配合indicator_status5-8分/天
C403生产问题单修复indicator_status8分

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: 按以下步骤排查:

  1. 查看数据清洗日志

    • 位置:backend/logs/数据清洗日志_YYYYMMDD_HHMMSS.log
    • 查看是否有数据被过滤及过滤原因
  2. 查看后端日志

    • 搜索关键字:表 *_pa_kpi_cXXX
    • 查看解析数量和清洗数量
  3. 检查Excel数据

    • 确认字段映射是否正确
    • 确认数据是否完整

Q3: 如何添加新的指标计算?

A: 按以下步骤操作:

  1. 添加数据库字段

    ALTER TABLE *_pa_kpi_cXXX 
    ADD COLUMN indicator_score DECIMAL(5,2) NULL COMMENT '指标得分';
    

  2. 更新Model层

    indicator_score: Mapped[float | None] = mapped_column(Numeric(5, 2), nullable=True, comment='指标得分')
    

  3. 更新Schema层

    indicator_score: float | None = Field(default=None, description='指标得分')
    

  4. 实现计算服务

    • 参考*_pa_kpi_c401/service.py的实现
  5. 集成到主流程

    • *_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周)

  1. 性能优化

    • 实现真正的异步并发计算
    • 添加Redis缓存层
    • 优化数据库批量操作
  2. 功能增强

    • 支持指标得分汇总统计
    • 支持得分趋势分析
    • 支持异常数据预警
  3. 用户体验

    • 优化前端加载速度
    • 添加数据导出功能
    • 完善错误提示信息

中期目标(1个月)

  1. 智能化

    • 自动识别异常数据
    • 智能推荐评分规则
  2. 可视化

    • 开发数据可视化大屏
    • 支持多维度数据分析

📚 参考资料