Compare commits
3 Commits
2026-06
...
796c79d58f
| Author | SHA1 | Date | |
|---|---|---|---|
| 796c79d58f | |||
| 66c187f3de | |||
| 9e502e6a80 |
@@ -76,6 +76,13 @@ public class ProjectRequirementController {
|
|||||||
return success(requirementService.getRequirementTree(pageReqVO));
|
return success(requirementService.getRequirementTree(pageReqVO));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/execution-options")
|
||||||
|
@Operation(summary = "获取执行可关联的项目需求下拉选项")
|
||||||
|
public CommonResult<PageResult<ProjectRequirementRespVO>> getExecutionRequirementOptions(
|
||||||
|
@Valid ProjectRequirementPageReqVO pageReqVO) {
|
||||||
|
return success(requirementService.getExecutionRequirementOptions(pageReqVO));
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/change-status")
|
@PostMapping("/change-status")
|
||||||
@Operation(summary = "变更需求状态")
|
@Operation(summary = "变更需求状态")
|
||||||
public CommonResult<Boolean> changeRequirementStatus(@Valid @RequestBody ProjectRequirementStatusActionReqVO reqVO) {
|
public CommonResult<Boolean> changeRequirementStatus(@Valid @RequestBody ProjectRequirementStatusActionReqVO reqVO) {
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ public class MonthlyReportApproveReqVO extends WorkReportStatusActionReqVO {
|
|||||||
|
|
||||||
@Schema(description = "面谈时间")
|
@Schema(description = "面谈时间")
|
||||||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY)
|
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY)
|
||||||
@NotNull
|
|
||||||
private LocalDate meetingDate;
|
private LocalDate meetingDate;
|
||||||
|
|
||||||
@Schema(description = "优势描述")
|
@Schema(description = "优势描述")
|
||||||
|
|||||||
@@ -114,6 +114,20 @@ public interface ProjectMapper extends BaseMapperX<ProjectDO> {
|
|||||||
.eq(ProjectDO::getId, id));
|
.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<ProjectDO>()
|
||||||
|
.eq(ProjectDO::getId, id));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 临期/逾期告警候选:计划完成日非空且 <= maxPlannedEndDate,状态不在排除集(终态+paused)。
|
* 临期/逾期告警候选:计划完成日非空且 <= maxPlannedEndDate,状态不在排除集(终态+paused)。
|
||||||
* excludeStatusCodes 为空时不排除任何状态(空集守卫,与既有口径一致)。
|
* excludeStatusCodes 为空时不排除任何状态(空集守卫,与既有口径一致)。
|
||||||
|
|||||||
@@ -22,20 +22,28 @@ public interface ProjectRequirementMapper extends BaseMapperX<ProjectRequirement
|
|||||||
* 分页查询需求列表
|
* 分页查询需求列表
|
||||||
*/
|
*/
|
||||||
default PageResult<ProjectRequirementDO> selectPage(ProjectRequirementPageReqVO reqVO) {
|
default PageResult<ProjectRequirementDO> selectPage(ProjectRequirementPageReqVO reqVO) {
|
||||||
return selectPage(reqVO, buildQueryWrapper(reqVO));
|
return selectPage(reqVO, buildQueryWrapper(reqVO, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询需求列表,并排除指定状态集合。
|
||||||
|
*/
|
||||||
|
default PageResult<ProjectRequirementDO> selectPage(ProjectRequirementPageReqVO reqVO, Collection<String> excludeStatusCodes) {
|
||||||
|
return selectPage(reqVO, buildQueryWrapper(reqVO, excludeStatusCodes));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询所有符合条件的需求列表,用于树查询
|
* 查询所有符合条件的需求列表,用于树查询
|
||||||
*/
|
*/
|
||||||
default List<ProjectRequirementDO> selectList(ProjectRequirementPageReqVO reqVO) {
|
default List<ProjectRequirementDO> selectList(ProjectRequirementPageReqVO reqVO) {
|
||||||
return selectList(buildQueryWrapper(reqVO));
|
return selectList(buildQueryWrapper(reqVO, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构建查询条件
|
* 构建查询条件
|
||||||
*/
|
*/
|
||||||
private LambdaQueryWrapperX<ProjectRequirementDO> buildQueryWrapper(ProjectRequirementPageReqVO reqVO) {
|
private LambdaQueryWrapperX<ProjectRequirementDO> buildQueryWrapper(ProjectRequirementPageReqVO reqVO,
|
||||||
|
Collection<String> excludeStatusCodes) {
|
||||||
LambdaQueryWrapperX<ProjectRequirementDO> queryWrapper = new LambdaQueryWrapperX<>();
|
LambdaQueryWrapperX<ProjectRequirementDO> queryWrapper = new LambdaQueryWrapperX<>();
|
||||||
if (StringUtils.hasText(reqVO.getTitle())) {
|
if (StringUtils.hasText(reqVO.getTitle())) {
|
||||||
queryWrapper.like(ProjectRequirementDO::getTitle, reqVO.getTitle());
|
queryWrapper.like(ProjectRequirementDO::getTitle, reqVO.getTitle());
|
||||||
@@ -49,8 +57,11 @@ public interface ProjectRequirementMapper extends BaseMapperX<ProjectRequirement
|
|||||||
.inIfPresent(ProjectRequirementDO::getModuleId, reqVO.getModuleIds())
|
.inIfPresent(ProjectRequirementDO::getModuleId, reqVO.getModuleIds())
|
||||||
.eqIfPresent(ProjectRequirementDO::getModuleId, reqVO.getModuleId())
|
.eqIfPresent(ProjectRequirementDO::getModuleId, reqVO.getModuleId())
|
||||||
.eqIfPresent(ProjectRequirementDO::getParentId, reqVO.getParentId())
|
.eqIfPresent(ProjectRequirementDO::getParentId, reqVO.getParentId())
|
||||||
.eq(ProjectRequirementDO::getProjectId, reqVO.getProjectId())
|
.eq(ProjectRequirementDO::getProjectId, reqVO.getProjectId());
|
||||||
.orderByAsc(ProjectRequirementDO::getSort)
|
if (excludeStatusCodes != null && !excludeStatusCodes.isEmpty()) {
|
||||||
|
queryWrapper.notIn(ProjectRequirementDO::getStatusCode, excludeStatusCodes);
|
||||||
|
}
|
||||||
|
queryWrapper.orderByAsc(ProjectRequirementDO::getSort)
|
||||||
.orderByDesc(ProjectRequirementDO::getCreateTime);
|
.orderByDesc(ProjectRequirementDO::getCreateTime);
|
||||||
return queryWrapper;
|
return queryWrapper;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,19 @@ import java.util.List;
|
|||||||
@Mapper
|
@Mapper
|
||||||
public interface ProjectStatusLogMapper extends BaseMapperX<ProjectStatusLogDO> {
|
public interface ProjectStatusLogMapper extends BaseMapperX<ProjectStatusLogDO> {
|
||||||
|
|
||||||
|
default ProjectStatusLogDO selectEarliestByProjectIdAndActionType(Long projectId, String actionType) {
|
||||||
|
if (projectId == null || actionType == null || actionType.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
List<ProjectStatusLogDO> list = selectList(new LambdaQueryWrapperX<ProjectStatusLogDO>()
|
||||||
|
.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<ProjectStatusLogDO> selectListByProjectId(Long projectId, String actionType, LocalDateTime[] operateTime) {
|
default List<ProjectStatusLogDO> selectListByProjectId(Long projectId, String actionType, LocalDateTime[] operateTime) {
|
||||||
return selectList(new LambdaQueryWrapperX<ProjectStatusLogDO>()
|
return selectList(new LambdaQueryWrapperX<ProjectStatusLogDO>()
|
||||||
.eq(ProjectStatusLogDO::getProjectId, projectId)
|
.eq(ProjectStatusLogDO::getProjectId, projectId)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.njcn.rdms.module.project.dal.mysql.project.task;
|
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.core.metadata.IPage;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.njcn.rdms.framework.common.pojo.PageResult;
|
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||||
@@ -115,16 +116,15 @@ public interface ProjectTaskMapper extends BaseMapperX<ProjectTaskDO> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 仅更新实际开始/结束日期。null 字段依据全局 FieldStrategy 不会被覆盖。
|
* 仅更新实际开始/结束日期。显式 set,允许把字段清空为 null。
|
||||||
*/
|
*/
|
||||||
default int updateActualDatesById(Long id, LocalDate actualStartDate, LocalDate actualEndDate) {
|
default int updateActualDatesById(Long id, LocalDate actualStartDate, LocalDate actualEndDate) {
|
||||||
if (actualStartDate == null && actualEndDate == null) {
|
if (id == null) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
ProjectTaskDO update = new ProjectTaskDO();
|
return update(null, new LambdaUpdateWrapper<ProjectTaskDO>()
|
||||||
update.setActualStartDate(actualStartDate);
|
.set(ProjectTaskDO::getActualStartDate, actualStartDate)
|
||||||
update.setActualEndDate(actualEndDate);
|
.set(ProjectTaskDO::getActualEndDate, actualEndDate)
|
||||||
return update(update, new LambdaQueryWrapperX<ProjectTaskDO>()
|
|
||||||
.eq(ProjectTaskDO::getId, id));
|
.eq(ProjectTaskDO::getId, id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.njcn.rdms.module.project.dal.mysql.project.task;
|
package com.njcn.rdms.module.project.dal.mysql.project.task;
|
||||||
|
|
||||||
import com.njcn.rdms.framework.common.pojo.PageResult;
|
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.mapper.BaseMapperX;
|
||||||
import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
|
import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||||
import com.njcn.rdms.module.project.controller.admin.project.task.vo.worklog.TaskWorklogPageReqVO;
|
import com.njcn.rdms.module.project.controller.admin.project.task.vo.worklog.TaskWorklogPageReqVO;
|
||||||
@@ -58,6 +59,19 @@ public interface TaskWorklogMapper extends BaseMapperX<TaskWorklogDO> {
|
|||||||
""")
|
""")
|
||||||
BigDecimal sumDurationByTaskId(@Param("taskId") Long taskId);
|
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。
|
* 批量任务工时小时数汇总,返回 [{taskId, total}]。用于详情/分页装配避免 N+1。
|
||||||
*/
|
*/
|
||||||
@@ -83,6 +97,22 @@ public interface TaskWorklogMapper extends BaseMapperX<TaskWorklogDO> {
|
|||||||
.eq(TaskWorklogDO::getTaskId, taskId)) > 0;
|
.eq(TaskWorklogDO::getTaskId, taskId)) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务下最早一条工作日志(按 start_date asc, create_time asc, id asc)。
|
||||||
|
*/
|
||||||
|
default TaskWorklogDO selectEarliestByTaskId(Long taskId) {
|
||||||
|
if (taskId == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
List<TaskWorklogDO> list = selectList(new LambdaQueryWrapperX<TaskWorklogDO>()
|
||||||
|
.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)。
|
* 取指定用户在该任务下最新的一条工时(按 end_date desc, create_time desc, id desc)。
|
||||||
* 用于 owner 填报后回查"本人最新一条"以同步任务进度。
|
* 用于 owner 填报后回查"本人最新一条"以同步任务进度。
|
||||||
@@ -102,6 +132,24 @@ public interface TaskWorklogMapper extends BaseMapperX<TaskWorklogDO> {
|
|||||||
return list.isEmpty() ? null : list.get(0);
|
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<TaskWorklogDO> list = selectList(new LambdaQueryWrapperX<TaskWorklogDO>()
|
||||||
|
.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 取首行。
|
* 取该 (taskId, userId) 下 endDate 严格早于给定值的"最近一条",按 end_date desc, id desc 取首行。
|
||||||
* 用于进度单调性校验(与"前一段"比较)。excludeId 不为 null 时排除自身(更新场景)。
|
* 用于进度单调性校验(与"前一段"比较)。excludeId 不为 null 时排除自身(更新场景)。
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.njcn.rdms.module.project.framework.notify;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 站内信跳转参数常量。
|
||||||
|
*/
|
||||||
|
public final class NotifyJumpConstants {
|
||||||
|
|
||||||
|
private NotifyJumpConstants() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final String PARAM_JUMP_TYPE = "jumpType";
|
||||||
|
public static final String PARAM_PROJECT_ID = "projectId";
|
||||||
|
|
||||||
|
public static final String JUMP_TYPE_PROJECT = "project";
|
||||||
|
public static final String JUMP_TYPE_EXECUTION = "execution";
|
||||||
|
public static final String JUMP_TYPE_TASK = "task";
|
||||||
|
|
||||||
|
}
|
||||||
@@ -32,6 +32,8 @@ public interface ProjectRequirementService {
|
|||||||
|
|
||||||
PageResult<ProjectRequirementRespVO> getRequirementTree(ProjectRequirementPageReqVO pageReqVO);
|
PageResult<ProjectRequirementRespVO> getRequirementTree(ProjectRequirementPageReqVO pageReqVO);
|
||||||
|
|
||||||
|
PageResult<ProjectRequirementRespVO> getExecutionRequirementOptions(ProjectRequirementPageReqVO pageReqVO);
|
||||||
|
|
||||||
void changeRequirementStatus(ProjectRequirementStatusActionReqVO reqVO);
|
void changeRequirementStatus(ProjectRequirementStatusActionReqVO reqVO);
|
||||||
|
|
||||||
void changeRequirementStatusForReview(ProjectRequirementStatusActionReqVO reqVO);
|
void changeRequirementStatusForReview(ProjectRequirementStatusActionReqVO reqVO);
|
||||||
|
|||||||
@@ -255,21 +255,9 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService
|
|||||||
@Override
|
@Override
|
||||||
@CheckObjectPermission(objectType = PROJECT_OBJECT_TYPE, objectId = "#pageReqVO.projectId", accessible = true)
|
@CheckObjectPermission(objectType = PROJECT_OBJECT_TYPE, objectId = "#pageReqVO.projectId", accessible = true)
|
||||||
public PageResult<ProjectRequirementRespVO> getRequirementPage(ProjectRequirementPageReqVO pageReqVO) {
|
public PageResult<ProjectRequirementRespVO> getRequirementPage(ProjectRequirementPageReqVO pageReqVO) {
|
||||||
if (pageReqVO.getModuleId() != null) {
|
normalizePageModuleFilter(pageReqVO);
|
||||||
if (isAllRequirementsModule(pageReqVO.getModuleId())) {
|
|
||||||
pageReqVO.setModuleId(null);
|
|
||||||
} else {
|
|
||||||
List<Long> moduleIds = getAllModuleIdsWithChildren(pageReqVO.getModuleId(), pageReqVO.getProjectId());
|
|
||||||
pageReqVO.setModuleIds(moduleIds);
|
|
||||||
pageReqVO.setModuleId(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
PageResult<ProjectRequirementDO> pageResult = requirementMapper.selectPage(pageReqVO);
|
PageResult<ProjectRequirementDO> pageResult = requirementMapper.selectPage(pageReqVO);
|
||||||
Map<String, ObjectStatusModelDO> statusModelMap = getStatusModelMap();
|
return toRequirementRespPage(pageResult);
|
||||||
List<ProjectRequirementRespVO> list = pageResult.getList().stream()
|
|
||||||
.map(requirement -> buildRequirementRespVO(requirement, statusModelMap))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
return new PageResult<>(list, pageResult.getTotal());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -325,6 +313,16 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService
|
|||||||
return new PageResult<>(list, (long) total);
|
return new PageResult<>(list, (long) total);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@CheckObjectPermission(objectType = PROJECT_OBJECT_TYPE, objectId = "#pageReqVO.projectId", accessible = true)
|
||||||
|
public PageResult<ProjectRequirementRespVO> getExecutionRequirementOptions(ProjectRequirementPageReqVO pageReqVO) {
|
||||||
|
normalizePageModuleFilter(pageReqVO);
|
||||||
|
Set<String> excludedStatusCodes = new HashSet<>(getTerminalStatusCodes());
|
||||||
|
excludedStatusCodes.add(STATUS_REVIEW_REJECTED);
|
||||||
|
PageResult<ProjectRequirementDO> pageResult = requirementMapper.selectPage(pageReqVO, excludedStatusCodes);
|
||||||
|
return toRequirementRespPage(pageResult);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
@CheckObjectPermission(objectType = PROJECT_OBJECT_TYPE, objectId = "#reqVO.projectId",
|
@CheckObjectPermission(objectType = PROJECT_OBJECT_TYPE, objectId = "#reqVO.projectId",
|
||||||
@@ -749,6 +747,27 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService
|
|||||||
return childrenMap;
|
return childrenMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void normalizePageModuleFilter(ProjectRequirementPageReqVO pageReqVO) {
|
||||||
|
if (pageReqVO.getModuleId() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isAllRequirementsModule(pageReqVO.getModuleId())) {
|
||||||
|
pageReqVO.setModuleId(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<Long> moduleIds = getAllModuleIdsWithChildren(pageReqVO.getModuleId(), pageReqVO.getProjectId());
|
||||||
|
pageReqVO.setModuleIds(moduleIds);
|
||||||
|
pageReqVO.setModuleId(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PageResult<ProjectRequirementRespVO> toRequirementRespPage(PageResult<ProjectRequirementDO> pageResult) {
|
||||||
|
Map<String, ObjectStatusModelDO> statusModelMap = getStatusModelMap();
|
||||||
|
List<ProjectRequirementRespVO> list = pageResult.getList().stream()
|
||||||
|
.map(requirement -> buildRequirementRespVO(requirement, statusModelMap))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
return new PageResult<>(list, pageResult.getTotal());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 一次性加载当前对象类型下的状态模型,供列表和树查询复用。
|
* 一次性加载当前对象类型下的状态模型,供列表和树查询复用。
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -68,6 +68,12 @@ public interface ProjectService {
|
|||||||
|
|
||||||
void autoStartProjectIfPending(Long projectId, String triggerAction);
|
void autoStartProjectIfPending(Long projectId, String triggerAction);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 以项目下最早一条任务工作日志的 startDate 为优先口径,同步项目实际开始日期。
|
||||||
|
* 若项目下尚无任务工作日志,则回退到首次 auto_start 状态日志日期;两者都没有时不更新。
|
||||||
|
*/
|
||||||
|
void syncActualStartDateFromWorklogs(Long projectId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 重算项目进度并落库:AVG(项目下所有根任务 progressRate),沿用 task 维度的 progress_excluded 排除集合。
|
* 重算项目进度并落库:AVG(项目下所有根任务 progressRate),沿用 task 维度的 progress_excluded 排除集合。
|
||||||
* 由任务侧在"任务进度变化 / 状态变更 / 创建删除 / 父子结构变化"等入口触发。
|
* 由任务侧在"任务进度变化 / 状态变更 / 创建删除 / 父子结构变化"等入口触发。
|
||||||
|
|||||||
@@ -44,10 +44,12 @@ 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.ProjectRequirementModuleMapper;
|
||||||
import com.njcn.rdms.module.project.dal.mysql.project.ProjectStatusLogMapper;
|
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.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.ObjectStatusModelMapper;
|
||||||
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusTransitionMapper;
|
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusTransitionMapper;
|
||||||
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
|
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
|
||||||
import com.njcn.rdms.module.project.enums.ProjectDictTypeConstants;
|
import com.njcn.rdms.module.project.enums.ProjectDictTypeConstants;
|
||||||
|
import com.njcn.rdms.module.project.framework.notify.NotifyJumpConstants;
|
||||||
import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
|
import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
|
||||||
import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
|
import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||||
import com.njcn.rdms.module.project.service.datascope.ObjectDataScope;
|
import com.njcn.rdms.module.project.service.datascope.ObjectDataScope;
|
||||||
@@ -122,6 +124,8 @@ class ProjectServiceImpl implements ProjectService {
|
|||||||
@Resource
|
@Resource
|
||||||
private ProjectTaskMapper projectTaskMapper;
|
private ProjectTaskMapper projectTaskMapper;
|
||||||
@Resource
|
@Resource
|
||||||
|
private TaskWorklogMapper taskWorklogMapper;
|
||||||
|
@Resource
|
||||||
private com.njcn.rdms.module.project.dal.mysql.project.execution.ProjectExecutionMapper projectExecutionMapper;
|
private com.njcn.rdms.module.project.dal.mysql.project.execution.ProjectExecutionMapper projectExecutionMapper;
|
||||||
@Resource
|
@Resource
|
||||||
private com.njcn.rdms.module.project.dal.mysql.project.ProjectRequirementMapper projectRequirementMapper;
|
private com.njcn.rdms.module.project.dal.mysql.project.ProjectRequirementMapper projectRequirementMapper;
|
||||||
@@ -276,6 +280,8 @@ class ProjectServiceImpl implements ProjectService {
|
|||||||
Map<String, Object> params = new HashMap<>();
|
Map<String, Object> params = new HashMap<>();
|
||||||
params.put("projectName", project.getProjectName());
|
params.put("projectName", project.getProjectName());
|
||||||
params.put("roleName", r.roleName());
|
params.put("roleName", r.roleName());
|
||||||
|
params.put(NotifyJumpConstants.PARAM_JUMP_TYPE, NotifyJumpConstants.JUMP_TYPE_PROJECT);
|
||||||
|
params.put(NotifyJumpConstants.PARAM_PROJECT_ID, String.valueOf(project.getId()));
|
||||||
applicationEventPublisher.publishEvent(
|
applicationEventPublisher.publishEvent(
|
||||||
com.njcn.rdms.module.project.framework.notify.NotifySendEvent.of(
|
com.njcn.rdms.module.project.framework.notify.NotifySendEvent.of(
|
||||||
java.util.List.of(r.userId()),
|
java.util.List.of(r.userId()),
|
||||||
@@ -1065,6 +1071,19 @@ class ProjectServiceImpl implements ProjectService {
|
|||||||
changeStatus(project, transition, ObjectActivityConstants.PROJECT_ACTION_AUTO_START, reason);
|
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
|
@Override
|
||||||
public void recalcProgress(Long projectId) {
|
public void recalcProgress(Long projectId) {
|
||||||
if (projectId == null) {
|
if (projectId == null) {
|
||||||
@@ -1494,6 +1513,40 @@ class ProjectServiceImpl implements ProjectService {
|
|||||||
|
|
||||||
writeProjectStatusLog(project, actionCode, fromStatus, toStatus, reason);
|
writeProjectStatusLog(project, actionCode, fromStatus, toStatus, reason);
|
||||||
writeBizAuditLog(project, actionCode, fromStatus, toStatus, null, 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) {
|
private ProjectContextProjectRespVO buildCurrentProject(ProjectDO project) {
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ 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.dal.mysql.status.ObjectStatusTransitionMapper;
|
||||||
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
|
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
|
||||||
import com.njcn.rdms.module.project.enums.ProjectDictTypeConstants;
|
import com.njcn.rdms.module.project.enums.ProjectDictTypeConstants;
|
||||||
|
import com.njcn.rdms.module.project.framework.notify.NotifyJumpConstants;
|
||||||
import com.njcn.rdms.module.project.framework.notify.NotifySendEvent;
|
import com.njcn.rdms.module.project.framework.notify.NotifySendEvent;
|
||||||
import com.njcn.rdms.module.project.framework.notify.NotifyTemplateCodeConstants;
|
import com.njcn.rdms.module.project.framework.notify.NotifyTemplateCodeConstants;
|
||||||
import com.njcn.rdms.module.project.util.DueRangeSupport;
|
import com.njcn.rdms.module.project.util.DueRangeSupport;
|
||||||
@@ -193,6 +194,8 @@ public class ProjectExecutionServiceImpl implements ProjectExecutionService {
|
|||||||
ProjectDO project = projectMapper.selectById(execution.getProjectId());
|
ProjectDO project = projectMapper.selectById(execution.getProjectId());
|
||||||
params.put("projectName", project == null ? "" : project.getProjectName());
|
params.put("projectName", project == null ? "" : project.getProjectName());
|
||||||
params.put("executionName", execution.getExecutionName());
|
params.put("executionName", execution.getExecutionName());
|
||||||
|
params.put(NotifyJumpConstants.PARAM_JUMP_TYPE, NotifyJumpConstants.JUMP_TYPE_EXECUTION);
|
||||||
|
params.put(NotifyJumpConstants.PARAM_PROJECT_ID, String.valueOf(execution.getProjectId()));
|
||||||
applicationEventPublisher.publishEvent(NotifySendEvent.of(recipients,
|
applicationEventPublisher.publishEvent(NotifySendEvent.of(recipients,
|
||||||
NotifyTemplateCodeConstants.EXECUTION_ASSIGNED, params,
|
NotifyTemplateCodeConstants.EXECUTION_ASSIGNED, params,
|
||||||
NotifyMessageLevelConstants.NORMAL, SecurityFrameworkUtils.getLoginUserId()));
|
NotifyMessageLevelConstants.NORMAL, SecurityFrameworkUtils.getLoginUserId()));
|
||||||
|
|||||||
@@ -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.controller.admin.project.task.vo.ProjectTaskStatusActionReqVO;
|
||||||
import com.njcn.rdms.module.project.dal.dataobject.project.task.ProjectTaskDO;
|
import com.njcn.rdms.module.project.dal.dataobject.project.task.ProjectTaskDO;
|
||||||
|
|
||||||
import java.time.LocalDate;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 项目任务 Service 接口。
|
* 项目任务 Service 接口。
|
||||||
*/
|
*/
|
||||||
@@ -89,7 +87,7 @@ public interface ProjectTaskService {
|
|||||||
/**
|
/**
|
||||||
* 由工时填报(5.11)触发的任务自动开始:当任务当前处于初始态(initialFlag=true)时,
|
* 由工时填报(5.11)触发的任务自动开始:当任务当前处于初始态(initialFlag=true)时,
|
||||||
* 走一次 {@link com.njcn.rdms.module.project.constant.ObjectActivityConstants#TASK_ACTION_AUTO_START}
|
* 走一次 {@link com.njcn.rdms.module.project.constant.ObjectActivityConstants#TASK_ACTION_AUTO_START}
|
||||||
* 动作把任务推进到下一状态,并复用 5.5 的钩子写入 actualStartDate / 状态日志 / 审计日志。
|
* 动作把任务推进到下一状态,并写状态日志 / 审计日志。
|
||||||
* <p>
|
* <p>
|
||||||
* 调用方必须已自行确认登录人是 task.ownerId(仅 owner 工时驱动状态推进,与 progressRate 同步口径一致)。
|
* 调用方必须已自行确认登录人是 task.ownerId(仅 owner 工时驱动状态推进,与 progressRate 同步口径一致)。
|
||||||
* 本方法**不走** @CheckObjectPermission,因为 5.11 持有的是 PERMISSION_WORKLOG,
|
* 本方法**不走** @CheckObjectPermission,因为 5.11 持有的是 PERMISSION_WORKLOG,
|
||||||
@@ -101,16 +99,15 @@ public interface ProjectTaskService {
|
|||||||
void internalAutoStartByWorklog(ProjectTaskDO task);
|
void internalAutoStartByWorklog(ProjectTaskDO task);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 工作日志填报触发:当任务"实际开始时间"为空时,用给定的开始日期回填,使"任务何时开始"
|
* 按工作日志事实回算任务实际开始/结束日期:
|
||||||
* 以最早一条工作日志的开始日期为准。owner / 协办人共用,不限任务当前状态。
|
* <ul>
|
||||||
|
* <li>actualStartDate = 该任务最早一条工作日志的 startDate(不区分填报人)</li>
|
||||||
|
* <li>actualEndDate = 该任务负责人本人所填、progressRate=100 的最晚一条工作日志 endDate</li>
|
||||||
|
* </ul>
|
||||||
* <p>
|
* <p>
|
||||||
* 与 {@link #internalAutoStartByWorklog} 解耦:本方法只回填 actualStartDate(不动实际结束日期、
|
* 当日志被删除或改动后,本方法也负责把对应字段清空或改写为新的回算结果。
|
||||||
* 不推进任务状态)。调用顺序应在 internalAutoStartByWorklog 之前,确保后者的回填钩子见到非空值即跳过,
|
|
||||||
* 不会再用"提交当天"覆盖工作日志的开始日期。
|
|
||||||
* <p>
|
|
||||||
* 任务实际开始时间已有值、或入参为空时静默 return。
|
|
||||||
*/
|
*/
|
||||||
void fillActualStartDateIfAbsent(Long taskId, LocalDate startDate);
|
void syncActualDatesFromWorklogs(Long taskId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 由执行 cancel 触发的级联:取消指定执行下所有未终态的顶层任务(parentTaskId IS NULL)。
|
* 由执行 cancel 触发的级联:取消指定执行下所有未终态的顶层任务(parentTaskId IS NULL)。
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusTransitionMappe
|
|||||||
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
|
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
|
||||||
import com.njcn.rdms.module.project.framework.attachment.AttachmentFileIdResolver;
|
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.attachment.AttachmentValidator;
|
||||||
|
import com.njcn.rdms.module.project.framework.notify.NotifyJumpConstants;
|
||||||
import com.njcn.rdms.module.project.framework.notify.NotifySendEvent;
|
import com.njcn.rdms.module.project.framework.notify.NotifySendEvent;
|
||||||
import com.njcn.rdms.module.project.framework.notify.NotifyTemplateCodeConstants;
|
import com.njcn.rdms.module.project.framework.notify.NotifyTemplateCodeConstants;
|
||||||
import com.njcn.rdms.module.project.framework.security.annotation.CheckObjectPermission;
|
import com.njcn.rdms.module.project.framework.security.annotation.CheckObjectPermission;
|
||||||
@@ -65,7 +66,6 @@ import org.springframework.util.StringUtils;
|
|||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.math.RoundingMode;
|
import java.math.RoundingMode;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.ZoneId;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@@ -90,8 +90,6 @@ import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class ProjectTaskServiceImpl implements ProjectTaskService {
|
public class ProjectTaskServiceImpl implements ProjectTaskService {
|
||||||
|
|
||||||
private static final ZoneId SERVER_ZONE = ZoneId.of("Asia/Shanghai");
|
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private ProjectMapper projectMapper;
|
private ProjectMapper projectMapper;
|
||||||
@Resource
|
@Resource
|
||||||
@@ -210,6 +208,8 @@ public class ProjectTaskServiceImpl implements ProjectTaskService {
|
|||||||
ProjectDO project = projectMapper.selectById(projectId);
|
ProjectDO project = projectMapper.selectById(projectId);
|
||||||
params.put("projectName", project == null ? "" : project.getProjectName());
|
params.put("projectName", project == null ? "" : project.getProjectName());
|
||||||
params.put("taskName", task.getTaskTitle());
|
params.put("taskName", task.getTaskTitle());
|
||||||
|
params.put(NotifyJumpConstants.PARAM_JUMP_TYPE, NotifyJumpConstants.JUMP_TYPE_TASK);
|
||||||
|
params.put(NotifyJumpConstants.PARAM_PROJECT_ID, String.valueOf(projectId));
|
||||||
applicationEventPublisher.publishEvent(NotifySendEvent.of(recipients,
|
applicationEventPublisher.publishEvent(NotifySendEvent.of(recipients,
|
||||||
NotifyTemplateCodeConstants.TASK_ASSIGNED, params,
|
NotifyTemplateCodeConstants.TASK_ASSIGNED, params,
|
||||||
NotifyMessageLevelConstants.NORMAL, SecurityFrameworkUtils.getLoginUserId()));
|
NotifyMessageLevelConstants.NORMAL, SecurityFrameworkUtils.getLoginUserId()));
|
||||||
@@ -447,7 +447,6 @@ public class ProjectTaskServiceImpl implements ProjectTaskService {
|
|||||||
|
|
||||||
writeTaskStatusLog(current, actionCode, fromStatus, toStatus, reason);
|
writeTaskStatusLog(current, actionCode, fromStatus, toStatus, reason);
|
||||||
writeTaskAuditLog(current, actionCode, fromStatus, toStatus, null, reason);
|
writeTaskAuditLog(current, actionCode, fromStatus, toStatus, null, reason);
|
||||||
maybeFillActualDates(current, fromStatus, toStatus);
|
|
||||||
// 通知执行:任务自动开始也属于"任务状态变更"事件,按 §6.4 触发表上报
|
// 通知执行:任务自动开始也属于"任务状态变更"事件,按 §6.4 触发表上报
|
||||||
projectExecutionService.onTaskStatusChanged(
|
projectExecutionService.onTaskStatusChanged(
|
||||||
current.getExecutionId(), current.getId(), fromStatus, toStatus, actionCode, reason);
|
current.getExecutionId(), current.getId(), fromStatus, toStatus, actionCode, reason);
|
||||||
@@ -455,17 +454,25 @@ public class ProjectTaskServiceImpl implements ProjectTaskService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void fillActualStartDateIfAbsent(Long taskId, LocalDate startDate) {
|
public void syncActualDatesFromWorklogs(Long taskId) {
|
||||||
if (taskId == null || startDate == null) {
|
if (taskId == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ProjectTaskDO current = projectTaskMapper.selectById(taskId);
|
ProjectTaskDO task = projectTaskMapper.selectById(taskId);
|
||||||
// 仅在实际开始时间为空时回填;已有值(含 internalAutoStartByWorklog 已写过)则保持不变
|
if (task == null) {
|
||||||
if (current == null || current.getActualStartDate() != null) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 只动开始日期:实际结束日期传 null,依据全局 FieldStrategy 不会被覆盖
|
TaskWorklogDO earliestWorklog = taskWorklogMapper.selectEarliestByTaskId(taskId);
|
||||||
projectTaskMapper.updateActualDatesById(taskId, startDate, null);
|
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
|
@Override
|
||||||
@@ -728,41 +735,6 @@ public class ProjectTaskServiceImpl implements ProjectTaskService {
|
|||||||
|| "resume".equals(actionCode);
|
|| "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) {
|
private ProjectDO validateEditableProject(Long projectId) {
|
||||||
ProjectDO project = validateProjectExists(projectId);
|
ProjectDO project = validateProjectExists(projectId);
|
||||||
ObjectStatusModelDO statusModel = objectStatusModelMapper
|
ObjectStatusModelDO statusModel = objectStatusModelMapper
|
||||||
@@ -916,7 +888,6 @@ public class ProjectTaskServiceImpl implements ProjectTaskService {
|
|||||||
|
|
||||||
writeTaskStatusLog(task, actionCode, fromStatus, toStatus, reason);
|
writeTaskStatusLog(task, actionCode, fromStatus, toStatus, reason);
|
||||||
writeTaskAuditLog(task, actionCode, fromStatus, toStatus, null, reason);
|
writeTaskAuditLog(task, actionCode, fromStatus, toStatus, null, reason);
|
||||||
maybeFillActualDates(task, fromStatus, toStatus);
|
|
||||||
|
|
||||||
// 完成动作:兜底把任务进度刷到 100%,并触发父任务 AVG 重算(forceCompleteProgress 内部触发项目刷新)
|
// 完成动作:兜底把任务进度刷到 100%,并触发父任务 AVG 重算(forceCompleteProgress 内部触发项目刷新)
|
||||||
if ("complete".equals(actionCode)) {
|
if ("complete".equals(actionCode)) {
|
||||||
|
|||||||
@@ -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.AttachmentFileIdResolver;
|
||||||
import com.njcn.rdms.module.project.framework.attachment.AttachmentValidator;
|
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.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.project.service.project.task.ProjectTaskService;
|
||||||
import com.njcn.rdms.module.system.api.dict.DictDataApi;
|
import com.njcn.rdms.module.system.api.dict.DictDataApi;
|
||||||
import com.njcn.rdms.module.system.api.user.AdminUserApi;
|
import com.njcn.rdms.module.system.api.user.AdminUserApi;
|
||||||
@@ -70,6 +71,8 @@ public class TaskWorklogServiceImpl implements TaskWorklogService {
|
|||||||
private DictDataApi dictDataApi;
|
private DictDataApi dictDataApi;
|
||||||
@Resource
|
@Resource
|
||||||
private AttachmentFileIdResolver attachmentFileIdResolver;
|
private AttachmentFileIdResolver attachmentFileIdResolver;
|
||||||
|
@Resource
|
||||||
|
private ProjectService projectService;
|
||||||
/**
|
/**
|
||||||
* 与 ProjectTaskService 互相依赖(ProjectTaskService 也注入本类),用 @Lazy 打破循环。
|
* 与 ProjectTaskService 互相依赖(ProjectTaskService 也注入本类),用 @Lazy 打破循环。
|
||||||
* 仅用于 owner 工时变更后同步任务自身进度并触发父任务汇总。
|
* 仅用于 owner 工时变更后同步任务自身进度并触发父任务汇总。
|
||||||
@@ -118,10 +121,8 @@ public class TaskWorklogServiceImpl implements TaskWorklogService {
|
|||||||
|
|
||||||
TaskWorklogDO worklog = buildWorklog(taskId, loginUserId, reqVO);
|
TaskWorklogDO worklog = buildWorklog(taskId, loginUserId, reqVO);
|
||||||
taskWorklogMapper.insert(worklog);
|
taskWorklogMapper.insert(worklog);
|
||||||
// 实际开始时间为空时,用本条工作日志的开始日期回填(owner / 协办人一致,不限任务当前状态)。
|
projectTaskService.syncActualDatesFromWorklogs(taskId);
|
||||||
// 必须在 internalAutoStartByWorklog 之前:先把开始日期落库,自动开始里的回填钩子见到非空值即跳过,
|
projectService.syncActualStartDateFromWorklogs(projectId);
|
||||||
// 不会再用"提交当天"覆盖工作日志填写的开始日期。
|
|
||||||
projectTaskService.fillActualStartDateIfAbsent(taskId, reqVO.getStartDate());
|
|
||||||
// 任意填报人(owner / 协办人)都触发任务自动开始(仅当任务仍处初始态时生效),推进任务状态。
|
// 任意填报人(owner / 协办人)都触发任务自动开始(仅当任务仍处初始态时生效),推进任务状态。
|
||||||
// 任务"是否开始"是客观事实,协办人开工同样代表任务已开始,不应等 owner 补工时才反映。
|
// 任务"是否开始"是客观事实,协办人开工同样代表任务已开始,不应等 owner 补工时才反映。
|
||||||
projectTaskService.internalAutoStartByWorklog(task);
|
projectTaskService.internalAutoStartByWorklog(task);
|
||||||
@@ -165,6 +166,8 @@ public class TaskWorklogServiceImpl implements TaskWorklogService {
|
|||||||
update.setWorkContent(normalizeNullableText(reqVO.getWorkContent()));
|
update.setWorkContent(normalizeNullableText(reqVO.getWorkContent()));
|
||||||
update.setAttachments(reqVO.getAttachments());
|
update.setAttachments(reqVO.getAttachments());
|
||||||
taskWorklogMapper.updateById(update);
|
taskWorklogMapper.updateById(update);
|
||||||
|
projectTaskService.syncActualDatesFromWorklogs(taskId);
|
||||||
|
projectService.syncActualStartDateFromWorklogs(projectId);
|
||||||
// owner 改自己工时:按"owner 最新一条"重算任务进度
|
// owner 改自己工时:按"owner 最新一条"重算任务进度
|
||||||
if (Objects.equals(worklog.getUserId(), task.getOwnerId())) {
|
if (Objects.equals(worklog.getUserId(), task.getOwnerId())) {
|
||||||
projectTaskService.syncOwnerProgressFromLatestWorklog(taskId);
|
projectTaskService.syncOwnerProgressFromLatestWorklog(taskId);
|
||||||
@@ -186,6 +189,8 @@ public class TaskWorklogServiceImpl implements TaskWorklogService {
|
|||||||
throw exception(ErrorCodeConstants.PROJECT_TASK_WORKLOG_DELETE_FORBIDDEN);
|
throw exception(ErrorCodeConstants.PROJECT_TASK_WORKLOG_DELETE_FORBIDDEN);
|
||||||
}
|
}
|
||||||
taskWorklogMapper.deleteById(worklog.getId());
|
taskWorklogMapper.deleteById(worklog.getId());
|
||||||
|
projectTaskService.syncActualDatesFromWorklogs(taskId);
|
||||||
|
projectService.syncActualStartDateFromWorklogs(projectId);
|
||||||
// 删的是 owner 自己的工时:按"owner 剩余最新一条"重算任务进度;
|
// 删的是 owner 自己的工时:按"owner 剩余最新一条"重算任务进度;
|
||||||
// 若 owner 已无任何工时,syncOwnerProgressFromLatestWorklog 内部会保留原值不动(不归零)
|
// 若 owner 已无任何工时,syncOwnerProgressFromLatestWorklog 内部会保留原值不动(不归零)
|
||||||
if (Objects.equals(worklog.getUserId(), task.getOwnerId())) {
|
if (Objects.equals(worklog.getUserId(), task.getOwnerId())) {
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil
|
|||||||
public class WorkReportDefaultDraftService {
|
public class WorkReportDefaultDraftService {
|
||||||
|
|
||||||
private static final String WORK_ITEM_MY_ITEMS = "我的事项";
|
private static final String WORK_ITEM_MY_ITEMS = "我的事项";
|
||||||
|
private static final String WORKLOG_DAY_SEPARATOR = "[[WR_DAY_SPLIT]]";
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private ObjectStatusModelMapper objectStatusModelMapper;
|
private ObjectStatusModelMapper objectStatusModelMapper;
|
||||||
@@ -759,7 +760,7 @@ public class WorkReportDefaultDraftService {
|
|||||||
builder.append(lineTitle);
|
builder.append(lineTitle);
|
||||||
appendBracket(builder, priority, latestProgressRate, totalWorkHours);
|
appendBracket(builder, priority, latestProgressRate, totalWorkHours);
|
||||||
if (!workContents.isEmpty()) {
|
if (!workContents.isEmpty()) {
|
||||||
builder.append(":").append(String.join(";", workContents));
|
builder.append(":").append(String.join(WORKLOG_DAY_SEPARATOR, workContents));
|
||||||
}
|
}
|
||||||
return builder.toString();
|
return builder.toString();
|
||||||
}
|
}
|
||||||
@@ -860,7 +861,7 @@ public class WorkReportDefaultDraftService {
|
|||||||
builder.append("(").append(String.join(" / ", parts)).append(")");
|
builder.append("(").append(String.join(" / ", parts)).append(")");
|
||||||
}
|
}
|
||||||
if (weeklyMode && worklogAggregate.hasWorkContent()) {
|
if (weeklyMode && worklogAggregate.hasWorkContent()) {
|
||||||
builder.append(":").append(String.join(";", worklogAggregate.workContents()));
|
builder.append(":").append(String.join(WORKLOG_DAY_SEPARATOR, worklogAggregate.workContents()));
|
||||||
}
|
}
|
||||||
return builder.toString();
|
return builder.toString();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -811,6 +811,10 @@ class ProjectServiceImplTest extends BaseMockitoUnitTest {
|
|||||||
com.njcn.rdms.module.project.framework.notify.NotifySendEvent first = captor.getAllValues().get(0);
|
com.njcn.rdms.module.project.framework.notify.NotifySendEvent first = captor.getAllValues().get(0);
|
||||||
assertEquals("project_created", first.getTemplateCode());
|
assertEquals("project_created", first.getTemplateCode());
|
||||||
assertEquals(9L, first.getOperatorUserId());
|
assertEquals(9L, first.getOperatorUserId());
|
||||||
|
assertEquals(com.njcn.rdms.module.project.framework.notify.NotifyJumpConstants.JUMP_TYPE_PROJECT,
|
||||||
|
first.getParams().get(com.njcn.rdms.module.project.framework.notify.NotifyJumpConstants.PARAM_JUMP_TYPE));
|
||||||
|
assertEquals("10",
|
||||||
|
first.getParams().get(com.njcn.rdms.module.project.framework.notify.NotifyJumpConstants.PARAM_PROJECT_ID));
|
||||||
assertEquals("项目经理", first.getParams().get("roleName"));
|
assertEquals("项目经理", first.getParams().get("roleName"));
|
||||||
assertEquals("灿能项目", first.getParams().get("projectName"));
|
assertEquals("灿能项目", first.getParams().get("projectName"));
|
||||||
assertTrue(first.getUserIds().contains(5L));
|
assertTrue(first.getUserIds().contains(5L));
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ 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.dal.mysql.status.ObjectStatusTransitionMapper;
|
||||||
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
|
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
|
||||||
import com.njcn.rdms.module.project.enums.ProjectDictTypeConstants;
|
import com.njcn.rdms.module.project.enums.ProjectDictTypeConstants;
|
||||||
|
import com.njcn.rdms.module.project.framework.notify.NotifyJumpConstants;
|
||||||
import com.njcn.rdms.module.project.framework.notify.NotifySendEvent;
|
import com.njcn.rdms.module.project.framework.notify.NotifySendEvent;
|
||||||
import com.njcn.rdms.module.project.service.project.ProjectRequirementService;
|
import com.njcn.rdms.module.project.service.project.ProjectRequirementService;
|
||||||
import com.njcn.rdms.module.system.api.dict.DictDataApi;
|
import com.njcn.rdms.module.system.api.dict.DictDataApi;
|
||||||
@@ -919,6 +920,9 @@ class ProjectExecutionServiceImplTest extends BaseMockitoUnitTest {
|
|||||||
assertEquals("execution_assigned", ev.getTemplateCode());
|
assertEquals("execution_assigned", ev.getTemplateCode());
|
||||||
assertEquals(7L, ev.getOperatorUserId());
|
assertEquals(7L, ev.getOperatorUserId());
|
||||||
assertTrue(ev.getUserIds().containsAll(List.of(5L, 6L)));
|
assertTrue(ev.getUserIds().containsAll(List.of(5L, 6L)));
|
||||||
|
assertEquals(NotifyJumpConstants.JUMP_TYPE_EXECUTION,
|
||||||
|
ev.getParams().get(NotifyJumpConstants.PARAM_JUMP_TYPE));
|
||||||
|
assertEquals("10", ev.getParams().get(NotifyJumpConstants.PARAM_PROJECT_ID));
|
||||||
assertEquals("灿能项目", ev.getParams().get("projectName"));
|
assertEquals("灿能项目", ev.getParams().get("projectName"));
|
||||||
assertEquals("一阶段", ev.getParams().get("executionName"));
|
assertEquals("一阶段", ev.getParams().get("executionName"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import com.njcn.rdms.module.project.dal.dataobject.project.task.ProjectTaskDO;
|
|||||||
import com.njcn.rdms.module.project.dal.dataobject.project.task.TaskAssigneeDO;
|
import com.njcn.rdms.module.project.dal.dataobject.project.task.TaskAssigneeDO;
|
||||||
import com.njcn.rdms.module.project.dal.mysql.project.ProjectMapper;
|
import com.njcn.rdms.module.project.dal.mysql.project.ProjectMapper;
|
||||||
import com.njcn.rdms.module.project.dal.mysql.project.task.TaskAssigneeMapper;
|
import com.njcn.rdms.module.project.dal.mysql.project.task.TaskAssigneeMapper;
|
||||||
|
import com.njcn.rdms.module.project.framework.notify.NotifyJumpConstants;
|
||||||
import com.njcn.rdms.module.project.framework.notify.NotifySendEvent;
|
import com.njcn.rdms.module.project.framework.notify.NotifySendEvent;
|
||||||
import com.njcn.rdms.module.project.framework.notify.NotifyTemplateCodeConstants;
|
import com.njcn.rdms.module.project.framework.notify.NotifyTemplateCodeConstants;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
@@ -63,6 +64,9 @@ class ProjectTaskServiceImplTest extends BaseMockitoUnitTest {
|
|||||||
ArgumentCaptor<NotifySendEvent> captor = ArgumentCaptor.forClass(NotifySendEvent.class);
|
ArgumentCaptor<NotifySendEvent> captor = ArgumentCaptor.forClass(NotifySendEvent.class);
|
||||||
verify(applicationEventPublisher).publishEvent(captor.capture());
|
verify(applicationEventPublisher).publishEvent(captor.capture());
|
||||||
NotifySendEvent event = captor.getValue();
|
NotifySendEvent event = captor.getValue();
|
||||||
|
assertEquals(NotifyJumpConstants.JUMP_TYPE_TASK,
|
||||||
|
event.getParams().get(NotifyJumpConstants.PARAM_JUMP_TYPE));
|
||||||
|
assertEquals("50", event.getParams().get(NotifyJumpConstants.PARAM_PROJECT_ID));
|
||||||
assertEquals(NotifyTemplateCodeConstants.TASK_ASSIGNED, event.getTemplateCode());
|
assertEquals(NotifyTemplateCodeConstants.TASK_ASSIGNED, event.getTemplateCode());
|
||||||
assertTrue(event.getUserIds().contains(1L)); // 负责人
|
assertTrue(event.getUserIds().contains(1L)); // 负责人
|
||||||
assertTrue(event.getUserIds().contains(2L)); // 协办人
|
assertTrue(event.getUserIds().contains(2L)); // 协办人
|
||||||
|
|||||||
@@ -131,6 +131,13 @@ public class FileController {
|
|||||||
// https://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/1432/
|
// https://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/1432/
|
||||||
path = URLUtil.decode(path, StandardCharsets.UTF_8, false);
|
path = URLUtil.decode(path, StandardCharsets.UTF_8, false);
|
||||||
|
|
||||||
|
FileDO file = fileService.getFileByConfigIdAndPath(configId, path);
|
||||||
|
if (file == null) {
|
||||||
|
log.warn("[getFileContent][configId({}) path({}) 文件不存在]", configId, path);
|
||||||
|
response.setStatus(HttpStatus.NOT_FOUND.value());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 读取内容
|
// 读取内容
|
||||||
byte[] content = fileService.getFileContent(configId, path);
|
byte[] content = fileService.getFileContent(configId, path);
|
||||||
if (content == null) {
|
if (content == null) {
|
||||||
@@ -138,7 +145,7 @@ public class FileController {
|
|||||||
response.setStatus(HttpStatus.NOT_FOUND.value());
|
response.setStatus(HttpStatus.NOT_FOUND.value());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
writeAttachment(response, path, content);
|
writeAttachment(response, StrUtil.blankToDefault(file.getName(), file.getPath()), content);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/page")
|
@GetMapping("/page")
|
||||||
|
|||||||
@@ -114,9 +114,10 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String presignGetUrl(String url, Integer expirationSeconds) {
|
public String presignGetUrl(String url, Integer expirationSeconds) {
|
||||||
// 1. 将 url 转换为 path
|
// 1. 将入参转换为对象 key
|
||||||
String path = StrUtil.removePrefix(url, config.getDomain() + "/");
|
// - upload(...) 这里传进来的是原始 path(未编码),绝不能先 decode
|
||||||
path = HttpUtils.decodeUtf8(HttpUtils.removeUrlQuery(path));
|
// - 历史记录 / 对外接口传进来的可能是完整 URL(可能已编码),这时才需要 decode
|
||||||
|
String path = resolveObjectKey(url);
|
||||||
|
|
||||||
// 2.1 情况一:公开访问:无需签名
|
// 2.1 情况一:公开访问:无需签名
|
||||||
// 考虑到老版本的兼容,所以必须是 config.getEnablePublicAccess() 为 false 时,才进行签名
|
// 考虑到老版本的兼容,所以必须是 config.getEnablePublicAccess() 为 false 时,才进行签名
|
||||||
@@ -134,6 +135,23 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
|
|||||||
return signedUrl.toString();
|
return signedUrl.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String resolveObjectKey(String urlOrPath) {
|
||||||
|
if (!HttpUtil.isHttp(urlOrPath) && !HttpUtil.isHttps(urlOrPath)) {
|
||||||
|
return urlOrPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
String cleaned = HttpUtils.removeUrlQuery(urlOrPath);
|
||||||
|
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 地址
|
* 基于 bucket + endpoint 构建访问的 Domain 地址
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -63,6 +63,15 @@ public interface FileService {
|
|||||||
Long createFile(FileCreateReqVO createReqVO);
|
Long createFile(FileCreateReqVO createReqVO);
|
||||||
FileDO getFile(Long id);
|
FileDO getFile(Long id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按 (configId, path) 获取文件记录。
|
||||||
|
*
|
||||||
|
* @param configId 文件配置编号
|
||||||
|
* @param path 文件存储路径
|
||||||
|
* @return 文件记录,不存在返回 null
|
||||||
|
*/
|
||||||
|
FileDO getFileByConfigIdAndPath(Long configId, String path);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通过文件 URL 获得文件。
|
* 通过文件 URL 获得文件。
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.njcn.rdms.module.system.service.file;
|
|||||||
import cn.hutool.core.date.LocalDateTimeUtil;
|
import cn.hutool.core.date.LocalDateTimeUtil;
|
||||||
import cn.hutool.core.io.FileUtil;
|
import cn.hutool.core.io.FileUtil;
|
||||||
import cn.hutool.core.lang.Assert;
|
import cn.hutool.core.lang.Assert;
|
||||||
|
import cn.hutool.core.util.IdUtil;
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import cn.hutool.crypto.digest.DigestUtil;
|
import cn.hutool.crypto.digest.DigestUtil;
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
@@ -42,14 +43,6 @@ public class FileServiceImpl implements FileService {
|
|||||||
* 目的:按照日期,进行分目录
|
* 目的:按照日期,进行分目录
|
||||||
*/
|
*/
|
||||||
static boolean PATH_PREFIX_DATE_ENABLE = true;
|
static boolean PATH_PREFIX_DATE_ENABLE = true;
|
||||||
/**
|
|
||||||
* 上传文件的后缀,是否包含时间戳
|
|
||||||
* <p>
|
|
||||||
* 目的:保证文件的唯一性,避免覆盖
|
|
||||||
* 定制:可按需调整成 UUID、或者其他方式
|
|
||||||
*/
|
|
||||||
static boolean PATH_SUFFIX_TIMESTAMP_ENABLE = true;
|
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private FileConfigService fileConfigService;
|
private FileConfigService fileConfigService;
|
||||||
|
|
||||||
@@ -103,34 +96,27 @@ public class FileServiceImpl implements FileService {
|
|||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
String generateUploadPath(String name, String directory) {
|
String generateUploadPath(String name, String directory) {
|
||||||
// 1. 生成前缀、后缀
|
// 1. 生成前缀,并将存储文件名切换为安全 key:
|
||||||
|
// path 仅承担对象存储 key / 路径职责,不再复用原文件名,避免 %、&、中文等字符进入 URI 路径。
|
||||||
String prefix = null;
|
String prefix = null;
|
||||||
if (PATH_PREFIX_DATE_ENABLE) {
|
if (PATH_PREFIX_DATE_ENABLE) {
|
||||||
prefix = LocalDateTimeUtil.format(LocalDateTimeUtil.now(), PURE_DATE_PATTERN);
|
prefix = LocalDateTimeUtil.format(LocalDateTimeUtil.now(), PURE_DATE_PATTERN);
|
||||||
}
|
}
|
||||||
String suffix = null;
|
String ext = FileUtil.extName(name);
|
||||||
if (PATH_SUFFIX_TIMESTAMP_ENABLE) {
|
String storageName = IdUtil.fastSimpleUUID();
|
||||||
suffix = String.valueOf(System.currentTimeMillis());
|
if (StrUtil.isNotEmpty(ext)) {
|
||||||
|
storageName = storageName + StrUtil.DOT + ext;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2.1 先拼接 suffix 后缀
|
// 2. 先拼接 prefix 前缀
|
||||||
if (StrUtil.isNotEmpty(suffix)) {
|
|
||||||
String ext = FileUtil.extName(name);
|
|
||||||
if (StrUtil.isNotEmpty(ext)) {
|
|
||||||
name = FileUtil.mainName(name) + StrUtil.C_UNDERLINE + suffix + StrUtil.DOT + ext;
|
|
||||||
} else {
|
|
||||||
name = name + StrUtil.C_UNDERLINE + suffix;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 2.2 再拼接 prefix 前缀
|
|
||||||
if (StrUtil.isNotEmpty(prefix)) {
|
if (StrUtil.isNotEmpty(prefix)) {
|
||||||
name = prefix + StrUtil.SLASH + name;
|
storageName = prefix + StrUtil.SLASH + storageName;
|
||||||
}
|
}
|
||||||
// 2.3 最后拼接 directory 目录
|
// 3. 最后拼接 directory 目录
|
||||||
if (StrUtil.isNotEmpty(directory)) {
|
if (StrUtil.isNotEmpty(directory)) {
|
||||||
name = directory + StrUtil.SLASH + name;
|
storageName = directory + StrUtil.SLASH + storageName;
|
||||||
}
|
}
|
||||||
return name;
|
return storageName;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -166,6 +152,14 @@ public class FileServiceImpl implements FileService {
|
|||||||
return validateFileExists(id);
|
return validateFileExists(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FileDO getFileByConfigIdAndPath(Long configId, String path) {
|
||||||
|
if (configId == null || StrUtil.isBlank(path)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return fileMapper.selectByConfigIdAndPath(configId, path);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public FileDO getFileByUrl(String url) {
|
public FileDO getFileByUrl(String url) {
|
||||||
if (StrUtil.isBlank(url)) {
|
if (StrUtil.isBlank(url)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user