diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/ProjectMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/ProjectMapper.java index 194115d..e63bf58 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/ProjectMapper.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/ProjectMapper.java @@ -114,6 +114,20 @@ public interface ProjectMapper extends BaseMapperX { .eq(ProjectDO::getId, id)); } + /** + * 仅更新实际开始/结束日期。null 字段依据全局 FieldStrategy 不会被覆盖。 + */ + default int updateActualDatesById(Long id, LocalDate actualStartDate, LocalDate actualEndDate) { + if (actualStartDate == null && actualEndDate == null) { + return 0; + } + ProjectDO update = new ProjectDO(); + update.setActualStartDate(actualStartDate); + update.setActualEndDate(actualEndDate); + return update(update, new LambdaQueryWrapperX() + .eq(ProjectDO::getId, id)); + } + /** * 临期/逾期告警候选:计划完成日非空且 <= maxPlannedEndDate,状态不在排除集(终态+paused)。 * excludeStatusCodes 为空时不排除任何状态(空集守卫,与既有口径一致)。 diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/ProjectStatusLogMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/ProjectStatusLogMapper.java index 3a03e7b..343b613 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/ProjectStatusLogMapper.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/ProjectStatusLogMapper.java @@ -12,6 +12,19 @@ import java.util.List; @Mapper public interface ProjectStatusLogMapper extends BaseMapperX { + default ProjectStatusLogDO selectEarliestByProjectIdAndActionType(Long projectId, String actionType) { + if (projectId == null || actionType == null || actionType.isBlank()) { + return null; + } + List list = selectList(new LambdaQueryWrapperX() + .eq(ProjectStatusLogDO::getProjectId, projectId) + .eq(ProjectStatusLogDO::getActionType, actionType.trim()) + .orderByAsc(BaseDO::getCreateTime) + .orderByAsc(ProjectStatusLogDO::getId) + .last("LIMIT 1")); + return list.isEmpty() ? null : list.get(0); + } + default List selectListByProjectId(Long projectId, String actionType, LocalDateTime[] operateTime) { return selectList(new LambdaQueryWrapperX() .eq(ProjectStatusLogDO::getProjectId, projectId) diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/task/ProjectTaskMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/task/ProjectTaskMapper.java index 39d968d..84d01f1 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/task/ProjectTaskMapper.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/task/ProjectTaskMapper.java @@ -1,5 +1,6 @@ package com.njcn.rdms.module.project.dal.mysql.project.task; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.njcn.rdms.framework.common.pojo.PageResult; @@ -115,16 +116,15 @@ public interface ProjectTaskMapper extends BaseMapperX { } /** - * 仅更新实际开始/结束日期。null 字段依据全局 FieldStrategy 不会被覆盖。 + * 仅更新实际开始/结束日期。显式 set,允许把字段清空为 null。 */ default int updateActualDatesById(Long id, LocalDate actualStartDate, LocalDate actualEndDate) { - if (actualStartDate == null && actualEndDate == null) { + if (id == null) { return 0; } - ProjectTaskDO update = new ProjectTaskDO(); - update.setActualStartDate(actualStartDate); - update.setActualEndDate(actualEndDate); - return update(update, new LambdaQueryWrapperX() + return update(null, new LambdaUpdateWrapper() + .set(ProjectTaskDO::getActualStartDate, actualStartDate) + .set(ProjectTaskDO::getActualEndDate, actualEndDate) .eq(ProjectTaskDO::getId, id)); } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/task/TaskWorklogMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/task/TaskWorklogMapper.java index 5399581..fcb6973 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/task/TaskWorklogMapper.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/task/TaskWorklogMapper.java @@ -1,6 +1,7 @@ package com.njcn.rdms.module.project.dal.mysql.project.task; import com.njcn.rdms.framework.common.pojo.PageResult; +import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO; import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX; import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX; import com.njcn.rdms.module.project.controller.admin.project.task.vo.worklog.TaskWorklogPageReqVO; @@ -58,6 +59,19 @@ public interface TaskWorklogMapper extends BaseMapperX { """) BigDecimal sumDurationByTaskId(@Param("taskId") Long taskId); + /** + * 项目下所有任务工作日志的最早开始日期。逻辑删除的任务 / 工作日志不参与统计。 + */ + @Select(""" + SELECT MIN(w.start_date) + FROM rdms_task_worklog w + INNER JOIN rdms_task t ON t.id = w.task_id + WHERE w.deleted = b'0' + AND t.deleted = b'0' + AND t.project_id = #{projectId} + """) + LocalDate selectEarliestStartDateByProjectId(@Param("projectId") Long projectId); + /** * 批量任务工时小时数汇总,返回 [{taskId, total}]。用于详情/分页装配避免 N+1。 */ @@ -83,6 +97,22 @@ public interface TaskWorklogMapper extends BaseMapperX { .eq(TaskWorklogDO::getTaskId, taskId)) > 0; } + /** + * 任务下最早一条工作日志(按 start_date asc, create_time asc, id asc)。 + */ + default TaskWorklogDO selectEarliestByTaskId(Long taskId) { + if (taskId == null) { + return null; + } + List list = selectList(new LambdaQueryWrapperX() + .eq(TaskWorklogDO::getTaskId, taskId) + .orderByAsc(TaskWorklogDO::getStartDate) + .orderByAsc(BaseDO::getCreateTime) + .orderByAsc(TaskWorklogDO::getId) + .last("LIMIT 1")); + return list.isEmpty() ? null : list.get(0); + } + /** * 取指定用户在该任务下最新的一条工时(按 end_date desc, create_time desc, id desc)。 * 用于 owner 填报后回查"本人最新一条"以同步任务进度。 @@ -102,6 +132,24 @@ public interface TaskWorklogMapper extends BaseMapperX { return list.isEmpty() ? null : list.get(0); } + /** + * 取该任务下指定用户 progressRate=100 的最晚一条工作日志(按 end_date desc, create_time desc, id desc)。 + */ + default TaskWorklogDO selectLatestCompletedByTaskIdAndUserId(Long taskId, Long userId) { + if (taskId == null || userId == null) { + return null; + } + List list = selectList(new LambdaQueryWrapperX() + .eq(TaskWorklogDO::getTaskId, taskId) + .eq(TaskWorklogDO::getUserId, userId) + .eq(TaskWorklogDO::getProgressRate, new BigDecimal("100")) + .orderByDesc(TaskWorklogDO::getEndDate) + .orderByDesc(BaseDO::getCreateTime) + .orderByDesc(TaskWorklogDO::getId) + .last("LIMIT 1")); + return list.isEmpty() ? null : list.get(0); + } + /** * 取该 (taskId, userId) 下 endDate 严格早于给定值的"最近一条",按 end_date desc, id desc 取首行。 * 用于进度单调性校验(与"前一段"比较)。excludeId 不为 null 时排除自身(更新场景)。 diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/ProjectService.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/ProjectService.java index 24abac1..f8c7a00 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/ProjectService.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/ProjectService.java @@ -68,6 +68,12 @@ public interface ProjectService { void autoStartProjectIfPending(Long projectId, String triggerAction); + /** + * 以项目下最早一条任务工作日志的 startDate 为优先口径,同步项目实际开始日期。 + * 若项目下尚无任务工作日志,则回退到首次 auto_start 状态日志日期;两者都没有时不更新。 + */ + void syncActualStartDateFromWorklogs(Long projectId); + /** * 重算项目进度并落库:AVG(项目下所有根任务 progressRate),沿用 task 维度的 progress_excluded 排除集合。 * 由任务侧在"任务进度变化 / 状态变更 / 创建删除 / 父子结构变化"等入口触发。 diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/ProjectServiceImpl.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/ProjectServiceImpl.java index 434d4c7..be4402a 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/ProjectServiceImpl.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/ProjectServiceImpl.java @@ -44,6 +44,7 @@ import com.njcn.rdms.module.project.dal.mysql.project.ProjectMapper; import com.njcn.rdms.module.project.dal.mysql.project.ProjectRequirementModuleMapper; import com.njcn.rdms.module.project.dal.mysql.project.ProjectStatusLogMapper; import com.njcn.rdms.module.project.dal.mysql.project.task.ProjectTaskMapper; +import com.njcn.rdms.module.project.dal.mysql.project.task.TaskWorklogMapper; import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusModelMapper; import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusTransitionMapper; import com.njcn.rdms.module.project.enums.ErrorCodeConstants; @@ -122,6 +123,8 @@ class ProjectServiceImpl implements ProjectService { @Resource private ProjectTaskMapper projectTaskMapper; @Resource + private TaskWorklogMapper taskWorklogMapper; + @Resource private com.njcn.rdms.module.project.dal.mysql.project.execution.ProjectExecutionMapper projectExecutionMapper; @Resource private com.njcn.rdms.module.project.dal.mysql.project.ProjectRequirementMapper projectRequirementMapper; @@ -1065,6 +1068,19 @@ class ProjectServiceImpl implements ProjectService { changeStatus(project, transition, ObjectActivityConstants.PROJECT_ACTION_AUTO_START, reason); } + @Override + @Transactional(rollbackFor = Exception.class) + public void syncActualStartDateFromWorklogs(Long projectId) { + if (projectId == null) { + return; + } + ProjectDO project = projectMapper.selectById(projectId); + if (project == null) { + return; + } + syncActualStartDate(project); + } + @Override public void recalcProgress(Long projectId) { if (projectId == null) { @@ -1494,6 +1510,40 @@ class ProjectServiceImpl implements ProjectService { writeProjectStatusLog(project, actionCode, fromStatus, toStatus, reason); writeBizAuditLog(project, actionCode, fromStatus, toStatus, null, reason); + if (ObjectActivityConstants.PROJECT_ACTION_AUTO_START.equals(actionCode)) { + syncActualStartDate(project); + } + } + + /** + * 项目实际开始日期口径: + * 1) 优先取项目下最早一条任务工作日志的 startDate; + * 2) 若无工作日志,则回退到首次 auto_start 状态日志的 createTime 所在日期; + * 3) 两者都没有时不更新。 + */ + private void syncActualStartDate(ProjectDO project) { + if (project == null || project.getId() == null) { + return; + } + LocalDate targetDate = resolveActualStartDate(project.getId()); + if (targetDate == null || Objects.equals(targetDate, project.getActualStartDate())) { + return; + } + projectMapper.updateActualDatesById(project.getId(), targetDate, null); + project.setActualStartDate(targetDate); + } + + private LocalDate resolveActualStartDate(Long projectId) { + LocalDate earliestWorklogDate = taskWorklogMapper.selectEarliestStartDateByProjectId(projectId); + if (earliestWorklogDate != null) { + return earliestWorklogDate; + } + ProjectStatusLogDO autoStartLog = projectStatusLogMapper + .selectEarliestByProjectIdAndActionType(projectId, ObjectActivityConstants.PROJECT_ACTION_AUTO_START); + if (autoStartLog == null || autoStartLog.getCreateTime() == null) { + return null; + } + return autoStartLog.getCreateTime().toLocalDate(); } private ProjectContextProjectRespVO buildCurrentProject(ProjectDO project) { diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/task/ProjectTaskService.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/task/ProjectTaskService.java index 25db686..2ef9efb 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/task/ProjectTaskService.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/task/ProjectTaskService.java @@ -9,8 +9,6 @@ import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTask import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskStatusActionReqVO; import com.njcn.rdms.module.project.dal.dataobject.project.task.ProjectTaskDO; -import java.time.LocalDate; - /** * 项目任务 Service 接口。 */ @@ -89,7 +87,7 @@ public interface ProjectTaskService { /** * 由工时填报(5.11)触发的任务自动开始:当任务当前处于初始态(initialFlag=true)时, * 走一次 {@link com.njcn.rdms.module.project.constant.ObjectActivityConstants#TASK_ACTION_AUTO_START} - * 动作把任务推进到下一状态,并复用 5.5 的钩子写入 actualStartDate / 状态日志 / 审计日志。 + * 动作把任务推进到下一状态,并写状态日志 / 审计日志。 *

* 调用方必须已自行确认登录人是 task.ownerId(仅 owner 工时驱动状态推进,与 progressRate 同步口径一致)。 * 本方法**不走** @CheckObjectPermission,因为 5.11 持有的是 PERMISSION_WORKLOG, @@ -101,16 +99,15 @@ public interface ProjectTaskService { void internalAutoStartByWorklog(ProjectTaskDO task); /** - * 工作日志填报触发:当任务"实际开始时间"为空时,用给定的开始日期回填,使"任务何时开始" - * 以最早一条工作日志的开始日期为准。owner / 协办人共用,不限任务当前状态。 + * 按工作日志事实回算任务实际开始/结束日期: + *

    + *
  • actualStartDate = 该任务最早一条工作日志的 startDate(不区分填报人)
  • + *
  • actualEndDate = 该任务负责人本人所填、progressRate=100 的最晚一条工作日志 endDate
  • + *
*

- * 与 {@link #internalAutoStartByWorklog} 解耦:本方法只回填 actualStartDate(不动实际结束日期、 - * 不推进任务状态)。调用顺序应在 internalAutoStartByWorklog 之前,确保后者的回填钩子见到非空值即跳过, - * 不会再用"提交当天"覆盖工作日志的开始日期。 - *

- * 任务实际开始时间已有值、或入参为空时静默 return。 + * 当日志被删除或改动后,本方法也负责把对应字段清空或改写为新的回算结果。 */ - void fillActualStartDateIfAbsent(Long taskId, LocalDate startDate); + void syncActualDatesFromWorklogs(Long taskId); /** * 由执行 cancel 触发的级联:取消指定执行下所有未终态的顶层任务(parentTaskId IS NULL)。 diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/task/ProjectTaskServiceImpl.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/task/ProjectTaskServiceImpl.java index e8cf554..451a565 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/task/ProjectTaskServiceImpl.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/task/ProjectTaskServiceImpl.java @@ -65,7 +65,6 @@ import org.springframework.util.StringUtils; import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDate; -import java.time.ZoneId; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -90,8 +89,6 @@ import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil @Slf4j public class ProjectTaskServiceImpl implements ProjectTaskService { - private static final ZoneId SERVER_ZONE = ZoneId.of("Asia/Shanghai"); - @Resource private ProjectMapper projectMapper; @Resource @@ -447,7 +444,6 @@ public class ProjectTaskServiceImpl implements ProjectTaskService { writeTaskStatusLog(current, actionCode, fromStatus, toStatus, reason); writeTaskAuditLog(current, actionCode, fromStatus, toStatus, null, reason); - maybeFillActualDates(current, fromStatus, toStatus); // 通知执行:任务自动开始也属于"任务状态变更"事件,按 §6.4 触发表上报 projectExecutionService.onTaskStatusChanged( current.getExecutionId(), current.getId(), fromStatus, toStatus, actionCode, reason); @@ -455,17 +451,25 @@ public class ProjectTaskServiceImpl implements ProjectTaskService { @Override @Transactional(rollbackFor = Exception.class) - public void fillActualStartDateIfAbsent(Long taskId, LocalDate startDate) { - if (taskId == null || startDate == null) { + public void syncActualDatesFromWorklogs(Long taskId) { + if (taskId == null) { return; } - ProjectTaskDO current = projectTaskMapper.selectById(taskId); - // 仅在实际开始时间为空时回填;已有值(含 internalAutoStartByWorklog 已写过)则保持不变 - if (current == null || current.getActualStartDate() != null) { + ProjectTaskDO task = projectTaskMapper.selectById(taskId); + if (task == null) { return; } - // 只动开始日期:实际结束日期传 null,依据全局 FieldStrategy 不会被覆盖 - projectTaskMapper.updateActualDatesById(taskId, startDate, null); + TaskWorklogDO earliestWorklog = taskWorklogMapper.selectEarliestByTaskId(taskId); + TaskWorklogDO latestOwnerCompletedWorklog = task.getOwnerId() == null + ? null + : taskWorklogMapper.selectLatestCompletedByTaskIdAndUserId(taskId, task.getOwnerId()); + LocalDate actualStartDate = earliestWorklog == null ? null : earliestWorklog.getStartDate(); + LocalDate actualEndDate = latestOwnerCompletedWorklog == null ? null : latestOwnerCompletedWorklog.getEndDate(); + if (Objects.equals(task.getActualStartDate(), actualStartDate) + && Objects.equals(task.getActualEndDate(), actualEndDate)) { + return; + } + projectTaskMapper.updateActualDatesById(taskId, actualStartDate, actualEndDate); } @Override @@ -728,41 +732,6 @@ public class ProjectTaskServiceImpl implements ProjectTaskService { || "resume".equals(actionCode); } - /** - * 根据通用状态语义位推导实际开始/结束日期: - * - 首次离开初始态(fromStatus.initialFlag=true)且未填写时,写入 actualStartDate - * - 进入终态(toStatus.terminalFlag=true)且未填写时,写入 actualEndDate - */ - private void maybeFillActualDates(ProjectTaskDO task, String fromStatus, String toStatus) { - LocalDate today = LocalDate.now(SERVER_ZONE); - LocalDate newActualStart = null; - LocalDate newActualEnd = null; - if (task.getActualStartDate() == null) { - ObjectStatusModelDO fromModel = objectStatusModelMapper - .selectByObjectTypeAndStatusCodeEnabled(ProjectTaskConstants.OBJECT_TYPE, fromStatus); - if (fromModel != null && Boolean.TRUE.equals(fromModel.getInitialFlag())) { - newActualStart = today; - } - } - if (task.getActualEndDate() == null) { - ObjectStatusModelDO toModel = objectStatusModelMapper - .selectByObjectTypeAndStatusCodeEnabled(ProjectTaskConstants.OBJECT_TYPE, toStatus); - if (toModel != null && Boolean.TRUE.equals(toModel.getTerminalFlag())) { - newActualEnd = today; - } - } - if (newActualStart == null && newActualEnd == null) { - return; - } - projectTaskMapper.updateActualDatesById(task.getId(), newActualStart, newActualEnd); - if (newActualStart != null) { - task.setActualStartDate(newActualStart); - } - if (newActualEnd != null) { - task.setActualEndDate(newActualEnd); - } - } - private ProjectDO validateEditableProject(Long projectId) { ProjectDO project = validateProjectExists(projectId); ObjectStatusModelDO statusModel = objectStatusModelMapper @@ -916,7 +885,6 @@ public class ProjectTaskServiceImpl implements ProjectTaskService { writeTaskStatusLog(task, actionCode, fromStatus, toStatus, reason); writeTaskAuditLog(task, actionCode, fromStatus, toStatus, null, reason); - maybeFillActualDates(task, fromStatus, toStatus); // 完成动作:兜底把任务进度刷到 100%,并触发父任务 AVG 重算(forceCompleteProgress 内部触发项目刷新) if ("complete".equals(actionCode)) { diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/task/worklog/TaskWorklogServiceImpl.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/task/worklog/TaskWorklogServiceImpl.java index 235e0ff..f6bdfc8 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/task/worklog/TaskWorklogServiceImpl.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/task/worklog/TaskWorklogServiceImpl.java @@ -27,6 +27,7 @@ import com.njcn.rdms.module.project.enums.ProjectDictTypeConstants; import com.njcn.rdms.module.project.framework.attachment.AttachmentFileIdResolver; import com.njcn.rdms.module.project.framework.attachment.AttachmentValidator; import com.njcn.rdms.module.project.framework.security.annotation.CheckObjectPermission; +import com.njcn.rdms.module.project.service.project.ProjectService; import com.njcn.rdms.module.project.service.project.task.ProjectTaskService; import com.njcn.rdms.module.system.api.dict.DictDataApi; import com.njcn.rdms.module.system.api.user.AdminUserApi; @@ -70,6 +71,8 @@ public class TaskWorklogServiceImpl implements TaskWorklogService { private DictDataApi dictDataApi; @Resource private AttachmentFileIdResolver attachmentFileIdResolver; + @Resource + private ProjectService projectService; /** * 与 ProjectTaskService 互相依赖(ProjectTaskService 也注入本类),用 @Lazy 打破循环。 * 仅用于 owner 工时变更后同步任务自身进度并触发父任务汇总。 @@ -118,10 +121,8 @@ public class TaskWorklogServiceImpl implements TaskWorklogService { TaskWorklogDO worklog = buildWorklog(taskId, loginUserId, reqVO); taskWorklogMapper.insert(worklog); - // 实际开始时间为空时,用本条工作日志的开始日期回填(owner / 协办人一致,不限任务当前状态)。 - // 必须在 internalAutoStartByWorklog 之前:先把开始日期落库,自动开始里的回填钩子见到非空值即跳过, - // 不会再用"提交当天"覆盖工作日志填写的开始日期。 - projectTaskService.fillActualStartDateIfAbsent(taskId, reqVO.getStartDate()); + projectTaskService.syncActualDatesFromWorklogs(taskId); + projectService.syncActualStartDateFromWorklogs(projectId); // 任意填报人(owner / 协办人)都触发任务自动开始(仅当任务仍处初始态时生效),推进任务状态。 // 任务"是否开始"是客观事实,协办人开工同样代表任务已开始,不应等 owner 补工时才反映。 projectTaskService.internalAutoStartByWorklog(task); @@ -165,6 +166,8 @@ public class TaskWorklogServiceImpl implements TaskWorklogService { update.setWorkContent(normalizeNullableText(reqVO.getWorkContent())); update.setAttachments(reqVO.getAttachments()); taskWorklogMapper.updateById(update); + projectTaskService.syncActualDatesFromWorklogs(taskId); + projectService.syncActualStartDateFromWorklogs(projectId); // owner 改自己工时:按"owner 最新一条"重算任务进度 if (Objects.equals(worklog.getUserId(), task.getOwnerId())) { projectTaskService.syncOwnerProgressFromLatestWorklog(taskId); @@ -186,6 +189,8 @@ public class TaskWorklogServiceImpl implements TaskWorklogService { throw exception(ErrorCodeConstants.PROJECT_TASK_WORKLOG_DELETE_FORBIDDEN); } taskWorklogMapper.deleteById(worklog.getId()); + projectTaskService.syncActualDatesFromWorklogs(taskId); + projectService.syncActualStartDateFromWorklogs(projectId); // 删的是 owner 自己的工时:按"owner 剩余最新一条"重算任务进度; // 若 owner 已无任何工时,syncOwnerProgressFromLatestWorklog 内部会保留原值不动(不归零) if (Objects.equals(worklog.getUserId(), task.getOwnerId())) { diff --git a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/framework/file/core/client/s3/S3FileClient.java b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/framework/file/core/client/s3/S3FileClient.java index 65064b7..3efffd1 100644 --- a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/framework/file/core/client/s3/S3FileClient.java +++ b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/framework/file/core/client/s3/S3FileClient.java @@ -114,9 +114,10 @@ public class S3FileClient extends AbstractFileClient { @Override public String presignGetUrl(String url, Integer expirationSeconds) { - // 1. 将 url 转换为 path - String path = StrUtil.removePrefix(url, config.getDomain() + "/"); - path = HttpUtils.decodeUtf8(HttpUtils.removeUrlQuery(path)); + // 1. 将入参转换为对象 key + // - upload(...) 这里传进来的是原始 path(未编码),绝不能先 decode + // - 历史记录 / 对外接口传进来的可能是完整 URL(可能已编码),这时才需要 decode + String path = resolveObjectKey(url); // 2.1 情况一:公开访问:无需签名 // 考虑到老版本的兼容,所以必须是 config.getEnablePublicAccess() 为 false 时,才进行签名 @@ -134,6 +135,23 @@ public class S3FileClient extends AbstractFileClient { return signedUrl.toString(); } + private String resolveObjectKey(String urlOrPath) { + String cleaned = HttpUtils.removeUrlQuery(urlOrPath); + if (!HttpUtil.isHttp(cleaned) && !HttpUtil.isHttps(cleaned)) { + return cleaned; + } + + String path = StrUtil.removePrefix(cleaned, config.getDomain() + "/"); + if (StrUtil.equals(path, cleaned)) { + URI uri = URI.create(cleaned); + path = StrUtil.removePrefix(StrUtil.nullToEmpty(uri.getPath()), "/"); + if (Boolean.TRUE.equals(config.getEnablePathStyleAccess())) { + path = StrUtil.removePrefix(path, config.getBucket() + "/"); + } + } + return HttpUtils.decodeUtf8(path); + } + /** * 基于 bucket + endpoint 构建访问的 Domain 地址 *