feat(project): 新增工作台我的任务功能并优化团队负载统计

- 新增 MyTaskController 提供跨项目的我的任务分页查询接口
- 实现个人事项和任务的团队负载统计功能,支持临期/逾期计数
- 优化任务状态视图服务,支持批量加载生命周期视图
- 新增多用户工时周聚合查询功能
- 完善相关 VO 类定义和数据库映射配置
- 添加单元测试验证批量加载和权限过滤逻辑
This commit is contained in:
2026-06-12 19:50:02 +08:00
parent 54bcf7d8ae
commit 41fe5aa5ca
24 changed files with 1700 additions and 267 deletions

View File

@@ -0,0 +1,57 @@
package com.njcn.rdms.module.project.controller.admin.project.project;
import com.njcn.rdms.framework.common.pojo.CommonResult;
import com.njcn.rdms.module.project.controller.admin.project.project.vo.workbench.MyWorklogWeekRespVO;
import com.njcn.rdms.module.project.controller.admin.project.project.vo.workbench.TeamLoadRespVO;
import com.njcn.rdms.module.project.controller.admin.project.project.vo.workbench.TeamWorklogWeekRespVO;
import com.njcn.rdms.module.project.service.project.MyTeamService;
import com.njcn.rdms.module.project.service.project.MyWorklogService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.constraints.NotNull;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDate;
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - 工作台统计(团队负载/工时周聚合)")
@RestController
@RequestMapping("/project/project/me")
@Validated
public class MyWorkbenchController {
@Resource
private MyTeamService myTeamService;
@Resource
private MyWorklogService myWorklogService;
@GetMapping("/team-load")
@Operation(summary = "团队负载统计(团队=自己+管理链路当前生效直接下级)")
public CommonResult<TeamLoadRespVO> getTeamLoad() {
return success(myTeamService.getTeamLoad());
}
@GetMapping("/worklog-week")
@Operation(summary = "我的工时周聚合(逐日为均摊推算值;weekStart 任意日期自动归一到周一)")
public CommonResult<MyWorklogWeekRespVO> getMyWorklogWeek(
@RequestParam("weekStart")
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) @NotNull(message = "weekStart 不能为空") LocalDate weekStart) {
return success(myWorklogService.getMyWorklogWeek(weekStart));
}
@GetMapping("/team-worklog-week")
@Operation(summary = "团队工时周聚合(成员集合与 team-load 同口径)")
public CommonResult<TeamWorklogWeekRespVO> getTeamWorklogWeek(
@RequestParam("weekStart")
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) @NotNull(message = "weekStart 不能为空") LocalDate weekStart) {
return success(myWorklogService.getTeamWorklogWeek(weekStart));
}
}

View File

@@ -0,0 +1,37 @@
package com.njcn.rdms.module.project.controller.admin.project.project.vo.workbench;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
@Schema(description = "管理后台 - 工作台「我的工时周聚合」Response VO")
@Data
public class MyWorklogWeekRespVO {
@Schema(description = "所选周周一", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-06-08")
private LocalDate weekStart;
@Schema(description = "周一~周五逐日工时(固定 5 元素,均摊推算值,保留 2 位小数)", requiredMode = Schema.RequiredMode.REQUIRED)
private List<BigDecimal> dailyHours;
@Schema(description = "本周工时按归属分布(hours 降序,personal/other 排在项目后)", requiredMode = Schema.RequiredMode.REQUIRED)
private List<DistributionItemVO> distribution;
@Schema(description = "工时分布项")
@Data
public static class DistributionItemVO {
@Schema(description = "项目编号;kind != project 时为 null", example = "1923456789012345678")
@JsonSerialize(using = ToStringSerializer.class)
private Long projectId;
@Schema(description = "项目名称;kind != project 时为 null", example = "收银台 V3")
private String projectName;
@Schema(description = "归属:project=项目任务 / personal=个人事项 / other=无法归类", requiredMode = Schema.RequiredMode.REQUIRED, example = "project")
private String kind;
@Schema(description = "本周工时合计(保留 2 位小数)", requiredMode = Schema.RequiredMode.REQUIRED, example = "12.5")
private BigDecimal hours;
}
}

View File

@@ -0,0 +1,47 @@
package com.njcn.rdms.module.project.controller.admin.project.project.vo.workbench;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Schema(description = "管理后台 - 工作台「团队负载」Response VO")
@Data
public class TeamLoadRespVO {
@Schema(description = "团队成员负载列表;members[0] 恒为当前用户,其余按压力降序", requiredMode = Schema.RequiredMode.REQUIRED)
private List<MemberVO> members;
@Schema(description = "单个成员的负载")
@Data
public static class MemberVO {
@Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1001")
@JsonSerialize(using = ToStringSerializer.class)
private Long userId;
@Schema(description = "用户昵称", example = "张三")
private String userNickname;
@Schema(description = "未完成任务按归属分布(项目任务行 + 个人事项行)", requiredMode = Schema.RequiredMode.REQUIRED)
private List<LoadItemVO> items;
@Schema(description = "临期数:今天 <= 计划结束 <= 今天+3,未完成(任务+个人事项)", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
private Integer dueSoonCount;
@Schema(description = "逾期数:计划结束 < 今天,未完成(任务+个人事项)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer overdueCount;
}
@Schema(description = "负载分布项")
@Data
public static class LoadItemVO {
@Schema(description = "项目编号;kind != project 时为 null", example = "1923456789012345678")
@JsonSerialize(using = ToStringSerializer.class)
private Long projectId;
@Schema(description = "项目名称;kind != project 时为 null", example = "收银台 V3")
private String projectName;
@Schema(description = "归属类型:project=项目任务 / personal=个人事项", requiredMode = Schema.RequiredMode.REQUIRED, example = "project")
private String kind;
@Schema(description = "未完成数", requiredMode = Schema.RequiredMode.REQUIRED, example = "4")
private Integer count;
}
}

View File

@@ -0,0 +1,32 @@
package com.njcn.rdms.module.project.controller.admin.project.project.vo.workbench;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDate;
import java.util.List;
@Schema(description = "管理后台 - 工作台「团队工时周聚合」Response VO")
@Data
public class TeamWorklogWeekRespVO {
@Schema(description = "所选周周一", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-06-08")
private LocalDate weekStart;
@Schema(description = "团队成员工时列表;members[0] 恒为当前用户;该周无填报的成员 items 为空数组", requiredMode = Schema.RequiredMode.REQUIRED)
private List<MemberVO> members;
@Schema(description = "单个成员的周工时")
@Data
public static class MemberVO {
@Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1001")
@JsonSerialize(using = ToStringSerializer.class)
private Long userId;
@Schema(description = "用户昵称", example = "张三")
private String userNickname;
@Schema(description = "本周工时按归属分布", requiredMode = Schema.RequiredMode.REQUIRED)
private List<MyWorklogWeekRespVO.DistributionItemVO> items;
}
}

View File

@@ -0,0 +1,39 @@
package com.njcn.rdms.module.project.controller.admin.project.task;
import com.njcn.rdms.framework.common.pojo.CommonResult;
import com.njcn.rdms.framework.common.pojo.PageParam;
import com.njcn.rdms.framework.common.pojo.PageResult;
import com.njcn.rdms.module.project.controller.admin.project.task.vo.mytask.MyTaskPageReqVO;
import com.njcn.rdms.module.project.controller.admin.project.task.vo.mytask.MyTaskRespVO;
import com.njcn.rdms.module.project.service.project.task.MyTaskService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - 工作台「我的任务」(跨项目)")
@RestController
@RequestMapping("/project/project/me/tasks")
@Validated
public class MyTaskController {
@Resource
private MyTaskService myTaskService;
@GetMapping("/page")
@Operation(summary = "分页获取当前登录用户负责或协办的非终态任务(跨项目)")
public CommonResult<PageResult<MyTaskRespVO>> getMyTaskPage(@Valid MyTaskPageReqVO reqVO) {
// 前端固定传 pageSize=-1 拉全部;负数统一归一为 PAGE_SIZE_NONE与 me 系列一致
if (reqVO.getPageSize() != null && reqVO.getPageSize() < 0) {
reqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
}
return success(myTaskService.getMyTaskPage(reqVO));
}
}

View File

@@ -0,0 +1,16 @@
package com.njcn.rdms.module.project.controller.admin.project.task.vo.mytask;
import com.njcn.rdms.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Schema(description = "管理后台 - 工作台「我的任务」分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class MyTaskPageReqVO extends PageParam {
@Schema(description = "参与类型过滤:owner=我负责 / collaborator=我协办;缺省=并集", example = "owner")
private String involveType;
}

View File

@@ -0,0 +1,57 @@
package com.njcn.rdms.module.project.controller.admin.project.task.vo.mytask;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskLifecycleActionRespVO;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "管理后台 - 工作台「我的任务」Response VO")
@Data
public class MyTaskRespVO {
@Schema(description = "任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1934567890123456789")
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
@Schema(description = "任务标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "支付回调接口联调遗留问题处理")
private String taskTitle;
@Schema(description = "所属项目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1923456789012345678")
@JsonSerialize(using = ToStringSerializer.class)
private Long projectId;
@Schema(description = "所属项目名称", example = "收银台 V3")
private String projectName;
@Schema(description = "所属执行编号(可空)", example = "1928888888888888888")
@JsonSerialize(using = ToStringSerializer.class)
private Long executionId;
@Schema(description = "所属执行名称(可空)", example = "后端联调")
private String executionName;
@Schema(description = "任务状态编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "active")
private String statusCode;
@Schema(description = "任务状态名称", example = "进行中")
private String statusName;
@Schema(description = "优先级(字典 rdms_req_priority 原样返回)", example = "1")
private String priority;
@Schema(description = "计划结束日期(可空)", example = "2026-06-15")
private LocalDate plannedEndDate;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
@Schema(description = "我的身份:owner=负责人 / collaborator=协办人;双重身份返回 owner", requiredMode = Schema.RequiredMode.REQUIRED, example = "owner")
private String myRole;
@Schema(description = "父任务编号(可空)", example = "1934567890123456788")
@JsonSerialize(using = ToStringSerializer.class)
private Long parentTaskId;
@Schema(description = "任务进度,范围 0-100", requiredMode = Schema.RequiredMode.REQUIRED, example = "60")
private BigDecimal progressRate;
@Schema(description = "是否终态", requiredMode = Schema.RequiredMode.REQUIRED, example = "false")
private Boolean terminal;
@Schema(description = "当前状态是否允许编辑", example = "true")
private Boolean allowEdit;
@Schema(description = "当前登录用户在当前状态下可执行的任务生命周期动作;无动作返回空数组")
private List<ProjectTaskLifecycleActionRespVO> availableActions;
}

View File

@@ -7,10 +7,14 @@ import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.njcn.rdms.module.project.controller.admin.personal.vo.item.PersonalItemPageReqVO;
import com.njcn.rdms.module.project.dal.dataobject.personal.PersonalItemDO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.springframework.util.StringUtils;
import java.time.LocalDate;
import java.util.Collection;
import java.util.List;
import java.util.Map;
@Mapper
public interface PersonalItemMapper extends BaseMapperX<PersonalItemDO> {
@@ -77,4 +81,37 @@ public interface PersonalItemMapper extends BaseMapperX<PersonalItemDO> {
queryWrapper.orderByAsc(PersonalItemDO::getId);
return selectList(queryWrapper);
}
/**
* 团队负载:一批成员的非终态个人事项计数(按 owner_id 分组),带临期/逾期。
* 个人事项状态机复用 task 对象类型,terminalStatusCodes 与任务同源。
* 返回 Map:ownerId / itemCount / dueSoonCount / overdueCount(均 Long)。
*/
@Select("""
<script>
SELECT owner_id AS ownerId,
CAST(COUNT(*) AS SIGNED) AS itemCount,
CAST(SUM(CASE WHEN planned_end_date IS NOT NULL
AND planned_end_date &gt;= #{today}
AND planned_end_date &lt;= #{dueSoonEnd}
THEN 1 ELSE 0 END) AS SIGNED) AS dueSoonCount,
CAST(SUM(CASE WHEN planned_end_date IS NOT NULL
AND planned_end_date &lt; #{today}
THEN 1 ELSE 0 END) AS SIGNED) AS overdueCount
FROM rdms_personal_item
WHERE deleted = b'0'
AND owner_id IN
<foreach collection="ownerIds" item="uid" open="(" separator="," close=")">#{uid}</foreach>
<if test="terminalStatusCodes != null and !terminalStatusCodes.isEmpty()">
AND status_code NOT IN
<foreach collection="terminalStatusCodes" item="tc" open="(" separator="," close=")">#{tc}</foreach>
</if>
GROUP BY owner_id
</script>
""")
List<Map<String, Object>> selectLoadStatsByOwnerIds(
@Param("ownerIds") Collection<Long> ownerIds,
@Param("terminalStatusCodes") Collection<String> terminalStatusCodes,
@Param("today") LocalDate today,
@Param("dueSoonEnd") LocalDate dueSoonEnd);
}

View File

@@ -859,4 +859,107 @@ public interface ProjectTaskMapper extends BaseMapperX<ProjectTaskDO> {
List<ProjectTaskDO> selectInvolvedListByUserIdAndStatusNot(@Param("userId") Long userId,
@Param("excludedStatusCode") String excludedStatusCode);
/**
* 工作台「我的任务」:当前用户为负责人或在岗协办人的非终态任务(跨项目)。
* involveTypeowner=仅我负责 / collaborator=仅我协办(排除我同时是负责人的)/ 其他或 null=并集。
* 排序计划结束升序、空值最后同日按创建时间升序id 兜底。
*/
@Select("""
<script>
SELECT t.*
FROM rdms_task t
<where>
t.deleted = b'0'
<choose>
<when test="involveType == 'owner'">
AND t.owner_id = #{userId}
</when>
<when test="involveType == 'collaborator'">
AND (t.owner_id IS NULL OR t.owner_id != #{userId})
AND EXISTS (
SELECT 1 FROM rdms_task_assignee a
WHERE a.task_id = t.id
AND a.user_id = #{userId}
AND a.removed_at IS NULL
AND a.deleted = b'0'
)
</when>
<otherwise>
AND (
t.owner_id = #{userId}
OR EXISTS (
SELECT 1 FROM rdms_task_assignee a
WHERE a.task_id = t.id
AND a.user_id = #{userId}
AND a.removed_at IS NULL
AND a.deleted = b'0'
)
)
</otherwise>
</choose>
<if test="terminalStatusCodes != null and !terminalStatusCodes.isEmpty()">
AND t.status_code NOT IN
<foreach collection="terminalStatusCodes" item="tc" open="(" separator="," close=")">#{tc}</foreach>
</if>
</where>
ORDER BY (t.planned_end_date IS NULL) ASC, t.planned_end_date ASC, t.create_time ASC, t.id ASC
</script>
""")
List<ProjectTaskDO> selectMyUnfinishedInvolvedList(@Param("userId") Long userId,
@Param("involveType") String involveType,
@Param("terminalStatusCodes") Collection<String> terminalStatusCodes);
/**
* 团队负载:一批成员的非终态任务计数,按 (成员, 项目) 分组,一次扫表带出临期/逾期。
* 身份口径与「我的任务」一致(负责人 在岗协办);UNION ALL 两个视角,
* 协办分支排除"自己同时是负责人"的行防双计(owner_id 为 NULL 时保留)。
* 临期:today <= planned_end_date <= dueSoonEnd;逾期:planned_end_date < today。
* 返回 Map:userId / projectId / taskCount / dueSoonCount / overdueCount(均 Long)。
*/
@Select("""
<script>
SELECT x.user_id AS userId,
x.project_id AS projectId,
CAST(COUNT(*) AS SIGNED) AS taskCount,
CAST(SUM(CASE WHEN x.planned_end_date IS NOT NULL
AND x.planned_end_date &gt;= #{today}
AND x.planned_end_date &lt;= #{dueSoonEnd}
THEN 1 ELSE 0 END) AS SIGNED) AS dueSoonCount,
CAST(SUM(CASE WHEN x.planned_end_date IS NOT NULL
AND x.planned_end_date &lt; #{today}
THEN 1 ELSE 0 END) AS SIGNED) AS overdueCount
FROM (
SELECT t.owner_id AS user_id, t.project_id, t.planned_end_date
FROM rdms_task t
WHERE t.deleted = b'0'
AND t.owner_id IN
<foreach collection="userIds" item="uid" open="(" separator="," close=")">#{uid}</foreach>
<if test="terminalStatusCodes != null and !terminalStatusCodes.isEmpty()">
AND t.status_code NOT IN
<foreach collection="terminalStatusCodes" item="tc" open="(" separator="," close=")">#{tc}</foreach>
</if>
UNION ALL
SELECT a.user_id, t.project_id, t.planned_end_date
FROM rdms_task t
JOIN rdms_task_assignee a ON a.task_id = t.id
AND a.removed_at IS NULL
AND a.deleted = b'0'
WHERE t.deleted = b'0'
AND a.user_id IN
<foreach collection="userIds" item="uid" open="(" separator="," close=")">#{uid}</foreach>
AND (t.owner_id IS NULL OR t.owner_id != a.user_id)
<if test="terminalStatusCodes != null and !terminalStatusCodes.isEmpty()">
AND t.status_code NOT IN
<foreach collection="terminalStatusCodes" item="tc" open="(" separator="," close=")">#{tc}</foreach>
</if>
) x
GROUP BY x.user_id, x.project_id
</script>
""")
List<Map<String, Object>> selectInvolvedLoadStatsByUserIds(
@Param("userIds") Collection<Long> userIds,
@Param("terminalStatusCodes") Collection<String> terminalStatusCodes,
@Param("today") LocalDate today,
@Param("dueSoonEnd") LocalDate dueSoonEnd);
}

View File

@@ -214,4 +214,19 @@ public interface TaskWorklogMapper extends BaseMapperX<TaskWorklogDO> {
.orderByAsc(TaskWorklogDO::getId));
}
/** selectListByUserIdAndPeriod 的多用户版,团队工时周聚合用。段相交语义相同。 */
default List<TaskWorklogDO> selectListByUserIdsAndPeriod(Collection<Long> userIds,
LocalDate startDate, LocalDate endDate) {
if (userIds == null || userIds.isEmpty() || startDate == null || endDate == null) {
return List.of();
}
return selectList(new LambdaQueryWrapperX<TaskWorklogDO>()
.in(TaskWorklogDO::getUserId, userIds)
.le(TaskWorklogDO::getStartDate, endDate)
.ge(TaskWorklogDO::getEndDate, startDate)
.orderByAsc(TaskWorklogDO::getUserId)
.orderByAsc(TaskWorklogDO::getEndDate)
.orderByAsc(TaskWorklogDO::getId));
}
}

View File

@@ -0,0 +1,22 @@
package com.njcn.rdms.module.project.service.project;
import com.njcn.rdms.module.project.controller.admin.project.project.vo.workbench.TeamLoadRespVO;
import java.util.List;
public interface MyTeamService {
/**
* 解析当前登录用户的团队成员集合(口径:自己 + 管理链路当前生效的直接下级)。
* 返回顺序:第 0 个恒为当前用户自己;昵称已回填。普通员工(无下级)只返回自己。
*/
List<TeamMember> resolveTeamMembers();
/** 工作台「团队负载」:每个团队成员的未完成任务/个人事项分布 + 临期/逾期。 */
TeamLoadRespVO getTeamLoad();
/** 团队成员(userId + 昵称)。 */
record TeamMember(Long userId, String nickname) {
}
}

View File

@@ -0,0 +1,162 @@
package com.njcn.rdms.module.project.service.project;
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
import com.njcn.rdms.module.project.constant.ProjectTaskConstants;
import com.njcn.rdms.module.project.controller.admin.project.project.vo.workbench.TeamLoadRespVO;
import com.njcn.rdms.module.project.dal.dataobject.project.ProjectDO;
import com.njcn.rdms.module.project.dal.mysql.personal.PersonalItemMapper;
import com.njcn.rdms.module.project.dal.mysql.project.ProjectMapper;
import com.njcn.rdms.module.project.dal.mysql.project.task.ProjectTaskMapper;
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusModelMapper;
import com.njcn.rdms.module.system.api.user.AdminUserApi;
import com.njcn.rdms.module.system.api.user.UserManagementRelationApi;
import com.njcn.rdms.module.system.api.user.dto.AdminUserRespDTO;
import com.njcn.rdms.module.system.api.user.dto.UserManagementRelationRespDTO;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
@Service
public class MyTeamServiceImpl implements MyTeamService {
/** 临期窗口天数:计划结束 <= 今天 + 3(2026-06-12 前端口径确认)。 */
private static final int DUE_SOON_DAYS = 3;
@Resource
private ProjectTaskMapper projectTaskMapper;
@Resource
private PersonalItemMapper personalItemMapper;
@Resource
private ProjectMapper projectMapper;
@Resource
private ObjectStatusModelMapper objectStatusModelMapper;
@Resource
private UserManagementRelationApi userManagementRelationApi;
@Resource
private AdminUserApi adminUserApi;
@Override
public List<TeamMember> resolveTeamMembers() {
Long me = SecurityFrameworkUtils.getLoginUserId();
// 管理链路 RPC 不过滤生效期(system 侧直接按 manager 查),这里按 effectiveFrom/Until 过滤当前生效
List<UserManagementRelationRespDTO> relations = userManagementRelationApi
.getRelationListByManagerUserId(me).getCheckedData();
LocalDateTime now = LocalDateTime.now();
LinkedHashSet<Long> userIds = new LinkedHashSet<>();
userIds.add(me);
if (relations != null) {
relations.stream()
.filter(r -> r.getSubordinateUserId() != null)
.filter(r -> r.getEffectiveFrom() == null || !r.getEffectiveFrom().isAfter(now))
.filter(r -> r.getEffectiveUntil() == null || r.getEffectiveUntil().isAfter(now))
.map(UserManagementRelationRespDTO::getSubordinateUserId)
.forEach(userIds::add);
}
Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(userIds);
return userIds.stream().map(uid -> {
AdminUserRespDTO user = userMap.get(uid);
return new TeamMember(uid, user == null ? null : user.getNickname());
}).collect(Collectors.toList());
}
@Override
public TeamLoadRespVO getTeamLoad() {
List<TeamMember> members = resolveTeamMembers();
List<Long> userIds = members.stream().map(TeamMember::userId).collect(Collectors.toList());
LocalDate today = LocalDate.now();
LocalDate dueSoonEnd = today.plusDays(DUE_SOON_DAYS);
// 个人事项状态机复用 task 对象类型,终态同源
List<String> terminal = objectStatusModelMapper
.selectTerminalStatusCodesByObjectTypeEnabled(ProjectTaskConstants.OBJECT_TYPE);
// 任务:按 (成员, 项目) 分组;个人事项:按成员分组。各一次扫表,无逐人查询
List<Map<String, Object>> taskRows = projectTaskMapper
.selectInvolvedLoadStatsByUserIds(userIds, terminal, today, dueSoonEnd);
List<Map<String, Object>> personalRows = personalItemMapper
.selectLoadStatsByOwnerIds(userIds, terminal, today, dueSoonEnd);
// 项目名批量回填
Set<Long> projectIds = taskRows.stream().map(r -> asLong(r.get("projectId")))
.filter(Objects::nonNull).collect(Collectors.toSet());
Map<Long, ProjectDO> projectMap = projectIds.isEmpty() ? Collections.emptyMap()
: projectMapper.selectBatchIds(projectIds).stream()
.collect(Collectors.toMap(ProjectDO::getId, Function.identity(), (a, b) -> a));
// 任务行按成员分组
Map<Long, List<Map<String, Object>>> taskRowsByUser = taskRows.stream()
.collect(Collectors.groupingBy(r -> asLong(r.get("userId")), LinkedHashMap::new, Collectors.toList()));
Map<Long, Map<String, Object>> personalRowByUser = personalRows.stream()
.collect(Collectors.toMap(r -> asLong(r.get("ownerId")), Function.identity(), (a, b) -> a));
// 组装每个成员
List<TeamLoadRespVO.MemberVO> memberVOs = members.stream().map(m -> {
TeamLoadRespVO.MemberVO vo = new TeamLoadRespVO.MemberVO();
vo.setUserId(m.userId());
vo.setUserNickname(m.nickname());
List<TeamLoadRespVO.LoadItemVO> items = new ArrayList<>();
long dueSoon = 0;
long overdue = 0;
for (Map<String, Object> row : taskRowsByUser.getOrDefault(m.userId(), Collections.emptyList())) {
TeamLoadRespVO.LoadItemVO item = new TeamLoadRespVO.LoadItemVO();
Long pid = asLong(row.get("projectId"));
item.setProjectId(pid);
ProjectDO project = pid == null ? null : projectMap.get(pid);
item.setProjectName(project == null ? null : project.getProjectName());
item.setKind("project");
item.setCount((int) unbox(row.get("taskCount")));
items.add(item);
dueSoon += unbox(row.get("dueSoonCount"));
overdue += unbox(row.get("overdueCount"));
}
// 项目行按 count 降序
items.sort(Comparator.comparing(TeamLoadRespVO.LoadItemVO::getCount,
Comparator.reverseOrder()));
Map<String, Object> personal = personalRowByUser.get(m.userId());
if (personal != null && unbox(personal.get("itemCount")) > 0) {
TeamLoadRespVO.LoadItemVO item = new TeamLoadRespVO.LoadItemVO();
item.setKind("personal");
item.setCount((int) unbox(personal.get("itemCount")));
items.add(item); // 个人事项行固定排在项目行之后
dueSoon += unbox(personal.get("dueSoonCount"));
overdue += unbox(personal.get("overdueCount"));
}
vo.setItems(items);
vo.setDueSoonCount((int) dueSoon);
vo.setOverdueCount((int) overdue);
return vo;
}).collect(Collectors.toList());
// members[0] 恒为自己;其余按压力(总未完成+临期+逾期)降序
TeamLoadRespVO.MemberVO self = memberVOs.get(0);
List<TeamLoadRespVO.MemberVO> rest = new ArrayList<>(memberVOs.subList(1, memberVOs.size()));
rest.sort(Comparator.comparingLong(MyTeamServiceImpl::pressure).reversed());
List<TeamLoadRespVO.MemberVO> ordered = new ArrayList<>();
ordered.add(self);
ordered.addAll(rest);
TeamLoadRespVO resp = new TeamLoadRespVO();
resp.setMembers(ordered);
return resp;
}
private static long pressure(TeamLoadRespVO.MemberVO vo) {
long total = vo.getItems().stream().mapToLong(TeamLoadRespVO.LoadItemVO::getCount).sum();
return total + vo.getDueSoonCount() + vo.getOverdueCount();
}
private static Long asLong(Object v) {
return v == null ? null : ((Number) v).longValue();
}
private static long unbox(Object v) {
return v == null ? 0L : ((Number) v).longValue();
}
}

View File

@@ -0,0 +1,16 @@
package com.njcn.rdms.module.project.service.project;
import com.njcn.rdms.module.project.controller.admin.project.project.vo.workbench.MyWorklogWeekRespVO;
import com.njcn.rdms.module.project.controller.admin.project.project.vo.workbench.TeamWorklogWeekRespVO;
import java.time.LocalDate;
public interface MyWorklogService {
/** 我的工时周聚合。weekStart 任意日期自动归一到所在周周一。 */
MyWorklogWeekRespVO getMyWorklogWeek(LocalDate weekStart);
/** 团队工时周聚合(成员集合与团队负载同口径)。 */
TeamWorklogWeekRespVO getTeamWorklogWeek(LocalDate weekStart);
}

View File

@@ -0,0 +1,168 @@
package com.njcn.rdms.module.project.service.project;
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
import com.njcn.rdms.module.project.controller.admin.project.project.vo.workbench.MyWorklogWeekRespVO;
import com.njcn.rdms.module.project.controller.admin.project.project.vo.workbench.TeamWorklogWeekRespVO;
import com.njcn.rdms.module.project.dal.dataobject.personal.PersonalItemDO;
import com.njcn.rdms.module.project.dal.dataobject.project.ProjectDO;
import com.njcn.rdms.module.project.dal.dataobject.project.task.ProjectTaskDO;
import com.njcn.rdms.module.project.dal.dataobject.project.task.TaskWorklogDO;
import com.njcn.rdms.module.project.dal.mysql.personal.PersonalItemMapper;
import com.njcn.rdms.module.project.dal.mysql.project.ProjectMapper;
import com.njcn.rdms.module.project.dal.mysql.project.task.ProjectTaskMapper;
import com.njcn.rdms.module.project.dal.mysql.project.task.TaskWorklogMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
@Service
public class MyWorklogServiceImpl implements MyWorklogService {
@Resource
private TaskWorklogMapper taskWorklogMapper;
@Resource
private ProjectTaskMapper projectTaskMapper;
@Resource
private PersonalItemMapper personalItemMapper;
@Resource
private ProjectMapper projectMapper;
@Resource
private MyTeamService myTeamService;
@Override
public MyWorklogWeekRespVO getMyWorklogWeek(LocalDate weekStart) {
Long me = SecurityFrameworkUtils.getLoginUserId();
LocalDate monday = MyWorklogWeekSupport.normalizeToMonday(weekStart);
// 查询区间含周末:周六/周日的段要兜底归周五,必须查回来
List<TaskWorklogDO> worklogs = taskWorklogMapper
.selectListByUserIdAndPeriod(me, monday, monday.plusDays(6));
// 逐日累计(scale=4)与每任务份额累计
Map<LocalDate, BigDecimal> daily = new LinkedHashMap<>();
Map<Long, BigDecimal> hoursByTask = new HashMap<>();
for (TaskWorklogDO w : worklogs) {
Map<LocalDate, BigDecimal> shares = MyWorklogWeekSupport
.apportionToWeek(w.getStartDate(), w.getEndDate(), w.getDurationHours(), monday);
for (Map.Entry<LocalDate, BigDecimal> e : shares.entrySet()) {
daily.merge(e.getKey(), e.getValue(), BigDecimal::add);
hoursByTask.merge(w.getTaskId(), e.getValue(), BigDecimal::add);
}
}
MyWorklogWeekRespVO resp = new MyWorklogWeekRespVO();
resp.setWeekStart(monday);
List<BigDecimal> dailyHours = new ArrayList<>(5);
for (int i = 0; i < 5; i++) {
dailyHours.add(daily.getOrDefault(monday.plusDays(i), BigDecimal.ZERO)
.setScale(2, RoundingMode.HALF_UP));
}
resp.setDailyHours(dailyHours);
resp.setDistribution(buildDistribution(hoursByTask));
return resp;
}
@Override
public TeamWorklogWeekRespVO getTeamWorklogWeek(LocalDate weekStart) {
LocalDate monday = MyWorklogWeekSupport.normalizeToMonday(weekStart);
List<MyTeamService.TeamMember> members = myTeamService.resolveTeamMembers();
List<Long> userIds = members.stream().map(MyTeamService.TeamMember::userId).collect(Collectors.toList());
List<TaskWorklogDO> worklogs = taskWorklogMapper
.selectListByUserIdsAndPeriod(userIds, monday, monday.plusDays(6));
// 每成员每任务的本周份额合计
Map<Long, Map<Long, BigDecimal>> hoursByUserAndTask = new LinkedHashMap<>();
for (TaskWorklogDO w : worklogs) {
BigDecimal weekTotal = MyWorklogWeekSupport
.apportionToWeek(w.getStartDate(), w.getEndDate(), w.getDurationHours(), monday)
.values().stream().reduce(BigDecimal.ZERO, BigDecimal::add);
if (weekTotal.signum() > 0) {
hoursByUserAndTask.computeIfAbsent(w.getUserId(), k -> new HashMap<>())
.merge(w.getTaskId(), weekTotal, BigDecimal::add);
}
}
TeamWorklogWeekRespVO resp = new TeamWorklogWeekRespVO();
resp.setWeekStart(monday);
resp.setMembers(members.stream().map(m -> {
TeamWorklogWeekRespVO.MemberVO vo = new TeamWorklogWeekRespVO.MemberVO();
vo.setUserId(m.userId());
vo.setUserNickname(m.nickname());
vo.setItems(buildDistribution(
hoursByUserAndTask.getOrDefault(m.userId(), Collections.emptyMap())));
return vo;
}).collect(Collectors.toList()));
return resp;
}
/**
* 把 taskId→本周工时 聚成归属分布:工时表的 task_id 既可能指向任务也可能指向个人事项
* (个人事项工时复用 rdms_task_worklog),先按任务表归 project,再按个人事项表归 personal,
* 都对不上归 other。排序:project 行按 hours 降序,personal/other 殿后。
* 被软删的任务/事项的残留工时归 other(两表主键同为雪花 id 全局不撞,不存在误归类)。
*/
private List<MyWorklogWeekRespVO.DistributionItemVO> buildDistribution(Map<Long, BigDecimal> hoursByTask) {
if (hoursByTask.isEmpty()) {
return Collections.emptyList();
}
Set<Long> taskIds = hoursByTask.keySet();
Map<Long, ProjectTaskDO> taskMap = projectTaskMapper.selectBatchIds(taskIds).stream()
.collect(Collectors.toMap(ProjectTaskDO::getId, Function.identity(), (a, b) -> a));
Set<Long> personalIds = taskIds.stream()
.filter(id -> !taskMap.containsKey(id)).collect(Collectors.toSet());
Set<Long> personalHit = personalIds.isEmpty() ? Collections.emptySet()
: personalItemMapper.selectBatchIds(personalIds).stream()
.map(PersonalItemDO::getId).collect(Collectors.toSet());
// 聚合:projectId→hours / personal / other
Map<Long, BigDecimal> hoursByProject = new LinkedHashMap<>();
BigDecimal personalHours = BigDecimal.ZERO;
BigDecimal otherHours = BigDecimal.ZERO;
for (Map.Entry<Long, BigDecimal> e : hoursByTask.entrySet()) {
ProjectTaskDO task = taskMap.get(e.getKey());
if (task != null) {
hoursByProject.merge(task.getProjectId(), e.getValue(), BigDecimal::add);
} else if (personalHit.contains(e.getKey())) {
personalHours = personalHours.add(e.getValue());
} else {
otherHours = otherHours.add(e.getValue());
}
}
Map<Long, ProjectDO> projectMap = hoursByProject.isEmpty() ? Collections.emptyMap()
: projectMapper.selectBatchIds(hoursByProject.keySet()).stream()
.collect(Collectors.toMap(ProjectDO::getId, Function.identity(), (a, b) -> a));
List<MyWorklogWeekRespVO.DistributionItemVO> items = new ArrayList<>();
hoursByProject.entrySet().stream()
.sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
.forEach(e -> {
MyWorklogWeekRespVO.DistributionItemVO item = new MyWorklogWeekRespVO.DistributionItemVO();
item.setProjectId(e.getKey());
ProjectDO project = projectMap.get(e.getKey());
item.setProjectName(project == null ? null : project.getProjectName());
item.setKind("project");
item.setHours(e.getValue().setScale(2, RoundingMode.HALF_UP));
items.add(item);
});
if (personalHours.signum() > 0) {
MyWorklogWeekRespVO.DistributionItemVO item = new MyWorklogWeekRespVO.DistributionItemVO();
item.setKind("personal");
item.setHours(personalHours.setScale(2, RoundingMode.HALF_UP));
items.add(item);
}
if (otherHours.signum() > 0) {
MyWorklogWeekRespVO.DistributionItemVO item = new MyWorklogWeekRespVO.DistributionItemVO();
item.setKind("other");
item.setHours(otherHours.setScale(2, RoundingMode.HALF_UP));
items.add(item);
}
return items;
}
}

View File

@@ -0,0 +1,63 @@
package com.njcn.rdms.module.project.service.project;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 工时周聚合的纯函数支撑:周一归一 + 按段均摊。
* 口径(2026-06-12 与用户确认):工时按段填报无逐日明细,均摊分母 = 段内工作日(周一~周五)数;
* 纯周末段全额兜底归段结束日前最近的工作日;份额 scale=4,最终展示由调用方聚合后 setScale(2)。
*/
final class MyWorklogWeekSupport {
private MyWorklogWeekSupport() {
}
/** 任意日期归一到所在 ISO 周的周一(weekStart 入参容错,不强制前端必须传周一)。 */
static LocalDate normalizeToMonday(LocalDate date) {
return date.with(DayOfWeek.MONDAY);
}
/**
* 把一笔按段填报的工时摊到所选周的工作日。
* 返回 [weekMonday, weekMonday+4] 内各工作日的份额(scale=4);无份额的日不出现在 map。
*
* <p>前置条件:weekMonday 必须已归一到 ISO 周的周一(调用方先调 {@link #normalizeToMonday})。
* 方法内不做防御归一,以便在调用方传入非周一时尽早暴露 bug。
*/
static Map<LocalDate, BigDecimal> apportionToWeek(LocalDate segStart, LocalDate segEnd,
BigDecimal hours, LocalDate weekMonday) {
Map<LocalDate, BigDecimal> result = new LinkedHashMap<>();
if (segStart == null || segEnd == null || hours == null || segStart.isAfter(segEnd)) {
return result;
}
LocalDate weekFriday = weekMonday.plusDays(4);
List<LocalDate> workdays = segStart.datesUntil(segEnd.plusDays(1))
.filter(d -> d.getDayOfWeek().getValue() <= 5)
.toList();
if (workdays.isEmpty()) {
// 纯周末段:全额归段结束日前最近的工作日(周六/周日 → 周五)
LocalDate fallback = segEnd;
while (fallback.getDayOfWeek().getValue() > 5) {
fallback = fallback.minusDays(1);
}
if (!fallback.isBefore(weekMonday) && !fallback.isAfter(weekFriday)) {
result.put(fallback, hours.setScale(4, RoundingMode.HALF_UP));
}
return result;
}
BigDecimal share = hours.divide(BigDecimal.valueOf(workdays.size()), 4, RoundingMode.HALF_UP);
for (LocalDate d : workdays) {
if (!d.isBefore(weekMonday) && !d.isAfter(weekFriday)) {
result.put(d, share);
}
}
return result;
}
}

View File

@@ -0,0 +1,12 @@
package com.njcn.rdms.module.project.service.project.task;
import com.njcn.rdms.framework.common.pojo.PageResult;
import com.njcn.rdms.module.project.controller.admin.project.task.vo.mytask.MyTaskPageReqVO;
import com.njcn.rdms.module.project.controller.admin.project.task.vo.mytask.MyTaskRespVO;
public interface MyTaskService {
/** 工作台「我的任务」:当前登录用户负责或在岗协办的非终态任务,跨项目分页。 */
PageResult<MyTaskRespVO> getMyTaskPage(MyTaskPageReqVO reqVO);
}

View File

@@ -0,0 +1,121 @@
package com.njcn.rdms.module.project.service.project.task;
import com.njcn.rdms.framework.common.pojo.PageParam;
import com.njcn.rdms.framework.common.pojo.PageResult;
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
import com.njcn.rdms.module.project.constant.ProjectTaskConstants;
import com.njcn.rdms.module.project.controller.admin.project.task.vo.mytask.MyTaskPageReqVO;
import com.njcn.rdms.module.project.controller.admin.project.task.vo.mytask.MyTaskRespVO;
import com.njcn.rdms.module.project.dal.dataobject.project.ProjectDO;
import com.njcn.rdms.module.project.dal.dataobject.project.execution.ProjectExecutionDO;
import com.njcn.rdms.module.project.dal.dataobject.project.task.ProjectTaskDO;
import com.njcn.rdms.module.project.dal.mysql.project.ProjectMapper;
import com.njcn.rdms.module.project.dal.mysql.project.execution.ProjectExecutionMapper;
import com.njcn.rdms.module.project.dal.mysql.project.task.ProjectTaskMapper;
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusModelMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
@Service
public class MyTaskServiceImpl implements MyTaskService {
@Resource
private ProjectTaskMapper projectTaskMapper;
@Resource
private ProjectMapper projectMapper;
@Resource
private ProjectExecutionMapper projectExecutionMapper;
@Resource
private ObjectStatusModelMapper objectStatusModelMapper;
@Resource
private ProjectTaskStatusViewService projectTaskStatusViewService;
@Override
public PageResult<MyTaskRespVO> getMyTaskPage(MyTaskPageReqVO reqVO) {
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
// 终态运行时推导SQL 内对空集有 <if> 守卫
List<String> taskTerminal = objectStatusModelMapper
.selectTerminalStatusCodesByObjectTypeEnabled(ProjectTaskConstants.OBJECT_TYPE);
List<ProjectTaskDO> tasks = projectTaskMapper
.selectMyUnfinishedInvolvedList(loginUserId, reqVO.getInvolveType(), taskTerminal);
if (tasks.isEmpty()) {
return new PageResult<>(Collections.emptyList(), 0L);
}
// 项目名 / 执行名批量回填
Set<Long> projectIds = tasks.stream().map(ProjectTaskDO::getProjectId)
.filter(Objects::nonNull).collect(Collectors.toSet());
Map<Long, ProjectDO> projectMap = projectIds.isEmpty() ? Collections.emptyMap()
: projectMapper.selectBatchIds(projectIds).stream()
.collect(Collectors.toMap(ProjectDO::getId, Function.identity(), (a, b) -> a));
Set<Long> executionIds = tasks.stream().map(ProjectTaskDO::getExecutionId)
.filter(Objects::nonNull).collect(Collectors.toSet());
Map<Long, ProjectExecutionDO> executionMap = executionIds.isEmpty() ? Collections.emptyMap()
: projectExecutionMapper.selectBatchIds(executionIds).stream()
.collect(Collectors.toMap(ProjectExecutionDO::getId, Function.identity(), (a, b) -> a));
List<MyTaskRespVO> all = tasks.stream().map(t -> {
MyTaskRespVO vo = new MyTaskRespVO();
vo.setId(t.getId());
vo.setTaskTitle(t.getTaskTitle());
vo.setProjectId(t.getProjectId());
ProjectDO project = projectMap.get(t.getProjectId());
vo.setProjectName(project == null ? null : project.getProjectName());
vo.setExecutionId(t.getExecutionId());
ProjectExecutionDO execution = executionMap.get(t.getExecutionId());
vo.setExecutionName(execution == null ? null : execution.getExecutionName());
vo.setStatusCode(t.getStatusCode());
vo.setPriority(t.getPriority());
vo.setPlannedEndDate(t.getPlannedEndDate());
vo.setCreateTime(t.getCreateTime());
vo.setParentTaskId(t.getParentTaskId());
vo.setProgressRate(normalizeProgress(t.getProgressRate()));
// SQL 已保证至少一种身份owner 优先(双重身份返 owner
vo.setMyRole(Objects.equals(t.getOwnerId(), loginUserId) ? "owner" : "collaborator");
return vo;
}).collect(Collectors.toList());
applyLifecycle(tasks, all);
return paginate(all, reqVO);
}
private void applyLifecycle(List<ProjectTaskDO> tasks, List<MyTaskRespVO> vos) {
Map<Long, ProjectTaskStatusViewService.ProjectTaskLifecycleView> lifecycleMap =
projectTaskStatusViewService.getLifecycleMap(tasks);
for (MyTaskRespVO vo : vos) {
ProjectTaskStatusViewService.ProjectTaskLifecycleView lifecycle = lifecycleMap.get(vo.getId());
if (lifecycle == null) {
vo.setAvailableActions(Collections.emptyList());
continue;
}
vo.setStatusName(lifecycle.statusName());
vo.setTerminal(lifecycle.terminal());
vo.setAllowEdit(lifecycle.allowEdit());
vo.setAvailableActions(lifecycle.availableActions());
}
}
private BigDecimal normalizeProgress(BigDecimal progressRate) {
return progressRate == null ? BigDecimal.ZERO : progressRate;
}
/** 内存分页,与 MyProjectServiceImpl#paginate 同款pageSize<0 拉全部)。 */
private <T> PageResult<T> paginate(List<T> all, PageParam reqVO) {
long total = all.size();
Integer pageSize = reqVO.getPageSize();
if (pageSize == null || pageSize < 0) {
return new PageResult<>(all, total);
}
int pageNo = reqVO.getPageNo() == null || reqVO.getPageNo() < 1 ? 1 : reqVO.getPageNo();
int fromIndex = Math.min((pageNo - 1) * pageSize, all.size());
int toIndex = Math.min(fromIndex + pageSize, all.size());
return new PageResult<>(all.subList(fromIndex, toIndex), total);
}
}

View File

@@ -4,6 +4,7 @@ import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
import com.njcn.rdms.module.project.constant.ObjectActivityConstants;
import com.njcn.rdms.module.project.constant.ProjectTaskConstants;
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskLifecycleActionRespVO;
import com.njcn.rdms.module.project.dal.dataobject.project.task.ProjectTaskDO;
import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusModelDO;
import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusTransitionDO;
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusModelMapper;
@@ -14,9 +15,13 @@ import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -62,14 +67,56 @@ public class ProjectTaskStatusViewService {
);
}
public Map<Long, ProjectTaskLifecycleView> getLifecycleMap(List<ProjectTaskDO> tasks) {
if (tasks == null || tasks.isEmpty()) {
return Collections.emptyMap();
}
Map<String, ObjectStatusModelDO> statusModelMap = objectStatusModelMapper
.selectListByObjectTypeEnabled(ProjectTaskConstants.OBJECT_TYPE).stream()
.collect(Collectors.toMap(ObjectStatusModelDO::getStatusCode, Function.identity(), (a, b) -> a));
List<String> statusCodes = tasks.stream()
.map(ProjectTaskDO::getStatusCode)
.filter(Objects::nonNull)
.distinct()
.toList();
Map<String, List<ObjectStatusTransitionDO>> transitionMap = statusCodes.isEmpty()
? Collections.emptyMap()
: objectStatusTransitionMapper
.selectListByObjectTypeAndFromStatuses(ProjectTaskConstants.OBJECT_TYPE, statusCodes).stream()
.collect(Collectors.groupingBy(ObjectStatusTransitionDO::getFromStatusCode));
Long currentUserId = SecurityFrameworkUtils.getLoginUserId();
Map<Long, ProjectTaskLifecycleView> result = new LinkedHashMap<>();
for (ProjectTaskDO task : tasks) {
ObjectStatusModelDO statusModel = statusModelMap.get(task.getStatusCode());
if (statusModel == null) {
throw exception(ErrorCodeConstants.PROJECT_TASK_STATUS_MODEL_NOT_EXISTS_OR_DISABLED);
}
result.put(task.getId(), new ProjectTaskLifecycleView(
statusModel.getStatusName(),
statusModel.getTerminalFlag(),
statusModel.getAllowEdit(),
buildAvailableActions(
transitionMap.getOrDefault(task.getStatusCode(), Collections.emptyList()),
task.getOwnerId(), task.getProgressRate(), currentUserId)
));
}
return result;
}
private List<ProjectTaskLifecycleActionRespVO> buildAvailableActions(String statusCode, Long ownerId,
BigDecimal progressRate) {
List<ObjectStatusTransitionDO> transitions = objectStatusTransitionMapper
.selectListByObjectTypeAndFromStatus(ProjectTaskConstants.OBJECT_TYPE, statusCode);
return buildAvailableActions(transitions, ownerId, progressRate, SecurityFrameworkUtils.getLoginUserId());
}
private List<ProjectTaskLifecycleActionRespVO> buildAvailableActions(List<ObjectStatusTransitionDO> transitions,
Long ownerId,
BigDecimal progressRate,
Long currentUserId) {
if (transitions == null || transitions.isEmpty()) {
return Collections.emptyList();
}
Long currentUserId = SecurityFrameworkUtils.getLoginUserId();
return transitions.stream()
// 剔除系统级动作 auto_start由工时填报触发不暴露给前端按钮
.filter(transition -> !ObjectActivityConstants.TASK_ACTION_AUTO_START.equals(transition.getActionCode()))