fix(工作报告、我的绩效、产品/项目、消息通知): 修复已知的一些问题。

This commit is contained in:
dk
2026-06-28 14:02:56 +08:00
parent 9a61de0273
commit e5b08e4728
9 changed files with 764 additions and 342 deletions

View File

@@ -13,7 +13,7 @@ public class TeamPerformanceSummaryRespVO {
private String periodMonthStart; private String periodMonthStart;
private String periodMonthEnd; private String periodMonthEnd;
private Integer totalSheetCount; private Integer expectedSheetCount;
private Integer pendingSendCount; private Integer pendingSendCount;
private Integer pendingConfirmCount; private Integer pendingConfirmCount;
private BigDecimal confirmedRate; private BigDecimal confirmedRate;
@@ -25,6 +25,7 @@ public class TeamPerformanceSummaryRespVO {
public static class PendingSendUser { public static class PendingSendUser {
private Long userId; private Long userId;
private String userNickname; private String userNickname;
private String periodMonth;
private Long managerUserId; private Long managerUserId;
private String managerName; private String managerName;
private Long sheetId; private Long sheetId;
@@ -35,6 +36,7 @@ public class TeamPerformanceSummaryRespVO {
public static class PendingConfirmUser { public static class PendingConfirmUser {
private Long userId; private Long userId;
private String userNickname; private String userNickname;
private String periodMonth;
private Long sheetId; private Long sheetId;
private LocalDateTime sentTime; private LocalDateTime sentTime;
} }

View File

@@ -20,4 +20,7 @@ public class TeamReportRemindReqVO {
@Schema(description = "催办用户 ID 列表;不传则催办全部待提交用户") @Schema(description = "催办用户 ID 列表;不传则催办全部待提交用户")
private List<Long> userIds; private List<Long> userIds;
@Schema(description = "项目 ID仅项目半月报按单个项目槽位催办时传入", example = "2042074259501088770")
private Long projectId;
} }

View File

@@ -7,6 +7,6 @@ import lombok.Data;
@Data @Data
public class TeamReportRemindRespVO { public class TeamReportRemindRespVO {
@Schema(description = "实际催办", requiredMode = Schema.RequiredMode.REQUIRED, example = "4") @Schema(description = "实际催办数", requiredMode = Schema.RequiredMode.REQUIRED, example = "4")
private Integer remindedCount; private Integer remindedCount;
} }

View File

@@ -17,24 +17,24 @@ public class TeamReportSummaryRespVO {
@Schema(description = "实际查询周期结束日期") @Schema(description = "实际查询周期结束日期")
private LocalDate periodEndDate; private LocalDate periodEndDate;
@Schema(description = "应填", requiredMode = Schema.RequiredMode.REQUIRED, example = "18") @Schema(description = "应填报告", requiredMode = Schema.RequiredMode.REQUIRED, example = "18")
private Integer totalShouldSubmit; private Integer expectedReportCount;
@Schema(description = "已提交", requiredMode = Schema.RequiredMode.REQUIRED, example = "12") @Schema(description = "已提交报告", requiredMode = Schema.RequiredMode.REQUIRED, example = "12")
private Integer submittedCount; private Integer submittedReportCount;
@Schema(description = "未提交", requiredMode = Schema.RequiredMode.REQUIRED, example = "6") @Schema(description = "未提交报告", requiredMode = Schema.RequiredMode.REQUIRED, example = "6")
private Integer unsubmittedCount; private Integer unsubmittedReportCount;
@Schema(description = "待审批", requiredMode = Schema.RequiredMode.REQUIRED, example = "5") @Schema(description = "待审批报告", requiredMode = Schema.RequiredMode.REQUIRED, example = "5")
private Integer pendingApprovalCount; private Integer pendingApprovalReportCount;
@Schema(description = "未提交人员列表") @Schema(description = "未提交报告槽位列表")
private List<PendingUser> unsubmittedUsers; private List<UnsubmittedReport> unsubmittedReports;
@Schema(description = "未提交人员") @Schema(description = "未提交报告槽位")
@Data @Data
public static class PendingUser { public static class UnsubmittedReport {
@Schema(description = "用户 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "2042074259501088770") @Schema(description = "用户 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "2042074259501088770")
@com.fasterxml.jackson.databind.annotation.JsonSerialize(using = ToStringSerializer.class) @com.fasterxml.jackson.databind.annotation.JsonSerialize(using = ToStringSerializer.class)
@@ -42,5 +42,19 @@ public class TeamReportSummaryRespVO {
@Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "戴坤") @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "戴坤")
private String userNickname; private String userNickname;
@Schema(description = "周期主键", requiredMode = Schema.RequiredMode.REQUIRED,
example = "weekly-2026-06-22-2026-06-28")
private String periodKey;
@Schema(description = "周期名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-06-22 至 2026-06-28")
private String periodLabel;
@Schema(description = "项目 ID仅项目半月报返回", example = "2042074259501088770")
@com.fasterxml.jackson.databind.annotation.JsonSerialize(using = ToStringSerializer.class)
private Long projectId;
@Schema(description = "项目名称,仅项目半月报返回", example = "RDMS")
private String projectName;
} }
} }

View File

@@ -1,5 +1,7 @@
package com.njcn.rdms.module.project.dal.dataobject.product; package com.njcn.rdms.module.project.dal.dataobject.product;
import com.baomidou.mybatisplus.annotation.FieldStrategy;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO; import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
@@ -42,6 +44,7 @@ public class ProductDO extends BaseDO {
/** /**
* 产品描述 * 产品描述
*/ */
@TableField(updateStrategy = FieldStrategy.ALWAYS)
private String description; private String description;
/** /**
* 最近一次状态动作原因 * 最近一次状态动作原因

View File

@@ -1,5 +1,7 @@
package com.njcn.rdms.module.project.dal.dataobject.project; package com.njcn.rdms.module.project.dal.dataobject.project;
import com.baomidou.mybatisplus.annotation.FieldStrategy;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO; import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
@@ -81,6 +83,7 @@ public class ProjectDO extends BaseDO {
/** /**
* 项目说明 * 项目说明
*/ */
@TableField(updateStrategy = FieldStrategy.ALWAYS)
private String projectDesc; private String projectDesc;
/** /**
* 最近一次状态动作原因 * 最近一次状态动作原因

View File

@@ -28,6 +28,10 @@ public class NotifyTemplateCodeConstants {
/** 工作报告团队催办:主管催办下属提交指定周期工作报告 */ /** 工作报告团队催办:主管催办下属提交指定周期工作报告 */
public static final String WORK_REPORT_TEAM_REMIND = "work_report_team_remind"; public static final String WORK_REPORT_TEAM_REMIND = "work_report_team_remind";
/** 项目半月报团队催办:主管按项目槽位催办下属提交项目半月报 */
public static final String WORK_REPORT_PROJECT_HALF_MONTH_TEAM_REMIND =
"work_report_project_half_month_team_remind";
/** 执行指派:创建执行后通知负责人 + 协办人 */ /** 执行指派:创建执行后通知负责人 + 协办人 */
public static final String EXECUTION_ASSIGNED = "execution_assigned"; public static final String EXECUTION_ASSIGNED = "execution_assigned";

View File

@@ -1,7 +1,6 @@
package com.njcn.rdms.module.project.service.performance.team; package com.njcn.rdms.module.project.service.performance.team;
import com.njcn.rdms.framework.common.pojo.CommonResult; import com.njcn.rdms.framework.common.pojo.CommonResult;
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
import com.njcn.rdms.module.project.constant.PerformanceConstants; import com.njcn.rdms.module.project.constant.PerformanceConstants;
import com.njcn.rdms.module.project.controller.admin.performance.team.vo.TeamPerformanceRemindReqVO; import com.njcn.rdms.module.project.controller.admin.performance.team.vo.TeamPerformanceRemindReqVO;
import com.njcn.rdms.module.project.controller.admin.performance.team.vo.TeamPerformanceRemindRespVO; import com.njcn.rdms.module.project.controller.admin.performance.team.vo.TeamPerformanceRemindRespVO;
@@ -12,6 +11,8 @@ import com.njcn.rdms.module.project.dal.mysql.performance.PerformanceSheetMapper
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.service.team.TeamDashboardAccessService; import com.njcn.rdms.module.project.service.team.TeamDashboardAccessService;
import com.njcn.rdms.module.system.api.dept.DeptApi;
import com.njcn.rdms.module.system.api.dept.dto.DeptRespDTO;
import com.njcn.rdms.module.system.api.user.AdminUserApi; 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.UserManagementRelationApi;
import com.njcn.rdms.module.system.api.user.dto.AdminUserRespDTO; import com.njcn.rdms.module.system.api.user.dto.AdminUserRespDTO;
@@ -48,6 +49,8 @@ public class TeamPerformanceServiceImpl implements TeamPerformanceService {
@Resource @Resource
private AdminUserApi adminUserApi; private AdminUserApi adminUserApi;
@Resource @Resource
private DeptApi deptApi;
@Resource
private UserManagementRelationApi userManagementRelationApi; private UserManagementRelationApi userManagementRelationApi;
@Resource @Resource
private ApplicationEventPublisher applicationEventPublisher; private ApplicationEventPublisher applicationEventPublisher;
@@ -58,23 +61,59 @@ public class TeamPerformanceServiceImpl implements TeamPerformanceService {
String[] range = normalizePeriodMonthRange( String[] range = normalizePeriodMonthRange(
reqVO == null ? null : reqVO.getPeriodMonthStart(), reqVO == null ? null : reqVO.getPeriodMonthStart(),
reqVO == null ? null : reqVO.getPeriodMonthEnd()); reqVO == null ? null : reqVO.getPeriodMonthEnd());
List<String> periodMonths = buildPeriodMonths(range[0], range[1]);
List<Long> subordinateIds = teamDashboardAccessService.getAllSubordinateUserIds(); List<Long> subordinateIds = teamDashboardAccessService.getAllSubordinateUserIds();
Map<Long, AdminUserRespDTO> userMap = loadUserMap(subordinateIds);
Map<Long, DeptRespDTO> deptMap = loadDeptMap(userMap.values());
List<PerformanceSheetDO> sheets = performanceSheetMapper List<PerformanceSheetDO> sheets = performanceSheetMapper
.selectListByEmployeeIdsAndPeriodMonthRange(subordinateIds, range[0], range[1]); .selectListByEmployeeIdsAndPeriodMonthRange(subordinateIds, range[0], range[1]);
Map<Long, List<PerformanceSheetDO>> sheetsByEmployee = sheets.stream() Map<String, PerformanceSheetDO> sheetBySlot = buildSheetBySlot(sheets);
.collect(Collectors.groupingBy(PerformanceSheetDO::getEmployeeId)); Map<Long, AdminUserRespDTO> directManagerCache = new HashMap<>();
Map<Long, AdminUserRespDTO> userMap = loadUserMap(subordinateIds); List<TeamPerformanceSummaryRespVO.PendingSendUser> pendingSendUsers = new ArrayList<>();
List<TeamPerformanceSummaryRespVO.PendingConfirmUser> pendingConfirmUsers = new ArrayList<>();
Map<String, DeptAverageAccumulator> deptAverageMap = new LinkedHashMap<>();
int confirmedCount = 0;
for (Long subordinateId : subordinateIds) {
AdminUserRespDTO user = userMap.get(subordinateId);
for (String periodMonth : periodMonths) {
PerformanceSheetDO sheet = sheetBySlot.get(slotKey(subordinateId, periodMonth));
DeptBucket deptBucket = resolveDeptBucket(sheet, user, deptMap);
DeptAverageAccumulator accumulator = touchDeptAverageAccumulator(deptAverageMap, deptBucket);
if (accumulator != null) {
accumulator.incrementExpectedCount();
}
if (isPendingSendStatus(sheet == null ? null : sheet.getStatusCode())) {
pendingSendUsers.add(buildPendingSendUser(
subordinateId, periodMonth, sheet, user, directManagerCache));
continue;
}
if (PerformanceConstants.STATUS_SENT.equals(sheet.getStatusCode())) {
pendingConfirmUsers.add(buildPendingConfirmUser(sheet, user, periodMonth));
continue;
}
if (PerformanceConstants.STATUS_CONFIRMED.equals(sheet.getStatusCode())) {
confirmedCount++;
if (accumulator != null) {
accumulator.addConfirmedScore(sheet.getActualScoreTotal());
}
}
}
}
int expectedSheetCount = subordinateIds.size() * periodMonths.size();
TeamPerformanceSummaryRespVO respVO = new TeamPerformanceSummaryRespVO(); TeamPerformanceSummaryRespVO respVO = new TeamPerformanceSummaryRespVO();
respVO.setPeriodMonthStart(range[0]); respVO.setPeriodMonthStart(range[0]);
respVO.setPeriodMonthEnd(range[1]); respVO.setPeriodMonthEnd(range[1]);
respVO.setTotalSheetCount(sheets.size()); respVO.setExpectedSheetCount(expectedSheetCount);
respVO.setPendingSendUsers(buildPendingSendUsers(subordinateIds, sheetsByEmployee, userMap)); respVO.setPendingSendUsers(pendingSendUsers);
respVO.setPendingConfirmUsers(buildPendingConfirmUsers(sheets, userMap)); respVO.setPendingConfirmUsers(pendingConfirmUsers);
respVO.setPendingSendCount(respVO.getPendingSendUsers().size()); respVO.setPendingSendCount(pendingSendUsers.size());
respVO.setPendingConfirmCount(respVO.getPendingConfirmUsers().size()); respVO.setPendingConfirmCount(pendingConfirmUsers.size());
respVO.setConfirmedRate(calculateConfirmedRate(sheets)); respVO.setConfirmedRate(calculateConfirmedRate(expectedSheetCount, confirmedCount));
respVO.setDeptOrgAverages(buildDeptOrgAverages(sheets)); respVO.setDeptOrgAverages(deptAverageMap.values().stream()
.map(DeptAverageAccumulator::toRespVO)
.toList());
return respVO; return respVO;
} }
@@ -83,6 +122,7 @@ public class TeamPerformanceServiceImpl implements TeamPerformanceService {
public TeamPerformanceRemindRespVO remind(TeamPerformanceRemindReqVO reqVO) { public TeamPerformanceRemindRespVO remind(TeamPerformanceRemindReqVO reqVO) {
teamDashboardAccessService.validateTeamDashboardPermission(PerformanceConstants.PERMISSION_TEAM_DASHBOARD); teamDashboardAccessService.validateTeamDashboardPermission(PerformanceConstants.PERMISSION_TEAM_DASHBOARD);
String[] range = normalizePeriodMonthRange(reqVO.getPeriodMonthStart(), reqVO.getPeriodMonthEnd()); String[] range = normalizePeriodMonthRange(reqVO.getPeriodMonthStart(), reqVO.getPeriodMonthEnd());
List<String> periodMonths = buildPeriodMonths(range[0], range[1]);
List<Long> subordinateIds = teamDashboardAccessService.getAllSubordinateUserIds(); List<Long> subordinateIds = teamDashboardAccessService.getAllSubordinateUserIds();
List<Long> targetEmployeeIds = reqVO.getUserIds() == null List<Long> targetEmployeeIds = reqVO.getUserIds() == null
? subordinateIds ? subordinateIds
@@ -90,15 +130,14 @@ public class TeamPerformanceServiceImpl implements TeamPerformanceService {
reqVO.getUserIds(), PerformanceConstants.PERMISSION_TEAM_DASHBOARD); reqVO.getUserIds(), PerformanceConstants.PERMISSION_TEAM_DASHBOARD);
List<PerformanceSheetDO> sheets = performanceSheetMapper List<PerformanceSheetDO> sheets = performanceSheetMapper
.selectListByEmployeeIdsAndPeriodMonthRange(targetEmployeeIds, range[0], range[1]); .selectListByEmployeeIdsAndPeriodMonthRange(targetEmployeeIds, range[0], range[1]);
Map<Long, List<PerformanceSheetDO>> sheetsByEmployee = sheets.stream() Map<String, PerformanceSheetDO> sheetBySlot = buildSheetBySlot(sheets);
.collect(Collectors.groupingBy(PerformanceSheetDO::getEmployeeId));
Map<Long, AdminUserRespDTO> userMap = loadUserMap(targetEmployeeIds); Map<Long, AdminUserRespDTO> userMap = loadUserMap(targetEmployeeIds);
int remindedCount; int remindedCount;
if (PerformanceConstants.REMIND_TYPE_PENDING_CONFIRM.equals(reqVO.getRemindType())) { if (PerformanceConstants.REMIND_TYPE_PENDING_CONFIRM.equals(reqVO.getRemindType())) {
remindedCount = remindPendingConfirm(sheets); remindedCount = remindPendingConfirm(sheets);
} else if (PerformanceConstants.REMIND_TYPE_PENDING_SEND.equals(reqVO.getRemindType())) { } else if (PerformanceConstants.REMIND_TYPE_PENDING_SEND.equals(reqVO.getRemindType())) {
remindedCount = remindPendingSend(targetEmployeeIds, sheetsByEmployee, userMap, range[0]); remindedCount = remindPendingSend(targetEmployeeIds, periodMonths, sheetBySlot, userMap);
} else { } else {
throw invalidParamException("催办类型不合法"); throw invalidParamException("催办类型不合法");
} }
@@ -107,78 +146,12 @@ public class TeamPerformanceServiceImpl implements TeamPerformanceService {
return respVO; return respVO;
} }
private List<TeamPerformanceSummaryRespVO.PendingSendUser> buildPendingSendUsers( private BigDecimal calculateConfirmedRate(int expectedSheetCount, int confirmedCount) {
List<Long> subordinateIds, Map<Long, List<PerformanceSheetDO>> sheetsByEmployee, if (expectedSheetCount <= 0) {
Map<Long, AdminUserRespDTO> userMap) {
List<TeamPerformanceSummaryRespVO.PendingSendUser> result = new ArrayList<>();
for (Long subordinateId : subordinateIds) {
List<PerformanceSheetDO> employeeSheets = sheetsByEmployee.getOrDefault(subordinateId, List.of());
if (!hasPendingSend(employeeSheets)) {
continue;
}
AdminUserRespDTO user = userMap.get(subordinateId);
PerformanceSheetDO firstPending = employeeSheets.isEmpty() ? null : employeeSheets.get(0);
AdminUserRespDTO manager = firstPending == null ? loadDirectManagerQuietly(subordinateId) : null;
TeamPerformanceSummaryRespVO.PendingSendUser item = new TeamPerformanceSummaryRespVO.PendingSendUser();
item.setUserId(subordinateId);
item.setUserNickname(user == null ? "" : defaultText(user.getNickname()));
item.setManagerUserId(
firstPending == null ? (manager == null ? null : manager.getId()) : firstPending.getManagerId());
item.setManagerName(firstPending == null ? (manager == null ? "" : defaultText(manager.getNickname()))
: firstPending.getManagerName());
item.setSheetId(firstPending == null ? null : firstPending.getId());
item.setStatusCode(firstPending == null ? null : firstPending.getStatusCode());
result.add(item);
}
return result;
}
private List<TeamPerformanceSummaryRespVO.PendingConfirmUser> buildPendingConfirmUsers(
List<PerformanceSheetDO> sheets, Map<Long, AdminUserRespDTO> userMap) {
return sheets.stream()
.filter(sheet -> PerformanceConstants.STATUS_SENT.equals(sheet.getStatusCode()))
.map(sheet -> {
AdminUserRespDTO user = userMap.get(sheet.getEmployeeId());
TeamPerformanceSummaryRespVO.PendingConfirmUser item = new TeamPerformanceSummaryRespVO.PendingConfirmUser();
item.setUserId(sheet.getEmployeeId());
item.setUserNickname(
user == null ? defaultText(sheet.getEmployeeName()) : defaultText(user.getNickname()));
item.setSheetId(sheet.getId());
item.setSentTime(sheet.getSentTime());
return item;
})
.toList();
}
private List<TeamPerformanceSummaryRespVO.DeptOrgAverage> buildDeptOrgAverages(List<PerformanceSheetDO> sheets) {
Map<String, DeptAverageAccumulator> averages = new LinkedHashMap<>();
for (PerformanceSheetDO sheet : sheets) {
if (!PerformanceConstants.STATUS_CONFIRMED.equals(sheet.getStatusCode())
|| sheet.getActualScoreTotal() == null
|| sheet.getEmployeeDeptId() == null
|| (!PerformanceConstants.ORG_TYPE_DIRECTION.equals(sheet.getDeptOrgType())
&& !PerformanceConstants.ORG_TYPE_FUNCTION.equals(sheet.getDeptOrgType()))) {
continue;
}
String key = sheet.getEmployeeDeptId() + ":" + sheet.getDeptOrgType();
averages.computeIfAbsent(key, ignored -> new DeptAverageAccumulator(
sheet.getEmployeeDeptId(), sheet.getEmployeeDeptName(), sheet.getDeptOrgType()))
.add(sheet.getActualScoreTotal());
}
return averages.values().stream()
.map(DeptAverageAccumulator::toRespVO)
.toList();
}
private BigDecimal calculateConfirmedRate(List<PerformanceSheetDO> sheets) {
if (sheets.isEmpty()) {
return BigDecimal.ZERO.setScale(2); return BigDecimal.ZERO.setScale(2);
} }
long confirmed = sheets.stream() return BigDecimal.valueOf(confirmedCount * 100L)
.filter(sheet -> PerformanceConstants.STATUS_CONFIRMED.equals(sheet.getStatusCode())) .divide(BigDecimal.valueOf(expectedSheetCount), 2, RoundingMode.HALF_UP);
.count();
return BigDecimal.valueOf(confirmed * 100)
.divide(BigDecimal.valueOf(sheets.size()), 2, RoundingMode.HALF_UP);
} }
private int remindPendingConfirm(List<PerformanceSheetDO> sheets) { private int remindPendingConfirm(List<PerformanceSheetDO> sheets) {
@@ -194,27 +167,31 @@ public class TeamPerformanceServiceImpl implements TeamPerformanceService {
return count; return count;
} }
private int remindPendingSend(List<Long> targetEmployeeIds, Map<Long, List<PerformanceSheetDO>> sheetsByEmployee, private int remindPendingSend(List<Long> targetEmployeeIds, List<String> periodMonths,
Map<Long, AdminUserRespDTO> userMap, String defaultPeriodMonth) { Map<String, PerformanceSheetDO> sheetBySlot, Map<Long, AdminUserRespDTO> userMap) {
int count = 0; int count = 0;
Map<Long, AdminUserRespDTO> directManagerCache = new HashMap<>();
for (Long employeeId : targetEmployeeIds) { for (Long employeeId : targetEmployeeIds) {
List<PerformanceSheetDO> employeeSheets = sheetsByEmployee.getOrDefault(employeeId, List.of()); PendingSendContext pendingContext = findPendingSendContext(employeeId, periodMonths, sheetBySlot);
if (!hasPendingSend(employeeSheets)) { if (pendingContext == null) {
continue; continue;
} }
PerformanceSheetDO firstPending = employeeSheets.isEmpty() ? null : employeeSheets.get(0); PerformanceSheetDO pendingSheet = pendingContext.sheet();
AdminUserRespDTO manager = firstPending == null ? loadDirectManagerQuietly(employeeId) : null; AdminUserRespDTO manager = pendingSheet == null
Long managerId = firstPending == null ? (manager == null ? null : manager.getId()) ? directManagerCache.computeIfAbsent(employeeId, this::loadDirectManagerQuietly)
: firstPending.getManagerId(); : null;
Long managerId = pendingSheet != null && pendingSheet.getManagerId() != null
? pendingSheet.getManagerId()
: (manager == null ? null : manager.getId());
if (managerId == null) { if (managerId == null) {
continue; continue;
} }
String employeeName = firstPending == null String employeeName = pendingSheet == null
? defaultText(userMap.get(employeeId) == null ? null : userMap.get(employeeId).getNickname()) ? defaultText(userMap.get(employeeId) == null ? null : userMap.get(employeeId).getNickname())
: firstPending.getEmployeeName(); : defaultText(pendingSheet.getEmployeeName());
PerformanceSheetDO noticeSheet = firstPending == null PerformanceSheetDO noticeSheet = pendingSheet == null
? buildVirtualSheet(defaultPeriodMonth, employeeId, employeeName, manager) ? buildVirtualSheet(pendingContext.periodMonth(), employeeId, employeeName, manager)
: firstPending; : pendingSheet;
publishNotice(List.of(managerId), publishNotice(List.of(managerId),
NotifyTemplateCodeConstants.PERFORMANCE_PENDING_SEND_REMIND, noticeSheet, employeeName); NotifyTemplateCodeConstants.PERFORMANCE_PENDING_SEND_REMIND, noticeSheet, employeeName);
count++; count++;
@@ -248,6 +225,21 @@ public class TeamPerformanceServiceImpl implements TeamPerformanceService {
templateCode, params, NotifyMessageLevelConstants.REMIND)); templateCode, params, NotifyMessageLevelConstants.REMIND));
} }
private Map<Long, DeptRespDTO> loadDeptMap(Collection<AdminUserRespDTO> users) {
if (users == null || users.isEmpty()) {
return Map.of();
}
LinkedHashSet<Long> deptIds = users.stream()
.filter(Objects::nonNull)
.map(AdminUserRespDTO::getDeptId)
.filter(Objects::nonNull)
.collect(Collectors.toCollection(LinkedHashSet::new));
if (deptIds.isEmpty()) {
return Map.of();
}
return deptApi.getDeptMap(deptIds);
}
private Map<Long, AdminUserRespDTO> loadUserMap(Collection<Long> userIds) { private Map<Long, AdminUserRespDTO> loadUserMap(Collection<Long> userIds) {
if (userIds == null || userIds.isEmpty()) { if (userIds == null || userIds.isEmpty()) {
return Map.of(); return Map.of();
@@ -271,9 +263,142 @@ public class TeamPerformanceServiceImpl implements TeamPerformanceService {
} }
} }
/** private Map<String, PerformanceSheetDO> buildSheetBySlot(List<PerformanceSheetDO> sheets) {
* 校验并归一化月份区间,两者均为空时默认当前月。 if (sheets == null || sheets.isEmpty()) {
*/ return Map.of();
}
return sheets.stream()
.filter(Objects::nonNull)
.filter(sheet -> sheet.getEmployeeId() != null)
.filter(sheet -> StringUtils.hasText(sheet.getPeriodMonth()))
.collect(Collectors.toMap(
sheet -> slotKey(sheet.getEmployeeId(), sheet.getPeriodMonth()),
sheet -> sheet,
this::preferLatestSheet,
LinkedHashMap::new));
}
private PerformanceSheetDO preferLatestSheet(PerformanceSheetDO left, PerformanceSheetDO right) {
if (left == null) {
return right;
}
if (right == null) {
return left;
}
if (left.getUpdateTime() != null && right.getUpdateTime() != null) {
return left.getUpdateTime().isAfter(right.getUpdateTime()) ? left : right;
}
if (left.getCreateTime() != null && right.getCreateTime() != null) {
return left.getCreateTime().isAfter(right.getCreateTime()) ? left : right;
}
if (left.getId() != null && right.getId() != null) {
return left.getId() >= right.getId() ? left : right;
}
return left;
}
private List<String> buildPeriodMonths(String periodMonthStart, String periodMonthEnd) {
YearMonth start = YearMonth.parse(periodMonthStart);
YearMonth end = YearMonth.parse(periodMonthEnd);
List<String> result = new ArrayList<>();
for (YearMonth current = start; !current.isAfter(end); current = current.plusMonths(1)) {
result.add(current.toString());
}
return result;
}
private String slotKey(Long employeeId, String periodMonth) {
return employeeId + ":" + periodMonth;
}
private PendingSendContext findPendingSendContext(Long employeeId, List<String> periodMonths,
Map<String, PerformanceSheetDO> sheetBySlot) {
for (String periodMonth : periodMonths) {
PerformanceSheetDO sheet = sheetBySlot.get(slotKey(employeeId, periodMonth));
if (isPendingSendStatus(sheet == null ? null : sheet.getStatusCode())) {
return new PendingSendContext(periodMonth, sheet);
}
}
return null;
}
private boolean isPendingSendStatus(String statusCode) {
return statusCode == null
|| PerformanceConstants.STATUS_DRAFT.equals(statusCode)
|| PerformanceConstants.STATUS_REJECTED.equals(statusCode);
}
private TeamPerformanceSummaryRespVO.PendingSendUser buildPendingSendUser(Long userId, String periodMonth,
PerformanceSheetDO sheet, AdminUserRespDTO user, Map<Long, AdminUserRespDTO> directManagerCache) {
AdminUserRespDTO manager = null;
if (sheet == null || sheet.getManagerId() == null || !StringUtils.hasText(sheet.getManagerName())) {
manager = directManagerCache.computeIfAbsent(userId, this::loadDirectManagerQuietly);
}
TeamPerformanceSummaryRespVO.PendingSendUser item = new TeamPerformanceSummaryRespVO.PendingSendUser();
item.setUserId(userId);
item.setUserNickname(resolveUserNickname(user, sheet));
item.setPeriodMonth(periodMonth);
item.setManagerUserId(sheet != null && sheet.getManagerId() != null
? sheet.getManagerId()
: (manager == null ? null : manager.getId()));
item.setManagerName(sheet != null && StringUtils.hasText(sheet.getManagerName())
? defaultText(sheet.getManagerName())
: (manager == null ? "" : defaultText(manager.getNickname())));
item.setSheetId(sheet == null ? null : sheet.getId());
item.setStatusCode(sheet == null ? null : sheet.getStatusCode());
return item;
}
private TeamPerformanceSummaryRespVO.PendingConfirmUser buildPendingConfirmUser(PerformanceSheetDO sheet,
AdminUserRespDTO user, String periodMonth) {
TeamPerformanceSummaryRespVO.PendingConfirmUser item = new TeamPerformanceSummaryRespVO.PendingConfirmUser();
item.setUserId(sheet.getEmployeeId());
item.setUserNickname(resolveUserNickname(user, sheet));
item.setPeriodMonth(periodMonth);
item.setSheetId(sheet.getId());
item.setSentTime(sheet.getSentTime());
return item;
}
private String resolveUserNickname(AdminUserRespDTO user, PerformanceSheetDO sheet) {
if (user != null && StringUtils.hasText(user.getNickname())) {
return defaultText(user.getNickname());
}
return sheet == null ? "" : defaultText(sheet.getEmployeeName());
}
private DeptAverageAccumulator touchDeptAverageAccumulator(Map<String, DeptAverageAccumulator> averageMap,
DeptBucket deptBucket) {
if (deptBucket == null) {
return null;
}
return averageMap.computeIfAbsent(deptBucket.key(), ignored ->
new DeptAverageAccumulator(deptBucket.deptId(), deptBucket.deptName(), deptBucket.deptOrgType()));
}
private DeptBucket resolveDeptBucket(PerformanceSheetDO sheet, AdminUserRespDTO user,
Map<Long, DeptRespDTO> deptMap) {
if (sheet != null
&& sheet.getEmployeeDeptId() != null
&& isDeptAverageOrgType(sheet.getDeptOrgType())) {
return new DeptBucket(sheet.getEmployeeDeptId(),
defaultText(sheet.getEmployeeDeptName()), sheet.getDeptOrgType());
}
if (user == null || user.getDeptId() == null) {
return null;
}
DeptRespDTO dept = deptMap.get(user.getDeptId());
if (dept == null || !isDeptAverageOrgType(dept.getOrgType())) {
return null;
}
return new DeptBucket(dept.getId(), defaultText(dept.getName()), dept.getOrgType());
}
private boolean isDeptAverageOrgType(String orgType) {
return PerformanceConstants.ORG_TYPE_DIRECTION.equals(orgType)
|| PerformanceConstants.ORG_TYPE_FUNCTION.equals(orgType);
}
private String[] normalizePeriodMonthRange(String start, String end) { private String[] normalizePeriodMonthRange(String start, String end) {
String normalizedStart = StringUtils.hasText(start) ? normalizeSinglePeriodMonth(start) : null; String normalizedStart = StringUtils.hasText(start) ? normalizeSinglePeriodMonth(start) : null;
String normalizedEnd = StringUtils.hasText(end) ? normalizeSinglePeriodMonth(end) : null; String normalizedEnd = StringUtils.hasText(end) ? normalizeSinglePeriodMonth(end) : null;
@@ -281,7 +406,13 @@ public class TeamPerformanceServiceImpl implements TeamPerformanceService {
String currentMonth = YearMonth.now().toString(); String currentMonth = YearMonth.now().toString();
return new String[] { currentMonth, currentMonth }; return new String[] { currentMonth, currentMonth };
} }
if (normalizedStart != null && normalizedEnd != null && normalizedStart.compareTo(normalizedEnd) > 0) { if (normalizedStart == null) {
normalizedStart = normalizedEnd;
}
if (normalizedEnd == null) {
normalizedEnd = normalizedStart;
}
if (normalizedStart.compareTo(normalizedEnd) > 0) {
throw invalidParamException("绩效月份起始不能晚于结束月份"); throw invalidParamException("绩效月份起始不能晚于结束月份");
} }
return new String[] { normalizedStart, normalizedEnd }; return new String[] { normalizedStart, normalizedEnd };
@@ -295,17 +426,6 @@ public class TeamPerformanceServiceImpl implements TeamPerformanceService {
} }
} }
/**
* 判断员工在区间内的绩效表是否存在“待发送”状态(无记录 / 含 draft / 含 rejected
*/
private boolean hasPendingSend(List<PerformanceSheetDO> sheets) {
if (sheets == null || sheets.isEmpty()) {
return true;
}
return sheets.stream().anyMatch(sheet -> PerformanceConstants.STATUS_DRAFT.equals(sheet.getStatusCode())
|| PerformanceConstants.STATUS_REJECTED.equals(sheet.getStatusCode()));
}
private String statusName(String statusCode) { private String statusName(String statusCode) {
if (statusCode == null) { if (statusCode == null) {
return "未创建"; return "未创建";
@@ -329,7 +449,8 @@ public class TeamPerformanceServiceImpl implements TeamPerformanceService {
private final String deptName; private final String deptName;
private final String deptOrgType; private final String deptOrgType;
private BigDecimal total = BigDecimal.ZERO; private BigDecimal total = BigDecimal.ZERO;
private int count = 0; private int expectedCount = 0;
private int confirmedCount = 0;
DeptAverageAccumulator(Long deptId, String deptName, String deptOrgType) { DeptAverageAccumulator(Long deptId, String deptName, String deptOrgType) {
this.deptId = deptId; this.deptId = deptId;
@@ -337,9 +458,15 @@ public class TeamPerformanceServiceImpl implements TeamPerformanceService {
this.deptOrgType = deptOrgType; this.deptOrgType = deptOrgType;
} }
void add(BigDecimal score) { void incrementExpectedCount() {
expectedCount++;
}
void addConfirmedScore(BigDecimal score) {
confirmedCount++;
if (score != null) {
total = total.add(score); total = total.add(score);
count++; }
} }
TeamPerformanceSummaryRespVO.DeptOrgAverage toRespVO() { TeamPerformanceSummaryRespVO.DeptOrgAverage toRespVO() {
@@ -347,11 +474,20 @@ public class TeamPerformanceServiceImpl implements TeamPerformanceService {
respVO.setDeptId(deptId); respVO.setDeptId(deptId);
respVO.setDeptName(deptName); respVO.setDeptName(deptName);
respVO.setDeptOrgType(deptOrgType); respVO.setDeptOrgType(deptOrgType);
respVO.setConfirmedCount(count); respVO.setConfirmedCount(confirmedCount);
respVO.setAverageScore(count == 0 respVO.setAverageScore(expectedCount == 0
? BigDecimal.ZERO.setScale(2) ? BigDecimal.ZERO.setScale(2)
: total.divide(BigDecimal.valueOf(count), 2, RoundingMode.HALF_UP)); : total.divide(BigDecimal.valueOf(expectedCount), 2, RoundingMode.HALF_UP));
return respVO; return respVO;
} }
} }
private record PendingSendContext(String periodMonth, PerformanceSheetDO sheet) {
}
private record DeptBucket(Long deptId, String deptName, String deptOrgType) {
private String key() {
return deptId + ":" + deptOrgType;
}
}
} }

View File

@@ -28,7 +28,11 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import java.time.DayOfWeek;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoField;
import java.time.temporal.TemporalAdjusters;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
@@ -72,29 +76,23 @@ public class TeamWorkReportServiceImpl implements TeamWorkReportService {
public TeamReportSummaryRespVO getSummary(TeamReportSummaryReqVO reqVO) { public TeamReportSummaryRespVO getSummary(TeamReportSummaryReqVO reqVO) {
teamDashboardAccessService.validateTeamDashboardPermission(WorkReportConstants.PERMISSION_TEAM_DASHBOARD); teamDashboardAccessService.validateTeamDashboardPermission(WorkReportConstants.PERMISSION_TEAM_DASHBOARD);
String reportType = normalizeReportType(reqVO.getReportType()); String reportType = normalizeReportType(reqVO.getReportType());
boolean useDateRange = reqVO.getPeriodStartDate() != null || reqVO.getPeriodEndDate() != null; DateRange dateRange = normalizeDateRange(reqVO.getPeriodStartDate(), reqVO.getPeriodEndDate());
ReportContext context; ReportContext context;
LocalDate respStartDate; if (dateRange != null) {
LocalDate respEndDate; context = buildReportContextByDateRange(reportType, dateRange.startDate(), dateRange.endDate());
if (useDateRange) {
respStartDate = reqVO.getPeriodStartDate();
respEndDate = reqVO.getPeriodEndDate();
context = buildReportContextByDateRange(reportType, respStartDate, respEndDate);
} else { } else {
context = buildReportContext(reportType, reqVO.getPeriodKey()); context = buildReportContext(reportType, reqVO.getPeriodKey());
respStartDate = null;
respEndDate = null;
} }
List<TeamReportSummaryRespVO.UnsubmittedReport> unsubmittedReports = buildUnsubmittedReports(context);
TeamReportSummaryRespVO respVO = new TeamReportSummaryRespVO(); TeamReportSummaryRespVO respVO = new TeamReportSummaryRespVO();
respVO.setPeriodStartDate(respStartDate); respVO.setPeriodStartDate(dateRange == null ? null : dateRange.startDate());
respVO.setPeriodEndDate(respEndDate); respVO.setPeriodEndDate(dateRange == null ? null : dateRange.endDate());
respVO.setTotalShouldSubmit(context.expectedUserIds().size()); respVO.setExpectedReportCount(context.expectedSlots().size());
respVO.setSubmittedCount(context.submittedUserIds().size()); respVO.setSubmittedReportCount(context.submittedSlotKeys().size());
respVO.setPendingApprovalCount(context.pendingApprovalUserIds().size()); respVO.setUnsubmittedReportCount(unsubmittedReports.size());
List<TeamReportSummaryRespVO.PendingUser> unsubmittedUsers = buildPendingUsers(context.expectedUserIds(), respVO.setPendingApprovalReportCount(context.pendingApprovalSlotKeys().size());
context.submittedUserIds()); respVO.setUnsubmittedReports(unsubmittedReports);
respVO.setUnsubmittedUsers(unsubmittedUsers);
respVO.setUnsubmittedCount(unsubmittedUsers.size());
return respVO; return respVO;
} }
@@ -104,117 +102,193 @@ public class TeamWorkReportServiceImpl implements TeamWorkReportService {
teamDashboardAccessService.validateTeamDashboardPermission(WorkReportConstants.PERMISSION_TEAM_DASHBOARD); teamDashboardAccessService.validateTeamDashboardPermission(WorkReportConstants.PERMISSION_TEAM_DASHBOARD);
String reportType = normalizeReportType(reqVO.getReportType()); String reportType = normalizeReportType(reqVO.getReportType());
ReportContext context = buildReportContext(reportType, reqVO.getPeriodKey()); ReportContext context = buildReportContext(reportType, reqVO.getPeriodKey());
List<Long> remindUserIds = resolveRemindUserIds(reqVO.getUserIds(), context); int remindedCount = WorkReportConstants.REPORT_TYPE_PROJECT.equals(reportType)
? remindProjectReport(reqVO, context)
: remindPersonalReport(reportType, reqVO, context);
if (!remindUserIds.isEmpty()) { TeamReportRemindRespVO respVO = new TeamReportRemindRespVO();
respVO.setRemindedCount(remindedCount);
return respVO;
}
private int remindPersonalReport(String reportType, TeamReportRemindReqVO reqVO, ReportContext context) {
List<Long> remindUserIds = resolveRemindUserIds(reqVO.getUserIds(), context);
if (remindUserIds.isEmpty()) {
return 0;
}
Map<String, Object> params = new HashMap<>(); Map<String, Object> params = new HashMap<>();
params.put("reportTypeName", reportTypeDisplayName(reportType)); params.put("reportTypeName", reportTypeDisplayName(reportType));
params.put("periodKey", reqVO.getPeriodKey()); params.put("periodKey", formatPeriodKeyForRemind(reportType, reqVO.getPeriodKey()));
params.put("managerName", defaultText(SecurityFrameworkUtils.getLoginUserNickname())); params.put("managerName", defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
applicationEventPublisher.publishEvent(NotifySendEvent.of(remindUserIds, applicationEventPublisher.publishEvent(NotifySendEvent.of(remindUserIds,
NotifyTemplateCodeConstants.WORK_REPORT_TEAM_REMIND, params, NotifyMessageLevelConstants.REMIND)); NotifyTemplateCodeConstants.WORK_REPORT_TEAM_REMIND, params, NotifyMessageLevelConstants.REMIND));
return remindUserIds.size();
} }
TeamReportRemindRespVO respVO = new TeamReportRemindRespVO(); private int remindProjectReport(TeamReportRemindReqVO reqVO, ReportContext context) {
respVO.setRemindedCount(remindUserIds.size()); List<ExpectedReportSlot> remindSlots = resolveProjectRemindSlots(reqVO, context);
return respVO; if (remindSlots.isEmpty()) {
return 0;
}
String managerName = defaultText(SecurityFrameworkUtils.getLoginUserNickname());
for (ExpectedReportSlot slot : remindSlots) {
Map<String, Object> params = new HashMap<>();
params.put("projectName", slot.projectName());
params.put("periodLabel", slot.periodLabel());
params.put("managerName", managerName);
applicationEventPublisher.publishEvent(NotifySendEvent.of(List.of(slot.userId()),
NotifyTemplateCodeConstants.WORK_REPORT_PROJECT_HALF_MONTH_TEAM_REMIND,
params, NotifyMessageLevelConstants.REMIND));
}
return remindSlots.size();
} }
private ReportContext buildReportContext(String reportType, String periodKey) { private ReportContext buildReportContext(String reportType, String periodKey) {
if (!StringUtils.hasText(periodKey)) { if (!StringUtils.hasText(periodKey)) {
throw invalidParamException("周期主键不能为空"); throw invalidParamException("周期主键不能为空");
} }
ReportPeriod period = parseRequiredPeriod(reportType, periodKey);
return buildReportContextByPeriods(reportType, List.of(period));
}
private ReportContext buildReportContextByDateRange(String reportType, LocalDate startDate, LocalDate endDate) {
return buildReportContextByPeriods(reportType, buildPeriodsByDateRange(reportType, startDate, endDate));
}
private ReportContext buildReportContextByPeriods(String reportType, List<ReportPeriod> periods) {
if (WorkReportConstants.REPORT_TYPE_PROJECT.equals(reportType)) { if (WorkReportConstants.REPORT_TYPE_PROJECT.equals(reportType)) {
return buildProjectContext(periodKey); return buildProjectContext(periods);
} }
List<Long> subordinateIds = teamDashboardAccessService.getAllSubordinateUserIds(); List<Long> subordinateIds = teamDashboardAccessService.getAllSubordinateUserIds();
if (subordinateIds.isEmpty()) { if (subordinateIds.isEmpty() || periods.isEmpty()) {
return new ReportContext(Collections.emptyList(), Collections.emptySet(), Collections.emptySet()); return ReportContext.empty();
} }
if (WorkReportConstants.REPORT_TYPE_WEEKLY.equals(reportType)) { if (WorkReportConstants.REPORT_TYPE_WEEKLY.equals(reportType)) {
return buildWeeklyContext(periodKey, subordinateIds); return buildWeeklyContext(periods, subordinateIds);
} }
return buildMonthlyContext(periodKey, subordinateIds); return buildMonthlyContext(periods, subordinateIds);
} }
private ReportContext buildWeeklyContext(String periodKey, List<Long> subordinateIds) { private ReportContext buildWeeklyContext(List<ReportPeriod> periods, List<Long> subordinateIds) {
List<WeeklyReportDO> reports = weeklyReportMapper.selectListByReporterIdsAndPeriodKey( Map<String, ExpectedReportSlot> expectedSlotMap = buildPersonalExpectedSlotMap(subordinateIds, periods);
subordinateIds, periodKey, SUBMITTED_STATUS_CODES); List<WeeklyReportDO> reports = weeklyReportMapper.selectListByReporterIdsAndPeriodDateRange(
Set<Long> submittedUserIds = new LinkedHashSet<>(); subordinateIds, periods.get(0).startDate(), periods.get(periods.size() - 1).startDate(),
Set<Long> pendingApprovalUserIds = new LinkedHashSet<>(); SUBMITTED_STATUS_CODES);
for (WeeklyReportDO report : reports) { return buildPersonalReportContext(expectedSlotMap, reports.stream()
if (report == null || report.getReporterId() == null) { .filter(Objects::nonNull)
continue; .filter(report -> report.getReporterId() != null)
} .map(report -> new ActualReportSlot(
submittedUserIds.add(report.getReporterId()); personalSlotKey(report.getReporterId(), report.getPeriodKey()),
if (WorkReportConstants.STATUS_PENDING_APPROVAL.equals(report.getStatusCode())) { report.getStatusCode()))
pendingApprovalUserIds.add(report.getReporterId()); .toList());
}
}
return new ReportContext(subordinateIds, submittedUserIds, pendingApprovalUserIds);
} }
private ReportContext buildMonthlyContext(String periodKey, List<Long> subordinateIds) { private ReportContext buildMonthlyContext(List<ReportPeriod> periods, List<Long> subordinateIds) {
List<MonthlyReportDO> reports = monthlyReportMapper.selectListByReporterIdsAndPeriodKey( Map<String, ExpectedReportSlot> expectedSlotMap = buildPersonalExpectedSlotMap(subordinateIds, periods);
subordinateIds, periodKey, SUBMITTED_STATUS_CODES); List<MonthlyReportDO> reports = monthlyReportMapper.selectListByReporterIdsAndPeriodDateRange(
Set<Long> submittedUserIds = new LinkedHashSet<>(); subordinateIds, periods.get(0).startDate(), periods.get(periods.size() - 1).startDate(),
Set<Long> pendingApprovalUserIds = new LinkedHashSet<>(); SUBMITTED_STATUS_CODES);
for (MonthlyReportDO report : reports) { return buildPersonalReportContext(expectedSlotMap, reports.stream()
if (report == null || report.getReporterId() == null) { .filter(Objects::nonNull)
continue; .filter(report -> report.getReporterId() != null)
} .map(report -> new ActualReportSlot(
submittedUserIds.add(report.getReporterId()); personalSlotKey(report.getReporterId(), report.getPeriodKey()),
if (WorkReportConstants.STATUS_PENDING_APPROVAL.equals(report.getStatusCode())) { report.getStatusCode()))
pendingApprovalUserIds.add(report.getReporterId()); .toList());
}
}
return new ReportContext(subordinateIds, submittedUserIds, pendingApprovalUserIds);
} }
private ReportContext buildProjectContext(String periodKey) { private ReportContext buildProjectContext(List<ReportPeriod> periods) {
List<Long> subordinateIds = teamDashboardAccessService.getAllSubordinateUserIds(); List<Long> subordinateIds = teamDashboardAccessService.getAllSubordinateUserIds();
if (subordinateIds.isEmpty()) { if (subordinateIds.isEmpty() || periods.isEmpty()) {
return new ReportContext(Collections.emptyList(), Collections.emptySet(), Collections.emptySet()); return ReportContext.empty();
} }
List<ProjectDO> activeProjects = loadActiveProjectsForSubordinates(subordinateIds); List<ProjectDO> activeProjects = loadActiveProjectsForSubordinates(subordinateIds);
if (activeProjects.isEmpty()) { if (activeProjects.isEmpty()) {
return new ReportContext(Collections.emptyList(), Collections.emptySet(), Collections.emptySet()); return ReportContext.empty();
} }
Map<Long, List<ProjectDO>> projectsByOwner = activeProjects.stream() Map<String, ExpectedReportSlot> expectedSlotMap = buildProjectExpectedSlotMap(activeProjects, periods);
LinkedHashSet<Long> ownerIds = activeProjects.stream()
.map(ProjectDO::getManagerUserId)
.filter(Objects::nonNull) .filter(Objects::nonNull)
.filter(project -> project.getManagerUserId() != null) .collect(Collectors.toCollection(LinkedHashSet::new));
.collect(Collectors.groupingBy(ProjectDO::getManagerUserId, LinkedHashMap::new, Collectors.toList())); if (ownerIds.isEmpty()) {
LinkedHashSet<Long> expectedUserIds = new LinkedHashSet<>(projectsByOwner.keySet()); return ReportContext.empty();
List<ProjectReportDO> reports = projectReportMapper.selectListByProjectOwnerIdsAndPeriodKey( }
expectedUserIds, periodKey, SUBMITTED_STATUS_CODES); List<ProjectReportDO> reports = projectReportMapper.selectListByProjectOwnerIdsAndPeriodDateRange(
Map<Long, Set<Long>> submittedProjectsByOwner = new HashMap<>(); ownerIds, periods.get(0).startDate(), periods.get(periods.size() - 1).startDate(),
Set<Long> submittedUserIds = new LinkedHashSet<>(); SUBMITTED_STATUS_CODES);
Set<Long> pendingApprovalUserIds = new LinkedHashSet<>(); Set<String> submittedSlotKeys = new LinkedHashSet<>();
Set<String> pendingApprovalSlotKeys = new LinkedHashSet<>();
for (ProjectReportDO report : reports) { for (ProjectReportDO report : reports) {
if (report == null || report.getProjectOwnerId() == null || report.getProjectId() == null) { if (report == null || report.getProjectOwnerId() == null || report.getProjectId() == null) {
continue; continue;
} }
Long ownerId = report.getProjectOwnerId(); String slotKey = projectSlotKey(report.getProjectOwnerId(), report.getProjectId(), report.getPeriodKey());
if (!expectedUserIds.contains(ownerId)) { if (!expectedSlotMap.containsKey(slotKey)) {
continue; continue;
} }
submittedSlotKeys.add(slotKey);
if (WorkReportConstants.STATUS_PENDING_APPROVAL.equals(report.getStatusCode())) { if (WorkReportConstants.STATUS_PENDING_APPROVAL.equals(report.getStatusCode())) {
pendingApprovalUserIds.add(ownerId); pendingApprovalSlotKeys.add(slotKey);
}
submittedProjectsByOwner.computeIfAbsent(ownerId, key -> new LinkedHashSet<>()).add(report.getProjectId());
}
for (Map.Entry<Long, List<ProjectDO>> entry : projectsByOwner.entrySet()) {
Long ownerId = entry.getKey();
Set<Long> submittedProjectIds = submittedProjectsByOwner.getOrDefault(ownerId, Collections.emptySet());
boolean allSubmitted = entry.getValue().stream()
.map(ProjectDO::getId)
.filter(Objects::nonNull)
.allMatch(submittedProjectIds::contains);
if (allSubmitted) {
submittedUserIds.add(ownerId);
} }
} }
return new ReportContext(new ArrayList<>(expectedUserIds), submittedUserIds, pendingApprovalUserIds); return new ReportContext(new ArrayList<>(expectedSlotMap.values()), submittedSlotKeys, pendingApprovalSlotKeys);
}
private ReportContext buildPersonalReportContext(Map<String, ExpectedReportSlot> expectedSlotMap,
List<ActualReportSlot> actualSlots) {
if (expectedSlotMap.isEmpty()) {
return ReportContext.empty();
}
Set<String> submittedSlotKeys = new LinkedHashSet<>();
Set<String> pendingApprovalSlotKeys = new LinkedHashSet<>();
for (ActualReportSlot actualSlot : actualSlots) {
if (actualSlot == null || !expectedSlotMap.containsKey(actualSlot.slotKey())) {
continue;
}
submittedSlotKeys.add(actualSlot.slotKey());
if (WorkReportConstants.STATUS_PENDING_APPROVAL.equals(actualSlot.statusCode())) {
pendingApprovalSlotKeys.add(actualSlot.slotKey());
}
}
return new ReportContext(new ArrayList<>(expectedSlotMap.values()), submittedSlotKeys, pendingApprovalSlotKeys);
}
private Map<String, ExpectedReportSlot> buildPersonalExpectedSlotMap(List<Long> subordinateIds,
List<ReportPeriod> periods) {
LinkedHashMap<String, ExpectedReportSlot> result = new LinkedHashMap<>();
for (Long subordinateId : subordinateIds) {
if (subordinateId == null) {
continue;
}
for (ReportPeriod period : periods) {
String slotKey = personalSlotKey(subordinateId, period.periodKey());
result.put(slotKey, new ExpectedReportSlot(
slotKey, subordinateId, period.periodKey(), period.periodLabel(), null, null));
}
}
return result;
}
private Map<String, ExpectedReportSlot> buildProjectExpectedSlotMap(List<ProjectDO> activeProjects,
List<ReportPeriod> periods) {
LinkedHashMap<String, ExpectedReportSlot> result = new LinkedHashMap<>();
for (ProjectDO project : activeProjects) {
if (project == null || project.getManagerUserId() == null || project.getId() == null) {
continue;
}
for (ReportPeriod period : periods) {
String slotKey = projectSlotKey(project.getManagerUserId(), project.getId(), period.periodKey());
result.put(slotKey, new ExpectedReportSlot(
slotKey,
project.getManagerUserId(),
period.periodKey(),
period.periodLabel(),
project.getId(),
defaultText(project.getProjectName())));
}
}
return result;
} }
private List<ProjectDO> loadActiveProjectsForSubordinates(Collection<Long> subordinateIds) { private List<ProjectDO> loadActiveProjectsForSubordinates(Collection<Long> subordinateIds) {
@@ -236,8 +310,11 @@ public class TeamWorkReportServiceImpl implements TeamWorkReportService {
} }
private List<Long> resolveRemindUserIds(List<Long> requestedUserIds, ReportContext context) { private List<Long> resolveRemindUserIds(List<Long> requestedUserIds, ReportContext context) {
LinkedHashSet<Long> unsubmittedUserIds = new LinkedHashSet<>(context.expectedUserIds()); LinkedHashSet<Long> unsubmittedUserIds = context.expectedSlots().stream()
unsubmittedUserIds.removeAll(context.submittedUserIds()); .filter(slot -> !context.submittedSlotKeys().contains(slot.slotKey()))
.map(ExpectedReportSlot::userId)
.filter(Objects::nonNull)
.collect(Collectors.toCollection(LinkedHashSet::new));
if (requestedUserIds == null) { if (requestedUserIds == null) {
return new ArrayList<>(unsubmittedUserIds); return new ArrayList<>(unsubmittedUserIds);
} }
@@ -248,34 +325,216 @@ public class TeamWorkReportServiceImpl implements TeamWorkReportService {
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
private List<TeamReportSummaryRespVO.PendingUser> buildPendingUsers(List<Long> expectedUserIds, private List<ExpectedReportSlot> resolveProjectRemindSlots(TeamReportRemindReqVO reqVO, ReportContext context) {
Set<Long> submittedUserIds) { List<ExpectedReportSlot> pendingSlots = context.expectedSlots().stream()
LinkedHashSet<Long> pendingIds = new LinkedHashSet<>(expectedUserIds); .filter(slot -> !context.submittedSlotKeys().contains(slot.slotKey()))
pendingIds.removeAll(submittedUserIds); .toList();
if (pendingIds.isEmpty()) { if (pendingSlots.isEmpty()) {
return Collections.emptyList(); return Collections.emptyList();
} }
CommonResult<List<AdminUserRespDTO>> result = adminUserApi.getUserList(pendingIds);
List<AdminUserRespDTO> users = result == null ? null : result.getCheckedData(); Set<Long> validatedUserIds = reqVO.getUserIds() == null
if (users == null || users.isEmpty()) { ? null
users = Collections.emptyList(); : new LinkedHashSet<>(teamDashboardAccessService.resolveRequestedSubordinateUserIds(
reqVO.getUserIds(), WorkReportConstants.PERMISSION_TEAM_DASHBOARD));
Long requestedProjectId = reqVO.getProjectId();
return pendingSlots.stream()
.filter(slot -> validatedUserIds == null || validatedUserIds.contains(slot.userId()))
.filter(slot -> requestedProjectId == null || Objects.equals(slot.projectId(), requestedProjectId))
.toList();
} }
Map<Long, AdminUserRespDTO> userMap = users.stream()
private List<TeamReportSummaryRespVO.UnsubmittedReport> buildUnsubmittedReports(ReportContext context) {
List<ExpectedReportSlot> pendingSlots = context.expectedSlots().stream()
.filter(slot -> !context.submittedSlotKeys().contains(slot.slotKey()))
.toList();
if (pendingSlots.isEmpty()) {
return Collections.emptyList();
}
LinkedHashSet<Long> userIds = pendingSlots.stream()
.map(ExpectedReportSlot::userId)
.filter(Objects::nonNull) .filter(Objects::nonNull)
.filter(user -> user.getId() != null) .collect(Collectors.toCollection(LinkedHashSet::new));
.collect(Collectors.toMap(AdminUserRespDTO::getId, user -> user, (left, right) -> left, Map<Long, AdminUserRespDTO> userMap = loadUserMap(userIds);
LinkedHashMap::new));
List<TeamReportSummaryRespVO.PendingUser> respList = new ArrayList<>(); List<TeamReportSummaryRespVO.UnsubmittedReport> respList = new ArrayList<>();
for (Long pendingId : pendingIds) { for (ExpectedReportSlot pendingSlot : pendingSlots) {
AdminUserRespDTO user = userMap.get(pendingId); AdminUserRespDTO user = userMap.get(pendingSlot.userId());
TeamReportSummaryRespVO.PendingUser item = new TeamReportSummaryRespVO.PendingUser(); TeamReportSummaryRespVO.UnsubmittedReport item = new TeamReportSummaryRespVO.UnsubmittedReport();
item.setUserId(pendingId); item.setUserId(pendingSlot.userId());
item.setUserNickname(user == null ? "" : defaultText(user.getNickname())); item.setUserNickname(user == null ? "" : defaultText(user.getNickname()));
item.setPeriodKey(pendingSlot.periodKey());
item.setPeriodLabel(pendingSlot.periodLabel());
item.setProjectId(pendingSlot.projectId());
item.setProjectName(pendingSlot.projectName());
respList.add(item); respList.add(item);
} }
return respList; return respList;
} }
private Map<Long, AdminUserRespDTO> loadUserMap(Collection<Long> userIds) {
if (userIds == null || userIds.isEmpty()) {
return Map.of();
}
CommonResult<List<AdminUserRespDTO>> result = adminUserApi.getUserList(userIds);
List<AdminUserRespDTO> users = result == null ? null : result.getCheckedData();
if (users == null || users.isEmpty()) {
return Map.of();
}
return users.stream()
.filter(Objects::nonNull)
.filter(user -> user.getId() != null)
.collect(Collectors.toMap(
AdminUserRespDTO::getId, user -> user, (left, right) -> left, LinkedHashMap::new));
}
private List<ReportPeriod> buildPeriodsByDateRange(String reportType, LocalDate startDate, LocalDate endDate) {
return switch (reportType) {
case WorkReportConstants.REPORT_TYPE_WEEKLY -> buildWeeklyPeriodsByDateRange(startDate, endDate);
case WorkReportConstants.REPORT_TYPE_MONTHLY -> buildMonthlyPeriodsByDateRange(startDate, endDate);
case WorkReportConstants.REPORT_TYPE_PROJECT -> buildProjectPeriodsByDateRange(startDate, endDate);
default -> throw invalidParamException("报告类型不合法");
};
}
private List<ReportPeriod> buildWeeklyPeriodsByDateRange(LocalDate startDate, LocalDate endDate) {
LocalDate firstStart = startDate.with(TemporalAdjusters.nextOrSame(DayOfWeek.MONDAY));
if (firstStart.isAfter(endDate)) {
return Collections.emptyList();
}
List<ReportPeriod> periods = new ArrayList<>();
for (LocalDate currentStart = firstStart; !currentStart.isAfter(endDate); currentStart = currentStart.plusWeeks(1)) {
periods.add(buildWeeklyPeriod(currentStart, currentStart.plusDays(6)));
}
return periods;
}
private List<ReportPeriod> buildMonthlyPeriodsByDateRange(LocalDate startDate, LocalDate endDate) {
LocalDate firstStart = startDate.withDayOfMonth(1);
if (firstStart.isBefore(startDate)) {
firstStart = firstStart.plusMonths(1);
}
if (firstStart.isAfter(endDate)) {
return Collections.emptyList();
}
List<ReportPeriod> periods = new ArrayList<>();
for (LocalDate currentStart = firstStart; !currentStart.isAfter(endDate); currentStart = currentStart.plusMonths(1)) {
periods.add(buildMonthlyPeriod(currentStart));
}
return periods;
}
private List<ReportPeriod> buildProjectPeriodsByDateRange(LocalDate startDate, LocalDate endDate) {
LocalDate firstStart;
int day = startDate.getDayOfMonth();
if (day <= 1) {
firstStart = startDate.withDayOfMonth(1);
} else if (day <= 16) {
firstStart = startDate.withDayOfMonth(16);
} else {
firstStart = startDate.plusMonths(1).withDayOfMonth(1);
}
if (firstStart.isAfter(endDate)) {
return Collections.emptyList();
}
List<ReportPeriod> periods = new ArrayList<>();
for (LocalDate currentStart = firstStart; !currentStart.isAfter(endDate); currentStart = nextHalfMonthStart(currentStart)) {
periods.add(buildProjectPeriod(currentStart));
}
return periods;
}
private LocalDate nextHalfMonthStart(LocalDate currentStart) {
if (currentStart.getDayOfMonth() == 1) {
return currentStart.withDayOfMonth(16);
}
return currentStart.plusMonths(1).withDayOfMonth(1);
}
private ReportPeriod parseRequiredPeriod(String reportType, String periodKey) {
return switch (reportType) {
case WorkReportConstants.REPORT_TYPE_WEEKLY -> parseWeeklyPeriod(periodKey);
case WorkReportConstants.REPORT_TYPE_MONTHLY -> parseMonthlyPeriod(periodKey);
case WorkReportConstants.REPORT_TYPE_PROJECT -> parseProjectPeriod(periodKey);
default -> throw invalidParamException("报告类型不合法");
};
}
private ReportPeriod parseWeeklyPeriod(String periodKey) {
String[] parts = periodKey.split("-");
if (parts.length != 7 || !WorkReportConstants.REPORT_TYPE_WEEKLY.equals(parts[0])) {
throw invalidParamException("周报周期主键不合法");
}
LocalDate startDate = parsePeriodDate(parts, 1);
LocalDate endDate = parsePeriodDate(parts, 4);
return buildWeeklyPeriod(startDate, endDate);
}
private ReportPeriod parseMonthlyPeriod(String periodKey) {
String[] parts = periodKey.split("-");
if (parts.length != 7 || !WorkReportConstants.REPORT_TYPE_MONTHLY.equals(parts[0])) {
throw invalidParamException("月报周期主键不合法");
}
LocalDate startDate = parsePeriodDate(parts, 1);
return buildMonthlyPeriod(startDate);
}
private ReportPeriod parseProjectPeriod(String periodKey) {
String[] parts = periodKey.split("-");
if (parts.length != 8 || !WorkReportConstants.REPORT_TYPE_PROJECT.equals(parts[0])) {
throw invalidParamException("项目半月报周期主键不合法");
}
LocalDate startDate = parsePeriodDate(parts, 1);
return buildProjectPeriod(startDate);
}
private ReportPeriod buildWeeklyPeriod(LocalDate startDate, LocalDate endDate) {
return new ReportPeriod(
String.format("weekly-%s-%s", startDate, endDate),
String.format("%s 至 %s", startDate, endDate),
startDate,
endDate);
}
private ReportPeriod buildMonthlyPeriod(LocalDate startDate) {
LocalDate normalizedStartDate = startDate.withDayOfMonth(1);
LocalDate endDate = normalizedStartDate.withDayOfMonth(normalizedStartDate.lengthOfMonth());
return new ReportPeriod(
String.format("monthly-%s-%s", normalizedStartDate, endDate),
String.format("%d-%02d", normalizedStartDate.getYear(), normalizedStartDate.getMonthValue()),
normalizedStartDate,
endDate);
}
private ReportPeriod buildProjectPeriod(LocalDate startDate) {
LocalDate normalizedStartDate = startDate.getDayOfMonth() <= 15
? startDate.withDayOfMonth(1)
: startDate.withDayOfMonth(16);
boolean firstHalf = normalizedStartDate.getDayOfMonth() == 1;
LocalDate endDate = firstHalf
? normalizedStartDate.withDayOfMonth(15)
: normalizedStartDate.withDayOfMonth(normalizedStartDate.lengthOfMonth());
String half = firstHalf ? "上半月" : "下半月";
return new ReportPeriod(
String.format("project-%s-%s-%d", normalizedStartDate, endDate, firstHalf ? 1 : 2),
String.format("%d-%02d %s", normalizedStartDate.getYear(), normalizedStartDate.getMonthValue(), half),
normalizedStartDate,
endDate);
}
private DateRange normalizeDateRange(LocalDate startDate, LocalDate endDate) {
if (startDate == null && endDate == null) {
return null;
}
LocalDate normalizedStartDate = startDate == null ? endDate : startDate;
LocalDate normalizedEndDate = endDate == null ? startDate : endDate;
if (normalizedStartDate.isAfter(normalizedEndDate)) {
throw invalidParamException("周期起始日期不能晚于结束日期");
}
return new DateRange(normalizedStartDate, normalizedEndDate);
}
private String normalizeReportType(String reportType) { private String normalizeReportType(String reportType) {
if (WorkReportConstants.REPORT_TYPE_WEEKLY.equals(reportType) if (WorkReportConstants.REPORT_TYPE_WEEKLY.equals(reportType)
|| WorkReportConstants.REPORT_TYPE_MONTHLY.equals(reportType) || WorkReportConstants.REPORT_TYPE_MONTHLY.equals(reportType)
@@ -294,107 +553,105 @@ public class TeamWorkReportServiceImpl implements TeamWorkReportService {
}; };
} }
private String formatPeriodKeyForRemind(String reportType, String periodKey) {
if (!StringUtils.hasText(periodKey)) {
return "";
}
try {
return switch (reportType) {
case WorkReportConstants.REPORT_TYPE_WEEKLY -> formatWeeklyPeriodKey(periodKey);
case WorkReportConstants.REPORT_TYPE_MONTHLY -> formatMonthlyPeriodKey(periodKey);
case WorkReportConstants.REPORT_TYPE_PROJECT -> formatProjectPeriodKey(periodKey);
default -> periodKey;
};
} catch (RuntimeException ex) {
return periodKey;
}
}
private String formatWeeklyPeriodKey(String periodKey) {
String[] parts = periodKey.split("-");
if (parts.length != 7 || !WorkReportConstants.REPORT_TYPE_WEEKLY.equals(parts[0])) {
return periodKey;
}
LocalDate startDate = parsePeriodDate(parts, 1);
LocalDate endDate = parsePeriodDate(parts, 4);
int weekOfYear = startDate.get(ChronoField.ALIGNED_WEEK_OF_YEAR);
return startDate.getYear() + "" + weekOfYear + "周(" + formatMonthDayRange(startDate, endDate) + "";
}
private String formatMonthlyPeriodKey(String periodKey) {
String[] parts = periodKey.split("-");
if (parts.length != 7 || !WorkReportConstants.REPORT_TYPE_MONTHLY.equals(parts[0])) {
return periodKey;
}
LocalDate startDate = parsePeriodDate(parts, 1);
return startDate.getYear() + "" + startDate.getMonthValue() + "";
}
private String formatProjectPeriodKey(String periodKey) {
String[] parts = periodKey.split("-");
if (parts.length != 8 || !WorkReportConstants.REPORT_TYPE_PROJECT.equals(parts[0])) {
return periodKey;
}
LocalDate startDate = parsePeriodDate(parts, 1);
String half = switch (parts[7]) {
case "1" -> "上半月";
case "2" -> "下半月";
default -> startDate.getDayOfMonth() <= 15 ? "上半月" : "下半月";
};
return startDate.getYear() + "" + startDate.getMonthValue() + "" + half;
}
private LocalDate parsePeriodDate(String[] parts, int startIndex) {
try {
return LocalDate.parse(parts[startIndex] + "-" + parts[startIndex + 1] + "-" + parts[startIndex + 2]);
} catch (DateTimeParseException ex) {
throw new IllegalArgumentException("Invalid period date", ex);
}
}
private String formatMonthDayRange(LocalDate startDate, LocalDate endDate) {
return startDate.getMonthValue() + "" + startDate.getDayOfMonth()
+ "日-" + endDate.getMonthValue() + "" + endDate.getDayOfMonth() + "";
}
private String personalSlotKey(Long userId, String periodKey) {
return userId + ":" + periodKey;
}
private String projectSlotKey(Long userId, Long projectId, String periodKey) {
return userId + ":" + projectId + ":" + periodKey;
}
private String defaultText(String text) { private String defaultText(String text) {
return StringUtils.hasText(text) ? text.trim() : ""; return StringUtils.hasText(text) ? text.trim() : "";
} }
private ReportContext buildReportContextByDateRange(String reportType, LocalDate startDate, LocalDate endDate) { private record DateRange(LocalDate startDate, LocalDate endDate) {
if (WorkReportConstants.REPORT_TYPE_PROJECT.equals(reportType)) {
return buildProjectContextByDateRange(startDate, endDate);
} }
List<Long> subordinateIds = teamDashboardAccessService.getAllSubordinateUserIds();
if (subordinateIds.isEmpty()) { private record ReportPeriod(String periodKey, String periodLabel, LocalDate startDate, LocalDate endDate) {
}
private record ExpectedReportSlot(
String slotKey,
Long userId,
String periodKey,
String periodLabel,
Long projectId,
String projectName) {
}
private record ActualReportSlot(String slotKey, String statusCode) {
}
private record ReportContext(
List<ExpectedReportSlot> expectedSlots,
Set<String> submittedSlotKeys,
Set<String> pendingApprovalSlotKeys) {
private static ReportContext empty() {
return new ReportContext(Collections.emptyList(), Collections.emptySet(), Collections.emptySet()); return new ReportContext(Collections.emptyList(), Collections.emptySet(), Collections.emptySet());
} }
if (WorkReportConstants.REPORT_TYPE_WEEKLY.equals(reportType)) {
return buildWeeklyContextByDateRange(startDate, endDate, subordinateIds);
}
return buildMonthlyContextByDateRange(startDate, endDate, subordinateIds);
}
private ReportContext buildWeeklyContextByDateRange(LocalDate startDate, LocalDate endDate,
List<Long> subordinateIds) {
List<WeeklyReportDO> reports = weeklyReportMapper.selectListByReporterIdsAndPeriodDateRange(
subordinateIds, startDate, endDate, SUBMITTED_STATUS_CODES);
Set<Long> submittedUserIds = new LinkedHashSet<>();
Set<Long> pendingApprovalUserIds = new LinkedHashSet<>();
for (WeeklyReportDO report : reports) {
if (report == null || report.getReporterId() == null) {
continue;
}
submittedUserIds.add(report.getReporterId());
if (WorkReportConstants.STATUS_PENDING_APPROVAL.equals(report.getStatusCode())) {
pendingApprovalUserIds.add(report.getReporterId());
}
}
return new ReportContext(subordinateIds, submittedUserIds, pendingApprovalUserIds);
}
private ReportContext buildMonthlyContextByDateRange(LocalDate startDate, LocalDate endDate,
List<Long> subordinateIds) {
List<MonthlyReportDO> reports = monthlyReportMapper.selectListByReporterIdsAndPeriodDateRange(
subordinateIds, startDate, endDate, SUBMITTED_STATUS_CODES);
Set<Long> submittedUserIds = new LinkedHashSet<>();
Set<Long> pendingApprovalUserIds = new LinkedHashSet<>();
for (MonthlyReportDO report : reports) {
if (report == null || report.getReporterId() == null) {
continue;
}
submittedUserIds.add(report.getReporterId());
if (WorkReportConstants.STATUS_PENDING_APPROVAL.equals(report.getStatusCode())) {
pendingApprovalUserIds.add(report.getReporterId());
}
}
return new ReportContext(subordinateIds, submittedUserIds, pendingApprovalUserIds);
}
private ReportContext buildProjectContextByDateRange(LocalDate startDate, LocalDate endDate) {
List<Long> subordinateIds = teamDashboardAccessService.getAllSubordinateUserIds();
if (subordinateIds.isEmpty()) {
return new ReportContext(Collections.emptyList(), Collections.emptySet(), Collections.emptySet());
}
List<ProjectDO> activeProjects = loadActiveProjectsForSubordinates(subordinateIds);
if (activeProjects.isEmpty()) {
return new ReportContext(Collections.emptyList(), Collections.emptySet(), Collections.emptySet());
}
Map<Long, List<ProjectDO>> projectsByOwner = activeProjects.stream()
.filter(Objects::nonNull)
.filter(project -> project.getManagerUserId() != null)
.collect(Collectors.groupingBy(ProjectDO::getManagerUserId, LinkedHashMap::new, Collectors.toList()));
LinkedHashSet<Long> expectedUserIds = new LinkedHashSet<>(projectsByOwner.keySet());
List<ProjectReportDO> reports = projectReportMapper.selectListByProjectOwnerIdsAndPeriodDateRange(
expectedUserIds, startDate, endDate, SUBMITTED_STATUS_CODES);
Map<Long, Set<Long>> submittedProjectsByOwner = new HashMap<>();
Set<Long> submittedUserIds = new LinkedHashSet<>();
Set<Long> pendingApprovalUserIds = new LinkedHashSet<>();
for (ProjectReportDO report : reports) {
if (report == null || report.getProjectOwnerId() == null || report.getProjectId() == null) {
continue;
}
Long ownerId = report.getProjectOwnerId();
if (!expectedUserIds.contains(ownerId)) {
continue;
}
if (WorkReportConstants.STATUS_PENDING_APPROVAL.equals(report.getStatusCode())) {
pendingApprovalUserIds.add(ownerId);
}
submittedProjectsByOwner.computeIfAbsent(ownerId, key -> new LinkedHashSet<>()).add(report.getProjectId());
}
for (Map.Entry<Long, List<ProjectDO>> entry : projectsByOwner.entrySet()) {
Long ownerId = entry.getKey();
Set<Long> submittedProjectIds = submittedProjectsByOwner.getOrDefault(ownerId, Collections.emptySet());
boolean allSubmitted = entry.getValue().stream()
.map(ProjectDO::getId)
.filter(Objects::nonNull)
.allMatch(submittedProjectIds::contains);
if (allSubmitted) {
submittedUserIds.add(ownerId);
}
}
return new ReportContext(new ArrayList<>(expectedUserIds), submittedUserIds, pendingApprovalUserIds);
}
private record ReportContext(List<Long> expectedUserIds, Set<Long> submittedUserIds,
Set<Long> pendingApprovalUserIds) {
} }
} }