feat: 所有页面积分显示支持小数点后两位

- Analytics.vue: 设计师绩效、用户需求、扣分明细、CSV导出
- DesignerHall.vue: 设计师卡片累计积分
- TaskHall.vue: 任务卡片积分、typeDetails积分
- LeaderWorkbench.vue: 任务积分
- MyTasks.vue: 任务积分
- CreateTask.vue: 预估积分、任务类型积分
- RewardRecords.vue: 累计积分、奖励积分
- Goals.vue: 奖励积分
- IncentiveCenter.vue: 用户积分、奖励积分
- Contests.vue: 竞赛得分、奖励积分
- CheckIn.vue: 历史奖励积分
- ScorePoolManagement.vue: 积分变动
- Achievements.vue: 奖励积分
This commit is contained in:
Your Name 2026-01-15 15:39:32 +08:00
parent b47e322870
commit 3922d3653b
23 changed files with 349 additions and 140 deletions

BIN
backend-src.tar.gz Normal file

Binary file not shown.

View File

@ -16,6 +16,10 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
* 任务管理控制器
*/
@ -59,14 +63,25 @@ public class TaskController {
@Parameter(description = "任务状态") @RequestParam(required = false) String status,
@Parameter(description = "任务类型ID") @RequestParam(required = false) Long taskTypeId,
@Parameter(description = "发布者部门ID") @RequestParam(required = false) Long publisherDeptId,
@Parameter(description = "发布者部门ID列表(逗号分隔)") @RequestParam(required = false) String publisherDeptIds,
@Parameter(description = "接收部门ID") @RequestParam(required = false) Long receiverDeptId,
@Parameter(description = "页码") @RequestParam(defaultValue = "1") Integer page,
@Parameter(description = "每页数量") @RequestParam(defaultValue = "20") Integer pageSize) {
log.info("查询任务列表: status={}, taskTypeId={}, publisherDeptId={}, receiverDeptId={}, page={}, pageSize={}",
status, taskTypeId, publisherDeptId, receiverDeptId, page, pageSize);
log.info("查询任务列表: status={}, taskTypeId={}, publisherDeptId={}, publisherDeptIds={}, receiverDeptId={}, page={}, pageSize={}",
status, taskTypeId, publisherDeptId, publisherDeptIds, receiverDeptId, page, pageSize);
IPage<Task> taskPage = taskService.getTaskList(page, pageSize, status, taskTypeId, publisherDeptId, receiverDeptId);
// 处理多个发布者部门ID
List<Long> publisherDeptIdList = null;
if (publisherDeptIds != null && !publisherDeptIds.isEmpty()) {
publisherDeptIdList = Arrays.stream(publisherDeptIds.split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.map(Long::parseLong)
.collect(Collectors.toList());
}
IPage<Task> taskPage = taskService.getTaskList(page, pageSize, status, taskTypeId, publisherDeptId, publisherDeptIdList, receiverDeptId);
return Result.success(taskPage);
}

View File

@ -56,9 +56,9 @@ public class Task implements Serializable {
private Integer quantity;
/**
* 预估积分
* 预估积分(支持小数,保留2位)
*/
private Integer score;
private BigDecimal score;
/**
* 实际积分奖励
@ -408,8 +408,8 @@ public class Task implements Serializable {
public Integer getQuantity() { return quantity; }
public void setQuantity(Integer quantity) { this.quantity = quantity; }
public Integer getScore() { return score; }
public void setScore(Integer score) { this.score = score; }
public BigDecimal getScore() { return score; }
public void setScore(BigDecimal score) { this.score = score; }
public BigDecimal getScoreReward() { return scoreReward; }
public void setScoreReward(BigDecimal scoreReward) { this.scoreReward = scoreReward; }

View File

@ -52,14 +52,14 @@ public class TaskItem implements Serializable {
private Integer quantity;
/**
* 单价积分
* 单价积分(支持小数,保留2位)
*/
private Integer unitScore;
private BigDecimal unitScore;
/**
* 总积分
* 总积分(支持小数,保留2位)
*/
private Integer totalScore;
private BigDecimal totalScore;
/**
* 预估工时

View File

@ -326,7 +326,7 @@ public class AnalyticsService {
if (t.getScoreReward() != null && t.getScoreReward().compareTo(BigDecimal.ZERO) > 0) {
return t.getScoreReward();
}
return t.getScore() != null ? BigDecimal.valueOf(t.getScore()) : BigDecimal.ZERO;
return t.getScore() != null ? t.getScore() : BigDecimal.ZERO;
})
.reduce(BigDecimal.ZERO, BigDecimal::add);
@ -679,7 +679,7 @@ public class AnalyticsService {
if (task.getScoreReward() != null && task.getScoreReward().compareTo(BigDecimal.ZERO) > 0) {
taskScore = task.getScoreReward();
} else if (task.getScore() != null) {
taskScore = BigDecimal.valueOf(task.getScore());
taskScore = task.getScore();
}
// 计算已完成任务的积分和延误扣分
@ -760,7 +760,7 @@ public class AnalyticsService {
// 如果完成时间超过预期deadline,计算延误扣分
if (expectedDeadline != null && item.getCompleteTime().isAfter(expectedDeadline)) {
BigDecimal itemScore = item.getTotalScore() != null ? BigDecimal.valueOf(item.getTotalScore()) : BigDecimal.ZERO;
BigDecimal itemScore = item.getTotalScore() != null ? item.getTotalScore() : BigDecimal.ZERO;
if (itemScore.compareTo(BigDecimal.ZERO) > 0) {
// 计算延误小时数(只计算工作时间段内的延误)
@ -795,7 +795,7 @@ public class AnalyticsService {
// 如果当前时间超过预期deadline,计算延误扣分
if (expectedDeadline != null && now.isAfter(expectedDeadline)) {
BigDecimal itemScore = item.getTotalScore() != null ? BigDecimal.valueOf(item.getTotalScore()) : BigDecimal.ZERO;
BigDecimal itemScore = item.getTotalScore() != null ? item.getTotalScore() : BigDecimal.ZERO;
if (itemScore.compareTo(BigDecimal.ZERO) > 0) {
// 计算延误小时数(只计算工作时间段内的延误)
@ -1492,7 +1492,7 @@ public class AnalyticsService {
if (task.getScoreReward() != null && task.getScoreReward().compareTo(BigDecimal.ZERO) > 0) {
taskScore = task.getScoreReward();
} else if (task.getScore() != null) {
taskScore = BigDecimal.valueOf(task.getScore());
taskScore = task.getScore();
}
if (taskScore.compareTo(BigDecimal.ZERO) > 0) {
@ -1544,7 +1544,7 @@ public class AnalyticsService {
if (task.getScoreReward() != null && task.getScoreReward().compareTo(BigDecimal.ZERO) > 0) {
taskScore = task.getScoreReward();
} else if (task.getScore() != null) {
taskScore = BigDecimal.valueOf(task.getScore());
taskScore = task.getScore();
}
log.info("进行中任务积分计算: taskNo={}, taskScore={}", task.getTaskNo(), taskScore);
@ -1622,7 +1622,7 @@ public class AnalyticsService {
// 如果完成时间超过预期deadline,计算延误扣分
if (expectedDeadline != null && item.getCompleteTime().isAfter(expectedDeadline)) {
BigDecimal itemScore = item.getTotalScore() != null ? BigDecimal.valueOf(item.getTotalScore()) : BigDecimal.ZERO;
BigDecimal itemScore = item.getTotalScore() != null ? item.getTotalScore() : BigDecimal.ZERO;
if (itemScore.compareTo(BigDecimal.ZERO) > 0) {
// 计算延误小时数(只计算工作时间段内的延误)
@ -1678,7 +1678,7 @@ public class AnalyticsService {
// 如果当前时间超过预期deadline,计算延误扣分
if (expectedDeadline != null && now.isAfter(expectedDeadline)) {
BigDecimal itemScore = item.getTotalScore() != null ? BigDecimal.valueOf(item.getTotalScore()) : BigDecimal.ZERO;
BigDecimal itemScore = item.getTotalScore() != null ? item.getTotalScore() : BigDecimal.ZERO;
if (itemScore.compareTo(BigDecimal.ZERO) > 0) {
// 计算延误小时数(只计算工作时间段内的延误)

View File

@ -285,16 +285,18 @@ public class DesignerService {
// 累计获得积分(主任务 + 子任务)
// 优先使用 scoreReward(实际获得积分),如果没有则使用 score(任务积分)
Integer mainTaskScore = completedTasks.stream()
.mapToInt(task -> {
.map(task -> {
if (task.getScoreReward() != null && task.getScoreReward().compareTo(java.math.BigDecimal.ZERO) > 0) {
return task.getScoreReward().intValue();
return task.getScoreReward();
}
return task.getScore() != null ? task.getScore() : 0;
return task.getScore() != null ? task.getScore() : java.math.BigDecimal.ZERO;
})
.sum();
.reduce(java.math.BigDecimal.ZERO, java.math.BigDecimal::add)
.intValue();
Integer subTaskScore = completedSubTasks.stream()
.mapToInt(item -> item.getTotalScore() != null ? item.getTotalScore() : 0)
.sum();
.map(item -> item.getTotalScore() != null ? item.getTotalScore() : java.math.BigDecimal.ZERO)
.reduce(java.math.BigDecimal.ZERO, java.math.BigDecimal::add)
.intValue();
Integer totalScore = mainTaskScore + subTaskScore;
stats.setTotalScore(totalScore);
log.debug("设计师积分统计: userId={}, mainTaskScore={}, subTaskScore={}, totalScore={}",
@ -432,8 +434,9 @@ public class DesignerService {
);
Integer totalScore = completedTasks.stream()
.mapToInt(task -> task.getScore() != null ? task.getScore() : 0)
.sum();
.map(task -> task.getScore() != null ? task.getScore() : java.math.BigDecimal.ZERO)
.reduce(java.math.BigDecimal.ZERO, java.math.BigDecimal::add)
.intValue();
stats.setTotalScore(totalScore);
// 当月延期任务数

View File

@ -248,7 +248,7 @@ public class TaskApprovalService {
// 扣除部门积分
try {
teamScorePoolService.deductScore(task.getPublisherDeptId(), task.getScore());
teamScorePoolService.deductScore(task.getPublisherDeptId(), task.getScore() != null ? task.getScore().intValue() : 0);
} catch (Exception e) {
log.error("扣除部门积分失败", e);
throw new BusinessException(ResultCode.ERROR, "扣除部门积分失败:" + e.getMessage());
@ -704,7 +704,7 @@ public class TaskApprovalService {
}
// 重新计算积分
int totalScore = taskService.calculateTotalScore(updates.getTaskTypes());
java.math.BigDecimal totalScore = taskService.calculateTotalScoreDecimal(updates.getTaskTypes());
task.setScore(totalScore);
}
}

View File

@ -848,7 +848,9 @@ public class TaskItemService {
// 发送钉钉群通知并@指定的设计师
try {
int totalScore = groupItems.stream().mapToInt(item -> item.getTotalScore() != null ? item.getTotalScore() : 0).sum();
java.math.BigDecimal totalScore = groupItems.stream()
.map(item -> item.getTotalScore() != null ? item.getTotalScore() : java.math.BigDecimal.ZERO)
.reduce(java.math.BigDecimal.ZERO, java.math.BigDecimal::add);
String title = "子任务组已指派";
StringBuilder content = new StringBuilder();
content.append("-------------------------------- \n");
@ -1256,8 +1258,8 @@ public class TaskItemService {
}
}
// 计算子任务组总积分
groupInfo.put("totalScore", item.getTotalScore() != null ? item.getTotalScore() : 0);
// 计算子任务组总积分(BigDecimal类型)
groupInfo.put("totalScore", item.getTotalScore() != null ? item.getTotalScore() : java.math.BigDecimal.ZERO);
// 获取前置子任务的素材下载链接(流水线继承)
if (item.getGroupNo() != null && item.getGroupNo() > 1) {
@ -1294,10 +1296,10 @@ public class TaskItemService {
} else {
Map<String, Object> groupInfo = groupMap.get(key);
groupInfo.put("itemCount", (Integer) groupInfo.get("itemCount") + 1);
// 累加积分
Integer currentScore = (Integer) groupInfo.get("totalScore");
Integer itemScore = item.getTotalScore() != null ? item.getTotalScore() : 0;
groupInfo.put("totalScore", currentScore + itemScore);
// 累加积分(BigDecimal类型)
java.math.BigDecimal currentScore = (java.math.BigDecimal) groupInfo.get("totalScore");
java.math.BigDecimal itemScore = item.getTotalScore() != null ? item.getTotalScore() : java.math.BigDecimal.ZERO;
groupInfo.put("totalScore", currentScore.add(itemScore));
// 添加到子任务列表
@SuppressWarnings("unchecked")

View File

@ -115,19 +115,19 @@ public class TaskService {
}
}
// 计算总积分和工时:取第一个类型的单价
// 计算总积分和工时:取第一个类型的单价(保留2位小数)
Long firstTypeId = Long.parseLong(typeIds[0].trim());
var firstType = configService.getDemandTypeById(firstTypeId);
int totalScore = firstType.getUnitScore().multiply(BigDecimal.valueOf(task.getQuantity())).intValue();
BigDecimal totalScoreDecimal = firstType.getUnitScore().multiply(BigDecimal.valueOf(task.getQuantity())).setScale(2, java.math.RoundingMode.HALF_UP);
BigDecimal totalHours = firstType.getEstimatedHours() != null ? firstType.getEstimatedHours().multiply(BigDecimal.valueOf(task.getQuantity())) : BigDecimal.ZERO;
task.setScore(totalScore);
task.setScore(totalScoreDecimal);
task.setEstimatedHours(totalHours);
// 兼容性:同时保存第一个类型到task_type_id
task.setTaskTypeId(firstTypeId);
log.info("创建多类型任务: typeIds={}, score={}", task.getTaskTypeIds(), totalScore);
log.info("创建多类型任务: typeIds={}, score={}", task.getTaskTypeIds(), totalScoreDecimal);
}
// 兼容单类型模式
else if (task.getTaskTypeId() != null) {
@ -136,17 +136,17 @@ public class TaskService {
throw new BusinessException(ResultCode.PARAM_ERROR, "任务类型不存在");
}
// 计算总积分和工时
int totalScore = demandType.getUnitScore().multiply(BigDecimal.valueOf(task.getQuantity())).intValue();
// 计算总积分和工时(保留2位小数)
BigDecimal totalScoreSingle = demandType.getUnitScore().multiply(BigDecimal.valueOf(task.getQuantity())).setScale(2, java.math.RoundingMode.HALF_UP);
BigDecimal totalHours = demandType.getEstimatedHours() != null ? demandType.getEstimatedHours().multiply(BigDecimal.valueOf(task.getQuantity())) : BigDecimal.ZERO;
task.setScore(totalScore);
task.setScore(totalScoreSingle);
task.setEstimatedHours(totalHours);
// 自动填充taskTypeIds字段
task.setTaskTypeIds(String.valueOf(task.getTaskTypeId()));
log.info("创建单类型任务: typeId={}, score={}", task.getTaskTypeId(), totalScore);
log.info("创建单类型任务: typeId={}, score={}", task.getTaskTypeId(), totalScoreSingle);
} else {
throw new BusinessException(ResultCode.PARAM_ERROR, "请选择任务类型");
}
@ -155,12 +155,13 @@ public class TaskService {
// 检查部门积分是否充足(管理员跳过积分检查)
boolean isAdminPublisher = task.getPublisherId() != null && isAdminUser(task.getPublisherId());
if (!isAdminPublisher && task.getPublisherDeptId() != null && task.getScore() != null) {
if (!teamScorePoolService.checkBalance(task.getPublisherDeptId(), task.getScore())) {
int scoreInt = task.getScore().intValue();
if (!teamScorePoolService.checkBalance(task.getPublisherDeptId(), scoreInt)) {
throw new BusinessException(ResultCode.INSUFFICIENT_SCORE, "部门积分余额不足");
}
// 检查周期消耗限制
if (!teamScorePoolService.checkPeriodLimit(task.getPublisherDeptId(), task.getScore())) {
if (!teamScorePoolService.checkPeriodLimit(task.getPublisherDeptId(), scoreInt)) {
throw new BusinessException(ResultCode.SCORE_LIMIT_EXCEEDED, "超过本周期积分消耗限制");
}
}
@ -250,8 +251,8 @@ public class TaskService {
throw new BusinessException(ResultCode.PARAM_ERROR, "流水线任务至少需要一个子任务");
}
// 计算总积分和工时
int totalScore = 0;
// 计算总积分和工时(使用BigDecimal保留小数精度)
BigDecimal totalScoreDecimal = BigDecimal.ZERO;
double totalHours = 0;
for (com.designhub.dto.PipelineTaskDTO.TaskItemDTO itemDto : dto.getItems()) {
var demandType = configService.getDemandTypeById(itemDto.getTaskTypeId());
@ -259,20 +260,24 @@ public class TaskService {
throw new BusinessException(ResultCode.PARAM_ERROR, "任务类型ID无效: " + itemDto.getTaskTypeId());
}
int itemScore = demandType.getUnitScore().multiply(BigDecimal.valueOf(itemDto.getQuantity())).intValue();
BigDecimal itemScore = demandType.getUnitScore().multiply(BigDecimal.valueOf(itemDto.getQuantity()));
double itemHours = demandType.getEstimatedHours() != null ? demandType.getEstimatedHours().multiply(BigDecimal.valueOf(itemDto.getQuantity())).doubleValue() : 0;
totalScore += itemScore;
totalScoreDecimal = totalScoreDecimal.add(itemScore);
totalHours += itemHours;
}
// 保留2位小数
totalScoreDecimal = totalScoreDecimal.setScale(2, java.math.RoundingMode.HALF_UP);
int totalScoreInt = totalScoreDecimal.intValue(); // 用于积分检查
// 检查部门积分是否充足(管理员跳过积分检查)
if (!isAdmin && user.getDeptId() != null) {
if (!teamScorePoolService.checkBalance(user.getDeptId(), totalScore)) {
if (!teamScorePoolService.checkBalance(user.getDeptId(), totalScoreInt)) {
throw new BusinessException(ResultCode.INSUFFICIENT_SCORE, "部门积分余额不足");
}
if (!teamScorePoolService.checkPeriodLimit(user.getDeptId(), totalScore)) {
if (!teamScorePoolService.checkPeriodLimit(user.getDeptId(), totalScoreInt)) {
throw new BusinessException(ResultCode.SCORE_LIMIT_EXCEEDED, "超过本周期积分消耗限制");
}
}
@ -290,7 +295,7 @@ public class TaskService {
task.setPublisherDeptId(user.getDeptId());
task.setIsPipeline(1);
task.setPipelineMode(dto.getPipelineMode());
task.setScore(totalScore);
task.setScore(totalScoreDecimal);
task.setEstimatedHours(BigDecimal.valueOf(totalHours));
task.setBrandId(dto.getBrandId());
task.setCategoryId(dto.getCategoryId());
@ -495,8 +500,8 @@ public class TaskService {
int currentDeptIndex = deptIndexMap.getOrDefault(deptKey, 1);
int subIndex = groupSubIndexMap.getOrDefault(groupNo, 1);
// 汇总该分组的积分、工时、数量
int groupTotalScore = 0;
// 汇总该分组的积分、工时、数量(使用 BigDecimal 保留小数精度)
BigDecimal groupTotalScoreDecimal = BigDecimal.ZERO;
BigDecimal groupTotalHours = BigDecimal.ZERO;
int groupTotalQuantity = 0;
Long firstTaskTypeId = null;
@ -505,12 +510,13 @@ public class TaskService {
for (com.designhub.dto.PipelineTaskDTO.TaskItemDTO itemDto : groupItems) {
var demandType = configService.getDemandTypeById(itemDto.getTaskTypeId());
int itemScore = demandType.getUnitScore().multiply(BigDecimal.valueOf(itemDto.getQuantity())).intValue();
// 使用 BigDecimal 保留小数精度
BigDecimal itemScoreDecimal = demandType.getUnitScore().multiply(BigDecimal.valueOf(itemDto.getQuantity()));
BigDecimal itemHours = demandType.getEstimatedHours() != null
? demandType.getEstimatedHours().multiply(BigDecimal.valueOf(itemDto.getQuantity()))
: BigDecimal.ZERO;
groupTotalScore += itemScore;
groupTotalScoreDecimal = groupTotalScoreDecimal.add(itemScoreDecimal);
groupTotalHours = groupTotalHours.add(itemHours);
groupTotalQuantity += itemDto.getQuantity();
@ -524,7 +530,8 @@ public class TaskService {
detail.put("typeName", demandType.getName());
detail.put("quantity", itemDto.getQuantity());
detail.put("unitScore", demandType.getUnitScore());
detail.put("totalScore", itemScore);
// 保存精确的小数积分值(保留2位小数)
detail.put("totalScore", itemScoreDecimal.setScale(2, java.math.RoundingMode.HALF_UP).doubleValue());
// 如果指定了设计师,也保存到明细中
if (itemDto.getReceiverId() != null) {
detail.put("receiverId", itemDto.getReceiverId());
@ -536,6 +543,9 @@ public class TaskService {
typeDetailsList.add(detail);
}
// 保留2位小数
BigDecimal groupTotalScore = groupTotalScoreDecimal.setScale(2, java.math.RoundingMode.HALF_UP);
// 创建子任务
com.designhub.entity.TaskItem item = new com.designhub.entity.TaskItem();
item.setTaskId(task.getId());
@ -551,7 +561,10 @@ public class TaskService {
log.warn("序列化任务类型明细失败: {}", e.getMessage());
}
item.setQuantity(groupTotalQuantity);
item.setUnitScore(groupTotalScore / Math.max(groupTotalQuantity, 1)); // 平均单价
// 计算平均单价(保留2位小数)
item.setUnitScore(groupTotalQuantity > 0
? groupTotalScore.divide(BigDecimal.valueOf(groupTotalQuantity), 2, java.math.RoundingMode.HALF_UP)
: BigDecimal.ZERO);
item.setTotalScore(groupTotalScore);
item.setEstimatedHours(groupTotalHours);
item.setSortOrder(itemIndex);
@ -668,7 +681,7 @@ public class TaskService {
/**
* 获取任务列表
*/
public IPage<Task> getTaskList(Integer page, Integer pageSize, String status, Long taskTypeId, Long publisherDeptId, Long receiverDeptId) {
public IPage<Task> getTaskList(Integer page, Integer pageSize, String status, Long taskTypeId, Long publisherDeptId, List<Long> publisherDeptIds, Long receiverDeptId) {
Page<Task> pageParam = new Page<>(page, pageSize);
LambdaQueryWrapper<Task> wrapper = new LambdaQueryWrapper<>();
@ -683,7 +696,11 @@ public class TaskService {
}
// 根据发布者部门筛选(部门负责人视角切换时使用)
if (publisherDeptId != null) {
// 优先使用 publisherDeptIds(多个部门ID列表)
if (publisherDeptIds != null && !publisherDeptIds.isEmpty()) {
wrapper.in(Task::getPublisherDeptId, publisherDeptIds);
log.info("按多个发布者部门筛选: publisherDeptIds={}", publisherDeptIds);
} else if (publisherDeptId != null) {
wrapper.eq(Task::getPublisherDeptId, publisherDeptId);
}
@ -694,6 +711,9 @@ public class TaskService {
wrapper.in(Task::getReceiverDeptId, deptAndChildIds);
}
// 未删除的任务
wrapper.eq(Task::getDeleted, 0);
// 按更新时间倒序
wrapper.orderByDesc(Task::getUpdateTime);
@ -1119,6 +1139,31 @@ public class TaskService {
// 设置子任务列表(包含处理人信息和deadline)
task.setSubTasks(subTasks);
// 聚合所有子任务的 typeDetails
java.util.List<java.util.Map<String, Object>> allDetails = new java.util.ArrayList<>();
for (TaskItem item : subTasks) {
if (item.getTypeDetails() != null && !item.getTypeDetails().isEmpty()) {
try {
com.fasterxml.jackson.databind.ObjectMapper objectMapper = new com.fasterxml.jackson.databind.ObjectMapper();
java.util.List<java.util.Map<String, Object>> itemDetails = objectMapper.readValue(
item.getTypeDetails(),
new com.fasterxml.jackson.core.type.TypeReference<java.util.List<java.util.Map<String, Object>>>() {}
);
allDetails.addAll(itemDetails);
} catch (Exception e) {
log.warn("解析子任务 {} 的 typeDetails 失败: {}", item.getId(), e.getMessage());
}
}
}
if (!allDetails.isEmpty()) {
try {
com.fasterxml.jackson.databind.ObjectMapper objectMapper = new com.fasterxml.jackson.databind.ObjectMapper();
task.setTypeDetails(objectMapper.writeValueAsString(allDetails));
} catch (Exception e) {
log.warn("序列化任务 {} 的 typeDetails 失败: {}", task.getId(), e.getMessage());
}
}
// 如果主任务没有receiverName,从子任务中获取接单人
if (task.getReceiverName() == null || task.getReceiverName().isEmpty()) {
// 收集所有不同的接单人名称
@ -1292,8 +1337,8 @@ public class TaskService {
throw new BusinessException(ResultCode.PARAM_ERROR, "流水线任务至少需要一个子任务");
}
// 计算总积分和工时
int totalScore = 0;
// 计算总积分和工时(使用BigDecimal保留小数精度)
BigDecimal totalScoreDecimal = BigDecimal.ZERO;
double totalHours = 0;
for (com.designhub.dto.PipelineTaskDTO.TaskItemDTO itemDto : dto.getItems()) {
var demandType = configService.getDemandTypeById(itemDto.getTaskTypeId());
@ -1301,22 +1346,26 @@ public class TaskService {
throw new BusinessException(ResultCode.PARAM_ERROR, "任务类型ID无效: " + itemDto.getTaskTypeId());
}
int itemScore = demandType.getUnitScore().multiply(BigDecimal.valueOf(itemDto.getQuantity())).intValue();
BigDecimal itemScore = demandType.getUnitScore().multiply(BigDecimal.valueOf(itemDto.getQuantity()));
double itemHours = demandType.getEstimatedHours() != null ?
demandType.getEstimatedHours().multiply(BigDecimal.valueOf(itemDto.getQuantity())).doubleValue() : 0;
totalScore += itemScore;
totalScoreDecimal = totalScoreDecimal.add(itemScore);
totalHours += itemHours;
}
// 保留2位小数
totalScoreDecimal = totalScoreDecimal.setScale(2, java.math.RoundingMode.HALF_UP);
int totalScoreInt = totalScoreDecimal.intValue(); // 用于积分检查
// 检查部门积分是否充足(管理员跳过积分检查)
User user = userService.getUserById(userId);
if (!isAdmin && user.getDeptId() != null) {
if (!teamScorePoolService.checkBalance(user.getDeptId(), totalScore)) {
if (!teamScorePoolService.checkBalance(user.getDeptId(), totalScoreInt)) {
throw new BusinessException(ResultCode.INSUFFICIENT_SCORE, "部门积分余额不足");
}
if (!teamScorePoolService.checkPeriodLimit(user.getDeptId(), totalScore)) {
if (!teamScorePoolService.checkPeriodLimit(user.getDeptId(), totalScoreInt)) {
throw new BusinessException(ResultCode.SCORE_LIMIT_EXCEEDED, "超过本周期积分消耗限制");
}
}
@ -1324,7 +1373,7 @@ public class TaskService {
// 更新主任务信息
existTask.setTitle(dto.getTitle());
existTask.setPipelineMode(dto.getPipelineMode());
existTask.setScore(totalScore);
existTask.setScore(totalScoreDecimal);
existTask.setEstimatedHours(BigDecimal.valueOf(totalHours));
existTask.setBrandId(dto.getBrandId());
existTask.setCategoryId(dto.getCategoryId());
@ -1518,8 +1567,8 @@ public class TaskService {
int currentDeptIndex = deptIndexMap.getOrDefault(deptKey, 1);
int subIndex = groupSubIndexMap.getOrDefault(groupNo, 1);
// 汇总该分组的积分、工时、数量
int groupTotalScore = 0;
// 汇总该分组的积分、工时、数量(使用 BigDecimal 保留小数精度)
BigDecimal groupTotalScoreDecimal = BigDecimal.ZERO;
BigDecimal groupTotalHours = BigDecimal.ZERO;
int groupTotalQuantity = 0;
Long firstTaskTypeId = null;
@ -1527,12 +1576,13 @@ public class TaskService {
for (com.designhub.dto.PipelineTaskDTO.TaskItemDTO itemDto : groupItems) {
var demandType = configService.getDemandTypeById(itemDto.getTaskTypeId());
int itemScore = demandType.getUnitScore().multiply(BigDecimal.valueOf(itemDto.getQuantity())).intValue();
// 使用 BigDecimal 保留小数精度
BigDecimal itemScoreDecimal = demandType.getUnitScore().multiply(BigDecimal.valueOf(itemDto.getQuantity()));
BigDecimal itemHours = demandType.getEstimatedHours() != null
? demandType.getEstimatedHours().multiply(BigDecimal.valueOf(itemDto.getQuantity()))
: BigDecimal.ZERO;
groupTotalScore += itemScore;
groupTotalScoreDecimal = groupTotalScoreDecimal.add(itemScoreDecimal);
groupTotalHours = groupTotalHours.add(itemHours);
groupTotalQuantity += itemDto.getQuantity();
@ -1546,7 +1596,8 @@ public class TaskService {
detail.put("typeName", demandType.getName());
detail.put("quantity", itemDto.getQuantity());
detail.put("unitScore", demandType.getUnitScore());
detail.put("totalScore", itemScore);
// 保存精确的小数积分值(保留2位小数)
detail.put("totalScore", itemScoreDecimal.setScale(2, java.math.RoundingMode.HALF_UP).doubleValue());
// 如果指定了设计师,也保存到明细中
if (itemDto.getReceiverId() != null) {
detail.put("receiverId", itemDto.getReceiverId());
@ -1558,6 +1609,9 @@ public class TaskService {
typeDetailsList.add(detail);
}
// 保留2位小数
BigDecimal groupTotalScore = groupTotalScoreDecimal.setScale(2, java.math.RoundingMode.HALF_UP);
// 创建子任务
com.designhub.entity.TaskItem item = new com.designhub.entity.TaskItem();
item.setTaskId(mainTask.getId());
@ -1572,7 +1626,10 @@ public class TaskService {
log.warn("序列化任务类型明细失败: {}", e.getMessage());
}
item.setQuantity(groupTotalQuantity);
item.setUnitScore(groupTotalScore / Math.max(groupTotalQuantity, 1));
// 计算平均单价(保留2位小数)
item.setUnitScore(groupTotalQuantity > 0
? groupTotalScore.divide(BigDecimal.valueOf(groupTotalQuantity), 2, java.math.RoundingMode.HALF_UP)
: BigDecimal.ZERO);
item.setTotalScore(groupTotalScore);
item.setEstimatedHours(groupTotalHours);
item.setSortOrder(itemIndex);
@ -1825,7 +1882,7 @@ public class TaskService {
// 奖励设计师所在部门积分(任务奖励)
if (task.getReceiverDeptId() != null && task.getScore() != null) {
teamScorePoolService.addScore(task.getReceiverDeptId(), task.getScore());
teamScorePoolService.addScore(task.getReceiverDeptId(), task.getScore().intValue());
// 如果有延误扣分,从积分池中扣除
if (delayRecord != null && delayRecord.getPenaltyScore() > 0
@ -1959,7 +2016,7 @@ public class TaskService {
// 返还积分(任务取消时返还给发布部门)
if (task.getPublisherDeptId() != null && task.getScore() != null) {
teamScorePoolService.addScore(task.getPublisherDeptId(), task.getScore());
teamScorePoolService.addScore(task.getPublisherDeptId(), task.getScore().intValue());
}
// 发送通知给接单设计师
@ -2127,13 +2184,14 @@ public class TaskService {
// 审核通过后扣除部门积分
if (task.getPublisherDeptId() != null && task.getScore() != null) {
int scoreInt = task.getScore().intValue();
// 检查积分是否充足
if (!teamScorePoolService.checkBalance(task.getPublisherDeptId(), task.getScore())) {
if (!teamScorePoolService.checkBalance(task.getPublisherDeptId(), scoreInt)) {
throw new BusinessException("部门积分余额不足,无法通过审核");
}
// 扣除积分
teamScorePoolService.consumeScore(task.getPublisherDeptId(), task.getScore());
teamScorePoolService.consumeScore(task.getPublisherDeptId(), scoreInt);
log.info("审核通过,扣除部门积分: deptId={}, score={}", task.getPublisherDeptId(), task.getScore());
}
} else if ("reject".equals(request.getResult())) {
@ -2492,8 +2550,9 @@ public class TaskService {
List<Task> publishedTasks = taskMapper.selectList(scoreQuery);
totalScore = publishedTasks.stream()
.filter(t -> t.getScore() != null)
.mapToInt(Task::getScore)
.sum();
.map(Task::getScore)
.reduce(BigDecimal.ZERO, BigDecimal::add)
.intValue();
log.debug("需求部门积分统计: deptIds={}, totalScore={}", deptAndChildIds, totalScore);
} else if (isDeptView && "receiving".equals(deptType)) {
// 接单部门视角:统计该部门设计师获得的总积分
@ -2813,7 +2872,7 @@ public class TaskService {
// 计算实际积分奖励(原积分 - 延误扣分)
BigDecimal earnedScore = BigDecimal.ZERO;
if (task.getScore() != null) {
earnedScore = BigDecimal.valueOf(task.getScore());
earnedScore = task.getScore();
BigDecimal actualReward = earnedScore.subtract(deductedScore);
if (actualReward.compareTo(BigDecimal.ZERO) < 0) {
actualReward = BigDecimal.ZERO; // 最少为0,不会变成负数
@ -3068,17 +3127,24 @@ public class TaskService {
}
/**
* 计算任务总积分(用于审核时修改)
* 计算任务总积分(用于审核时修改)- 返回整数
*/
public int calculateTotalScore(List<TaskUpdateDTO.TaskTypeUpdateDTO> taskTypes) {
int totalScore = 0;
return calculateTotalScoreDecimal(taskTypes).intValue();
}
/**
* 计算任务总积分(用于审核时修改)- 返回BigDecimal保留2位小数
*/
public BigDecimal calculateTotalScoreDecimal(List<TaskUpdateDTO.TaskTypeUpdateDTO> taskTypes) {
BigDecimal totalScore = BigDecimal.ZERO;
for (TaskUpdateDTO.TaskTypeUpdateDTO taskType : taskTypes) {
DemandType demandType = configService.getDemandTypeById(taskType.getTaskTypeId());
if (demandType != null && demandType.getUnitScore() != null) {
totalScore += demandType.getUnitScore().multiply(BigDecimal.valueOf(taskType.getQuantity())).intValue();
totalScore = totalScore.add(demandType.getUnitScore().multiply(BigDecimal.valueOf(taskType.getQuantity())));
}
}
return totalScore;
return totalScore.setScale(2, java.math.RoundingMode.HALF_UP);
}
/**

View File

@ -89,8 +89,8 @@ public class WarningTaskVO {
private Double hoursInfo;
/**
* 积分
* 积分(支持小数)
*/
private Integer score;
private java.math.BigDecimal score;
}

View File

@ -79,7 +79,7 @@
<!-- 奖励信息 -->
<div class="achievement-reward">
<Icon name="trophy" size="sm" />
奖励 {{ achievement.rewardScore }} 积分
奖励 {{ formatScore(achievement.rewardScore) }} 积分
</div>
<!-- 已解锁信息 -->
@ -162,6 +162,12 @@ const filteredAchievements = computed(() => {
return result
})
// 格式化积分(保留两位小数)
const formatScore = (score: number | null | undefined): string => {
if (score === null || score === undefined) return '0.00'
return Number(score).toFixed(2)
}
// 加载所有成就
const loadAchievements = async () => {
loading.value = true

View File

@ -162,6 +162,9 @@
<template v-if="column.key === 'rank'">
<span class="rank-badge" :class="getRankClass(index + 1)">{{ index + 1 }}</span>
</template>
<template v-else-if="column.key === 'totalScore'">
<span class="score-value">{{ formatScore(record.totalScore) }}</span>
</template>
<template v-else-if="column.key === 'completionRate'">
<a-progress
:percent="Math.round(record.completionRate)"
@ -250,7 +253,7 @@
<a-rate :value="record.avgRating" disabled allow-half :count="5" class="white-rate" />
</template>
<template v-else-if="column.key === 'totalScore'">
<span class="score-value">{{ record.totalScore || 0 }}</span>
<span class="score-value">{{ formatScore(record.totalScore) }}</span>
</template>
<template v-else-if="column.key === 'deductionScore'">
<a-button
@ -260,17 +263,17 @@
@click.stop="showDeductionDetails(record)"
:style="{ padding: 0, height: 'auto', color: '#ef4444' }"
>
{{ record.deductionScore || 0 }}
{{ formatScore(record.deductionScore) }}
</a-button>
<span v-else class="deduction-value">
{{ record.deductionScore || 0 }}
{{ formatScore(record.deductionScore) }}
</span>
</template>
<template v-else-if="column.key === 'netScore'">
<span class="net-score-value">{{ record.netScore || 0 }}</span>
<span class="net-score-value">{{ formatScore(record.netScore) }}</span>
</template>
<template v-else-if="column.key === 'targetScore'">
<span>{{ record.targetScore || '-' }}</span>
<span>{{ record.targetScore ? formatScore(record.targetScore) : '-' }}</span>
</template>
<template v-else-if="column.key === 'targetCompletionRate'">
<span :class="getTargetRateClass(record.targetCompletionRate)">
@ -445,7 +448,7 @@
<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>
<p class="total-deduction">总扣分:<span class="deduction-number">{{ formatScore(deductionDetail.totalDeduction) }}</span> 分</p>
</div>
<!-- 延误扣分明细 -->
@ -458,9 +461,12 @@
:scroll="{ y: 300 }"
row-key="taskId"
>
<template #bodyCell="{ column, record }">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'deductionScore'">
<span class="text-danger">{{ record.deductionScore }} 分</span>
<span class="text-danger">{{ formatScore(record.deductionScore) }} 分</span>
</template>
<template v-else-if="column.key === 'taskScore'">
<span>{{ formatScore(record.taskScore) }}</span>
</template>
<template v-else-if="column.key === 'overdueHours'">
<span>{{ record.overdueHours }} 小时</span>
@ -484,7 +490,7 @@
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'deductionScore'">
<span class="text-danger">{{ record.deductionScore }} 分</span>
<span class="text-danger">{{ formatScore(record.deductionScore) }} 分</span>
</template>
<template v-else-if="column.key === 'deductionDate'">
{{ formatDateOnly(record.deductionDate) }}
@ -723,7 +729,7 @@ const userColumns = [
{ title: '部门', dataIndex: 'deptName', key: 'deptName' },
{ title: '发起数', dataIndex: 'totalDemands', key: 'totalDemands', sorter: (a: any, b: any) => a.totalDemands - b.totalDemands, align: 'center' },
{ title: '完成数', dataIndex: 'completedDemands', key: 'completedDemands', align: 'center' },
{ title: '总积分', dataIndex: 'totalScore', key: 'totalScore', align: 'center' },
{ title: '总积分', key: 'totalScore', align: 'center' },
{ title: '完成率', key: 'completionRate', width: 150, align: 'center' }
]
@ -1010,7 +1016,7 @@ const exportData = () => {
csvContent += '排名,用户名,部门,发起数,完成数,总积分,完成率\n'
statistics.userStats.forEach((user: any, index: number) => {
const rate = user.totalDemands > 0 ? ((user.completedDemands / user.totalDemands) * 100).toFixed(1) : '0.0'
csvContent += `${index + 1},${user.userName || '-'},${user.deptName || '-'},${user.totalDemands},${user.completedDemands},${user.totalScore || 0},${rate}%\n`
csvContent += `${index + 1},${user.userName || '-'},${user.deptName || '-'},${user.totalDemands},${user.completedDemands},${formatScore(user.totalScore)},${rate}%\n`
})
csvContent += '\n'
}
@ -1029,9 +1035,9 @@ const exportData = () => {
// 4. 设计师绩效
if (designerPerformance.designerStats && designerPerformance.designerStats.length > 0) {
csvContent += '【设计师绩效】\n'
csvContent += '排名,设计师,部门,接单量,完成量,完成率,准时率,评分\n'
csvContent += '排名,设计师,部门,接单量,完成量,获得积分,扣分,净积分,目标积分,目标完成率,准时率,评分\n'
designerPerformance.designerStats.forEach((designer: any, index: number) => {
csvContent += `${index + 1},${designer.realName || '-'},${designer.deptName || '-'},${designer.receivedCount || 0},${designer.completedCount || 0},${designer.completionRate || 0}%,${designer.onTimeRate || 0}%,${designer.avgRating?.toFixed(1) || '-'}\n`
csvContent += `${index + 1},${designer.realName || '-'},${designer.deptName || '-'},${designer.receivedCount || 0},${designer.completedCount || 0},${formatScore(designer.totalScore)},${formatScore(designer.deductionScore)},${formatScore(designer.netScore)},${designer.targetScore ? formatScore(designer.targetScore) : '-'},${designer.targetScore > 0 ? (designer.targetCompletionRate || 0) + '%' : '-'},${designer.onTimeRate || 0}%,${designer.avgRating?.toFixed(1) || '-'}\n`
})
csvContent += '\n'
}
@ -1142,6 +1148,12 @@ const formatDateOnly = (date: string | undefined) => {
return dayjs(date).format('YYYY-MM-DD')
}
// 格式化积分(保留两位小数)
const formatScore = (score: number | null | undefined): string => {
if (score === null || score === undefined) return '0.00'
return Number(score).toFixed(2)
}
// 单独加载设计师绩效数据(用于部门筛选)
const loadDesignerData = async () => {
loading.value = true

View File

@ -156,7 +156,7 @@
连续 {{ record.continuousDays }} 天
</div>
<div class="history-reward">
+{{ record.rewardScore }} 积分
+{{ formatScore(record.rewardScore) }} 积分
</div>
</div>
</div>
@ -264,6 +264,12 @@ const calculateReward = (continuousDays: number): number => {
}
}
// 格式化积分(保留两位小数)
const formatScore = (score: number | null | undefined): string => {
if (score === null || score === undefined) return '0.00'
return Number(score).toFixed(2)
}
// 格式化日期
const formatDate = (dateStr: string) => {
const date = new Date(dateStr)

View File

@ -72,7 +72,7 @@
<div class="data-label">当前排名</div>
</div>
<div class="data-item">
<div class="data-value">{{ myContestData.totalScore }}</div>
<div class="data-value">{{ formatScore(myContestData.totalScore) }}</div>
<div class="data-label">竞赛得分</div>
</div>
<div class="data-item">
@ -86,7 +86,7 @@
</div>
<div class="reward-info" v-if="myContestData.rewardScore > 0">
<span class="reward-label">可领取奖励:</span>
<span class="reward-value">{{ myContestData.rewardScore }} 积分</span>
<span class="reward-value">{{ formatScore(myContestData.rewardScore) }} 积分</span>
<a-button
type="primary"
size="small"
@ -126,7 +126,7 @@
<span class="stat">{{ participant.taskCompleted }} 任务</span>
<span class="stat">{{ participant.fiveStarCount }} 五星</span>
</div>
<div class="participant-score">{{ participant.totalScore }} 分</div>
<div class="participant-score">{{ formatScore(participant.totalScore) }} 分</div>
</div>
</div>
</div>
@ -290,6 +290,12 @@ const contestForm = ref({
endTime: null as Dayjs | null
})
// 格式化积分(保留两位小数)
const formatScore = (score: number | null | undefined): string => {
if (score === null || score === undefined) return '0.00'
return Number(score).toFixed(2)
}
const loadData = async () => {
loading.value = true
try {

View File

@ -196,7 +196,7 @@
<span class="group-count">({{ group.items.length }}个任务)</span>
</div>
<div class="group-summary">
<span class="summary-item">{{ group.totalScore }}分</span>
<span class="summary-item">{{ formatScore(group.totalScore) }}分</span>
<span class="summary-divider">|</span>
<span class="summary-item">{{ formatWorkHours(group.totalHours) }}</span>
</div>
@ -210,7 +210,7 @@
<span class="item-icon">└─</span>
<span class="item-name">{{ item.name }}</span>
<span class="item-quantity">× {{ item.quantity }}</span>
<span class="item-score">= {{ item.totalScore }}分</span>
<span class="item-score">= {{ formatScore(item.totalScore) }}分</span>
</div>
</div>
</div>
@ -971,6 +971,12 @@ watch(
}
)
// 格式化积分(保留两位小数)
const formatScore = (score: number | null | undefined): string => {
if (score === null || score === undefined) return '0.00'
return Number(score).toFixed(2)
}
// 计算预估积分(累加所有任务类型)
const estimatedScore = computed(() => {
let totalScore = 0
@ -984,7 +990,7 @@ const estimatedScore = computed(() => {
}
}
return totalScore
return formatScore(totalScore)
})
// 计算累计时效(小时)

View File

@ -171,7 +171,7 @@
</div>
<div class="stat-item">
<div class="stat-label">累计积分</div>
<div class="stat-value text-brand">{{ designer.totalScore }}</div>
<div class="stat-value text-brand">{{ formatScore(designer.totalScore) }}</div>
</div>
</div>
@ -356,6 +356,12 @@ const formatDate = (date: string | null | undefined) => {
return dayjs(date).format('MM/DD HH:mm')
}
// 格式化积分(保留两位小数)
const formatScore = (score: number | null | undefined): string => {
if (score === null || score === undefined) return '0.00'
return Number(score).toFixed(2)
}
// 获取进度条颜色
const getProgressColor = (progress: number) => {
if (progress >= 80) return '#31f08b' // 绿色

View File

@ -40,7 +40,7 @@
<p class="preset-desc">{{ preset.description }}</p>
<div class="preset-reward">
<Icon name="trophy" size="sm" color="#FFD700" />
<span>奖励 {{ preset.reward }} 积分</span>
<span>奖励 {{ formatScore(preset.reward) }} 积分</span>
</div>
</div>
</div>
@ -104,7 +104,7 @@
<div class="goal-reward" v-if="goal.rewardScore > 0">
<Icon name="trophy" size="sm" color="#FFD700" />
<span>完成奖励 {{ goal.rewardScore }} 积分</span>
<span>完成奖励 {{ formatScore(goal.rewardScore) }} 积分</span>
</div>
</div>
</div>
@ -140,7 +140,7 @@
:disabled="goal.rewardClaimed === 1"
@click="handleClaimReward(goal)"
>
{{ goal.rewardClaimed === 1 ? '已领取' : `领取 ${goal.rewardScore} 积分` }}
{{ goal.rewardClaimed === 1 ? '已领取' : `领取 ${formatScore(goal.rewardScore)} 积分` }}
</a-button>
</div>
</div>
@ -262,6 +262,12 @@ const goalForm = ref({
rewardScore: 50
})
// 格式化积分(保留两位小数)
const formatScore = (score: number | null | undefined): string => {
if (score === null || score === undefined) return '0.00'
return Number(score).toFixed(2)
}
const loadData = async () => {
loading.value = true
try {

View File

@ -252,8 +252,14 @@ import {
const router = useRouter()
const authStore = useAuthStore()
// 格式化积分(保留两位小数)
const formatScore = (score: number | null | undefined): string => {
if (score === null || score === undefined) return '0.00'
return Number(score).toFixed(2)
}
// 用户数据
const userScore = computed(() => authStore.userInfo?.score || 0)
const userScore = computed(() => formatScore(authStore.userInfo?.score))
const continuousCheckIn = ref(0)
const achievementCount = ref(0)
const activeGoalCount = ref(0)
@ -341,7 +347,7 @@ const formatTime = (time: string) => {
const parseRewardValue = (value: string) => {
try {
const obj = JSON.parse(value)
if (obj.score) return `+${obj.score} 积分`
if (obj.score) return `+${formatScore(obj.score)} 积分`
if (obj.badge) return `获得徽章`
if (obj.title) return `获得称号`
return '+奖励'

View File

@ -109,7 +109,7 @@
</div>
<div class="info-col">
<span class="label">积分</span>
<span class="value text-brand">{{ task.score }}</span>
<span class="value text-brand">{{ formatScore(task.score) }}</span>
</div>
<div v-if="task.completeTime" class="info-col">
<span class="label">完成时间</span>
@ -257,6 +257,12 @@ const formatTime = (time: string | undefined) => {
return dayjs(time).format('YYYY-MM-DD HH:mm')
}
// 格式化积分(保留两位小数)
const formatScore = (score: number | null | undefined): string => {
if (score === null || score === undefined) return '0.00'
return Number(score).toFixed(2)
}
// 判断是否逾期
const isOverdue = (deadline: string) => {
return dayjs(deadline).isBefore(dayjs())

View File

@ -176,7 +176,7 @@
</div>
<div class="info-col">
<span class="label">积分</span>
<span class="value text-brand">{{ task.score }}</span>
<span class="value text-brand">{{ formatScore(task.score) }}</span>
</div>
<div class="info-col">
<span class="label">预计工时</span>
@ -581,6 +581,12 @@ const formatTime = (time: string | undefined) => {
return dayjs(time).format('YYYY-MM-DD HH:mm')
}
// 格式化积分(保留两位小数)
const formatScore = (score: number | null | undefined): string => {
if (score === null || score === undefined) return '0.00'
return Number(score).toFixed(2)
}
// 判断是否逾期
const isOverdue = (deadline: string) => {
return dayjs(deadline).isBefore(dayjs())

View File

@ -90,7 +90,7 @@ const columns = [
]
const totalScore = computed(() => {
return records.value.reduce((sum, r) => {
const total = records.value.reduce((sum, r) => {
try {
const obj = JSON.parse(r.rewardValue)
return sum + (obj.score || 0)
@ -98,8 +98,15 @@ const totalScore = computed(() => {
return sum
}
}, 0)
return formatScore(total)
})
// 格式化积分(保留两位小数)
const formatScore = (score: number | null | undefined): string => {
if (score === null || score === undefined) return '0.00'
return Number(score).toFixed(2)
}
const totalCount = computed(() => total.value)
const getTypeColor = (type: string) => {
@ -125,7 +132,7 @@ const getTypeName = (type: string) => {
const parseRewardValue = (value: string) => {
try {
const obj = JSON.parse(value)
if (obj.score) return `+${obj.score} 积分`
if (obj.score) return `+${formatScore(obj.score)} 积分`
if (obj.badge) return `徽章: ${obj.badge}`
if (obj.title) return `称号: ${obj.title}`
return value

View File

@ -256,7 +256,7 @@
</div>
<div class="info-item">
<span class="label">预估积分</span>
<span class="value text-brand">{{ task.totalScore || task.score || '--' }} 分</span>
<span class="value text-brand">{{ formatScore(task.totalScore || task.score) }} 分</span>
</div>
<div class="info-item">
<span class="label">数量</span>
@ -278,7 +278,7 @@
<div class="type-details-list">
<div v-for="(detail, idx) in parseTypeDetails(task.typeDetails)" :key="idx" class="type-detail-item">
<span class="type-name">└ {{ detail.typeName }}</span>
<span class="type-score">x{{ detail.quantity }} = {{ detail.totalScore }}分</span>
<span class="type-score">x{{ detail.quantity }} = {{ formatScore(detail.totalScore) }}分</span>
</div>
</div>
</div>
@ -588,7 +588,7 @@
</div>
<div class="info-item">
<span class="label">预估积分</span>
<span class="value text-brand">{{ task.totalScore || task.score || 0 }} 分</span>
<span class="value text-brand">{{ formatScore(task.totalScore || task.score) }} 分</span>
</div>
<div class="info-item">
<span class="label">{{ task._itemType === 'taskGroup' ? '子任务数' : '数量' }}</span>
@ -610,7 +610,7 @@
<div class="type-details-list">
<div v-for="(detail, idx) in parseTypeDetails(task.typeDetails)" :key="idx" class="type-detail-item">
<span class="type-name">└ {{ detail.typeName }}</span>
<span class="type-score">x{{ detail.quantity }} = {{ detail.totalScore }}分</span>
<span class="type-score">x{{ detail.quantity }} = {{ formatScore(detail.totalScore) }}分</span>
</div>
</div>
</div>
@ -948,7 +948,7 @@
</div>
<div class="info-item">
<span class="label">积分</span>
<span class="value text-brand">{{ task.score }} 分</span>
<span class="value text-brand">{{ formatScore(task.score) }} 分</span>
</div>
<div class="info-item">
<span class="label">数量</span>
@ -996,7 +996,7 @@
<div class="type-details-list">
<div v-for="(detail, idx) in parseTypeDetails(task.typeDetails)" :key="idx" class="type-detail-item">
<span class="type-name">└ {{ detail.typeName }}</span>
<span class="type-score">x{{ detail.quantity }} = {{ detail.totalScore }}分</span>
<span class="type-score">x{{ detail.quantity }} = {{ formatScore(detail.totalScore) }}分</span>
</div>
</div>
</div>
@ -1341,7 +1341,7 @@
</div>
<div class="info-item">
<span class="label">获得积分</span>
<span class="value text-brand">{{ task.score }} 分</span>
<span class="value text-brand">{{ formatScore(task.score) }} 分</span>
</div>
<div class="info-item">
<span class="label">数量</span>
@ -1712,7 +1712,7 @@
</div>
<div class="info-item">
<span class="label">积分</span>
<span class="value text-brand">{{ task.score }} 分</span>
<span class="value text-brand">{{ formatScore(task.score) }} 分</span>
</div>
<div class="info-item">
<span class="label">数量</span>
@ -1760,7 +1760,7 @@
<div class="type-details-list">
<div v-for="(detail, idx) in parseTypeDetails(task.typeDetails)" :key="idx" class="type-detail-item">
<span class="type-name">└ {{ detail.typeName }}</span>
<span class="type-score">x{{ detail.quantity }} = {{ detail.totalScore }}分</span>
<span class="type-score">x{{ detail.quantity }} = {{ formatScore(detail.totalScore) }}分</span>
</div>
</div>
</div>
@ -2891,6 +2891,12 @@ const formatDate = (date: string | undefined) => {
return dayjs(date).format('YYYY-MM-DD')
}
// 格式化积分(保留两位小数)
const formatScore = (score: number | null | undefined): string => {
if (score === null || score === undefined) return '0.00'
return Number(score).toFixed(2)
}
// 格式化截止时间
const formatDeadline = (deadline: string | undefined) => {
if (!deadline) return '--'
@ -4251,6 +4257,14 @@ const loadInProgressTasks = async () => {
const isAdmin = userInfo.isAdmin === 1 || userInfo.isAdmin === true
const isDeptLeader = userInfo.isDepartmentLeader === 1 || managedDepartments.value.length > 0
console.log('=== loadInProgressTasks 调试信息 ===')
console.log('userInfo.isDepartmentLeader:', userInfo.isDepartmentLeader)
console.log('managedDepartments.value:', managedDepartments.value)
console.log('isDeptLeader:', isDeptLeader)
console.log('isAdmin:', isAdmin)
console.log('currentDeptView.value:', currentDeptView.value)
console.log('userDeptType.value:', userDeptType.value)
let allTasks: any[] = []
let totalCount = 0
@ -4306,17 +4320,21 @@ const loadInProgressTasks = async () => {
}
} else {
// 需求部门视角:看该部门发起的任务(通过 deptId 参数)
console.log('需求部门视角切换,查询部门ID:', currentDeptView.value)
// 需求部门负责人:不传递status参数,查询所有非草稿的任务
const { status, ...paramsWithoutStatus } = params
const taskResponse = await taskApi.getTasks({
...params,
...paramsWithoutStatus,
publisherDeptId: currentDeptView.value // 按发布者部门过滤
})
if (taskResponse.data && taskResponse.data.records) {
// 需求部门负责人:显示所有已发布且未完成的任务(pending、in_progress、review等)
const tasksWithInProgress = taskResponse.data.records.filter((task: any) => {
if (task.isPipeline === 1) {
return (task.subTaskInProgress || 0) > 0 || task.status === 'in_progress'
}
return task.status === 'in_progress'
// 排除草稿和已完成的任务
const excludeStatuses = ['draft', 'completed', 'cancelled']
return !excludeStatuses.includes(task.status)
})
console.log('需求部门视角切换 - 过滤后任务数:', tasksWithInProgress.length)
allTasks = tasksWithInProgress.map((task: any) => ({
...task,
isSubTask: false
@ -4337,17 +4355,43 @@ const loadInProgressTasks = async () => {
totalCount += itemResponse.data.total || 0
}
} else {
// 需求部门负责人:看自己部门发起的任务
// 需求部门负责人:看自己管理的所有需求部门发起的任务
// 获取用户管理的所有需求部门ID(包括自己所属的部门)
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
const demandDeptIds: number[] = []
console.log('需求部门负责人 - userDeptId:', userDeptId.value, 'userDeptType:', userDeptType.value)
console.log('需求部门负责人 - userInfo.managedDepartments:', userInfo.managedDepartments)
// 添加用户自己的部门(如果是需求部门)
if (userDeptId.value && userDeptType.value === 'demand') {
demandDeptIds.push(userDeptId.value)
}
// 添加用户管理的所有需求部门
if (userInfo.managedDepartments && Array.isArray(userInfo.managedDepartments)) {
userInfo.managedDepartments.forEach((dept: any) => {
console.log('检查管理的部门:', dept.deptId, dept.deptName, 'deptType:', dept.deptType)
if (dept.deptType === 'demand' && !demandDeptIds.includes(dept.deptId)) {
demandDeptIds.push(dept.deptId)
}
})
}
console.log('需求部门负责人查询的部门ID列表:', demandDeptIds)
// 需求部门负责人:不传递status参数,查询所有非草稿的任务
const { status: _, ...paramsWithoutStatus } = params
const taskResponse = await taskApi.getTasks({
...params,
publisherDeptId: userDeptId.value // 按发布者部门过滤
...paramsWithoutStatus,
publisherDeptIds: demandDeptIds.join(',') // 按多个发布者部门过滤
})
if (taskResponse.data && taskResponse.data.records) {
// 需求部门负责人:显示所有已发布且未完成的任务(pending、in_progress、review等)
const tasksWithInProgress = taskResponse.data.records.filter((task: any) => {
if (task.isPipeline === 1) {
return (task.subTaskInProgress || 0) > 0 || task.status === 'in_progress'
}
return task.status === 'in_progress'
// 排除草稿和已完成的任务
const excludeStatuses = ['draft', 'completed', 'cancelled']
return !excludeStatuses.includes(task.status)
})
allTasks = tasksWithInProgress.map((task: any) => ({
...task,

View File

@ -326,7 +326,7 @@
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'scoreChange'">
<span :class="record.scoreChange > 0 ? 'text-green' : 'text-red'">
{{ record.scoreChange > 0 ? '+' : '' }}{{ record.scoreChange }}
{{ record.scoreChange > 0 ? '+' : '' }}{{ formatScore(record.scoreChange) }}
</span>
</template>
<template v-else-if="column.key === 'rewardType'">
@ -419,6 +419,12 @@ const recordPagination = ref({
total: 0
})
// 格式化积分(保留两位小数)
const formatScore = (score: number | null | undefined): string => {
if (score === null || score === undefined) return '0.00'
return Number(score).toFixed(2)
}
const loadData = async () => {
loading.value = true
try {