feat: 添加设计师扣分明细查看功能,点击扣分数字可查看详细扣分记录
This commit is contained in:
parent
d9d8c24e2c
commit
607270c158
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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: []
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@ -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
140
扣分逻辑说明.md
Normal 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;
|
||||
}
|
||||
```
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user