feat(工作报告团队视角): 工作报告现在可以查看团队视角了(查看下属)。

fix(工作报告定时生成): 修复工作报告定时生成带来的一些问题。
This commit is contained in:
dk
2026-06-14 23:54:16 +08:00
parent 32cc855cf0
commit 05deca3b5c
36 changed files with 1305 additions and 47 deletions

View File

@@ -241,6 +241,8 @@ public interface ErrorCodeConstants {
ErrorCode OVERTIME_APPLICATION_APPROVER_SELF_FORBIDDEN = new ErrorCode(1_008_009_009, "审核人不能选择申请人本人");
ErrorCode OVERTIME_APPLICATION_READ_FORBIDDEN = new ErrorCode(1_008_009_010, "无权查看该加班申请");
ErrorCode OVERTIME_APPLICATION_DELETE_ONLY_REJECTED = new ErrorCode(1_008_009_011, "仅已退回的加班申请允许删除");
ErrorCode TEAM_DASHBOARD_PERMISSION_REQUIRED = new ErrorCode(1_008_009_012, "当前用户无团队视角权限");
ErrorCode TEAM_DASHBOARD_SUBORDINATE_SCOPE_INVALID = new ErrorCode(1_008_009_013, "查询目标超出当前用户下属范围");
// ========== 工作报告 1_008_010_xxx ==========
ErrorCode WORK_REPORT_NOT_EXISTS = new ErrorCode(1_008_010_001, "工作报告不存在");
ErrorCode WORK_REPORT_STATUS_MODEL_NOT_EXISTS_OR_DISABLED = new ErrorCode(1_008_010_002, "工作报告状态定义不存在或已停用");

View File

@@ -0,0 +1,12 @@
package com.njcn.rdms.module.project.constant;
/**
* 团队视角常量。
*/
public final class TeamDashboardConstants {
private TeamDashboardConstants() {
}
public static final String PERMISSION = "project:work-report:team-dashboard";
}

View File

@@ -0,0 +1,35 @@
package com.njcn.rdms.module.project.controller.admin.overtime.team;
import com.njcn.rdms.framework.common.pojo.CommonResult;
import com.njcn.rdms.module.project.constant.TeamDashboardConstants;
import com.njcn.rdms.module.project.controller.admin.overtime.team.vo.TeamOvertimeSummaryReqVO;
import com.njcn.rdms.module.project.controller.admin.overtime.team.vo.TeamOvertimeSummaryRespVO;
import com.njcn.rdms.module.project.service.overtime.team.TeamOvertimeService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - 团队加班申请")
@RestController
@RequestMapping("/project/overtime-applications/team")
@Validated
public class TeamOvertimeController {
@Resource
private TeamOvertimeService teamOvertimeService;
@GetMapping("/summary")
@Operation(summary = "获取团队加班申请统计")
@PreAuthorize("@ss.hasPermission('" + TeamDashboardConstants.PERMISSION + "')")
public CommonResult<TeamOvertimeSummaryRespVO> getSummary(@Valid TeamOvertimeSummaryReqVO reqVO) {
return success(teamOvertimeService.getSummary(reqVO));
}
}

View File

@@ -0,0 +1,12 @@
package com.njcn.rdms.module.project.controller.admin.overtime.team.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "管理后台 - 团队加班申请统计 Request VO")
@Data
public class TeamOvertimeSummaryReqVO {
@Schema(description = "统计月份,不传默认当前月", example = "2026-06")
private String month;
}

View File

@@ -0,0 +1,24 @@
package com.njcn.rdms.module.project.controller.admin.overtime.team.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "管理后台 - 团队加班申请统计 Response VO")
@Data
public class TeamOvertimeSummaryRespVO {
@Schema(description = "统计月份", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-06")
private String month;
@Schema(description = "申请总数", requiredMode = Schema.RequiredMode.REQUIRED, example = "12")
private Integer totalApplicationCount;
@Schema(description = "待审批数", requiredMode = Schema.RequiredMode.REQUIRED, example = "3")
private Integer pendingCount;
@Schema(description = "已通过数", requiredMode = Schema.RequiredMode.REQUIRED, example = "7")
private Integer approvedCount;
@Schema(description = "已退回数", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
private Integer rejectedCount;
}

View File

@@ -9,6 +9,7 @@ import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import static com.njcn.rdms.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY;
import static com.njcn.rdms.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@@ -18,6 +19,9 @@ import static com.njcn.rdms.framework.common.util.date.DateUtils.FORMAT_YEAR_MON
@EqualsAndHashCode(callSuper = true)
public class OvertimeApplicationPageReqVO extends PageParam {
@Schema(description = "团队视角下的申请人用户编号列表")
private List<Long> applicantIds;
@Schema(description = "关键词,匹配加班原因或加班内容", example = "上线")
private String keyword;

View File

@@ -5,8 +5,13 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
@Schema(description = "管理后台 - 月报分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class MonthlyReportPageReqVO extends WorkReportBasePageReqVO {
@Schema(description = "团队视角下的填报人用户编号列表")
private List<Long> reporterIds;
}

View File

@@ -5,11 +5,16 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
@Schema(description = "管理后台 - 项目半月报分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class ProjectReportPageReqVO extends WorkReportBasePageReqVO {
@Schema(description = "团队视角下的项目负责人用户编号列表")
private List<Long> projectOwnerIds;
@Schema(description = "项目编号")
private Long projectId;

View File

@@ -0,0 +1,46 @@
package com.njcn.rdms.module.project.controller.admin.workreport.team;
import com.njcn.rdms.framework.common.pojo.CommonResult;
import com.njcn.rdms.module.project.constant.TeamDashboardConstants;
import com.njcn.rdms.module.project.controller.admin.workreport.team.vo.TeamReportRemindReqVO;
import com.njcn.rdms.module.project.controller.admin.workreport.team.vo.TeamReportRemindRespVO;
import com.njcn.rdms.module.project.controller.admin.workreport.team.vo.TeamReportSummaryReqVO;
import com.njcn.rdms.module.project.controller.admin.workreport.team.vo.TeamReportSummaryRespVO;
import com.njcn.rdms.module.project.service.workreport.team.TeamWorkReportService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - 团队工作报告")
@RestController
@RequestMapping("/project/work-reports/team")
@Validated
public class TeamWorkReportController {
@Resource
private TeamWorkReportService teamWorkReportService;
@GetMapping("/summary")
@Operation(summary = "获取团队工作报告统计")
@PreAuthorize("@ss.hasPermission('" + TeamDashboardConstants.PERMISSION + "')")
public CommonResult<TeamReportSummaryRespVO> getSummary(@Valid TeamReportSummaryReqVO reqVO) {
return success(teamWorkReportService.getSummary(reqVO));
}
@PostMapping("/remind")
@Operation(summary = "催办团队工作报告")
@PreAuthorize("@ss.hasPermission('" + TeamDashboardConstants.PERMISSION + "')")
public CommonResult<TeamReportRemindRespVO> remind(@Valid @RequestBody TeamReportRemindReqVO reqVO) {
return success(teamWorkReportService.remind(reqVO));
}
}

View File

@@ -0,0 +1,23 @@
package com.njcn.rdms.module.project.controller.admin.workreport.team.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import java.util.List;
@Schema(description = "管理后台 - 团队工作报告催办 Request VO")
@Data
public class TeamReportRemindReqVO {
@Schema(description = "报告类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "weekly")
@NotBlank(message = "报告类型不能为空")
private String reportType;
@Schema(description = "周期主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "weekly-2026-06-08-2026-06-14")
@NotBlank(message = "周期主键不能为空")
private String periodKey;
@Schema(description = "催办用户 ID 列表;不传则催办全部待提交用户")
private List<Long> userIds;
}

View File

@@ -0,0 +1,12 @@
package com.njcn.rdms.module.project.controller.admin.workreport.team.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "管理后台 - 团队工作报告催办 Response VO")
@Data
public class TeamReportRemindRespVO {
@Schema(description = "实际催办人数", requiredMode = Schema.RequiredMode.REQUIRED, example = "4")
private Integer remindedCount;
}

View File

@@ -0,0 +1,18 @@
package com.njcn.rdms.module.project.controller.admin.workreport.team.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Schema(description = "管理后台 - 团队工作报告统计 Request VO")
@Data
public class TeamReportSummaryReqVO {
@Schema(description = "报告类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "weekly")
@NotBlank(message = "报告类型不能为空")
private String reportType;
@Schema(description = "周期主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "weekly-2026-06-08-2026-06-14")
@NotBlank(message = "周期主键不能为空")
private String periodKey;
}

View File

@@ -0,0 +1,39 @@
package com.njcn.rdms.module.project.controller.admin.workreport.team.vo;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Schema(description = "管理后台 - 团队工作报告统计 Response VO")
@Data
public class TeamReportSummaryRespVO {
@Schema(description = "应填人数", requiredMode = Schema.RequiredMode.REQUIRED, example = "18")
private Integer totalShouldSubmit;
@Schema(description = "已提交人数", requiredMode = Schema.RequiredMode.REQUIRED, example = "12")
private Integer submittedCount;
@Schema(description = "未提交人数", requiredMode = Schema.RequiredMode.REQUIRED, example = "6")
private Integer unsubmittedCount;
@Schema(description = "待审批人数", requiredMode = Schema.RequiredMode.REQUIRED, example = "5")
private Integer pendingApprovalCount;
@Schema(description = "未提交人员列表")
private List<PendingUser> unsubmittedUsers;
@Schema(description = "未提交人员")
@Data
public static class PendingUser {
@Schema(description = "用户 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "2042074259501088770")
@com.fasterxml.jackson.databind.annotation.JsonSerialize(using = ToStringSerializer.class)
private Long userId;
@Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "戴坤")
private String userNickname;
}
}

View File

@@ -5,11 +5,16 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
@Schema(description = "管理后台 - 周报分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class WeeklyReportPageReqVO extends WorkReportBasePageReqVO {
@Schema(description = "团队视角下的填报人用户编号列表")
private List<Long> reporterIds;
@Schema(description = "是否出差")
private Boolean isBusinessTrip;
}

View File

@@ -10,6 +10,7 @@ import org.apache.ibatis.annotations.Mapper;
import org.springframework.util.StringUtils;
import java.util.Collection;
import java.util.List;
@Mapper
public interface OvertimeApplicationMapper extends BaseMapperX<OvertimeApplicationDO> {
@@ -22,6 +23,22 @@ public interface OvertimeApplicationMapper extends BaseMapperX<OvertimeApplicati
return selectPage(reqVO, queryWrapper);
}
default PageResult<OvertimeApplicationDO> selectMyPage(Collection<Long> applicantIds,
OvertimeApplicationPageReqVO reqVO,
Collection<String> allowedStatusCodes) {
if (applicantIds == null || applicantIds.isEmpty() || (allowedStatusCodes != null && allowedStatusCodes.isEmpty())) {
return new PageResult<>(List.of(), 0L);
}
LambdaQueryWrapperX<OvertimeApplicationDO> queryWrapper = buildPageQuery(reqVO);
queryWrapper.in(OvertimeApplicationDO::getApplicantId, applicantIds);
if (allowedStatusCodes != null) {
queryWrapper.in(OvertimeApplicationDO::getStatusCode, allowedStatusCodes);
}
queryWrapper.orderByDesc(OvertimeApplicationDO::getSubmitTime)
.orderByDesc(OvertimeApplicationDO::getId);
return selectPage(reqVO, queryWrapper);
}
default PageResult<OvertimeApplicationDO> selectApprovalPage(Long approverId, OvertimeApplicationPageReqVO reqVO) {
LambdaQueryWrapperX<OvertimeApplicationDO> queryWrapper = buildPageQuery(reqVO);
queryWrapper.eq(OvertimeApplicationDO::getApproverId, approverId);

View File

@@ -75,6 +75,20 @@ public interface ProjectMapper extends BaseMapperX<ProjectDO> {
return selectList(queryWrapper);
}
default List<ProjectDO> selectListByManagerUserIdsAndStatusCodesNotIn(Collection<Long> managerUserIds,
Collection<String> statusCodes) {
if (managerUserIds == null || managerUserIds.isEmpty()) {
return List.of();
}
LambdaQueryWrapperX<ProjectDO> queryWrapper = new LambdaQueryWrapperX<ProjectDO>()
.in(ProjectDO::getManagerUserId, managerUserIds)
.orderByDesc(BaseDO::getCreateTime);
if (statusCodes != null && !statusCodes.isEmpty()) {
queryWrapper.notIn(ProjectDO::getStatusCode, statusCodes);
}
return selectList(queryWrapper);
}
default int updateStatusByIdAndStatus(Long id, String fromStatus, String toStatus, String lastStatusReason) {
ProjectDO update = new ProjectDO();
update.setStatusCode(toStatus);

View File

@@ -23,6 +23,23 @@ public interface MonthlyReportMapper extends BaseMapperX<MonthlyReportDO> {
.eq(MonthlyReportDO::getPeriodKey, periodKey));
}
default List<MonthlyReportDO> selectListByReporterIdsAndPeriodKey(Collection<Long> reporterIds, String periodKey,
Collection<String> allowedStatusCodes) {
if (reporterIds == null || reporterIds.isEmpty() || !StringUtils.hasText(periodKey)) {
return List.of();
}
LambdaQueryWrapperX<MonthlyReportDO> wrapper = new LambdaQueryWrapperX<MonthlyReportDO>()
.in(MonthlyReportDO::getReporterId, reporterIds)
.eq(MonthlyReportDO::getPeriodKey, periodKey);
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) {
return selectReporterPage(reporterId, reqVO, null);
}
@@ -42,6 +59,21 @@ public interface MonthlyReportMapper extends BaseMapperX<MonthlyReportDO> {
return selectPage(reqVO, wrapper);
}
default PageResult<MonthlyReportDO> selectReporterPage(Collection<Long> reporterIds, MonthlyReportPageReqVO reqVO,
Collection<String> allowedStatusCodes) {
if (reporterIds == null || reporterIds.isEmpty() || (allowedStatusCodes != null && allowedStatusCodes.isEmpty())) {
return new PageResult<>(List.of(), 0L);
}
LambdaQueryWrapperX<MonthlyReportDO> wrapper = buildPageQuery(reqVO)
.in(MonthlyReportDO::getReporterId, reporterIds)
.orderByDesc(MonthlyReportDO::getPeriodStartDate)
.orderByDesc(MonthlyReportDO::getId);
if (allowedStatusCodes != null) {
wrapper.in(MonthlyReportDO::getStatusCode, allowedStatusCodes);
}
return selectPage(reqVO, wrapper);
}
default PageResult<MonthlyReportDO> selectApprovalPage(Long supervisorUserId, MonthlyReportPageReqVO reqVO) {
LambdaQueryWrapperX<MonthlyReportDO> wrapper = buildPageQuery(reqVO)
.eq(MonthlyReportDO::getSupervisorUserId, supervisorUserId)

View File

@@ -29,6 +29,24 @@ public interface ProjectReportMapper extends BaseMapperX<ProjectReportDO> {
.eq(ProjectReportDO::getProjectOwnerId, projectOwnerId));
}
default List<ProjectReportDO> selectListByProjectOwnerIdsAndPeriodKey(Collection<Long> projectOwnerIds,
String periodKey,
Collection<String> allowedStatusCodes) {
if (projectOwnerIds == null || projectOwnerIds.isEmpty() || !StringUtils.hasText(periodKey)) {
return List.of();
}
LambdaQueryWrapperX<ProjectReportDO> wrapper = new LambdaQueryWrapperX<ProjectReportDO>()
.in(ProjectReportDO::getProjectOwnerId, projectOwnerIds)
.eq(ProjectReportDO::getPeriodKey, periodKey);
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) {
return selectReporterPage(reporterId, reqVO, null);
}
@@ -48,6 +66,21 @@ public interface ProjectReportMapper extends BaseMapperX<ProjectReportDO> {
return selectPage(reqVO, wrapper);
}
default PageResult<ProjectReportDO> selectReporterPage(Collection<Long> reporterIds, ProjectReportPageReqVO reqVO,
Collection<String> allowedStatusCodes) {
if (reporterIds == null || reporterIds.isEmpty() || (allowedStatusCodes != null && allowedStatusCodes.isEmpty())) {
return new PageResult<>(List.of(), 0L);
}
LambdaQueryWrapperX<ProjectReportDO> wrapper = buildPageQuery(reqVO)
.in(ProjectReportDO::getProjectOwnerId, reporterIds)
.orderByDesc(ProjectReportDO::getPeriodStartDate)
.orderByDesc(ProjectReportDO::getId);
if (allowedStatusCodes != null) {
wrapper.in(ProjectReportDO::getStatusCode, allowedStatusCodes);
}
return selectPage(reqVO, wrapper);
}
default PageResult<ProjectReportDO> selectApprovalPage(Long supervisorUserId, ProjectReportPageReqVO reqVO) {
LambdaQueryWrapperX<ProjectReportDO> wrapper = buildPageQuery(reqVO)
.eq(ProjectReportDO::getSupervisorUserId, supervisorUserId)

View File

@@ -23,6 +23,23 @@ public interface WeeklyReportMapper extends BaseMapperX<WeeklyReportDO> {
.eq(WeeklyReportDO::getPeriodKey, periodKey));
}
default List<WeeklyReportDO> selectListByReporterIdsAndPeriodKey(Collection<Long> reporterIds, String periodKey,
Collection<String> allowedStatusCodes) {
if (reporterIds == null || reporterIds.isEmpty() || !StringUtils.hasText(periodKey)) {
return List.of();
}
LambdaQueryWrapperX<WeeklyReportDO> wrapper = new LambdaQueryWrapperX<WeeklyReportDO>()
.in(WeeklyReportDO::getReporterId, reporterIds)
.eq(WeeklyReportDO::getPeriodKey, periodKey);
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) {
return selectReporterPage(reporterId, reqVO, null);
}
@@ -42,6 +59,21 @@ public interface WeeklyReportMapper extends BaseMapperX<WeeklyReportDO> {
return selectPage(reqVO, wrapper);
}
default PageResult<WeeklyReportDO> selectReporterPage(Collection<Long> reporterIds, WeeklyReportPageReqVO reqVO,
Collection<String> allowedStatusCodes) {
if (reporterIds == null || reporterIds.isEmpty() || (allowedStatusCodes != null && allowedStatusCodes.isEmpty())) {
return new PageResult<>(List.of(), 0L);
}
LambdaQueryWrapperX<WeeklyReportDO> wrapper = buildPageQuery(reqVO)
.in(WeeklyReportDO::getReporterId, reporterIds)
.orderByDesc(WeeklyReportDO::getPeriodStartDate)
.orderByDesc(WeeklyReportDO::getId);
if (allowedStatusCodes != null) {
wrapper.in(WeeklyReportDO::getStatusCode, allowedStatusCodes);
}
return selectPage(reqVO, wrapper);
}
default PageResult<WeeklyReportDO> selectApprovalPage(Long supervisorUserId, WeeklyReportPageReqVO reqVO) {
LambdaQueryWrapperX<WeeklyReportDO> wrapper = buildPageQuery(reqVO)
.eq(WeeklyReportDO::getSupervisorUserId, supervisorUserId)

View File

@@ -25,4 +25,7 @@ public class NotifyTemplateCodeConstants {
/** 逾期提醒-协办人:等级低一等的弱化文案 */
public static final String DUE_ALERT_OVERDUE_ASSIGNEE = "due_alert_overdue_assignee";
/** 工作报告团队催办:主管催办下属提交指定周期工作报告 */
public static final String WORK_REPORT_TEAM_REMIND = "work_report_team_remind";
}

View File

@@ -32,6 +32,7 @@ import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusModelMapper;
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusTransitionMapper;
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
import com.njcn.rdms.module.project.service.status.StatusActionTextResolver;
import com.njcn.rdms.module.project.service.team.TeamDashboardAccessService;
import com.njcn.rdms.module.system.api.user.AdminUserApi;
import com.njcn.rdms.module.system.api.user.dto.AdminUserRespDTO;
import jakarta.annotation.Resource;
@@ -46,6 +47,7 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Collection;
import java.util.function.Function;
import java.util.stream.Collectors;
@@ -55,6 +57,11 @@ import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil
@Service
public class OvertimeApplicationServiceImpl implements OvertimeApplicationService {
private static final List<String> TEAM_VISIBLE_STATUS_CODES = List.of(
OvertimeApplicationConstants.STATUS_PENDING,
OvertimeApplicationConstants.STATUS_APPROVED,
OvertimeApplicationConstants.STATUS_REJECTED);
@Resource
private OvertimeApplicationMapper overtimeApplicationMapper;
@Resource
@@ -71,6 +78,8 @@ public class OvertimeApplicationServiceImpl implements OvertimeApplicationServic
private StatusActionTextResolver statusActionTextResolver;
@Resource
private AdminUserApi adminUserApi;
@Resource
private TeamDashboardAccessService teamDashboardAccessService;
@Override
@Transactional(rollbackFor = Exception.class)
@@ -185,7 +194,13 @@ public class OvertimeApplicationServiceImpl implements OvertimeApplicationServic
@Override
public PageResult<OvertimeApplicationRespVO> getMyPage(OvertimeApplicationPageReqVO reqVO) {
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
PageResult<OvertimeApplicationDO> page = overtimeApplicationMapper.selectMyPage(loginUserId, reqVO);
PageResult<OvertimeApplicationDO> page;
if (reqVO.getApplicantIds() != null) {
List<Long> applicantIds = teamDashboardAccessService.resolveRequestedSubordinateUserIds(reqVO.getApplicantIds());
page = overtimeApplicationMapper.selectMyPage(applicantIds, reqVO, TEAM_VISIBLE_STATUS_CODES);
} else {
page = overtimeApplicationMapper.selectMyPage(loginUserId, reqVO);
}
return BeanUtils.toBean(page, OvertimeApplicationRespVO.class, this::applyStatusView);
}
@@ -368,7 +383,8 @@ public class OvertimeApplicationServiceImpl implements OvertimeApplicationServic
OvertimeApplicationDO application = validateApplicationExists(id);
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
if (!Objects.equals(application.getApplicantId(), loginUserId)
&& !Objects.equals(application.getApproverId(), loginUserId)) {
&& !Objects.equals(application.getApproverId(), loginUserId)
&& !teamDashboardAccessService.canReadSubordinateUser(application.getApplicantId())) {
throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_READ_FORBIDDEN);
}
return application;

View File

@@ -0,0 +1,9 @@
package com.njcn.rdms.module.project.service.overtime.team;
import com.njcn.rdms.module.project.controller.admin.overtime.team.vo.TeamOvertimeSummaryReqVO;
import com.njcn.rdms.module.project.controller.admin.overtime.team.vo.TeamOvertimeSummaryRespVO;
public interface TeamOvertimeService {
TeamOvertimeSummaryRespVO getSummary(TeamOvertimeSummaryReqVO reqVO);
}

View File

@@ -0,0 +1,86 @@
package com.njcn.rdms.module.project.service.overtime.team;
import com.njcn.rdms.framework.common.pojo.PageParam;
import com.njcn.rdms.framework.common.pojo.PageResult;
import com.njcn.rdms.module.project.constant.OvertimeApplicationConstants;
import com.njcn.rdms.module.project.controller.admin.overtime.team.vo.TeamOvertimeSummaryReqVO;
import com.njcn.rdms.module.project.controller.admin.overtime.team.vo.TeamOvertimeSummaryRespVO;
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationPageReqVO;
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationRespVO;
import com.njcn.rdms.module.project.service.overtime.OvertimeApplicationService;
import com.njcn.rdms.module.project.service.team.TeamDashboardAccessService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.time.LocalDate;
import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.List;
import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
@Service
public class TeamOvertimeServiceImpl implements TeamOvertimeService {
private static final DateTimeFormatter MONTH_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM");
@Resource
private TeamDashboardAccessService teamDashboardAccessService;
@Resource
private OvertimeApplicationService overtimeApplicationService;
@Override
public TeamOvertimeSummaryRespVO getSummary(TeamOvertimeSummaryReqVO reqVO) {
teamDashboardAccessService.validateTeamDashboardPermission();
List<Long> subordinateIds = teamDashboardAccessService.getAllSubordinateUserIds();
YearMonth month = parseMonth(reqVO == null ? null : reqVO.getMonth());
TeamOvertimeSummaryRespVO respVO = new TeamOvertimeSummaryRespVO();
respVO.setMonth(month.format(MONTH_FORMATTER));
if (subordinateIds.isEmpty()) {
respVO.setTotalApplicationCount(0);
respVO.setPendingCount(0);
respVO.setApprovedCount(0);
respVO.setRejectedCount(0);
return respVO;
}
OvertimeApplicationPageReqVO pageReqVO = new OvertimeApplicationPageReqVO();
pageReqVO.setApplicantIds(subordinateIds);
pageReqVO.setPageNo(1);
pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
pageReqVO.setOvertimeDate(new LocalDate[]{month.atDay(1), month.atEndOfMonth()});
PageResult<OvertimeApplicationRespVO> page = overtimeApplicationService.getMyPage(pageReqVO);
int pendingCount = 0;
int approvedCount = 0;
int rejectedCount = 0;
for (OvertimeApplicationRespVO item : page.getList()) {
if (OvertimeApplicationConstants.STATUS_PENDING.equals(item.getStatusCode())) {
pendingCount++;
} else if (OvertimeApplicationConstants.STATUS_APPROVED.equals(item.getStatusCode())) {
approvedCount++;
} else if (OvertimeApplicationConstants.STATUS_REJECTED.equals(item.getStatusCode())) {
rejectedCount++;
}
}
respVO.setTotalApplicationCount(page.getList().size());
respVO.setPendingCount(pendingCount);
respVO.setApprovedCount(approvedCount);
respVO.setRejectedCount(rejectedCount);
return respVO;
}
private YearMonth parseMonth(String month) {
if (!StringUtils.hasText(month)) {
return YearMonth.now();
}
try {
return YearMonth.parse(month, MONTH_FORMATTER);
} catch (DateTimeParseException ex) {
throw invalidParamException("统计月份格式不正确,应为 yyyy-MM");
}
}
}

View File

@@ -0,0 +1,43 @@
package com.njcn.rdms.module.project.service.team;
import java.util.Collection;
import java.util.List;
import java.util.Set;
public interface TeamDashboardAccessService {
/**
* 校验当前用户具备团队视角权限。
*/
void validateTeamDashboardPermission();
/**
* 获取当前登录用户全部有效下属(不含本人)。
*
* @return 下属 ID 列表
*/
List<Long> getAllSubordinateUserIds();
/**
* 校验并解析团队查询的目标用户 ID。
*
* @param candidateUserIds 前端传入的目标用户 ID为空表示全部下属
* @return 校验后的目标用户 ID不含本人
*/
List<Long> resolveRequestedSubordinateUserIds(Collection<Long> candidateUserIds);
/**
* 判断当前登录用户是否可读取指定工作报告/加班申请所属人员的数据。
*
* @param userId 目标人员 ID
* @return 是否可读
*/
boolean canReadSubordinateUser(Long userId);
/**
* 获取当前登录用户下属集合。
*
* @return 下属集合
*/
Set<Long> getSubordinateUserIdSet();
}

View File

@@ -0,0 +1,112 @@
package com.njcn.rdms.module.project.service.team;
import com.njcn.rdms.framework.common.pojo.CommonResult;
import com.njcn.rdms.framework.security.core.service.SecurityFrameworkService;
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
import com.njcn.rdms.module.project.constant.TeamDashboardConstants;
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
import com.njcn.rdms.module.system.api.user.AdminUserApi;
import com.njcn.rdms.module.system.api.user.UserManagementRelationApi;
import com.njcn.rdms.module.system.api.user.dto.AdminUserRespDTO;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.exception;
@Service
public class TeamDashboardAccessServiceImpl implements TeamDashboardAccessService {
@Resource
private UserManagementRelationApi userManagementRelationApi;
@Resource
private AdminUserApi adminUserApi;
@Resource
private SecurityFrameworkService securityFrameworkService;
@Override
public void validateTeamDashboardPermission() {
if (!securityFrameworkService.hasPermission(TeamDashboardConstants.PERMISSION)) {
throw exception(ErrorCodeConstants.TEAM_DASHBOARD_PERMISSION_REQUIRED);
}
}
@Override
public List<Long> getAllSubordinateUserIds() {
return new ArrayList<>(getSubordinateUserIdSet());
}
@Override
public List<Long> resolveRequestedSubordinateUserIds(Collection<Long> candidateUserIds) {
validateTeamDashboardPermission();
Set<Long> allSubordinates = getSubordinateUserIdSet();
if (allSubordinates.isEmpty()) {
return Collections.emptyList();
}
if (candidateUserIds == null || candidateUserIds.isEmpty()) {
return new ArrayList<>(allSubordinates);
}
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
LinkedHashSet<Long> resolved = new LinkedHashSet<>();
for (Long candidateUserId : candidateUserIds) {
if (candidateUserId == null || Objects.equals(candidateUserId, loginUserId)) {
continue;
}
if (!allSubordinates.contains(candidateUserId)) {
throw exception(ErrorCodeConstants.TEAM_DASHBOARD_SUBORDINATE_SCOPE_INVALID);
}
resolved.add(candidateUserId);
}
return new ArrayList<>(resolved);
}
@Override
public boolean canReadSubordinateUser(Long userId) {
if (userId == null) {
return false;
}
if (!securityFrameworkService.hasPermission(TeamDashboardConstants.PERMISSION)) {
return false;
}
return getSubordinateUserIdSet().contains(userId);
}
@Override
public Set<Long> getSubordinateUserIdSet() {
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
if (loginUserId == null) {
return Collections.emptySet();
}
CommonResult<Set<Long>> result = userManagementRelationApi.getAllSubordinateUserIds(loginUserId);
Set<Long> rawIds = result == null || result.getCheckedData() == null
? Collections.emptySet()
: result.getCheckedData();
if (rawIds.isEmpty()) {
return Collections.emptySet();
}
LinkedHashSet<Long> uniqueIds = new LinkedHashSet<>(rawIds);
uniqueIds.remove(loginUserId);
if (uniqueIds.isEmpty()) {
return Collections.emptySet();
}
CommonResult<List<AdminUserRespDTO>> userResult = adminUserApi.getUserList(uniqueIds);
List<AdminUserRespDTO> users = userResult == null || userResult.getCheckedData() == null
? Collections.emptyList()
: userResult.getCheckedData();
LinkedHashSet<Long> availableIds = new LinkedHashSet<>();
for (AdminUserRespDTO user : users) {
if (user == null || user.getId() == null || !Objects.equals(user.getStatus(), 0)) {
continue;
}
availableIds.add(user.getId());
}
return availableIds;
}
}

View File

@@ -7,9 +7,23 @@ import com.njcn.rdms.framework.common.util.json.JsonUtils;
import com.njcn.rdms.framework.security.core.LoginUser;
import com.njcn.rdms.module.project.constant.ProjectObjectConstants;
import com.njcn.rdms.module.project.constant.WorkReportConstants;
import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.PersonalReportPlanItemReqVO;
import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.PersonalReportPlanItemRespVO;
import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.PersonalReportReviewItemReqVO;
import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.PersonalReportReviewItemRespVO;
import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportDefaultDraftReqVO;
import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportRespVO;
import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportSaveReqVO;
import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportDefaultDraftReqVO;
import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportItemReqVO;
import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportItemRespVO;
import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportRespVO;
import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportSaveReqVO;
import com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo.WeeklyReportDefaultDraftReqVO;
import com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo.WeeklyReportRespVO;
import com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo.WeeklyReportSaveReqVO;
import com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo.WeeklyReportTravelSegmentReqVO;
import com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo.WeeklyReportTravelSegmentRespVO;
import com.njcn.rdms.module.project.dal.dataobject.job.JobRunLogDO;
import com.njcn.rdms.module.project.dal.dataobject.project.ProjectDO;
import com.njcn.rdms.module.project.dal.mysql.job.JobRunLogMapper;
@@ -20,6 +34,7 @@ import com.njcn.rdms.module.project.dal.mysql.workreport.project.ProjectReportMa
import com.njcn.rdms.module.project.dal.mysql.workreport.weekly.WeeklyReportMapper;
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
import com.njcn.rdms.module.project.service.workreport.common.WorkReportCommonService;
import com.njcn.rdms.module.project.service.workreport.defaultdraft.WorkReportDefaultDraftService;
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;
@@ -73,6 +88,8 @@ public class WorkReportAutoGenerateService {
@Resource
private WorkReportCommonService workReportCommonService;
@Resource
private WorkReportDefaultDraftService workReportDefaultDraftService;
@Resource
private WeeklyReportMapper weeklyReportMapper;
@Resource
private MonthlyReportMapper monthlyReportMapper;
@@ -179,15 +196,7 @@ public class WorkReportAutoGenerateService {
result.setSkipCount(result.getSkipCount() + 1);
return;
}
WeeklyReportSaveReqVO reqVO = new WeeklyReportSaveReqVO();
reqVO.setPeriodKey(period.getPeriodKey());
reqVO.setPeriodLabel(period.getPeriodLabel());
reqVO.setPeriodStartDate(period.getPeriodStartDate());
reqVO.setPeriodEndDate(period.getPeriodEndDate());
reqVO.setIsBusinessTrip(Boolean.FALSE);
reqVO.setReviewItems(Collections.emptyList());
reqVO.setPlanItems(Collections.emptyList());
reqVO.setTravelSegments(Collections.emptyList());
WeeklyReportSaveReqVO reqVO = runAs(buildLoginUser(user), () -> buildWeeklySaveReqVOFromDefaultDraft(period));
runAs(buildLoginUser(user), () -> workReportCommonService.createWeeklyReport(reqVO, user.getId()));
result.setSuccessCount(result.getSuccessCount() + 1);
}, () -> "userId=" + user.getId());
@@ -200,13 +209,7 @@ public class WorkReportAutoGenerateService {
result.setSkipCount(result.getSkipCount() + 1);
return;
}
MonthlyReportSaveReqVO reqVO = new MonthlyReportSaveReqVO();
reqVO.setPeriodKey(period.getPeriodKey());
reqVO.setPeriodLabel(period.getPeriodLabel());
reqVO.setPeriodStartDate(period.getPeriodStartDate());
reqVO.setPeriodEndDate(period.getPeriodEndDate());
reqVO.setReviewItems(Collections.emptyList());
reqVO.setPlanItems(Collections.emptyList());
MonthlyReportSaveReqVO reqVO = runAs(buildLoginUser(user), () -> buildMonthlySaveReqVOFromDefaultDraft(period));
runAs(buildLoginUser(user), () -> workReportCommonService.createMonthlyReport(reqVO, user.getId()));
result.setSuccessCount(result.getSuccessCount() + 1);
}, () -> "userId=" + user.getId());
@@ -220,20 +223,156 @@ public class WorkReportAutoGenerateService {
result.setSkipCount(result.getSkipCount() + 1);
return;
}
ProjectReportSaveReqVO reqVO = new ProjectReportSaveReqVO();
reqVO.setProjectId(candidate.projectId());
reqVO.setPeriodKey(period.getPeriodKey());
reqVO.setPeriodLabel(period.getPeriodLabel());
reqVO.setPeriodStartDate(period.getPeriodStartDate());
reqVO.setPeriodEndDate(period.getPeriodEndDate());
reqVO.setFlag(period.getFlag());
reqVO.setCurrentItems(Collections.emptyList());
reqVO.setNextItems(Collections.emptyList());
ProjectReportSaveReqVO reqVO = runAs(buildLoginUser(candidate.user()),
() -> buildProjectSaveReqVOFromDefaultDraft(candidate.projectId(), period));
runAs(buildLoginUser(candidate.user()), () -> workReportCommonService.createProjectReport(reqVO, candidate.user().getId()));
result.setSuccessCount(result.getSuccessCount() + 1);
}, () -> "projectId=" + candidate.projectId() + ", userId=" + candidate.user().getId());
}
private WeeklyReportSaveReqVO buildWeeklySaveReqVOFromDefaultDraft(AutoGenPeriod period) {
WeeklyReportDefaultDraftReqVO draftReqVO = new WeeklyReportDefaultDraftReqVO();
draftReqVO.setPeriodKey(period.getPeriodKey());
draftReqVO.setPeriodLabel(period.getPeriodLabel());
draftReqVO.setPeriodStartDate(period.getPeriodStartDate());
draftReqVO.setPeriodEndDate(period.getPeriodEndDate());
WeeklyReportRespVO draft = workReportDefaultDraftService.previewWeeklyDefaultDraft(draftReqVO);
WeeklyReportSaveReqVO reqVO = new WeeklyReportSaveReqVO();
reqVO.setPeriodKey(draft.getPeriodKey());
reqVO.setPeriodLabel(draft.getPeriodLabel());
reqVO.setPeriodStartDate(draft.getPeriodStartDate());
reqVO.setPeriodEndDate(draft.getPeriodEndDate());
reqVO.setIsBusinessTrip(Boolean.TRUE.equals(draft.getIsBusinessTrip()));
reqVO.setReviewItems(toReviewItemReqList(draft.getReviewItems()));
reqVO.setPlanItems(toPlanItemReqList(draft.getPlanItems()));
reqVO.setTravelSegments(toTravelSegmentReqList(draft.getTravelSegments()));
return reqVO;
}
private MonthlyReportSaveReqVO buildMonthlySaveReqVOFromDefaultDraft(AutoGenPeriod period) {
MonthlyReportDefaultDraftReqVO draftReqVO = new MonthlyReportDefaultDraftReqVO();
draftReqVO.setPeriodKey(period.getPeriodKey());
draftReqVO.setPeriodLabel(period.getPeriodLabel());
draftReqVO.setPeriodStartDate(period.getPeriodStartDate());
draftReqVO.setPeriodEndDate(period.getPeriodEndDate());
MonthlyReportRespVO draft = workReportDefaultDraftService.previewMonthlyDefaultDraft(draftReqVO);
MonthlyReportSaveReqVO reqVO = new MonthlyReportSaveReqVO();
reqVO.setPeriodKey(draft.getPeriodKey());
reqVO.setPeriodLabel(draft.getPeriodLabel());
reqVO.setPeriodStartDate(draft.getPeriodStartDate());
reqVO.setPeriodEndDate(draft.getPeriodEndDate());
reqVO.setReviewItems(toReviewItemReqList(draft.getReviewItems()));
reqVO.setPlanItems(toPlanItemReqList(draft.getPlanItems()));
return reqVO;
}
private ProjectReportSaveReqVO buildProjectSaveReqVOFromDefaultDraft(Long projectId, AutoGenPeriod period) {
ProjectReportDefaultDraftReqVO draftReqVO = new ProjectReportDefaultDraftReqVO();
draftReqVO.setPeriodKey(period.getPeriodKey());
draftReqVO.setPeriodLabel(period.getPeriodLabel());
draftReqVO.setPeriodStartDate(period.getPeriodStartDate());
draftReqVO.setPeriodEndDate(period.getPeriodEndDate());
draftReqVO.setFlag(period.getFlag());
ProjectReportRespVO draft = workReportDefaultDraftService.previewProjectDefaultDraft(projectId, draftReqVO);
ProjectReportSaveReqVO reqVO = new ProjectReportSaveReqVO();
reqVO.setProjectId(projectId);
reqVO.setPeriodKey(draft.getPeriodKey());
reqVO.setPeriodLabel(draft.getPeriodLabel());
reqVO.setPeriodStartDate(draft.getPeriodStartDate());
reqVO.setPeriodEndDate(draft.getPeriodEndDate());
reqVO.setFlag(draft.getFlag());
reqVO.setProjectStatusDesc(draft.getProjectStatusDesc());
reqVO.setProjectProgressPlan(draft.getProjectProgressPlan());
reqVO.setProjectKeyPoints(draft.getProjectKeyPoints());
reqVO.setProjectProblems(draft.getProjectProblems());
reqVO.setCurrentItems(toProjectItemReqList(draft.getCurrentItems()));
reqVO.setNextItems(toProjectItemReqList(draft.getNextItems()));
return reqVO;
}
private List<PersonalReportReviewItemReqVO> toReviewItemReqList(List<PersonalReportReviewItemRespVO> source) {
if (source == null || source.isEmpty()) {
return Collections.emptyList();
}
List<PersonalReportReviewItemReqVO> result = new ArrayList<>(source.size());
for (PersonalReportReviewItemRespVO item : source) {
if (item == null) {
continue;
}
PersonalReportReviewItemReqVO target = new PersonalReportReviewItemReqVO();
target.setItemNumber(item.getItemNumber());
target.setItemTitle(item.getItemTitle());
target.setWorkHours(item.getWorkHours());
target.setContentText(item.getContentText());
target.setContentJson(item.getContentJson());
target.setReflectionText(item.getReflectionText());
result.add(target);
}
return result;
}
private List<PersonalReportPlanItemReqVO> toPlanItemReqList(List<PersonalReportPlanItemRespVO> source) {
if (source == null || source.isEmpty()) {
return Collections.emptyList();
}
List<PersonalReportPlanItemReqVO> result = new ArrayList<>(source.size());
for (PersonalReportPlanItemRespVO item : source) {
if (item == null) {
continue;
}
PersonalReportPlanItemReqVO target = new PersonalReportPlanItemReqVO();
target.setItemNumber(item.getItemNumber());
target.setItemTitle(item.getItemTitle());
target.setTargetText(item.getTargetText());
target.setTargetJson(item.getTargetJson());
target.setSupportNeed(item.getSupportNeed());
result.add(target);
}
return result;
}
private List<ProjectReportItemReqVO> toProjectItemReqList(List<ProjectReportItemRespVO> source) {
if (source == null || source.isEmpty()) {
return Collections.emptyList();
}
List<ProjectReportItemReqVO> result = new ArrayList<>(source.size());
for (ProjectReportItemRespVO item : source) {
if (item == null) {
continue;
}
ProjectReportItemReqVO target = new ProjectReportItemReqVO();
target.setItemTitle(item.getItemTitle());
target.setWorkHours(item.getWorkHours());
target.setPriorityCode(item.getPriorityCode());
target.setProgressRate(item.getProgressRate());
result.add(target);
}
return result;
}
private List<WeeklyReportTravelSegmentReqVO> toTravelSegmentReqList(List<WeeklyReportTravelSegmentRespVO> source) {
if (source == null || source.isEmpty()) {
return Collections.emptyList();
}
List<WeeklyReportTravelSegmentReqVO> result = new ArrayList<>(source.size());
for (WeeklyReportTravelSegmentRespVO item : source) {
if (item == null) {
continue;
}
WeeklyReportTravelSegmentReqVO target = new WeeklyReportTravelSegmentReqVO();
target.setSort(item.getSort());
target.setStartDate(item.getStartDate());
target.setEndDate(item.getEndDate());
target.setTravelDays(item.getTravelDays());
target.setLocation(item.getLocation());
result.add(target);
}
return result;
}
private void executeWithLock(String reportType, String periodKey, String subjectKey, AutoGenResult result,
Runnable action, Supplier<String> failContextSupplier) {
RLock lock = redissonClient.getLock(String.format(LOCK_KEY_TEMPLATE, reportType, periodKey, subjectKey));
@@ -346,29 +485,31 @@ public class WorkReportAutoGenerateService {
private AutoGenPeriod buildWeeklyPeriod(LocalDate today) {
LocalDate periodStartDate = today.minusDays(today.getDayOfWeek().getValue() - 1L);
LocalDate periodEndDate = periodStartDate.plusDays(6);
int week = periodStartDate.get(java.time.temporal.IsoFields.WEEK_OF_WEEK_BASED_YEAR);
int year = periodStartDate.get(java.time.temporal.IsoFields.WEEK_BASED_YEAR);
return new AutoGenPeriod()
.setPeriodKey(String.format("%d-W%02d", year, week))
.setPeriodLabel(String.format("%d 年第 %02d 周", year, week))
.setPeriodKey(String.format("weekly-%s-%s", periodStartDate, periodEndDate))
.setPeriodLabel(String.format("%s 至 %s", periodStartDate, periodEndDate))
.setPeriodStartDate(periodStartDate)
.setPeriodEndDate(periodEndDate);
}
private AutoGenPeriod buildMonthlyPeriod(LocalDate today) {
LocalDate periodStartDate = today.withDayOfMonth(1);
LocalDate periodEndDate = today.withDayOfMonth(today.lengthOfMonth());
return new AutoGenPeriod()
.setPeriodKey(String.format("%d-%02d", today.getYear(), today.getMonthValue()))
.setPeriodLabel(String.format("%d%02d", today.getYear(), today.getMonthValue()))
.setPeriodStartDate(today.withDayOfMonth(1))
.setPeriodEndDate(today.withDayOfMonth(today.lengthOfMonth()));
.setPeriodKey(String.format("monthly-%s-%s", periodStartDate, periodEndDate))
.setPeriodLabel(String.format("%d-%02d", today.getYear(), today.getMonthValue()))
.setPeriodStartDate(periodStartDate)
.setPeriodEndDate(periodEndDate);
}
private AutoGenPeriod buildProjectFirstHalfPeriod(LocalDate today) {
LocalDate periodStartDate = today.withDayOfMonth(1);
LocalDate periodEndDate = today.withDayOfMonth(15);
return new AutoGenPeriod()
.setPeriodKey(String.format("%d-%02d-01", today.getYear(), today.getMonthValue()))
.setPeriodLabel(String.format("%d%02d 上半月", today.getYear(), today.getMonthValue()))
.setPeriodStartDate(today.withDayOfMonth(1))
.setPeriodEndDate(today.withDayOfMonth(15))
.setPeriodKey(String.format("project-%s-%s-1", periodStartDate, periodEndDate))
.setPeriodLabel(String.format("%d-%02d 上半月", today.getYear(), today.getMonthValue()))
.setPeriodStartDate(periodStartDate)
.setPeriodEndDate(periodEndDate)
.setFlag(1);
}

View File

@@ -48,6 +48,7 @@ import com.njcn.rdms.module.project.dal.mysql.workreport.weekly.WeeklyReportMapp
import com.njcn.rdms.module.project.dal.mysql.workreport.weekly.WeeklyReportTravelSegmentMapper;
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
import com.njcn.rdms.module.project.service.status.StatusActionTextResolver;
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.PostApi;
import com.njcn.rdms.module.system.api.dept.dto.DeptRespDTO;
@@ -77,6 +78,10 @@ public class WorkReportCommonService {
private static final List<String> ALLOW_DELETE_STATUSES = List.of(
WorkReportConstants.STATUS_DRAFT,
WorkReportConstants.STATUS_REJECTED);
private static final List<String> TEAM_VISIBLE_STATUS_CODES = List.of(
WorkReportConstants.STATUS_PENDING_APPROVAL,
WorkReportConstants.STATUS_APPROVED,
WorkReportConstants.STATUS_REJECTED);
@Resource
private WeeklyReportMapper weeklyReportMapper;
@@ -124,6 +129,8 @@ public class WorkReportCommonService {
private ProjectMapper projectMapper;
@Resource
private UserObjectRoleMapper userObjectRoleMapper;
@Resource
private TeamDashboardAccessService teamDashboardAccessService;
public List<WorkReportStatusDictRespVO> getStatusDict() {
return objectStatusModelMapper.selectListByObjectTypeEnabled(WorkReportConstants.STATUS_OBJECT_TYPE).stream()
@@ -233,8 +240,13 @@ public class WorkReportCommonService {
public PageResult<WeeklyReportRespVO> getWeeklyReportPage(WeeklyReportPageReqVO reqVO) {
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
PageResult<WeeklyReportDO> pageResult = weeklyReportMapper.selectReporterPage(loginUserId, reqVO,
getEnabledStatusCodes());
PageResult<WeeklyReportDO> pageResult;
if (reqVO.getReporterIds() != null) {
List<Long> reporterIds = teamDashboardAccessService.resolveRequestedSubordinateUserIds(reqVO.getReporterIds());
pageResult = weeklyReportMapper.selectReporterPage(reporterIds, reqVO, TEAM_VISIBLE_STATUS_CODES);
} else {
pageResult = weeklyReportMapper.selectReporterPage(loginUserId, reqVO, getEnabledStatusCodes());
}
return new PageResult<>(pageResult.getList().stream()
.map(report -> toWeeklyRespVO(report, false))
.collect(Collectors.toList()), pageResult.getTotal());
@@ -392,8 +404,13 @@ public class WorkReportCommonService {
public PageResult<MonthlyReportRespVO> getMonthlyReportPage(MonthlyReportPageReqVO reqVO) {
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
PageResult<MonthlyReportDO> pageResult = monthlyReportMapper.selectReporterPage(loginUserId, reqVO,
getEnabledStatusCodes());
PageResult<MonthlyReportDO> pageResult;
if (reqVO.getReporterIds() != null) {
List<Long> reporterIds = teamDashboardAccessService.resolveRequestedSubordinateUserIds(reqVO.getReporterIds());
pageResult = monthlyReportMapper.selectReporterPage(reporterIds, reqVO, TEAM_VISIBLE_STATUS_CODES);
} else {
pageResult = monthlyReportMapper.selectReporterPage(loginUserId, reqVO, getEnabledStatusCodes());
}
return new PageResult<>(pageResult.getList().stream()
.map(report -> toMonthlyRespVO(report, false))
.collect(Collectors.toList()), pageResult.getTotal());
@@ -570,8 +587,13 @@ public class WorkReportCommonService {
public PageResult<ProjectReportRespVO> getProjectReportPage(ProjectReportPageReqVO reqVO) {
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
PageResult<ProjectReportDO> pageResult = projectReportMapper.selectReporterPage(loginUserId, reqVO,
getEnabledStatusCodes());
PageResult<ProjectReportDO> pageResult;
if (reqVO.getProjectOwnerIds() != null) {
List<Long> projectOwnerIds = teamDashboardAccessService.resolveRequestedSubordinateUserIds(reqVO.getProjectOwnerIds());
pageResult = projectReportMapper.selectReporterPage(projectOwnerIds, reqVO, TEAM_VISIBLE_STATUS_CODES);
} else {
pageResult = projectReportMapper.selectReporterPage(loginUserId, reqVO, getEnabledStatusCodes());
}
return new PageResult<>(pageResult.getList().stream()
.map(report -> toProjectRespVO(report, false))
.collect(Collectors.toList()), pageResult.getTotal());
@@ -764,7 +786,9 @@ public class WorkReportCommonService {
private void validateReadable(Long reporterId, Long supervisorUserId) {
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
if (!Objects.equals(loginUserId, reporterId) && !Objects.equals(loginUserId, supervisorUserId)) {
if (!Objects.equals(loginUserId, reporterId)
&& !Objects.equals(loginUserId, supervisorUserId)
&& !teamDashboardAccessService.canReadSubordinateUser(reporterId)) {
throw exception(ErrorCodeConstants.WORK_REPORT_READ_FORBIDDEN);
}
}

View File

@@ -0,0 +1,13 @@
package com.njcn.rdms.module.project.service.workreport.team;
import com.njcn.rdms.module.project.controller.admin.workreport.team.vo.TeamReportRemindReqVO;
import com.njcn.rdms.module.project.controller.admin.workreport.team.vo.TeamReportRemindRespVO;
import com.njcn.rdms.module.project.controller.admin.workreport.team.vo.TeamReportSummaryReqVO;
import com.njcn.rdms.module.project.controller.admin.workreport.team.vo.TeamReportSummaryRespVO;
public interface TeamWorkReportService {
TeamReportSummaryRespVO getSummary(TeamReportSummaryReqVO reqVO);
TeamReportRemindRespVO remind(TeamReportRemindReqVO reqVO);
}

View File

@@ -0,0 +1,283 @@
package com.njcn.rdms.module.project.service.workreport.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.WorkReportConstants;
import com.njcn.rdms.module.project.controller.admin.workreport.team.vo.TeamReportRemindReqVO;
import com.njcn.rdms.module.project.controller.admin.workreport.team.vo.TeamReportRemindRespVO;
import com.njcn.rdms.module.project.controller.admin.workreport.team.vo.TeamReportSummaryReqVO;
import com.njcn.rdms.module.project.controller.admin.workreport.team.vo.TeamReportSummaryRespVO;
import com.njcn.rdms.module.project.dal.dataobject.project.ProjectDO;
import com.njcn.rdms.module.project.dal.dataobject.workreport.monthly.MonthlyReportDO;
import com.njcn.rdms.module.project.dal.dataobject.workreport.project.ProjectReportDO;
import com.njcn.rdms.module.project.dal.dataobject.workreport.weekly.WeeklyReportDO;
import com.njcn.rdms.module.project.dal.mysql.project.ProjectMapper;
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusModelMapper;
import com.njcn.rdms.module.project.dal.mysql.workreport.monthly.MonthlyReportMapper;
import com.njcn.rdms.module.project.dal.mysql.workreport.project.ProjectReportMapper;
import com.njcn.rdms.module.project.dal.mysql.workreport.weekly.WeeklyReportMapper;
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.user.AdminUserApi;
import com.njcn.rdms.module.system.api.user.dto.AdminUserRespDTO;
import com.njcn.rdms.module.system.enums.notify.NotifyMessageLevelConstants;
import jakarta.annotation.Resource;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
@Service
public class TeamWorkReportServiceImpl implements TeamWorkReportService {
private static final List<String> SUBMITTED_STATUS_CODES = List.of(
WorkReportConstants.STATUS_PENDING_APPROVAL,
WorkReportConstants.STATUS_APPROVED,
WorkReportConstants.STATUS_REJECTED);
@Resource
private TeamDashboardAccessService teamDashboardAccessService;
@Resource
private AdminUserApi adminUserApi;
@Resource
private WeeklyReportMapper weeklyReportMapper;
@Resource
private MonthlyReportMapper monthlyReportMapper;
@Resource
private ProjectReportMapper projectReportMapper;
@Resource
private ProjectMapper projectMapper;
@Resource
private ObjectStatusModelMapper objectStatusModelMapper;
@Resource
private ApplicationEventPublisher applicationEventPublisher;
@Override
public TeamReportSummaryRespVO getSummary(TeamReportSummaryReqVO reqVO) {
teamDashboardAccessService.validateTeamDashboardPermission();
ReportContext context = buildReportContext(normalizeReportType(reqVO.getReportType()), reqVO.getPeriodKey());
TeamReportSummaryRespVO respVO = new TeamReportSummaryRespVO();
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());
return respVO;
}
@Override
@Transactional(rollbackFor = Exception.class)
public TeamReportRemindRespVO remind(TeamReportRemindReqVO reqVO) {
teamDashboardAccessService.validateTeamDashboardPermission();
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));
}
TeamReportRemindRespVO respVO = new TeamReportRemindRespVO();
respVO.setRemindedCount(remindUserIds.size());
return respVO;
}
private ReportContext buildReportContext(String reportType, String periodKey) {
if (!StringUtils.hasText(periodKey)) {
throw invalidParamException("周期主键不能为空");
}
if (WorkReportConstants.REPORT_TYPE_PROJECT.equals(reportType)) {
return buildProjectContext(periodKey);
}
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 buildWeeklyContext(periodKey, subordinateIds);
}
return buildMonthlyContext(periodKey, 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 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 buildProjectContext(String periodKey) {
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.selectListByProjectOwnerIdsAndPeriodKey(
expectedUserIds, periodKey, 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 List<ProjectDO> loadActiveProjectsForSubordinates(Collection<Long> subordinateIds) {
if (subordinateIds == null || subordinateIds.isEmpty()) {
return Collections.emptyList();
}
List<String> terminalStatusCodes = objectStatusModelMapper
.selectTerminalStatusCodesByObjectTypeEnabled(com.njcn.rdms.module.project.constant.ProjectObjectConstants.OBJECT_TYPE);
List<ProjectDO> allProjects = projectMapper.selectListByManagerUserIdsAndStatusCodesNotIn(
subordinateIds, terminalStatusCodes);
if (allProjects.isEmpty()) {
return Collections.emptyList();
}
return allProjects.stream()
.filter(Objects::nonNull)
.filter(project -> project.getManagerUserId() != null)
.collect(Collectors.toList());
}
private List<Long> resolveRemindUserIds(List<Long> requestedUserIds, ReportContext context) {
LinkedHashSet<Long> unsubmittedUserIds = new LinkedHashSet<>(context.expectedUserIds());
unsubmittedUserIds.removeAll(context.submittedUserIds());
if (requestedUserIds == null) {
return new ArrayList<>(unsubmittedUserIds);
}
List<Long> validatedIds = teamDashboardAccessService.resolveRequestedSubordinateUserIds(requestedUserIds);
return validatedIds.stream()
.filter(unsubmittedUserIds::contains)
.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()) {
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();
}
Map<Long, AdminUserRespDTO> userMap = users.stream()
.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);
item.setUserNickname(user == null ? "" : defaultText(user.getNickname()));
respList.add(item);
}
return respList;
}
private String normalizeReportType(String reportType) {
if (WorkReportConstants.REPORT_TYPE_WEEKLY.equals(reportType)
|| WorkReportConstants.REPORT_TYPE_MONTHLY.equals(reportType)
|| WorkReportConstants.REPORT_TYPE_PROJECT.equals(reportType)) {
return reportType;
}
throw invalidParamException("报告类型不合法");
}
private String reportTypeDisplayName(String reportType) {
return switch (reportType) {
case WorkReportConstants.REPORT_TYPE_WEEKLY -> "周报";
case WorkReportConstants.REPORT_TYPE_MONTHLY -> "月报";
case WorkReportConstants.REPORT_TYPE_PROJECT -> "项目半月报";
default -> reportType;
};
}
private String defaultText(String text) {
return StringUtils.hasText(text) ? text.trim() : "";
}
private record ReportContext(List<Long> expectedUserIds, Set<Long> submittedUserIds, Set<Long> pendingApprovalUserIds) {
}
}

View File

@@ -119,6 +119,6 @@ rdms:
enabled: true
cron: "0 0 12 1-31 * ?"
scope:
dept-ids: [101]
dept-ids: [100]
debug: false