feat: 添加设计师扣分明细查看功能,点击扣分数字可查看详细扣分记录

This commit is contained in:
Your Name 2026-01-13 16:49:50 +08:00
parent d9d8c24e2c
commit 607270c158
6 changed files with 701 additions and 2 deletions

View File

@ -3,6 +3,7 @@ package com.designhub.controller;
import com.designhub.common.Result;
import com.designhub.service.AnalyticsService;
import com.designhub.util.JwtUtil;
import com.designhub.vo.DeductionDetailVO;
import com.designhub.vo.DemandStatisticsVO;
import com.designhub.vo.DesignerPerformanceVO;
import com.designhub.vo.ProcessEfficiencyVO;
@ -88,5 +89,17 @@ public class AnalyticsController {
return Result.success(analyticsService.getWarningTasks(currentUserId));
}
@Operation(summary = "获取设计师扣分明细")
@GetMapping("/designer-deduction-details")
public Result<DeductionDetailVO> getDesignerDeductionDetails(
@RequestHeader("Authorization") String authHeader,
@RequestParam Long userId,
@RequestParam(required = false) String month
) {
log.info("获取设计师扣分明细: userId={}, month={}", userId, month);
return Result.success(analyticsService.getDesignerDeductionDetails(userId, month));
}
}

View File

@ -3,6 +3,7 @@ package com.designhub.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.designhub.entity.*;
import com.designhub.mapper.*;
import com.designhub.vo.DeductionDetailVO;
import com.designhub.vo.DemandStatisticsVO;
import com.designhub.vo.DesignerPerformanceVO;
import com.designhub.vo.ProcessEfficiencyVO;
@ -1277,5 +1278,170 @@ public class AnalyticsService {
return null;
}
/**
* 获取设计师扣分明细
* @param userId 设计师ID
* @param month 月份 yyyy-MM
* @return 扣分明细
*/
public DeductionDetailVO getDesignerDeductionDetails(Long userId, String month) {
log.info("获取设计师扣分明细: userId={}, month={}", userId, month);
// 计算时间范围
LocalDateTime startTime;
LocalDateTime endTime;
String currentMonth;
if (month != null && !month.isEmpty()) {
currentMonth = month;
try {
LocalDate monthDate = LocalDate.parse(month + "-01", DateTimeFormatter.ofPattern("yyyy-MM-dd"));
startTime = LocalDateTime.of(monthDate, LocalTime.MIN);
endTime = LocalDateTime.of(monthDate.with(TemporalAdjusters.lastDayOfMonth()), LocalTime.MAX);
} catch (Exception e) {
LocalDate now = LocalDate.now();
startTime = LocalDateTime.of(now.withDayOfMonth(1), LocalTime.MIN);
endTime = LocalDateTime.now();
currentMonth = now.format(DateTimeFormatter.ofPattern("yyyy-MM"));
}
} else {
LocalDate now = LocalDate.now();
startTime = LocalDateTime.of(now.withDayOfMonth(1), LocalTime.MIN);
endTime = LocalDateTime.now();
currentMonth = now.format(DateTimeFormatter.ofPattern("yyyy-MM"));
}
// 获取用户信息
User designer = userMapper.selectById(userId);
if (designer == null) {
log.warn("设计师不存在: userId={}", userId);
return DeductionDetailVO.builder()
.totalDeduction(BigDecimal.ZERO)
.delayDeductions(new ArrayList<>())
.progressDeductions(new ArrayList<>())
.build();
}
// 获取时间范围内的已完成任务
List<Task> completedTasks = taskMapper.selectList(
new LambdaQueryWrapper<Task>()
.eq(Task::getReceiverId, userId)
.eq(Task::getStatus, "completed")
.ge(Task::getCreateTime, startTime)
.le(Task::getCreateTime, endTime)
);
// 获取扣分比例配置
BigDecimal deductionRate = getDeductionRate(designer.getDeptId(), currentMonth);
// 计算延误扣分明细
List<DeductionDetailVO.DelayDeductionItem> delayDeductions = new ArrayList<>();
BigDecimal totalDelayDeduction = BigDecimal.ZERO;
for (Task task : completedTasks) {
if (task.getDeadline() != null && task.getActualEndTime() != null
&& task.getActualEndTime().isAfter(task.getDeadline())) {
// 计算任务积分
BigDecimal taskScore = BigDecimal.ZERO;
if (task.getScoreReward() != null && task.getScoreReward().compareTo(BigDecimal.ZERO) > 0) {
taskScore = task.getScoreReward();
} else if (task.getScore() != null) {
taskScore = BigDecimal.valueOf(task.getScore());
}
if (taskScore.compareTo(BigDecimal.ZERO) > 0) {
// 计算延误小时数
long overdueHours = Duration.between(task.getDeadline(), task.getActualEndTime()).toHours();
if (overdueHours > 0) {
// 计算扣分
BigDecimal taskDeduction = taskScore
.multiply(BigDecimal.valueOf(overdueHours))
.multiply(deductionRate)
.divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
// 扣分不能超过任务积分
if (taskDeduction.compareTo(taskScore) > 0) {
taskDeduction = taskScore;
}
totalDelayDeduction = totalDelayDeduction.add(taskDeduction);
delayDeductions.add(DeductionDetailVO.DelayDeductionItem.builder()
.taskId(task.getId())
.taskNo(task.getTaskNo())
.taskTitle(task.getTitle())
.taskScore(taskScore)
.deadline(task.getDeadline())
.actualEndTime(task.getActualEndTime())
.overdueHours(overdueHours)
.deductionScore(taskDeduction)
.deductionReason("任务延误 " + overdueHours + " 小时")
.build());
}
}
}
}
// 获取进度未更新扣分明细
List<DeductionDetailVO.ProgressDeductionItem> progressDeductions = new ArrayList<>();
BigDecimal totalProgressDeduction = BigDecimal.ZERO;
try {
if (progressDeductionLogMapper != null) {
// 解析月份
String[] monthParts = currentMonth.split("-");
if (monthParts.length == 2) {
int yearNum = Integer.parseInt(monthParts[0]);
int monthNum = Integer.parseInt(monthParts[1]);
// 查询该月的所有进度未更新扣分记录
LocalDate monthStartDate = LocalDate.of(yearNum, monthNum, 1);
LocalDate monthEndDate = monthStartDate.with(TemporalAdjusters.lastDayOfMonth());
List<ProgressDeductionLog> progressLogs = progressDeductionLogMapper.selectList(
new LambdaQueryWrapper<ProgressDeductionLog>()
.eq(ProgressDeductionLog::getUserId, userId)
.ge(ProgressDeductionLog::getDeductionDate, monthStartDate)
.le(ProgressDeductionLog::getDeductionDate, monthEndDate)
.isNull(ProgressDeductionLog::getTaskItemId) // 只查询主任务的扣分
.orderByDesc(ProgressDeductionLog::getDeductionDate)
);
for (ProgressDeductionLog log : progressLogs) {
if (log.getDeductionScore() != null && log.getDeductionScore().compareTo(BigDecimal.ZERO) > 0) {
totalProgressDeduction = totalProgressDeduction.add(log.getDeductionScore());
// 获取任务信息
Task task = taskMapper.selectById(log.getTaskId());
String taskNo = task != null ? task.getTaskNo() : "";
String taskTitle = task != null ? task.getTitle() : "任务已删除";
progressDeductions.add(DeductionDetailVO.ProgressDeductionItem.builder()
.taskId(log.getTaskId())
.taskNo(taskNo)
.taskTitle(taskTitle)
.deductionDate(log.getDeductionDate())
.deductionScore(log.getDeductionScore())
.reason(log.getReason() != null ? log.getReason() : "当日未更新任务进度")
.build());
}
}
}
}
} catch (Exception e) {
log.warn("获取进度未更新扣分明细失败: {}", e.getMessage());
}
BigDecimal totalDeduction = totalDelayDeduction.add(totalProgressDeduction);
return DeductionDetailVO.builder()
.totalDeduction(totalDeduction)
.delayDeductions(delayDeductions)
.progressDeductions(progressDeductions)
.build();
}
}

View File

@ -0,0 +1,71 @@
package com.designhub.vo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
/**
* 扣分明细VO
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DeductionDetailVO {
/**
* 总扣分
*/
private BigDecimal totalDeduction;
/**
* 延误扣分明细列表
*/
private List<DelayDeductionItem> delayDeductions;
/**
* 进度未更新扣分明细列表
*/
private List<ProgressDeductionItem> progressDeductions;
/**
* 延误扣分明细项
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class DelayDeductionItem {
private Long taskId;
private String taskNo;
private String taskTitle;
private BigDecimal taskScore;
private LocalDateTime deadline;
private LocalDateTime actualEndTime;
private Long overdueHours;
private BigDecimal deductionScore;
private String deductionReason;
}
/**
* 进度未更新扣分明细项
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class ProgressDeductionItem {
private Long taskId;
private String taskNo;
private String taskTitle;
private java.time.LocalDate deductionDate;
private BigDecimal deductionScore;
private String reason;
}
}

View File

@ -351,3 +351,60 @@ export const getWarningTasks = async (): Promise<WarningTask[]> => {
return [];
};
// ==================== 扣分明细类型 ====================
export interface DelayDeductionItem {
taskId: number;
taskNo: string;
taskTitle: string;
taskScore: number;
deadline: string;
actualEndTime: string;
overdueHours: number;
deductionScore: number;
deductionReason: string;
}
export interface ProgressDeductionItem {
taskId: number;
taskNo: string;
taskTitle: string;
deductionDate: string;
deductionScore: number;
reason: string;
}
export interface DeductionDetail {
totalDeduction: number;
delayDeductions: DelayDeductionItem[];
progressDeductions: ProgressDeductionItem[];
}
/**
* 获取设计师扣分明细
*/
export const getDesignerDeductionDetails = async (
userId: number,
month?: string
): Promise<DeductionDetail> => {
const params: any = { userId };
if (month) params.month = month;
const response = await request.get('/analytics/designer-deduction-details', { params });
if (response && typeof response === 'object') {
if ('data' in response && response.data !== undefined && response.data !== null) {
return response.data as DeductionDetail;
}
if ('totalDeduction' in response || 'delayDeductions' in response) {
return response as unknown as DeductionDetail;
}
}
return {
totalDeduction: 0,
delayDeductions: [],
progressDeductions: []
};
};

View File

@ -251,7 +251,13 @@
<span class="score-value">{{ record.totalScore || 0 }}</span>
</template>
<template v-else-if="column.key === 'deductionScore'">
<span class="deduction-value">{{ record.deductionScore || 0 }}</span>
<span
class="deduction-value clickable"
@click="showDeductionDetails(record)"
:style="{ cursor: (record.deductionScore && record.deductionScore > 0) ? 'pointer' : 'default' }"
>
{{ record.deductionScore || 0 }}
</span>
</template>
<template v-else-if="column.key === 'netScore'">
<span class="net-score-value">{{ record.netScore || 0 }}</span>
@ -417,6 +423,79 @@
</a-spin>
</div>
</div>
<!-- 扣分明细弹窗 -->
<a-modal
v-model:open="deductionDetailVisible"
title="扣分明细"
width="800px"
:footer="null"
class="deduction-detail-modal"
>
<a-spin :spinning="deductionDetailLoading">
<div v-if="currentDesigner" class="deduction-detail-content">
<div class="designer-info">
<h4>{{ currentDesigner.realName || currentDesigner.userName }}</h4>
<p class="designer-dept">部门:{{ currentDesigner.deptName }}</p>
<p class="total-deduction">总扣分:<span class="deduction-number">{{ deductionDetail.totalDeduction || 0 }}</span> 分</p>
</div>
<!-- 延误扣分明细 -->
<div v-if="deductionDetail.delayDeductions && deductionDetail.delayDeductions.length > 0" class="deduction-section">
<h5>延误扣分明细</h5>
<a-table
:columns="delayDeductionColumns"
:data-source="deductionDetail.delayDeductions"
:pagination="false"
size="small"
row-key="taskId"
class="deduction-table"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'deductionScore'">
<span class="text-danger">{{ record.deductionScore }} 分</span>
</template>
<template v-else-if="column.key === 'overdueHours'">
<span>{{ record.overdueHours }} 小时</span>
</template>
<template v-else-if="column.key === 'deadline' || column.key === 'actualEndTime'">
{{ formatDateTime(record[column.key]) }}
</template>
</template>
</a-table>
</div>
<!-- 进度未更新扣分明细 -->
<div v-if="deductionDetail.progressDeductions && deductionDetail.progressDeductions.length > 0" class="deduction-section">
<h5>进度未更新扣分明细</h5>
<a-table
:columns="progressDeductionColumns"
:data-source="deductionDetail.progressDeductions"
:pagination="false"
size="small"
row-key="taskId"
class="deduction-table"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'deductionScore'">
<span class="text-danger">{{ record.deductionScore }} 分</span>
</template>
<template v-else-if="column.key === 'deductionDate'">
{{ formatDateOnly(record.deductionDate) }}
</template>
</template>
</a-table>
</div>
<!-- 无扣分记录 -->
<div v-if="(!deductionDetail.delayDeductions || deductionDetail.delayDeductions.length === 0) &&
(!deductionDetail.progressDeductions || deductionDetail.progressDeductions.length === 0)"
class="no-data">
<p>暂无扣分记录</p>
</div>
</div>
</a-spin>
</a-modal>
</DefaultLayout>
</template>
@ -429,10 +508,14 @@ import {
getDesignerPerformance,
getProcessEfficiency,
getWarningTasks,
getDesignerDeductionDetails,
type DemandStatistics,
type DesignerPerformance,
type ProcessEfficiency,
type WarningTask
type WarningTask,
type DeductionDetail,
type DelayDeductionItem,
type ProgressDeductionItem
} from '@/api/analytics'
import { message } from 'ant-design-vue'
import * as echarts from 'echarts'
@ -524,6 +607,16 @@ const warningFilter = ref('all')
const selectedDeptId = ref<number | undefined>(undefined)
const departmentList = ref<any[]>([])
// 扣分明细弹窗
const deductionDetailVisible = ref(false)
const deductionDetailLoading = ref(false)
const currentDesigner = ref<any>(null)
const deductionDetail = ref<DeductionDetail>({
totalDeduction: 0,
delayDeductions: [],
progressDeductions: []
})
// 图表引用
const trendChartRef = ref<HTMLElement>()
const typeChartRef = ref<HTMLElement>()
@ -652,6 +745,26 @@ const warningColumns = [
{ title: '操作', key: 'action', width: 140, align: 'center' }
]
// 扣分明细表格列定义
const delayDeductionColumns = [
{ title: '任务编号', dataIndex: 'taskNo', key: 'taskNo', width: 120 },
{ title: '任务名称', dataIndex: 'taskTitle', key: 'taskTitle', ellipsis: true },
{ title: '任务积分', dataIndex: 'taskScore', key: 'taskScore', width: 100, align: 'center' },
{ title: '截止时间', key: 'deadline', width: 150 },
{ title: '实际完成时间', key: 'actualEndTime', width: 150 },
{ title: '延误时长', key: 'overdueHours', width: 100, align: 'center' },
{ title: '扣分', key: 'deductionScore', width: 100, align: 'center' },
{ title: '扣分原因', dataIndex: 'deductionReason', key: 'deductionReason', width: 150 }
]
const progressDeductionColumns = [
{ title: '任务编号', dataIndex: 'taskNo', key: 'taskNo', width: 120 },
{ title: '任务名称', dataIndex: 'taskTitle', key: 'taskTitle', ellipsis: true },
{ title: '扣分日期', key: 'deductionDate', width: 120 },
{ title: '扣分', key: 'deductionScore', width: 100, align: 'center' },
{ title: '扣分原因', dataIndex: 'reason', key: 'reason' }
]
// 方法
const handleMonthChange = (value: string | null) => {
selectedMonth.value = value
@ -973,6 +1086,40 @@ const handleDeptChange = (deptId: number | undefined) => {
loadDesignerData()
}
// 显示扣分明细
const showDeductionDetails = async (record: any) => {
if (!record.deductionScore || record.deductionScore <= 0) {
return // 没有扣分,不显示弹窗
}
currentDesigner.value = record
deductionDetailVisible.value = true
deductionDetailLoading.value = true
try {
const month = selectedMonth.value || undefined
const detail = await getDesignerDeductionDetails(record.userId, month)
deductionDetail.value = detail
} catch (error) {
message.error('加载扣分明细失败')
console.error('加载扣分明细失败:', error)
} finally {
deductionDetailLoading.value = false
}
}
// 格式化时间
const formatDateTime = (time: string | undefined) => {
if (!time) return '--'
return dayjs(time).format('YYYY-MM-DD HH:mm')
}
// 格式化日期
const formatDateOnly = (date: string | undefined) => {
if (!date) return '--'
return dayjs(date).format('YYYY-MM-DD')
}
// 单独加载设计师绩效数据(用于部门筛选)
const loadDesignerData = async () => {
loading.value = true
@ -1500,6 +1647,17 @@ onMounted(() => {
font-weight: 500;
color: #ef4444;
font-size: 14px;
&.clickable {
text-decoration: underline;
text-decoration-style: dashed;
text-underline-offset: 2px;
&:hover {
color: #ff6b6b;
text-decoration-color: #ff6b6b;
}
}
}
// 净积分样式
@ -1676,4 +1834,98 @@ onMounted(() => {
}
}
}
// 扣分明细弹窗样式
.deduction-detail-modal {
:deep(.ant-modal-content) {
background: #1f1f1f;
border: 1px solid #303030;
}
:deep(.ant-modal-header) {
background: #1f1f1f;
border-bottom: 1px solid #303030;
.ant-modal-title {
color: #ffffff;
}
}
:deep(.ant-modal-body) {
background: #1f1f1f;
padding: 24px;
}
}
.deduction-detail-content {
.designer-info {
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid #303030;
h4 {
color: #ffffff;
font-size: 18px;
font-weight: 600;
margin: 0 0 8px 0;
}
.designer-dept {
color: rgba(255, 255, 255, 0.65);
font-size: 14px;
margin: 0 0 8px 0;
}
.total-deduction {
color: rgba(255, 255, 255, 0.85);
font-size: 16px;
margin: 0;
.deduction-number {
color: #ef4444;
font-weight: 600;
font-size: 18px;
}
}
}
.deduction-section {
margin-bottom: 24px;
h5 {
color: #ffffff;
font-size: 16px;
font-weight: 600;
margin: 0 0 12px 0;
}
}
.deduction-table {
:deep(.ant-table) {
background: #141414;
.ant-table-thead > tr > th {
background: rgba(0, 0, 0, 0.4);
border-bottom: 1px solid #303030;
color: rgba(255, 255, 255, 0.85);
}
.ant-table-tbody > tr > td {
background: #141414;
border-bottom: 1px solid #262626;
color: rgba(255, 255, 255, 0.85);
}
.ant-table-tbody > tr:hover > td {
background: #1a1a1a;
}
}
}
.no-data {
text-align: center;
padding: 40px 0;
color: rgba(255, 255, 255, 0.45);
}
}
</style>

140
扣分逻辑说明.md Normal file
View File

@ -0,0 +1,140 @@
# 设计师绩效排行榜扣分逻辑说明
## 一、当前实现逻辑(数据分析页面)
数据分析页面的设计师绩效排行榜使用的是 `AnalyticsService.getDesignerPerformance` 方法,扣分逻辑如下:
### 1. 延误扣分计算公式
```
扣分 = 任务积分 × 延误小时数 × 扣分比例(默认1%)
```
**限制条件:**
- 扣分不能超过任务积分
- 如果计算结果超过任务积分,则扣分 = 任务积分
**扣分比例:**
- 默认值:1%(即 `BigDecimal.ONE`)
- 可以从部门配置中获取(`DesignerTargetScore.deductionRate`)
- 三级部门继承二级部门的配置
### 2. 进度未更新扣分
除了延误扣分,还会加上进度未更新扣分:
- 从 `progress_deduction_log` 表中查询当月的进度未更新扣分总额
- 进度未更新扣分 + 延误扣分 = 总扣分
### 3. 净积分计算
```
净积分 = 获得积分 - 总扣分
如果净积分 < 0,则净积分 = 0
```
## 二、需求文档中的扣分规则(DelayPenaltyService)
需求文档中定义的延误扣分规则(在 `DelayPenaltyService` 中实现,但**当前数据分析页面未使用**):
| 延误程度 | 延误时长 | 扣分规则 | 示例 |
|---------|---------|---------|------|
| 轻微延误 | < 2小时 | 每小时扣2分 | 延误1.5小时 → 扣3分 |
| 一般延误 | 2-8小时 | 每小时扣3分 | 延误5小时 → 扣15分 |
| 严重延误 | 8-24小时 | 每小时扣5分 | 延误10小时 → 扣50分 |
| 重大延误 | > 24小时 | 基础50分+每天10分 | 延误2天 → 扣70分 |
## 三、"若兰"被扣10分的情况分析
从图片数据看:
- **获得积分**:10分
- **延误扣分**:10分
- **净积分**:0分
### 可能的原因:
1. **情况A:延误扣分达到上限**
- 如果任务积分是10分,延误时间较长
- 按照公式:`10分 × 延误小时数 × 1% = 10分`
- 或者计算结果超过10分,被限制为10分
2. **情况B:延误扣分 + 进度未更新扣分 = 10分**
- 延误扣分 + 进度未更新扣分 = 10分
- 例如:延误扣分5分 + 进度未更新扣分5分 = 10分
## 四、代码位置
### 当前使用的扣分逻辑:
- 文件:`backend/src/main/java/com/designhub/service/AnalyticsService.java`
- 方法:`getDesignerPerformance`(第535行)
- 扣分计算:第680-698行(延误扣分)+ 第701-718行(进度未更新扣分)
- 扣分比例获取:`getDeductionRate`(第1146行)
### 需求文档中的扣分逻辑(未在数据分析页面使用):
- 文件:`backend/src/main/java/com/designhub/service/DelayPenaltyService.java`
- 方法:`calculateDelayPenalty`(第36行)
- 规则:按延误等级和固定扣分规则
## 五、建议复核的问题
1. **数据分析页面应该使用哪套扣分规则?**
- 当前实现:按百分比扣分(任务积分 × 延误小时数 × 扣分比例)
- 需求文档:按延误等级固定扣分(轻微/一般/严重/重大)
2. **扣分比例默认值是否合理?**
- 当前默认:1%
- 如果任务积分100分,延误10小时,扣分 = 100 × 10 × 1% = 10分
- 如果任务积分10分,延误100小时,扣分 = min(10 × 100 × 1%, 10) = 10分
3. **进度未更新扣分是否应该计入?**
- 当前实现:计入总扣分
- 是否需要单独显示?
4. **是否需要使用 DelayPenaltyRecord 表的数据?**
- 当前实现:直接从任务表计算,不使用 DelayPenaltyRecord
- 需求文档中有 DelayPenaltyRecord 表的设计,但数据分析页面未使用
## 六、相关代码片段
### 当前扣分计算逻辑(AnalyticsService.java 第680-698行):
```java
// 计算延误扣分(如果超时完成)
if (task.getDeadline() != null && task.getActualEndTime() != null
&& task.getActualEndTime().isAfter(task.getDeadline())) {
// 计算延误小时数
long overdueHours = Duration.between(task.getDeadline(), task.getActualEndTime()).toHours();
if (overdueHours > 0 && taskScore.compareTo(BigDecimal.ZERO) > 0) {
// 扣分 = 任务积分 * 延误小时数 * 扣分比例
BigDecimal taskDeduction = taskScore
.multiply(BigDecimal.valueOf(overdueHours))
.multiply(deductionRate)
.divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
// 扣分不能超过任务积分
if (taskDeduction.compareTo(taskScore) > 0) {
taskDeduction = taskScore;
}
deductionScore = deductionScore.add(taskDeduction);
}
}
```
### 需求文档中的扣分规则(DelayPenaltyService.java 第56-72行):
```java
if (delayHours < 2) {
// 轻微延误:每小时扣2分
delayLevel = "light";
penaltyScore = (int) Math.ceil(delayHours * 2.0);
} else if (delayHours < 8) {
// 一般延误:每小时扣3分
delayLevel = "general";
penaltyScore = (int) Math.ceil(delayHours * 3.0);
} else if (delayHours < 24) {
// 严重延误:每小时扣5分
delayLevel = "severe";
penaltyScore = (int) Math.ceil(delayHours * 5.0);
} else {
// 重大延误:基础50分+每天10分
delayLevel = "major";
penaltyScore = 50 + (int) delayDays * 10;
}
```