fix(加班申请、工作报告、我的绩效): 修复一系列bug、对不合理的地方进行调整。

This commit is contained in:
dk
2026-06-22 22:58:03 +08:00
parent b4f6eab64c
commit 856668ec25
12 changed files with 365 additions and 64 deletions

View File

@@ -2,11 +2,21 @@ package com.njcn.rdms.module.project.controller.admin.overtime.team.vo;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDate;
import static com.njcn.rdms.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY;
@Schema(description = "管理后台 - 团队加班申请统计 Request VO") @Schema(description = "管理后台 - 团队加班申请统计 Request VO")
@Data @Data
public class TeamOvertimeSummaryReqVO { public class TeamOvertimeSummaryReqVO {
@Schema(description = "统计月份,不传默认当前月", example = "2026-06") @Schema(description = "加班日期区间起始(不传默认当月第一天)", example = "2026-06-01")
private String month; @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY)
private LocalDate overtimeDateStart;
@Schema(description = "加班日期区间结束(不传默认当月最后一天)", example = "2026-06-30")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY)
private LocalDate overtimeDateEnd;
} }

View File

@@ -3,12 +3,17 @@ package com.njcn.rdms.module.project.controller.admin.overtime.team.vo;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import java.time.LocalDate;
@Schema(description = "管理后台 - 团队加班申请统计 Response VO") @Schema(description = "管理后台 - 团队加班申请统计 Response VO")
@Data @Data
public class TeamOvertimeSummaryRespVO { public class TeamOvertimeSummaryRespVO {
@Schema(description = "统计月份", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-06") @Schema(description = "实际查询加班日期区间起始", requiredMode = Schema.RequiredMode.REQUIRED)
private String month; private LocalDate overtimeDateStart;
@Schema(description = "实际查询加班日期区间结束", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDate overtimeDateEnd;
@Schema(description = "申请总数", requiredMode = Schema.RequiredMode.REQUIRED, example = "12") @Schema(description = "申请总数", requiredMode = Schema.RequiredMode.REQUIRED, example = "12")
private Integer totalApplicationCount; private Integer totalApplicationCount;

View File

@@ -1,10 +1,11 @@
package com.njcn.rdms.module.project.controller.admin.workreport.common; package com.njcn.rdms.module.project.controller.admin.workreport.common;
import com.njcn.rdms.framework.common.util.http.HttpUtils;
import com.njcn.rdms.module.project.service.workreport.export.WorkReportExportFile; import com.njcn.rdms.module.project.service.workreport.export.WorkReportExportFile;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException; import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
public final class WorkReportExportResponseUtils { public final class WorkReportExportResponseUtils {
@@ -12,7 +13,8 @@ public final class WorkReportExportResponseUtils {
} }
public static void write(HttpServletResponse response, WorkReportExportFile file) throws IOException { public static void write(HttpServletResponse response, WorkReportExportFile file) throws IOException {
response.addHeader("Content-Disposition", "attachment;filename=" + HttpUtils.encodeUtf8(file.filename())); response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''"
+ URLEncoder.encode(file.filename(), StandardCharsets.UTF_8).replace("+", "%20"));
response.setContentType(file.contentType()); response.setContentType(file.contentType());
response.setContentLength(file.content().length); response.setContentLength(file.content().length);
response.getOutputStream().write(file.content()); response.getOutputStream().write(file.content());

View File

@@ -3,6 +3,11 @@ package com.njcn.rdms.module.project.controller.admin.workreport.team.vo;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import lombok.Data; import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDate;
import static com.njcn.rdms.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY;
@Schema(description = "管理后台 - 团队工作报告统计 Request VO") @Schema(description = "管理后台 - 团队工作报告统计 Request VO")
@Data @Data
@@ -12,7 +17,14 @@ public class TeamReportSummaryReqVO {
@NotBlank(message = "报告类型不能为空") @NotBlank(message = "报告类型不能为空")
private String reportType; private String reportType;
@Schema(description = "周期主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "weekly-2026-06-08-2026-06-14") @Schema(description = "周期主键(单周期查询时使用,与日期区间二选一)", example = "weekly-2026-06-08-2026-06-14")
@NotBlank(message = "周期主键不能为空")
private String periodKey; private String periodKey;
@Schema(description = "周期起始日期(区间查询时使用,与 periodKey 二选一)", example = "2026-06-15")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY)
private LocalDate periodStartDate;
@Schema(description = "周期结束日期(区间查询时使用,与 periodKey 二选一)", example = "2026-06-28")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY)
private LocalDate periodEndDate;
} }

View File

@@ -4,12 +4,19 @@ import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import java.time.LocalDate;
import java.util.List; import java.util.List;
@Schema(description = "管理后台 - 团队工作报告统计 Response VO") @Schema(description = "管理后台 - 团队工作报告统计 Response VO")
@Data @Data
public class TeamReportSummaryRespVO { public class TeamReportSummaryRespVO {
@Schema(description = "实际查询周期起始日期")
private LocalDate periodStartDate;
@Schema(description = "实际查询周期结束日期")
private LocalDate periodEndDate;
@Schema(description = "应填人数", requiredMode = Schema.RequiredMode.REQUIRED, example = "18") @Schema(description = "应填人数", requiredMode = Schema.RequiredMode.REQUIRED, example = "18")
private Integer totalShouldSubmit; private Integer totalShouldSubmit;

View File

@@ -10,6 +10,7 @@ import com.njcn.rdms.module.project.dal.dataobject.workreport.monthly.MonthlyRep
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
@@ -40,6 +41,28 @@ public interface MonthlyReportMapper extends BaseMapperX<MonthlyReportDO> {
return selectList(wrapper); return selectList(wrapper);
} }
/**
* 按报告人 ID 列表与周期日期区间查询月报periodStartDate 落在 [startDate, endDate] 内)。
*/
default List<MonthlyReportDO> selectListByReporterIdsAndPeriodDateRange(Collection<Long> reporterIds,
LocalDate startDate, LocalDate endDate,
Collection<String> allowedStatusCodes) {
if (reporterIds == null || reporterIds.isEmpty()) {
return List.of();
}
LambdaQueryWrapperX<MonthlyReportDO> wrapper = new LambdaQueryWrapperX<MonthlyReportDO>()
.in(MonthlyReportDO::getReporterId, reporterIds)
.geIfPresent(MonthlyReportDO::getPeriodStartDate, startDate)
.leIfPresent(MonthlyReportDO::getPeriodStartDate, endDate);
if (allowedStatusCodes != null) {
if (allowedStatusCodes.isEmpty()) {
return List.of();
}
wrapper.in(MonthlyReportDO::getStatusCode, allowedStatusCodes);
}
return selectList(wrapper);
}
default PageResult<MonthlyReportDO> selectReporterPage(Long reporterId, MonthlyReportPageReqVO reqVO) { default PageResult<MonthlyReportDO> selectReporterPage(Long reporterId, MonthlyReportPageReqVO reqVO) {
return selectReporterPage(reporterId, reqVO, null); return selectReporterPage(reporterId, reqVO, null);
} }
@@ -61,7 +84,8 @@ public interface MonthlyReportMapper extends BaseMapperX<MonthlyReportDO> {
default PageResult<MonthlyReportDO> selectReporterPage(Collection<Long> reporterIds, MonthlyReportPageReqVO reqVO, default PageResult<MonthlyReportDO> selectReporterPage(Collection<Long> reporterIds, MonthlyReportPageReqVO reqVO,
Collection<String> allowedStatusCodes) { Collection<String> allowedStatusCodes) {
if (reporterIds == null || reporterIds.isEmpty() || (allowedStatusCodes != null && allowedStatusCodes.isEmpty())) { if (reporterIds == null || reporterIds.isEmpty()
|| (allowedStatusCodes != null && allowedStatusCodes.isEmpty())) {
return new PageResult<>(List.of(), 0L); return new PageResult<>(List.of(), 0L);
} }
LambdaQueryWrapperX<MonthlyReportDO> wrapper = buildPageQuery(reqVO) LambdaQueryWrapperX<MonthlyReportDO> wrapper = buildPageQuery(reqVO)

View File

@@ -12,6 +12,7 @@ import com.njcn.rdms.module.project.dal.dataobject.workreport.project.ProjectRep
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
@@ -47,6 +48,28 @@ public interface ProjectReportMapper extends BaseMapperX<ProjectReportDO> {
return selectList(wrapper); return selectList(wrapper);
} }
/**
* 按项目负责人 ID 列表与周期日期区间查询项目半月报periodStartDate 落在 [startDate, endDate] 内)。
*/
default List<ProjectReportDO> selectListByProjectOwnerIdsAndPeriodDateRange(Collection<Long> projectOwnerIds,
LocalDate startDate, LocalDate endDate,
Collection<String> allowedStatusCodes) {
if (projectOwnerIds == null || projectOwnerIds.isEmpty()) {
return List.of();
}
LambdaQueryWrapperX<ProjectReportDO> wrapper = new LambdaQueryWrapperX<ProjectReportDO>()
.in(ProjectReportDO::getProjectOwnerId, projectOwnerIds)
.geIfPresent(ProjectReportDO::getPeriodStartDate, startDate)
.leIfPresent(ProjectReportDO::getPeriodStartDate, endDate);
if (allowedStatusCodes != null) {
if (allowedStatusCodes.isEmpty()) {
return List.of();
}
wrapper.in(ProjectReportDO::getStatusCode, allowedStatusCodes);
}
return selectList(wrapper);
}
default PageResult<ProjectReportDO> selectReporterPage(Long reporterId, ProjectReportPageReqVO reqVO) { default PageResult<ProjectReportDO> selectReporterPage(Long reporterId, ProjectReportPageReqVO reqVO) {
return selectReporterPage(reporterId, reqVO, null); return selectReporterPage(reporterId, reqVO, null);
} }
@@ -68,7 +91,8 @@ public interface ProjectReportMapper extends BaseMapperX<ProjectReportDO> {
default PageResult<ProjectReportDO> selectReporterPage(Collection<Long> reporterIds, ProjectReportPageReqVO reqVO, default PageResult<ProjectReportDO> selectReporterPage(Collection<Long> reporterIds, ProjectReportPageReqVO reqVO,
Collection<String> allowedStatusCodes) { Collection<String> allowedStatusCodes) {
if (reporterIds == null || reporterIds.isEmpty() || (allowedStatusCodes != null && allowedStatusCodes.isEmpty())) { if (reporterIds == null || reporterIds.isEmpty()
|| (allowedStatusCodes != null && allowedStatusCodes.isEmpty())) {
return new PageResult<>(List.of(), 0L); return new PageResult<>(List.of(), 0L);
} }
LambdaQueryWrapperX<ProjectReportDO> wrapper = buildPageQuery(reqVO) LambdaQueryWrapperX<ProjectReportDO> wrapper = buildPageQuery(reqVO)

View File

@@ -10,6 +10,7 @@ import com.njcn.rdms.module.project.dal.dataobject.workreport.weekly.WeeklyRepor
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
@@ -40,6 +41,28 @@ public interface WeeklyReportMapper extends BaseMapperX<WeeklyReportDO> {
return selectList(wrapper); return selectList(wrapper);
} }
/**
* 按报告人 ID 列表与周期日期区间查询周报periodStartDate 落在 [startDate, endDate] 内)。
*/
default List<WeeklyReportDO> selectListByReporterIdsAndPeriodDateRange(Collection<Long> reporterIds,
LocalDate startDate, LocalDate endDate,
Collection<String> allowedStatusCodes) {
if (reporterIds == null || reporterIds.isEmpty()) {
return List.of();
}
LambdaQueryWrapperX<WeeklyReportDO> wrapper = new LambdaQueryWrapperX<WeeklyReportDO>()
.in(WeeklyReportDO::getReporterId, reporterIds)
.geIfPresent(WeeklyReportDO::getPeriodStartDate, startDate)
.leIfPresent(WeeklyReportDO::getPeriodStartDate, endDate);
if (allowedStatusCodes != null) {
if (allowedStatusCodes.isEmpty()) {
return List.of();
}
wrapper.in(WeeklyReportDO::getStatusCode, allowedStatusCodes);
}
return selectList(wrapper);
}
default PageResult<WeeklyReportDO> selectReporterPage(Long reporterId, WeeklyReportPageReqVO reqVO) { default PageResult<WeeklyReportDO> selectReporterPage(Long reporterId, WeeklyReportPageReqVO reqVO) {
return selectReporterPage(reporterId, reqVO, null); return selectReporterPage(reporterId, reqVO, null);
} }
@@ -61,7 +84,8 @@ public interface WeeklyReportMapper extends BaseMapperX<WeeklyReportDO> {
default PageResult<WeeklyReportDO> selectReporterPage(Collection<Long> reporterIds, WeeklyReportPageReqVO reqVO, default PageResult<WeeklyReportDO> selectReporterPage(Collection<Long> reporterIds, WeeklyReportPageReqVO reqVO,
Collection<String> allowedStatusCodes) { Collection<String> allowedStatusCodes) {
if (reporterIds == null || reporterIds.isEmpty() || (allowedStatusCodes != null && allowedStatusCodes.isEmpty())) { if (reporterIds == null || reporterIds.isEmpty()
|| (allowedStatusCodes != null && allowedStatusCodes.isEmpty())) {
return new PageResult<>(List.of(), 0L); return new PageResult<>(List.of(), 0L);
} }
LambdaQueryWrapperX<WeeklyReportDO> wrapper = buildPageQuery(reqVO) LambdaQueryWrapperX<WeeklyReportDO> wrapper = buildPageQuery(reqVO)

View File

@@ -11,21 +11,14 @@ import com.njcn.rdms.module.project.service.overtime.OvertimeApplicationService;
import com.njcn.rdms.module.project.service.team.TeamDashboardAccessService; import com.njcn.rdms.module.project.service.team.TeamDashboardAccessService;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.YearMonth; import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.List; import java.util.List;
import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
@Service @Service
public class TeamOvertimeServiceImpl implements TeamOvertimeService { public class TeamOvertimeServiceImpl implements TeamOvertimeService {
private static final DateTimeFormatter MONTH_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM");
@Resource @Resource
private TeamDashboardAccessService teamDashboardAccessService; private TeamDashboardAccessService teamDashboardAccessService;
@Resource @Resource
@@ -36,10 +29,15 @@ public class TeamOvertimeServiceImpl implements TeamOvertimeService {
teamDashboardAccessService.validateTeamDashboardPermission( teamDashboardAccessService.validateTeamDashboardPermission(
OvertimeApplicationConstants.PERMISSION_TEAM_DASHBOARD); OvertimeApplicationConstants.PERMISSION_TEAM_DASHBOARD);
List<Long> subordinateIds = teamDashboardAccessService.getAllSubordinateUserIds(); List<Long> subordinateIds = teamDashboardAccessService.getAllSubordinateUserIds();
YearMonth month = parseMonth(reqVO == null ? null : reqVO.getMonth()); LocalDate[] dateRange = normalizeDateRange(
reqVO == null ? null : reqVO.getOvertimeDateStart(),
reqVO == null ? null : reqVO.getOvertimeDateEnd());
LocalDate startDate = dateRange[0];
LocalDate endDate = dateRange[1];
TeamOvertimeSummaryRespVO respVO = new TeamOvertimeSummaryRespVO(); TeamOvertimeSummaryRespVO respVO = new TeamOvertimeSummaryRespVO();
respVO.setMonth(month.format(MONTH_FORMATTER)); respVO.setOvertimeDateStart(startDate);
respVO.setOvertimeDateEnd(endDate);
if (subordinateIds.isEmpty()) { if (subordinateIds.isEmpty()) {
respVO.setTotalApplicationCount(0); respVO.setTotalApplicationCount(0);
respVO.setPendingCount(0); respVO.setPendingCount(0);
@@ -52,7 +50,7 @@ public class TeamOvertimeServiceImpl implements TeamOvertimeService {
pageReqVO.setApplicantIds(subordinateIds); pageReqVO.setApplicantIds(subordinateIds);
pageReqVO.setPageNo(1); pageReqVO.setPageNo(1);
pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
pageReqVO.setOvertimeDate(new LocalDate[]{month.atDay(1), month.atEndOfMonth()}); pageReqVO.setOvertimeDate(new LocalDate[] { startDate, endDate });
PageResult<OvertimeApplicationRespVO> page = overtimeApplicationService.getMyPage(pageReqVO); PageResult<OvertimeApplicationRespVO> page = overtimeApplicationService.getMyPage(pageReqVO);
int pendingCount = 0; int pendingCount = 0;
@@ -74,14 +72,20 @@ public class TeamOvertimeServiceImpl implements TeamOvertimeService {
return respVO; return respVO;
} }
private YearMonth parseMonth(String month) { /**
if (!StringUtils.hasText(month)) { * 规范化日期区间:两者都不传时默认当月 [月初, 月末];支持半开区间。
return YearMonth.now(); */
private LocalDate[] normalizeDateRange(LocalDate start, LocalDate end) {
if (start == null && end == null) {
YearMonth currentMonth = YearMonth.now();
return new LocalDate[] { currentMonth.atDay(1), currentMonth.atEndOfMonth() };
} }
try { if (start == null) {
return YearMonth.parse(month, MONTH_FORMATTER); start = end.withDayOfMonth(1);
} catch (DateTimeParseException ex) {
throw invalidParamException("统计月份格式不正确,应为 yyyy-MM");
} }
if (end == null) {
end = start.withDayOfMonth(start.lengthOfMonth());
}
return new LocalDate[] { start, end };
} }
} }

View File

@@ -28,6 +28,7 @@ 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.LocalDate;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
@@ -70,12 +71,28 @@ public class TeamWorkReportServiceImpl implements TeamWorkReportService {
@Override @Override
public TeamReportSummaryRespVO getSummary(TeamReportSummaryReqVO reqVO) { public TeamReportSummaryRespVO getSummary(TeamReportSummaryReqVO reqVO) {
teamDashboardAccessService.validateTeamDashboardPermission(WorkReportConstants.PERMISSION_TEAM_DASHBOARD); teamDashboardAccessService.validateTeamDashboardPermission(WorkReportConstants.PERMISSION_TEAM_DASHBOARD);
ReportContext context = buildReportContext(normalizeReportType(reqVO.getReportType()), reqVO.getPeriodKey()); String reportType = normalizeReportType(reqVO.getReportType());
boolean useDateRange = reqVO.getPeriodStartDate() != null || reqVO.getPeriodEndDate() != null;
ReportContext context;
LocalDate respStartDate;
LocalDate respEndDate;
if (useDateRange) {
respStartDate = reqVO.getPeriodStartDate();
respEndDate = reqVO.getPeriodEndDate();
context = buildReportContextByDateRange(reportType, respStartDate, respEndDate);
} else {
context = buildReportContext(reportType, reqVO.getPeriodKey());
respStartDate = null;
respEndDate = null;
}
TeamReportSummaryRespVO respVO = new TeamReportSummaryRespVO(); TeamReportSummaryRespVO respVO = new TeamReportSummaryRespVO();
respVO.setPeriodStartDate(respStartDate);
respVO.setPeriodEndDate(respEndDate);
respVO.setTotalShouldSubmit(context.expectedUserIds().size()); respVO.setTotalShouldSubmit(context.expectedUserIds().size());
respVO.setSubmittedCount(context.submittedUserIds().size()); respVO.setSubmittedCount(context.submittedUserIds().size());
respVO.setPendingApprovalCount(context.pendingApprovalUserIds().size()); respVO.setPendingApprovalCount(context.pendingApprovalUserIds().size());
List<TeamReportSummaryRespVO.PendingUser> unsubmittedUsers = buildPendingUsers(context.expectedUserIds(), context.submittedUserIds()); List<TeamReportSummaryRespVO.PendingUser> unsubmittedUsers = buildPendingUsers(context.expectedUserIds(),
context.submittedUserIds());
respVO.setUnsubmittedUsers(unsubmittedUsers); respVO.setUnsubmittedUsers(unsubmittedUsers);
respVO.setUnsubmittedCount(unsubmittedUsers.size()); respVO.setUnsubmittedCount(unsubmittedUsers.size());
return respVO; return respVO;
@@ -205,7 +222,8 @@ public class TeamWorkReportServiceImpl implements TeamWorkReportService {
return Collections.emptyList(); return Collections.emptyList();
} }
List<String> terminalStatusCodes = objectStatusModelMapper List<String> terminalStatusCodes = objectStatusModelMapper
.selectTerminalStatusCodesByObjectTypeEnabled(com.njcn.rdms.module.project.constant.ProjectObjectConstants.OBJECT_TYPE); .selectTerminalStatusCodesByObjectTypeEnabled(
com.njcn.rdms.module.project.constant.ProjectObjectConstants.OBJECT_TYPE);
List<ProjectDO> allProjects = projectMapper.selectListByManagerUserIdsAndStatusCodesNotIn( List<ProjectDO> allProjects = projectMapper.selectListByManagerUserIdsAndStatusCodesNotIn(
subordinateIds, terminalStatusCodes); subordinateIds, terminalStatusCodes);
if (allProjects.isEmpty()) { if (allProjects.isEmpty()) {
@@ -245,7 +263,8 @@ public class TeamWorkReportServiceImpl implements TeamWorkReportService {
Map<Long, AdminUserRespDTO> userMap = users.stream() Map<Long, AdminUserRespDTO> userMap = users.stream()
.filter(Objects::nonNull) .filter(Objects::nonNull)
.filter(user -> user.getId() != null) .filter(user -> user.getId() != null)
.collect(Collectors.toMap(AdminUserRespDTO::getId, user -> user, (left, right) -> left, LinkedHashMap::new)); .collect(Collectors.toMap(AdminUserRespDTO::getId, user -> user, (left, right) -> left,
LinkedHashMap::new));
List<TeamReportSummaryRespVO.PendingUser> respList = new ArrayList<>(); List<TeamReportSummaryRespVO.PendingUser> respList = new ArrayList<>();
for (Long pendingId : pendingIds) { for (Long pendingId : pendingIds) {
AdminUserRespDTO user = userMap.get(pendingId); AdminUserRespDTO user = userMap.get(pendingId);
@@ -279,6 +298,103 @@ public class TeamWorkReportServiceImpl implements TeamWorkReportService {
return StringUtils.hasText(text) ? text.trim() : ""; return StringUtils.hasText(text) ? text.trim() : "";
} }
private record ReportContext(List<Long> expectedUserIds, Set<Long> submittedUserIds, Set<Long> pendingApprovalUserIds) { 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()) {
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) {
} }
} }

View File

@@ -18,7 +18,11 @@ import org.springframework.web.bind.annotation.*;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import static com.njcn.rdms.framework.common.pojo.CommonResult.success; import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
@@ -90,4 +94,24 @@ public class DeptController {
return success(BeanUtils.toBean(dept, DeptRespVO.class)); return success(BeanUtils.toBean(dept, DeptRespVO.class));
} }
@GetMapping("/list-self-and-children")
@Operation(summary = "获取部门自身及全部子部门")
@Parameter(name = "id", description = "部门编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('system:dept:query')")
public CommonResult<List<DeptSimpleRespVO>> getDeptSelfAndChildren(@RequestParam("id") Long id) {
DeptDO self = deptService.getDept(id);
if (self == null) {
return success(List.of());
}
List<DeptDO> combined = new ArrayList<>();
combined.add(self);
combined.addAll(deptService.getChildDeptList(id));
Map<Long, DeptDO> distinctMap = new LinkedHashMap<>();
combined.forEach(dept -> distinctMap.putIfAbsent(dept.getId(), dept));
List<DeptDO> result = new ArrayList<>(distinctMap.values());
result.sort(Comparator.comparing(DeptDO::getSort, Comparator.nullsLast(Integer::compareTo))
.thenComparing(DeptDO::getId, Comparator.nullsLast(Long::compareTo)));
return success(BeanUtils.toBean(result, DeptSimpleRespVO.class));
}
} }

View File

@@ -15,6 +15,7 @@ import com.njcn.rdms.module.system.dal.dataobject.dept.DeptDO;
import com.njcn.rdms.module.system.dal.dataobject.user.AdminUserDO; import com.njcn.rdms.module.system.dal.dataobject.user.AdminUserDO;
import com.njcn.rdms.module.system.dal.dataobject.user.UserManagementRelationDO; import com.njcn.rdms.module.system.dal.dataobject.user.UserManagementRelationDO;
import com.njcn.rdms.module.system.service.dept.DeptService; import com.njcn.rdms.module.system.service.dept.DeptService;
import com.njcn.rdms.module.system.service.user.AdminUserService;
import com.njcn.rdms.module.system.service.user.UserManagementRelationService; import com.njcn.rdms.module.system.service.user.UserManagementRelationService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
@@ -27,9 +28,13 @@ import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.io.IOException; import java.io.IOException;
import java.time.LocalDateTime;
import java.util.Comparator;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import static com.njcn.rdms.framework.apilog.core.enums.OperateTypeEnum.EXPORT; import static com.njcn.rdms.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
import static com.njcn.rdms.framework.common.pojo.CommonResult.success; import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
@@ -58,6 +63,9 @@ public class UserManagementRelationController {
@Resource @Resource
private UserManagementRelationService userManagementRelationService; private UserManagementRelationService userManagementRelationService;
@Resource
private AdminUserService adminUserService;
/** /**
* 创建用户管理链路 * 创建用户管理链路
* *
@@ -192,6 +200,47 @@ public class UserManagementRelationController {
return success(UserConvert.INSTANCE.convertSimpleList(users, deptMap)); return success(UserConvert.INSTANCE.convertSimpleList(users, deptMap));
} }
/**
* 获取某用户当前生效的直属下级列表。
*
* 口径:只返回直接下级,不递归孙级;仅返回当前生效的管理链路。
*/
@GetMapping("/direct-subordinates")
@Operation(summary = "获取某用户当前生效的直属下级列表")
@Parameter(name = "userId", description = "用户ID", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('system:user-management-relation:query')")
public CommonResult<List<UserSimpleRespVO>> getDirectSubordinates(@RequestParam("userId") Long userId) {
List<UserManagementRelationDO> relations = userManagementRelationService.getRelationListByManagerUserId(userId);
if (relations.isEmpty()) {
return success(Collections.emptyList());
}
LocalDateTime now = LocalDateTime.now();
Set<Long> subordinateIds = new LinkedHashSet<>();
relations.stream()
.filter(relation -> relation.getSubordinateUserId() != null)
.filter(relation -> relation.getEffectiveFrom() == null || !relation.getEffectiveFrom().isAfter(now))
.filter(relation -> relation.getEffectiveUntil() == null || relation.getEffectiveUntil().isAfter(now))
.map(UserManagementRelationDO::getSubordinateUserId)
.forEach(subordinateIds::add);
if (subordinateIds.isEmpty()) {
return success(Collections.emptyList());
}
List<AdminUserDO> users = adminUserService.getUserList(subordinateIds).stream()
.filter(adminUserService::isUserAvailable)
.sorted(Comparator.comparing(AdminUserDO::getSort, Comparator.nullsLast(Integer::compareTo))
.thenComparing(AdminUserDO::getId, Comparator.nullsLast(Long::compareTo)))
.toList();
if (users.isEmpty()) {
return success(Collections.emptyList());
}
Map<Long, DeptDO> deptMap = deptService.getDeptMap(convertList(users, AdminUserDO::getDeptId));
List<UserSimpleRespVO> result = UserConvert.INSTANCE.convertSimpleList(users, deptMap).stream()
.sorted(Comparator.comparing(UserSimpleRespVO::getSort, Comparator.nullsLast(Integer::compareTo))
.thenComparing(UserSimpleRespVO::getId, Comparator.nullsLast(Long::compareTo)))
.toList();
return success(result);
}
/** /**
* 获取用户管理链路树形结构 * 获取用户管理链路树形结构
* *