fix: 审核修改明细同步流水线子任务
This commit is contained in:
parent
088ba915ca
commit
4f9e17a1fc
@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user