fix(工作报告、我的绩效、产品/项目、消息通知): 修复已知的一些问题。
This commit is contained in:
@@ -13,7 +13,7 @@ public class TeamPerformanceSummaryRespVO {
|
||||
|
||||
private String periodMonthStart;
|
||||
private String periodMonthEnd;
|
||||
private Integer totalSheetCount;
|
||||
private Integer expectedSheetCount;
|
||||
private Integer pendingSendCount;
|
||||
private Integer pendingConfirmCount;
|
||||
private BigDecimal confirmedRate;
|
||||
@@ -25,6 +25,7 @@ public class TeamPerformanceSummaryRespVO {
|
||||
public static class PendingSendUser {
|
||||
private Long userId;
|
||||
private String userNickname;
|
||||
private String periodMonth;
|
||||
private Long managerUserId;
|
||||
private String managerName;
|
||||
private Long sheetId;
|
||||
@@ -35,6 +36,7 @@ public class TeamPerformanceSummaryRespVO {
|
||||
public static class PendingConfirmUser {
|
||||
private Long userId;
|
||||
private String userNickname;
|
||||
private String periodMonth;
|
||||
private Long sheetId;
|
||||
private LocalDateTime sentTime;
|
||||
}
|
||||
|
||||
@@ -20,4 +20,7 @@ public class TeamReportRemindReqVO {
|
||||
|
||||
@Schema(description = "催办用户 ID 列表;不传则催办全部待提交用户")
|
||||
private List<Long> userIds;
|
||||
|
||||
@Schema(description = "项目 ID,仅项目半月报按单个项目槽位催办时传入", example = "2042074259501088770")
|
||||
private Long projectId;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,6 @@ import lombok.Data;
|
||||
@Data
|
||||
public class TeamReportRemindRespVO {
|
||||
|
||||
@Schema(description = "实际催办人数", requiredMode = Schema.RequiredMode.REQUIRED, example = "4")
|
||||
@Schema(description = "实际催办数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "4")
|
||||
private Integer remindedCount;
|
||||
}
|
||||
|
||||
@@ -17,24 +17,24 @@ public class TeamReportSummaryRespVO {
|
||||
@Schema(description = "实际查询周期结束日期")
|
||||
private LocalDate periodEndDate;
|
||||
|
||||
@Schema(description = "应填人数", requiredMode = Schema.RequiredMode.REQUIRED, example = "18")
|
||||
private Integer totalShouldSubmit;
|
||||
@Schema(description = "应填报告数", requiredMode = Schema.RequiredMode.REQUIRED, example = "18")
|
||||
private Integer expectedReportCount;
|
||||
|
||||
@Schema(description = "已提交人数", requiredMode = Schema.RequiredMode.REQUIRED, example = "12")
|
||||
private Integer submittedCount;
|
||||
@Schema(description = "已提交报告数", requiredMode = Schema.RequiredMode.REQUIRED, example = "12")
|
||||
private Integer submittedReportCount;
|
||||
|
||||
@Schema(description = "未提交人数", requiredMode = Schema.RequiredMode.REQUIRED, example = "6")
|
||||
private Integer unsubmittedCount;
|
||||
@Schema(description = "未提交报告数", requiredMode = Schema.RequiredMode.REQUIRED, example = "6")
|
||||
private Integer unsubmittedReportCount;
|
||||
|
||||
@Schema(description = "待审批人数", requiredMode = Schema.RequiredMode.REQUIRED, example = "5")
|
||||
private Integer pendingApprovalCount;
|
||||
@Schema(description = "待审批报告数", requiredMode = Schema.RequiredMode.REQUIRED, example = "5")
|
||||
private Integer pendingApprovalReportCount;
|
||||
|
||||
@Schema(description = "未提交人员列表")
|
||||
private List<PendingUser> unsubmittedUsers;
|
||||
@Schema(description = "未提交报告槽位列表")
|
||||
private List<UnsubmittedReport> unsubmittedReports;
|
||||
|
||||
@Schema(description = "未提交人员")
|
||||
@Schema(description = "未提交报告槽位")
|
||||
@Data
|
||||
public static class PendingUser {
|
||||
public static class UnsubmittedReport {
|
||||
|
||||
@Schema(description = "用户 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "2042074259501088770")
|
||||
@com.fasterxml.jackson.databind.annotation.JsonSerialize(using = ToStringSerializer.class)
|
||||
@@ -42,5 +42,19 @@ public class TeamReportSummaryRespVO {
|
||||
|
||||
@Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "戴坤")
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
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.TableName;
|
||||
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;
|
||||
/**
|
||||
* 最近一次状态动作原因
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
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.TableName;
|
||||
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;
|
||||
/**
|
||||
* 最近一次状态动作原因
|
||||
|
||||
@@ -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_PROJECT_HALF_MONTH_TEAM_REMIND =
|
||||
"work_report_project_half_month_team_remind";
|
||||
|
||||
/** 执行指派:创建执行后通知负责人 + 协办人 */
|
||||
public static final String EXECUTION_ASSIGNED = "execution_assigned";
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.njcn.rdms.module.project.service.performance.team;
|
||||
|
||||
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.controller.admin.performance.team.vo.TeamPerformanceRemindReqVO;
|
||||
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.NotifyTemplateCodeConstants;
|
||||
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.UserManagementRelationApi;
|
||||
import com.njcn.rdms.module.system.api.user.dto.AdminUserRespDTO;
|
||||
@@ -48,6 +49,8 @@ public class TeamPerformanceServiceImpl implements TeamPerformanceService {
|
||||
@Resource
|
||||
private AdminUserApi adminUserApi;
|
||||
@Resource
|
||||
private DeptApi deptApi;
|
||||
@Resource
|
||||
private UserManagementRelationApi userManagementRelationApi;
|
||||
@Resource
|
||||
private ApplicationEventPublisher applicationEventPublisher;
|
||||
@@ -58,23 +61,59 @@ public class TeamPerformanceServiceImpl implements TeamPerformanceService {
|
||||
String[] range = normalizePeriodMonthRange(
|
||||
reqVO == null ? null : reqVO.getPeriodMonthStart(),
|
||||
reqVO == null ? null : reqVO.getPeriodMonthEnd());
|
||||
List<String> periodMonths = buildPeriodMonths(range[0], range[1]);
|
||||
List<Long> subordinateIds = teamDashboardAccessService.getAllSubordinateUserIds();
|
||||
Map<Long, AdminUserRespDTO> userMap = loadUserMap(subordinateIds);
|
||||
Map<Long, DeptRespDTO> deptMap = loadDeptMap(userMap.values());
|
||||
List<PerformanceSheetDO> sheets = performanceSheetMapper
|
||||
.selectListByEmployeeIdsAndPeriodMonthRange(subordinateIds, range[0], range[1]);
|
||||
Map<Long, List<PerformanceSheetDO>> sheetsByEmployee = sheets.stream()
|
||||
.collect(Collectors.groupingBy(PerformanceSheetDO::getEmployeeId));
|
||||
Map<Long, AdminUserRespDTO> userMap = loadUserMap(subordinateIds);
|
||||
Map<String, PerformanceSheetDO> sheetBySlot = buildSheetBySlot(sheets);
|
||||
Map<Long, AdminUserRespDTO> directManagerCache = new HashMap<>();
|
||||
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();
|
||||
respVO.setPeriodMonthStart(range[0]);
|
||||
respVO.setPeriodMonthEnd(range[1]);
|
||||
respVO.setTotalSheetCount(sheets.size());
|
||||
respVO.setPendingSendUsers(buildPendingSendUsers(subordinateIds, sheetsByEmployee, userMap));
|
||||
respVO.setPendingConfirmUsers(buildPendingConfirmUsers(sheets, userMap));
|
||||
respVO.setPendingSendCount(respVO.getPendingSendUsers().size());
|
||||
respVO.setPendingConfirmCount(respVO.getPendingConfirmUsers().size());
|
||||
respVO.setConfirmedRate(calculateConfirmedRate(sheets));
|
||||
respVO.setDeptOrgAverages(buildDeptOrgAverages(sheets));
|
||||
respVO.setExpectedSheetCount(expectedSheetCount);
|
||||
respVO.setPendingSendUsers(pendingSendUsers);
|
||||
respVO.setPendingConfirmUsers(pendingConfirmUsers);
|
||||
respVO.setPendingSendCount(pendingSendUsers.size());
|
||||
respVO.setPendingConfirmCount(pendingConfirmUsers.size());
|
||||
respVO.setConfirmedRate(calculateConfirmedRate(expectedSheetCount, confirmedCount));
|
||||
respVO.setDeptOrgAverages(deptAverageMap.values().stream()
|
||||
.map(DeptAverageAccumulator::toRespVO)
|
||||
.toList());
|
||||
return respVO;
|
||||
}
|
||||
|
||||
@@ -83,6 +122,7 @@ public class TeamPerformanceServiceImpl implements TeamPerformanceService {
|
||||
public TeamPerformanceRemindRespVO remind(TeamPerformanceRemindReqVO reqVO) {
|
||||
teamDashboardAccessService.validateTeamDashboardPermission(PerformanceConstants.PERMISSION_TEAM_DASHBOARD);
|
||||
String[] range = normalizePeriodMonthRange(reqVO.getPeriodMonthStart(), reqVO.getPeriodMonthEnd());
|
||||
List<String> periodMonths = buildPeriodMonths(range[0], range[1]);
|
||||
List<Long> subordinateIds = teamDashboardAccessService.getAllSubordinateUserIds();
|
||||
List<Long> targetEmployeeIds = reqVO.getUserIds() == null
|
||||
? subordinateIds
|
||||
@@ -90,15 +130,14 @@ public class TeamPerformanceServiceImpl implements TeamPerformanceService {
|
||||
reqVO.getUserIds(), PerformanceConstants.PERMISSION_TEAM_DASHBOARD);
|
||||
List<PerformanceSheetDO> sheets = performanceSheetMapper
|
||||
.selectListByEmployeeIdsAndPeriodMonthRange(targetEmployeeIds, range[0], range[1]);
|
||||
Map<Long, List<PerformanceSheetDO>> sheetsByEmployee = sheets.stream()
|
||||
.collect(Collectors.groupingBy(PerformanceSheetDO::getEmployeeId));
|
||||
Map<String, PerformanceSheetDO> sheetBySlot = buildSheetBySlot(sheets);
|
||||
Map<Long, AdminUserRespDTO> userMap = loadUserMap(targetEmployeeIds);
|
||||
|
||||
int remindedCount;
|
||||
if (PerformanceConstants.REMIND_TYPE_PENDING_CONFIRM.equals(reqVO.getRemindType())) {
|
||||
remindedCount = remindPendingConfirm(sheets);
|
||||
} else if (PerformanceConstants.REMIND_TYPE_PENDING_SEND.equals(reqVO.getRemindType())) {
|
||||
remindedCount = remindPendingSend(targetEmployeeIds, sheetsByEmployee, userMap, range[0]);
|
||||
remindedCount = remindPendingSend(targetEmployeeIds, periodMonths, sheetBySlot, userMap);
|
||||
} else {
|
||||
throw invalidParamException("催办类型不合法");
|
||||
}
|
||||
@@ -107,78 +146,12 @@ public class TeamPerformanceServiceImpl implements TeamPerformanceService {
|
||||
return respVO;
|
||||
}
|
||||
|
||||
private List<TeamPerformanceSummaryRespVO.PendingSendUser> buildPendingSendUsers(
|
||||
List<Long> subordinateIds, Map<Long, List<PerformanceSheetDO>> sheetsByEmployee,
|
||||
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()) {
|
||||
private BigDecimal calculateConfirmedRate(int expectedSheetCount, int confirmedCount) {
|
||||
if (expectedSheetCount <= 0) {
|
||||
return BigDecimal.ZERO.setScale(2);
|
||||
}
|
||||
long confirmed = sheets.stream()
|
||||
.filter(sheet -> PerformanceConstants.STATUS_CONFIRMED.equals(sheet.getStatusCode()))
|
||||
.count();
|
||||
return BigDecimal.valueOf(confirmed * 100)
|
||||
.divide(BigDecimal.valueOf(sheets.size()), 2, RoundingMode.HALF_UP);
|
||||
return BigDecimal.valueOf(confirmedCount * 100L)
|
||||
.divide(BigDecimal.valueOf(expectedSheetCount), 2, RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
private int remindPendingConfirm(List<PerformanceSheetDO> sheets) {
|
||||
@@ -194,27 +167,31 @@ public class TeamPerformanceServiceImpl implements TeamPerformanceService {
|
||||
return count;
|
||||
}
|
||||
|
||||
private int remindPendingSend(List<Long> targetEmployeeIds, Map<Long, List<PerformanceSheetDO>> sheetsByEmployee,
|
||||
Map<Long, AdminUserRespDTO> userMap, String defaultPeriodMonth) {
|
||||
private int remindPendingSend(List<Long> targetEmployeeIds, List<String> periodMonths,
|
||||
Map<String, PerformanceSheetDO> sheetBySlot, Map<Long, AdminUserRespDTO> userMap) {
|
||||
int count = 0;
|
||||
Map<Long, AdminUserRespDTO> directManagerCache = new HashMap<>();
|
||||
for (Long employeeId : targetEmployeeIds) {
|
||||
List<PerformanceSheetDO> employeeSheets = sheetsByEmployee.getOrDefault(employeeId, List.of());
|
||||
if (!hasPendingSend(employeeSheets)) {
|
||||
PendingSendContext pendingContext = findPendingSendContext(employeeId, periodMonths, sheetBySlot);
|
||||
if (pendingContext == null) {
|
||||
continue;
|
||||
}
|
||||
PerformanceSheetDO firstPending = employeeSheets.isEmpty() ? null : employeeSheets.get(0);
|
||||
AdminUserRespDTO manager = firstPending == null ? loadDirectManagerQuietly(employeeId) : null;
|
||||
Long managerId = firstPending == null ? (manager == null ? null : manager.getId())
|
||||
: firstPending.getManagerId();
|
||||
PerformanceSheetDO pendingSheet = pendingContext.sheet();
|
||||
AdminUserRespDTO manager = pendingSheet == null
|
||||
? directManagerCache.computeIfAbsent(employeeId, this::loadDirectManagerQuietly)
|
||||
: null;
|
||||
Long managerId = pendingSheet != null && pendingSheet.getManagerId() != null
|
||||
? pendingSheet.getManagerId()
|
||||
: (manager == null ? null : manager.getId());
|
||||
if (managerId == null) {
|
||||
continue;
|
||||
}
|
||||
String employeeName = firstPending == null
|
||||
String employeeName = pendingSheet == null
|
||||
? defaultText(userMap.get(employeeId) == null ? null : userMap.get(employeeId).getNickname())
|
||||
: firstPending.getEmployeeName();
|
||||
PerformanceSheetDO noticeSheet = firstPending == null
|
||||
? buildVirtualSheet(defaultPeriodMonth, employeeId, employeeName, manager)
|
||||
: firstPending;
|
||||
: defaultText(pendingSheet.getEmployeeName());
|
||||
PerformanceSheetDO noticeSheet = pendingSheet == null
|
||||
? buildVirtualSheet(pendingContext.periodMonth(), employeeId, employeeName, manager)
|
||||
: pendingSheet;
|
||||
publishNotice(List.of(managerId),
|
||||
NotifyTemplateCodeConstants.PERFORMANCE_PENDING_SEND_REMIND, noticeSheet, employeeName);
|
||||
count++;
|
||||
@@ -248,6 +225,21 @@ public class TeamPerformanceServiceImpl implements TeamPerformanceService {
|
||||
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) {
|
||||
if (userIds == null || userIds.isEmpty()) {
|
||||
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) {
|
||||
String normalizedStart = StringUtils.hasText(start) ? normalizeSinglePeriodMonth(start) : null;
|
||||
String normalizedEnd = StringUtils.hasText(end) ? normalizeSinglePeriodMonth(end) : null;
|
||||
@@ -281,7 +406,13 @@ public class TeamPerformanceServiceImpl implements TeamPerformanceService {
|
||||
String currentMonth = YearMonth.now().toString();
|
||||
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("绩效月份起始不能晚于结束月份");
|
||||
}
|
||||
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) {
|
||||
if (statusCode == null) {
|
||||
return "未创建";
|
||||
@@ -329,7 +449,8 @@ public class TeamPerformanceServiceImpl implements TeamPerformanceService {
|
||||
private final String deptName;
|
||||
private final String deptOrgType;
|
||||
private BigDecimal total = BigDecimal.ZERO;
|
||||
private int count = 0;
|
||||
private int expectedCount = 0;
|
||||
private int confirmedCount = 0;
|
||||
|
||||
DeptAverageAccumulator(Long deptId, String deptName, String deptOrgType) {
|
||||
this.deptId = deptId;
|
||||
@@ -337,9 +458,15 @@ public class TeamPerformanceServiceImpl implements TeamPerformanceService {
|
||||
this.deptOrgType = deptOrgType;
|
||||
}
|
||||
|
||||
void add(BigDecimal score) {
|
||||
total = total.add(score);
|
||||
count++;
|
||||
void incrementExpectedCount() {
|
||||
expectedCount++;
|
||||
}
|
||||
|
||||
void addConfirmedScore(BigDecimal score) {
|
||||
confirmedCount++;
|
||||
if (score != null) {
|
||||
total = total.add(score);
|
||||
}
|
||||
}
|
||||
|
||||
TeamPerformanceSummaryRespVO.DeptOrgAverage toRespVO() {
|
||||
@@ -347,11 +474,20 @@ public class TeamPerformanceServiceImpl implements TeamPerformanceService {
|
||||
respVO.setDeptId(deptId);
|
||||
respVO.setDeptName(deptName);
|
||||
respVO.setDeptOrgType(deptOrgType);
|
||||
respVO.setConfirmedCount(count);
|
||||
respVO.setAverageScore(count == 0
|
||||
respVO.setConfirmedCount(confirmedCount);
|
||||
respVO.setAverageScore(expectedCount == 0
|
||||
? BigDecimal.ZERO.setScale(2)
|
||||
: total.divide(BigDecimal.valueOf(count), 2, RoundingMode.HALF_UP));
|
||||
: total.divide(BigDecimal.valueOf(expectedCount), 2, RoundingMode.HALF_UP));
|
||||
return respVO;
|
||||
}
|
||||
}
|
||||
|
||||
private record PendingSendContext(String periodMonth, PerformanceSheetDO sheet) {
|
||||
}
|
||||
|
||||
private record DeptBucket(Long deptId, String deptName, String deptOrgType) {
|
||||
private String key() {
|
||||
return deptId + ":" + deptOrgType;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,11 @@ import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.DayOfWeek;
|
||||
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.Collection;
|
||||
import java.util.Collections;
|
||||
@@ -72,29 +76,23 @@ public class TeamWorkReportServiceImpl implements TeamWorkReportService {
|
||||
public TeamReportSummaryRespVO getSummary(TeamReportSummaryReqVO reqVO) {
|
||||
teamDashboardAccessService.validateTeamDashboardPermission(WorkReportConstants.PERMISSION_TEAM_DASHBOARD);
|
||||
String reportType = normalizeReportType(reqVO.getReportType());
|
||||
boolean useDateRange = reqVO.getPeriodStartDate() != null || reqVO.getPeriodEndDate() != null;
|
||||
DateRange dateRange = normalizeDateRange(reqVO.getPeriodStartDate(), reqVO.getPeriodEndDate());
|
||||
ReportContext context;
|
||||
LocalDate respStartDate;
|
||||
LocalDate respEndDate;
|
||||
if (useDateRange) {
|
||||
respStartDate = reqVO.getPeriodStartDate();
|
||||
respEndDate = reqVO.getPeriodEndDate();
|
||||
context = buildReportContextByDateRange(reportType, respStartDate, respEndDate);
|
||||
if (dateRange != null) {
|
||||
context = buildReportContextByDateRange(reportType, dateRange.startDate(), dateRange.endDate());
|
||||
} else {
|
||||
context = buildReportContext(reportType, reqVO.getPeriodKey());
|
||||
respStartDate = null;
|
||||
respEndDate = null;
|
||||
}
|
||||
|
||||
List<TeamReportSummaryRespVO.UnsubmittedReport> unsubmittedReports = buildUnsubmittedReports(context);
|
||||
TeamReportSummaryRespVO respVO = new TeamReportSummaryRespVO();
|
||||
respVO.setPeriodStartDate(respStartDate);
|
||||
respVO.setPeriodEndDate(respEndDate);
|
||||
respVO.setTotalShouldSubmit(context.expectedUserIds().size());
|
||||
respVO.setSubmittedCount(context.submittedUserIds().size());
|
||||
respVO.setPendingApprovalCount(context.pendingApprovalUserIds().size());
|
||||
List<TeamReportSummaryRespVO.PendingUser> unsubmittedUsers = buildPendingUsers(context.expectedUserIds(),
|
||||
context.submittedUserIds());
|
||||
respVO.setUnsubmittedUsers(unsubmittedUsers);
|
||||
respVO.setUnsubmittedCount(unsubmittedUsers.size());
|
||||
respVO.setPeriodStartDate(dateRange == null ? null : dateRange.startDate());
|
||||
respVO.setPeriodEndDate(dateRange == null ? null : dateRange.endDate());
|
||||
respVO.setExpectedReportCount(context.expectedSlots().size());
|
||||
respVO.setSubmittedReportCount(context.submittedSlotKeys().size());
|
||||
respVO.setUnsubmittedReportCount(unsubmittedReports.size());
|
||||
respVO.setPendingApprovalReportCount(context.pendingApprovalSlotKeys().size());
|
||||
respVO.setUnsubmittedReports(unsubmittedReports);
|
||||
return respVO;
|
||||
}
|
||||
|
||||
@@ -104,117 +102,193 @@ public class TeamWorkReportServiceImpl implements TeamWorkReportService {
|
||||
teamDashboardAccessService.validateTeamDashboardPermission(WorkReportConstants.PERMISSION_TEAM_DASHBOARD);
|
||||
String reportType = normalizeReportType(reqVO.getReportType());
|
||||
ReportContext context = buildReportContext(reportType, reqVO.getPeriodKey());
|
||||
List<Long> remindUserIds = resolveRemindUserIds(reqVO.getUserIds(), context);
|
||||
|
||||
if (!remindUserIds.isEmpty()) {
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("reportTypeName", reportTypeDisplayName(reportType));
|
||||
params.put("periodKey", reqVO.getPeriodKey());
|
||||
params.put("managerName", defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
|
||||
applicationEventPublisher.publishEvent(NotifySendEvent.of(remindUserIds,
|
||||
NotifyTemplateCodeConstants.WORK_REPORT_TEAM_REMIND, params, NotifyMessageLevelConstants.REMIND));
|
||||
}
|
||||
int remindedCount = WorkReportConstants.REPORT_TYPE_PROJECT.equals(reportType)
|
||||
? remindProjectReport(reqVO, context)
|
||||
: remindPersonalReport(reportType, reqVO, context);
|
||||
|
||||
TeamReportRemindRespVO respVO = new TeamReportRemindRespVO();
|
||||
respVO.setRemindedCount(remindUserIds.size());
|
||||
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<>();
|
||||
params.put("reportTypeName", reportTypeDisplayName(reportType));
|
||||
params.put("periodKey", formatPeriodKeyForRemind(reportType, reqVO.getPeriodKey()));
|
||||
params.put("managerName", defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
|
||||
applicationEventPublisher.publishEvent(NotifySendEvent.of(remindUserIds,
|
||||
NotifyTemplateCodeConstants.WORK_REPORT_TEAM_REMIND, params, NotifyMessageLevelConstants.REMIND));
|
||||
return remindUserIds.size();
|
||||
}
|
||||
|
||||
private int remindProjectReport(TeamReportRemindReqVO reqVO, ReportContext context) {
|
||||
List<ExpectedReportSlot> remindSlots = resolveProjectRemindSlots(reqVO, context);
|
||||
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) {
|
||||
if (!StringUtils.hasText(periodKey)) {
|
||||
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)) {
|
||||
return buildProjectContext(periodKey);
|
||||
return buildProjectContext(periods);
|
||||
}
|
||||
List<Long> subordinateIds = teamDashboardAccessService.getAllSubordinateUserIds();
|
||||
if (subordinateIds.isEmpty()) {
|
||||
return new ReportContext(Collections.emptyList(), Collections.emptySet(), Collections.emptySet());
|
||||
if (subordinateIds.isEmpty() || periods.isEmpty()) {
|
||||
return ReportContext.empty();
|
||||
}
|
||||
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) {
|
||||
List<WeeklyReportDO> reports = weeklyReportMapper.selectListByReporterIdsAndPeriodKey(
|
||||
subordinateIds, periodKey, 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 buildWeeklyContext(List<ReportPeriod> periods, List<Long> subordinateIds) {
|
||||
Map<String, ExpectedReportSlot> expectedSlotMap = buildPersonalExpectedSlotMap(subordinateIds, periods);
|
||||
List<WeeklyReportDO> reports = weeklyReportMapper.selectListByReporterIdsAndPeriodDateRange(
|
||||
subordinateIds, periods.get(0).startDate(), periods.get(periods.size() - 1).startDate(),
|
||||
SUBMITTED_STATUS_CODES);
|
||||
return buildPersonalReportContext(expectedSlotMap, reports.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.filter(report -> report.getReporterId() != null)
|
||||
.map(report -> new ActualReportSlot(
|
||||
personalSlotKey(report.getReporterId(), report.getPeriodKey()),
|
||||
report.getStatusCode()))
|
||||
.toList());
|
||||
}
|
||||
|
||||
private ReportContext buildMonthlyContext(String periodKey, List<Long> subordinateIds) {
|
||||
List<MonthlyReportDO> reports = monthlyReportMapper.selectListByReporterIdsAndPeriodKey(
|
||||
subordinateIds, periodKey, 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 buildMonthlyContext(List<ReportPeriod> periods, List<Long> subordinateIds) {
|
||||
Map<String, ExpectedReportSlot> expectedSlotMap = buildPersonalExpectedSlotMap(subordinateIds, periods);
|
||||
List<MonthlyReportDO> reports = monthlyReportMapper.selectListByReporterIdsAndPeriodDateRange(
|
||||
subordinateIds, periods.get(0).startDate(), periods.get(periods.size() - 1).startDate(),
|
||||
SUBMITTED_STATUS_CODES);
|
||||
return buildPersonalReportContext(expectedSlotMap, reports.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.filter(report -> report.getReporterId() != null)
|
||||
.map(report -> new ActualReportSlot(
|
||||
personalSlotKey(report.getReporterId(), report.getPeriodKey()),
|
||||
report.getStatusCode()))
|
||||
.toList());
|
||||
}
|
||||
|
||||
private ReportContext buildProjectContext(String periodKey) {
|
||||
private ReportContext buildProjectContext(List<ReportPeriod> periods) {
|
||||
List<Long> subordinateIds = teamDashboardAccessService.getAllSubordinateUserIds();
|
||||
if (subordinateIds.isEmpty()) {
|
||||
return new ReportContext(Collections.emptyList(), Collections.emptySet(), Collections.emptySet());
|
||||
if (subordinateIds.isEmpty() || periods.isEmpty()) {
|
||||
return ReportContext.empty();
|
||||
}
|
||||
List<ProjectDO> activeProjects = loadActiveProjectsForSubordinates(subordinateIds);
|
||||
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(project -> project.getManagerUserId() != null)
|
||||
.collect(Collectors.groupingBy(ProjectDO::getManagerUserId, LinkedHashMap::new, Collectors.toList()));
|
||||
LinkedHashSet<Long> expectedUserIds = new LinkedHashSet<>(projectsByOwner.keySet());
|
||||
List<ProjectReportDO> reports = projectReportMapper.selectListByProjectOwnerIdsAndPeriodKey(
|
||||
expectedUserIds, periodKey, SUBMITTED_STATUS_CODES);
|
||||
Map<Long, Set<Long>> submittedProjectsByOwner = new HashMap<>();
|
||||
Set<Long> submittedUserIds = new LinkedHashSet<>();
|
||||
Set<Long> pendingApprovalUserIds = new LinkedHashSet<>();
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
if (ownerIds.isEmpty()) {
|
||||
return ReportContext.empty();
|
||||
}
|
||||
List<ProjectReportDO> reports = projectReportMapper.selectListByProjectOwnerIdsAndPeriodDateRange(
|
||||
ownerIds, periods.get(0).startDate(), periods.get(periods.size() - 1).startDate(),
|
||||
SUBMITTED_STATUS_CODES);
|
||||
Set<String> submittedSlotKeys = new LinkedHashSet<>();
|
||||
Set<String> pendingApprovalSlotKeys = new LinkedHashSet<>();
|
||||
for (ProjectReportDO report : reports) {
|
||||
if (report == null || report.getProjectOwnerId() == null || report.getProjectId() == null) {
|
||||
continue;
|
||||
}
|
||||
Long ownerId = report.getProjectOwnerId();
|
||||
if (!expectedUserIds.contains(ownerId)) {
|
||||
String slotKey = projectSlotKey(report.getProjectOwnerId(), report.getProjectId(), report.getPeriodKey());
|
||||
if (!expectedSlotMap.containsKey(slotKey)) {
|
||||
continue;
|
||||
}
|
||||
submittedSlotKeys.add(slotKey);
|
||||
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);
|
||||
pendingApprovalSlotKeys.add(slotKey);
|
||||
}
|
||||
}
|
||||
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) {
|
||||
@@ -236,8 +310,11 @@ public class TeamWorkReportServiceImpl implements TeamWorkReportService {
|
||||
}
|
||||
|
||||
private List<Long> resolveRemindUserIds(List<Long> requestedUserIds, ReportContext context) {
|
||||
LinkedHashSet<Long> unsubmittedUserIds = new LinkedHashSet<>(context.expectedUserIds());
|
||||
unsubmittedUserIds.removeAll(context.submittedUserIds());
|
||||
LinkedHashSet<Long> unsubmittedUserIds = context.expectedSlots().stream()
|
||||
.filter(slot -> !context.submittedSlotKeys().contains(slot.slotKey()))
|
||||
.map(ExpectedReportSlot::userId)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
if (requestedUserIds == null) {
|
||||
return new ArrayList<>(unsubmittedUserIds);
|
||||
}
|
||||
@@ -248,34 +325,216 @@ public class TeamWorkReportServiceImpl implements TeamWorkReportService {
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private List<TeamReportSummaryRespVO.PendingUser> buildPendingUsers(List<Long> expectedUserIds,
|
||||
Set<Long> submittedUserIds) {
|
||||
LinkedHashSet<Long> pendingIds = new LinkedHashSet<>(expectedUserIds);
|
||||
pendingIds.removeAll(submittedUserIds);
|
||||
if (pendingIds.isEmpty()) {
|
||||
private List<ExpectedReportSlot> resolveProjectRemindSlots(TeamReportRemindReqVO reqVO, ReportContext context) {
|
||||
List<ExpectedReportSlot> pendingSlots = context.expectedSlots().stream()
|
||||
.filter(slot -> !context.submittedSlotKeys().contains(slot.slotKey()))
|
||||
.toList();
|
||||
if (pendingSlots.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
CommonResult<List<AdminUserRespDTO>> result = adminUserApi.getUserList(pendingIds);
|
||||
List<AdminUserRespDTO> users = result == null ? null : result.getCheckedData();
|
||||
if (users == null || users.isEmpty()) {
|
||||
users = Collections.emptyList();
|
||||
|
||||
Set<Long> validatedUserIds = reqVO.getUserIds() == null
|
||||
? null
|
||||
: 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();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
Map<Long, AdminUserRespDTO> userMap = users.stream()
|
||||
LinkedHashSet<Long> userIds = pendingSlots.stream()
|
||||
.map(ExpectedReportSlot::userId)
|
||||
.filter(Objects::nonNull)
|
||||
.filter(user -> user.getId() != null)
|
||||
.collect(Collectors.toMap(AdminUserRespDTO::getId, user -> user, (left, right) -> left,
|
||||
LinkedHashMap::new));
|
||||
List<TeamReportSummaryRespVO.PendingUser> respList = new ArrayList<>();
|
||||
for (Long pendingId : pendingIds) {
|
||||
AdminUserRespDTO user = userMap.get(pendingId);
|
||||
TeamReportSummaryRespVO.PendingUser item = new TeamReportSummaryRespVO.PendingUser();
|
||||
item.setUserId(pendingId);
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
Map<Long, AdminUserRespDTO> userMap = loadUserMap(userIds);
|
||||
|
||||
List<TeamReportSummaryRespVO.UnsubmittedReport> respList = new ArrayList<>();
|
||||
for (ExpectedReportSlot pendingSlot : pendingSlots) {
|
||||
AdminUserRespDTO user = userMap.get(pendingSlot.userId());
|
||||
TeamReportSummaryRespVO.UnsubmittedReport item = new TeamReportSummaryRespVO.UnsubmittedReport();
|
||||
item.setUserId(pendingSlot.userId());
|
||||
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);
|
||||
}
|
||||
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) {
|
||||
if (WorkReportConstants.REPORT_TYPE_WEEKLY.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) {
|
||||
return StringUtils.hasText(text) ? text.trim() : "";
|
||||
}
|
||||
|
||||
private ReportContext buildReportContextByDateRange(String reportType, 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 DateRange(LocalDate startDate, LocalDate endDate) {
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
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) {
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user