fix: 审核修改明细同步流水线子任务

This commit is contained in:
Your Name 2026-03-12 12:10:00 +08:00
parent 088ba915ca
commit 4f9e17a1fc

View File

@ -1067,11 +1067,32 @@ public class TaskApprovalService {
.collect(Collectors.joining(","));
task.setTaskTypeIds(taskTypeIds);
// 如果只有一个任务类型,更新quantity字段
if (updates.getTaskTypes().size() == 1) {
task.setQuantity(updates.getTaskTypes().get(0).getQuantity());
// 🔥 兼容:更新taskTypeId为第一个任务类型(用于筛选/展示)
if (updates.getTaskTypes().get(0) != null && updates.getTaskTypes().get(0).getTaskTypeId() != null) {
task.setTaskTypeId(updates.getTaskTypes().get(0).getTaskTypeId());
}
// 🔥 重新计算总数量与总工时(多任务类型也要更新quantity,否则后续节点/页面会出现数量不一致)
int totalQuantity = 0;
java.math.BigDecimal totalEstimatedHours = java.math.BigDecimal.ZERO;
for (TaskUpdateDTO.TaskTypeUpdateDTO typeUpdate : updates.getTaskTypes()) {
if (typeUpdate == null || typeUpdate.getTaskTypeId() == null) {
continue;
}
Integer qty = typeUpdate.getQuantity();
int q = qty != null ? qty : 0;
totalQuantity += q;
DemandType demandType = configService.getDemandTypeById(typeUpdate.getTaskTypeId());
if (demandType != null && demandType.getEstimatedHours() != null && q > 0) {
totalEstimatedHours = totalEstimatedHours.add(
demandType.getEstimatedHours().multiply(new java.math.BigDecimal(q))
);
}
}
task.setQuantity(totalQuantity);
task.setEstimatedHours(totalEstimatedHours.setScale(2, java.math.RoundingMode.HALF_UP));
// 重新计算积分
java.math.BigDecimal totalScore = taskService.calculateTotalScoreDecimal(updates.getTaskTypes());
task.setScore(totalScore);
@ -1087,10 +1108,12 @@ public class TaskApprovalService {
* 当审核人调整任务明细数量时,同步更新对应的子任务
*/
private void updateTaskItemsQuantityAndScore(Long taskId, List<TaskUpdateDTO.TaskTypeUpdateDTO> taskTypes) {
// 获取该任务的所有子任务
// 获取该任务的所有子任务(按sortOrder排序,确保与前端明细顺序一致)
List<TaskItem> taskItems = taskItemMapper.selectList(
new LambdaQueryWrapper<TaskItem>()
.eq(TaskItem::getTaskId, taskId)
.orderByAsc(TaskItem::getSortOrder)
.orderByAsc(TaskItem::getId)
);
if (taskItems == null || taskItems.isEmpty()) {
@ -1098,79 +1121,253 @@ public class TaskApprovalService {
return;
}
// 构建 taskTypeId -> (quantity, unitScore) 的映射
java.util.Map<Long, TaskUpdateDTO.TaskTypeUpdateDTO> typeUpdateMap = new java.util.HashMap<>();
// 构建更新明细的数量队列(支持同一typeId出现多次的情况)
java.util.Map<Long, java.util.Deque<Integer>> updateQtyQueues = new java.util.HashMap<>();
java.util.Map<Long, Integer> updateTypeCounts = new java.util.HashMap<>();
for (TaskUpdateDTO.TaskTypeUpdateDTO typeUpdate : taskTypes) {
typeUpdateMap.put(typeUpdate.getTaskTypeId(), typeUpdate);
if (typeUpdate == null || typeUpdate.getTaskTypeId() == null) {
continue;
}
int q = typeUpdate.getQuantity() != null ? typeUpdate.getQuantity() : 0;
updateQtyQueues.computeIfAbsent(typeUpdate.getTaskTypeId(), k -> new java.util.ArrayDeque<>()).add(q);
updateTypeCounts.merge(typeUpdate.getTaskTypeId(), 1, Integer::sum);
}
// 遍历子任务,更新数量和积分
// 解析现有 typeDetails 并统计明细条目数量(用于校验:仅允许改数量,不允许增删类型/条目)
java.util.Map<Long, Integer> existingTypeCounts = new java.util.HashMap<>();
java.util.Map<Long, com.fasterxml.jackson.databind.node.ArrayNode> parsedDetailsByItemId = new java.util.HashMap<>();
boolean hasAnyTypeDetails = false;
for (TaskItem item : taskItems) {
boolean needUpdate = false;
// 🔥 先更新 typeDetails JSON 字段(如果存在)
// 这样后面可以从更新后的 typeDetails 中正确计算总积分
if (item.getTypeDetails() != null && !item.getTypeDetails().isEmpty()) {
String updatedTypeDetails = updateTypeDetailsJson(item.getTypeDetails(), typeUpdateMap);
if (!item.getTypeDetails().equals(updatedTypeDetails)) {
item.setTypeDetails(updatedTypeDetails);
item.setUpdateTime(LocalDateTime.now());
needUpdate = true;
log.info("更新子任务typeDetails: itemId={}, newTypeDetails={}", item.getId(), updatedTypeDetails);
// 🔥 从更新后的 typeDetails 中重新计算总积分和总数量
// 这是修复多任务类型子任务积分计算错误的关键
java.math.BigDecimal newTotalScore = calculateTotalScoreFromTypeDetails(updatedTypeDetails);
int newTotalQuantity = calculateTotalQuantityFromTypeDetails(updatedTypeDetails);
if (newTotalScore != null) {
item.setTotalScore(newTotalScore);
log.info("从typeDetails重新计算子任务总积分: itemId={}, newTotalScore={}", item.getId(), newTotalScore);
}
if (newTotalQuantity > 0) {
item.setQuantity(newTotalQuantity);
// 重新计算平均单价
if (newTotalScore != null && newTotalQuantity > 0) {
item.setUnitScore(newTotalScore.divide(new java.math.BigDecimal(newTotalQuantity), 2, java.math.RoundingMode.HALF_UP));
}
log.info("从typeDetails重新计算子任务总数量: itemId={}, newTotalQuantity={}", item.getId(), newTotalQuantity);
if (item.getTypeDetails() == null || item.getTypeDetails().isEmpty()) {
continue;
}
hasAnyTypeDetails = true;
try {
com.fasterxml.jackson.databind.JsonNode rootNode = objectMapper.readTree(item.getTypeDetails());
if (!rootNode.isArray()) {
continue;
}
com.fasterxml.jackson.databind.node.ArrayNode arrayNode = (com.fasterxml.jackson.databind.node.ArrayNode) rootNode;
parsedDetailsByItemId.put(item.getId(), arrayNode);
for (int i = 0; i < arrayNode.size(); i++) {
com.fasterxml.jackson.databind.JsonNode detailNode = arrayNode.get(i);
if (detailNode != null && detailNode.has("typeId")) {
Long typeId = detailNode.get("typeId").asLong();
existingTypeCounts.merge(typeId, 1, Integer::sum);
}
}
} else {
// 如果没有 typeDetails,使用原来的单类型逻辑
if (item.getTaskTypeId() != null && typeUpdateMap.containsKey(item.getTaskTypeId())) {
TaskUpdateDTO.TaskTypeUpdateDTO typeUpdate = typeUpdateMap.get(item.getTaskTypeId());
Integer newQuantity = typeUpdate.getQuantity();
if (newQuantity != null && !newQuantity.equals(item.getQuantity())) {
// 获取单价积分
java.math.BigDecimal unitScore = item.getUnitScore();
if (unitScore == null) {
DemandType demandType = configService.getDemandTypeById(item.getTaskTypeId());
if (demandType != null && demandType.getUnitScore() != null) {
unitScore = demandType.getUnitScore();
} else {
unitScore = java.math.BigDecimal.ZERO;
}
}
item.setQuantity(newQuantity);
item.setTotalScore(unitScore.multiply(new java.math.BigDecimal(newQuantity)));
item.setUpdateTime(LocalDateTime.now());
needUpdate = true;
log.info("更新单类型子任务数量和积分: itemId={}, taskTypeId={}, newQuantity={}, newTotalScore={}",
item.getId(), item.getTaskTypeId(), newQuantity, item.getTotalScore());
} catch (Exception e) {
log.warn("解析子任务 {} 的 typeDetails 失败,跳过该子任务更新: {}", item.getId(), e.getMessage());
}
}
if (hasAnyTypeDetails) {
// 🔥 校验:避免前端增删/重排明细导致后端无法安全同步,出现“看似保存后又恢复”的问题
if (!existingTypeCounts.equals(updateTypeCounts)) {
log.warn("任务 {} 明细条目不匹配,拒绝更新。existingCounts={}, updateCounts={}",
taskId, existingTypeCounts, updateTypeCounts);
throw new BusinessException(ResultCode.PARAM_ERROR,
"任务明细调整仅支持修改数量(不支持新增/删除/合并明细条目)。如需增删任务类型,请前往“编辑任务”页面调整后再审核。");
}
// 缓存 DemandType(避免重复查询)
java.util.Map<Long, DemandType> demandTypeCache = new java.util.HashMap<>();
// 遍历子任务,按明细条目顺序逐条消费队列,更新 quantity/totalScore,并重算子任务总量/总分/工时
for (TaskItem item : taskItems) {
com.fasterxml.jackson.databind.node.ArrayNode arrayNode = parsedDetailsByItemId.get(item.getId());
if (arrayNode == null) {
continue;
}
boolean needUpdate = false;
int newTotalQuantity = 0;
java.math.BigDecimal newTotalScore = java.math.BigDecimal.ZERO;
java.math.BigDecimal newEstimatedHours = java.math.BigDecimal.ZERO;
for (int i = 0; i < arrayNode.size(); i++) {
com.fasterxml.jackson.databind.JsonNode detailNode = arrayNode.get(i);
if (detailNode == null || !detailNode.has("typeId")) {
continue;
}
Long typeId = detailNode.get("typeId").asLong();
java.util.Deque<Integer> queue = updateQtyQueues.get(typeId);
if (queue == null || queue.isEmpty()) {
// counts已校验一致,这里理论上不会发生
continue;
}
Integer updatedQty = queue.pollFirst();
int qty = updatedQty != null ? updatedQty : 0;
com.fasterxml.jackson.databind.node.ObjectNode obj = (com.fasterxml.jackson.databind.node.ObjectNode) detailNode;
int oldQty = obj.has("quantity") ? obj.get("quantity").asInt() : 0;
if (oldQty != qty) {
obj.put("quantity", qty);
needUpdate = true;
log.info("更新子任务明细数量: itemId={}, typeId={}, oldQty={}, newQty={}", item.getId(), typeId, oldQty, qty);
}
// unitScore:优先使用明细内的unitScore,缺失则从配置补齐
java.math.BigDecimal unitScore = null;
if (obj.has("unitScore") && !obj.get("unitScore").isNull()) {
try {
unitScore = new java.math.BigDecimal(obj.get("unitScore").asText());
} catch (Exception ignore) {
unitScore = null;
}
}
DemandType demandType = demandTypeCache.computeIfAbsent(typeId, id -> configService.getDemandTypeById(id));
if (unitScore == null) {
unitScore = (demandType != null && demandType.getUnitScore() != null) ? demandType.getUnitScore() : java.math.BigDecimal.ZERO;
obj.put("unitScore", unitScore);
needUpdate = true;
}
// 重新计算并写回 totalScore(保留2位小数)
java.math.BigDecimal lineTotalScore = unitScore.multiply(new java.math.BigDecimal(qty))
.setScale(2, java.math.RoundingMode.HALF_UP);
java.math.BigDecimal oldLineTotalScore = null;
if (obj.has("totalScore") && !obj.get("totalScore").isNull()) {
try {
oldLineTotalScore = new java.math.BigDecimal(obj.get("totalScore").asText());
} catch (Exception ignore) {
oldLineTotalScore = null;
}
}
if (oldLineTotalScore == null || oldLineTotalScore.compareTo(lineTotalScore) != 0) {
obj.put("totalScore", lineTotalScore.doubleValue());
needUpdate = true;
}
newTotalQuantity += qty;
newTotalScore = newTotalScore.add(lineTotalScore);
// 重新计算子任务工时
if (demandType != null && demandType.getEstimatedHours() != null && qty > 0) {
newEstimatedHours = newEstimatedHours.add(
demandType.getEstimatedHours().multiply(new java.math.BigDecimal(qty))
);
}
}
// 校验:确保每个typeId的队列都被完全消费
// (如果出现异常未消费完全,会在方法末尾统一检测)
// 更新子任务汇总字段(即使明细没变,也要修复历史不一致)
java.math.BigDecimal scoreScaled = newTotalScore.setScale(2, java.math.RoundingMode.HALF_UP);
java.math.BigDecimal hoursScaled = newEstimatedHours.setScale(2, java.math.RoundingMode.HALF_UP);
if (item.getQuantity() == null || item.getQuantity() != newTotalQuantity) {
item.setQuantity(newTotalQuantity);
needUpdate = true;
}
if (item.getTotalScore() == null || item.getTotalScore().compareTo(scoreScaled) != 0) {
item.setTotalScore(scoreScaled);
needUpdate = true;
}
java.math.BigDecimal avgUnitScore = newTotalQuantity > 0
? scoreScaled.divide(new java.math.BigDecimal(newTotalQuantity), 2, java.math.RoundingMode.HALF_UP)
: java.math.BigDecimal.ZERO;
if (item.getUnitScore() == null || item.getUnitScore().compareTo(avgUnitScore) != 0) {
item.setUnitScore(avgUnitScore);
needUpdate = true;
}
if (item.getEstimatedHours() == null || item.getEstimatedHours().compareTo(hoursScaled) != 0) {
item.setEstimatedHours(hoursScaled);
needUpdate = true;
}
if (needUpdate) {
try {
String updatedTypeDetails = objectMapper.writeValueAsString(arrayNode);
if (item.getTypeDetails() == null || !item.getTypeDetails().equals(updatedTypeDetails)) {
item.setTypeDetails(updatedTypeDetails);
}
} catch (Exception e) {
log.warn("序列化子任务 {} typeDetails 失败: {}", item.getId(), e.getMessage());
}
item.setUpdateTime(LocalDateTime.now());
taskItemMapper.updateById(item);
}
}
// 检查是否仍有未消费的更新队列(理论上不应有)
boolean hasRemaining = updateQtyQueues.values().stream().anyMatch(q -> q != null && !q.isEmpty());
if (hasRemaining) {
log.warn("任务 {} 明细更新存在未消费的更新队列: {}", taskId, updateQtyQueues);
throw new BusinessException(ResultCode.PARAM_ERROR,
"任务明细更新失败:明细条目不一致,请刷新页面后重试。");
}
log.info("完成子任务数量和积分更新(含重复typeId支持): taskId={}, 子任务数量={}", taskId, taskItems.size());
return;
}
// 没有typeDetails的旧数据:按 taskTypeId 顺序消费数量队列,更新 quantity/totalScore
java.util.Map<Long, DemandType> demandTypeCache = new java.util.HashMap<>();
for (TaskItem item : taskItems) {
if (item.getTaskTypeId() == null) {
continue;
}
java.util.Deque<Integer> queue = updateQtyQueues.get(item.getTaskTypeId());
if (queue == null || queue.isEmpty()) {
continue;
}
Integer newQty = queue.pollFirst();
int qty = newQty != null ? newQty : 0;
DemandType demandType = demandTypeCache.computeIfAbsent(item.getTaskTypeId(), id -> configService.getDemandTypeById(id));
java.math.BigDecimal unitScore = item.getUnitScore();
if (unitScore == null) {
unitScore = (demandType != null && demandType.getUnitScore() != null) ? demandType.getUnitScore() : java.math.BigDecimal.ZERO;
}
java.math.BigDecimal newTotalScore = unitScore.multiply(new java.math.BigDecimal(qty)).setScale(2, java.math.RoundingMode.HALF_UP);
java.math.BigDecimal newHours = java.math.BigDecimal.ZERO;
if (demandType != null && demandType.getEstimatedHours() != null && qty > 0) {
newHours = demandType.getEstimatedHours().multiply(new java.math.BigDecimal(qty)).setScale(2, java.math.RoundingMode.HALF_UP);
}
boolean needUpdate = false;
if (item.getQuantity() == null || item.getQuantity() != qty) {
item.setQuantity(qty);
needUpdate = true;
}
if (item.getTotalScore() == null || item.getTotalScore().compareTo(newTotalScore) != 0) {
item.setTotalScore(newTotalScore);
needUpdate = true;
}
if (item.getUnitScore() == null || item.getUnitScore().compareTo(unitScore) != 0) {
item.setUnitScore(unitScore);
needUpdate = true;
}
if (item.getEstimatedHours() == null || item.getEstimatedHours().compareTo(newHours) != 0) {
item.setEstimatedHours(newHours);
needUpdate = true;
}
if (needUpdate) {
item.setUpdateTime(LocalDateTime.now());
taskItemMapper.updateById(item);
}
}
log.info("完成子任务数量和积分更新: taskId={}, 子任务数量={}", taskId, taskItems.size());
// 如果还有未消费队列,说明更新类型与子任务不一致,拒绝更新避免出现数据看似回滚
boolean hasRemaining = updateQtyQueues.values().stream().anyMatch(q -> q != null && !q.isEmpty());
if (hasRemaining) {
log.warn("任务 {} 明细更新存在未消费的更新队列(旧数据模式): {}", taskId, updateQtyQueues);
throw new BusinessException(ResultCode.PARAM_ERROR,
"任务明细更新失败:子任务与任务明细不一致,请前往“编辑任务”页面调整。");
}
log.info("完成子任务数量和积分更新(旧数据模式): taskId={}, 子任务数量={}", taskId, taskItems.size());
}
/**