diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/controller/PerformanceReportController.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/controller/PerformanceReportController.java new file mode 100644 index 0000000..b4a2f6c --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/controller/PerformanceReportController.java @@ -0,0 +1,45 @@ +package com.ruoyi.oa.controller; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.helper.LoginHelper; +import com.ruoyi.oa.domain.vo.performance.MonthlyPerformanceReportVo; +import com.ruoyi.oa.service.IPerformanceReportService; +import lombok.RequiredArgsConstructor; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Date; + +@Validated +@RequiredArgsConstructor +@RestController +@RequestMapping("/oa/performance/report") +public class PerformanceReportController extends BaseController { + + private final IPerformanceReportService performanceReportService; + + /** + * 月度/区间绩效报告(汇总 + 明细) + * + * @param startDate 统计开始日期(含) + * @param endDate 统计结束日期(含) + * @param userId 可选:指定用户;不传则取当前登录用户 + */ + @GetMapping("/monthly") + public R monthly( + @RequestParam @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8") @DateTimeFormat(pattern = "yyyy-MM-dd") Date startDate, + @RequestParam @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8") @DateTimeFormat(pattern = "yyyy-MM-dd") Date endDate, + @RequestParam(required = false) Long userId + ) { + Long targetUserId = userId != null ? userId : LoginHelper.getUserId(); + return R.ok(performanceReportService.buildMonthlyReport(targetUserId, startDate, endDate)); + } +} + + diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/performance/HrmLeaveReqVo.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/performance/HrmLeaveReqVo.java new file mode 100644 index 0000000..ce4a66b --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/performance/HrmLeaveReqVo.java @@ -0,0 +1,34 @@ +package com.ruoyi.oa.domain.vo.performance; + +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; + +/** + * 请假明细(来自 hrm_leave_req,关联 hrm_employee.user_id 映射到系统用户) + */ +@Data +public class HrmLeaveReqVo { + + private Long bizId; + private Long empId; + private Long userId; + private Long projectId; + + private String leaveType; + private Date startTime; + private Date endTime; + private BigDecimal hours; + private String reason; + private String status; + + private String accessoryApplyIds; + private String accessoryReceiptIds; + private String remark; + + private Date createTime; + private String createBy; +} + + diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/performance/MonthlyPerformanceReportVo.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/performance/MonthlyPerformanceReportVo.java new file mode 100644 index 0000000..3d0dd94 --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/performance/MonthlyPerformanceReportVo.java @@ -0,0 +1,48 @@ +package com.ruoyi.oa.domain.vo.performance; + +import com.ruoyi.oa.domain.vo.OaProjectReportVo; +import com.ruoyi.oa.domain.vo.OaProjectScheduleStepVo; +import com.ruoyi.oa.domain.vo.SysOaAttendanceVo; +import com.ruoyi.oa.domain.vo.SysOaProjectVo; +import com.ruoyi.oa.domain.vo.SysOaTaskVo; +import lombok.Data; + +import java.util.Date; +import java.util.List; + +@Data +public class MonthlyPerformanceReportVo { + + /** 元信息 */ + private Long userId; + private String nickName; + private Date startDate; + private Date endDate; + private Date generatedAt; + + /** 汇总 */ + private MonthlyPerformanceSummaryVo summary; + + /** 明细:考勤(sys_oa_attendance) */ + private List attendanceDetails; + + /** 明细:请假(hrm_leave_req) */ + private List leaveDetails; + + /** 明细:负责人项目(sys_oa_project.functionary) */ + private List responsibleProjects; + + /** 明细:项目进度步骤(oa_project_schedule_step) */ + private List scheduleStepDetails; + + /** 明细:本人负责的进度步骤(node_header=本人) */ + private List myScheduleStepDetails; + + /** 明细:报工(oa_project_report) */ + private List projectReportDetails; + + /** 明细:任务(sys_oa_task,含 task_item 子项) */ + private List taskDetails; +} + + diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/performance/MonthlyPerformanceSummaryVo.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/performance/MonthlyPerformanceSummaryVo.java new file mode 100644 index 0000000..2d2f88a --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/performance/MonthlyPerformanceSummaryVo.java @@ -0,0 +1,35 @@ +package com.ruoyi.oa.domain.vo.performance; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class MonthlyPerformanceSummaryVo { + + /** 考勤 */ + private Long attendanceRecordCount; + private BigDecimal attendanceWorkHours; + private BigDecimal attendanceOvertimeHours; + + /** 请假 */ + private Long leaveRequestCount; + private BigDecimal leaveHours; + + /** 项目 */ + private Long responsibleProjectCount; + + /** 进度步骤(当前人作为步骤负责人 node_header) */ + private Long myScheduleStepCount; + private Long myScheduleStepCompletedCount; + + /** 报工 */ + private Long projectReportCount; + + /** 任务(sys_oa_task) */ + private Long taskCount; + private Long taskCompletedCount; + private Long taskItemCount; +} + + diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/mapper/PerformanceReportMapper.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/mapper/PerformanceReportMapper.java new file mode 100644 index 0000000..e1ee376 --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/mapper/PerformanceReportMapper.java @@ -0,0 +1,29 @@ +package com.ruoyi.oa.mapper; + +import com.ruoyi.oa.domain.vo.SysOaProjectVo; +import com.ruoyi.oa.domain.vo.performance.HrmLeaveReqVo; +import org.apache.ibatis.annotations.Param; + +import java.util.Date; +import java.util.List; + +/** + * 绩效报告聚合查询(跨模块表:hrm_* / oa_* / sys_*) + */ +public interface PerformanceReportMapper { + + /** + * 请假明细:通过 hrm_employee.user_id 映射到系统用户 + * 时间口径:与区间有交集(start_time <= endDate 且 end_time >= startDate) + */ + List selectLeaveDetails(@Param("userId") Long userId, + @Param("startDate") Date startDate, + @Param("endDate") Date endDate); + + /** + * 负责人项目:sys_oa_project.functionary 包含 nickName + */ + List selectResponsibleProjects(@Param("nickName") String nickName); +} + + diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/mapper/SysOaTaskMapper.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/mapper/SysOaTaskMapper.java index 908247f..74f773e 100644 --- a/ruoyi-oa/src/main/java/com/ruoyi/oa/mapper/SysOaTaskMapper.java +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/mapper/SysOaTaskMapper.java @@ -56,4 +56,11 @@ public interface SysOaTaskMapper extends BaseMapperPlus selectTaskItemsByTaskId(Long taskId); + /** + * 绩效报告:按用户 + 时间范围查询任务明细(包含 task_item 子项) + */ + List selectPerformanceTaskList(@Param("userId") Long userId, + @Param("startDate") Date startDate, + @Param("endDate") Date endDate); + } diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/service/IPerformanceReportService.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/service/IPerformanceReportService.java new file mode 100644 index 0000000..266e7fa --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/service/IPerformanceReportService.java @@ -0,0 +1,12 @@ +package com.ruoyi.oa.service; + +import com.ruoyi.oa.domain.vo.performance.MonthlyPerformanceReportVo; + +import java.util.Date; + +public interface IPerformanceReportService { + + MonthlyPerformanceReportVo buildMonthlyReport(Long userId, Date startDate, Date endDate); +} + + diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/service/impl/PerformanceReportServiceImpl.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/service/impl/PerformanceReportServiceImpl.java new file mode 100644 index 0000000..1e74772 --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/service/impl/PerformanceReportServiceImpl.java @@ -0,0 +1,266 @@ +package com.ruoyi.oa.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.ruoyi.common.helper.LoginHelper; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.oa.domain.SysOaAttendance; +import com.ruoyi.oa.domain.SysOaProject; +import com.ruoyi.oa.domain.vo.OaProjectReportVo; +import com.ruoyi.oa.domain.vo.OaProjectScheduleStepVo; +import com.ruoyi.oa.domain.vo.SysOaAttendanceVo; +import com.ruoyi.oa.domain.vo.SysOaProjectVo; +import com.ruoyi.oa.domain.vo.SysOaTaskVo; +import com.ruoyi.oa.domain.vo.performance.HrmLeaveReqVo; +import com.ruoyi.oa.domain.vo.performance.MonthlyPerformanceReportVo; +import com.ruoyi.oa.domain.vo.performance.MonthlyPerformanceSummaryVo; +import com.ruoyi.oa.mapper.*; +import com.ruoyi.oa.domain.bo.OaProjectReportBo; +import com.ruoyi.oa.service.IPerformanceReportService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import com.ruoyi.system.mapper.SysUserMapper; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.*; +import java.util.stream.Collectors; + +@RequiredArgsConstructor +@Service +public class PerformanceReportServiceImpl implements IPerformanceReportService { + + private final SysOaAttendanceMapper sysOaAttendanceMapper; + private final PerformanceReportMapper performanceReportMapper; + private final SysOaProjectMapper sysOaProjectMapper; + private final OaProjectScheduleStepMapper oaProjectScheduleStepMapper; + private final OaProjectReportMapper oaProjectReportMapper; + private final SysOaTaskMapper sysOaTaskMapper; + private final SysUserMapper sysUserMapper; + + @Override + public MonthlyPerformanceReportVo buildMonthlyReport(Long userId, Date startDate, Date endDate) { + MonthlyPerformanceReportVo vo = new MonthlyPerformanceReportVo(); + vo.setUserId(userId); + vo.setNickName(resolveNickName(userId)); + vo.setStartDate(startDate); + vo.setEndDate(endDate); + vo.setGeneratedAt(new Date()); + + // 1) 考勤明细 + List attendanceDetails = buildAttendanceDetails(userId, startDate, endDate); + vo.setAttendanceDetails(attendanceDetails); + + // 2) 请假明细(hrm_leave_req) + List leaveDetails = performanceReportMapper.selectLeaveDetails(userId, startDate, endDate); + vo.setLeaveDetails(leaveDetails); + + // 3) 负责人项目(functionary) + List responsibleProjects = performanceReportMapper.selectResponsibleProjects(vo.getNickName()); + vo.setResponsibleProjects(responsibleProjects); + + // 4) 项目进度步骤(按负责项目汇总出所有步骤;另给出本人负责步骤) + List scheduleSteps = buildScheduleStepsByProjects(responsibleProjects, startDate, endDate); + vo.setScheduleStepDetails(scheduleSteps); + + List mySteps = scheduleSteps.stream() + .filter(s -> StringUtils.isNotBlank(s.getNodeHeader()) && s.getNodeHeader().contains(vo.getNickName())) + .collect(Collectors.toList()); + vo.setMyScheduleStepDetails(mySteps); + + // 5) 报工明细(oa_project_report) + OaProjectReportBo reportBo = new OaProjectReportBo(); + reportBo.setUserId(userId); + reportBo.setStartDate(startDate); + reportBo.setEndDate(endDate); + List projectReports = oaProjectReportMapper.selectAll(reportBo); + vo.setProjectReportDetails(projectReports); + + // 6) 任务明细(sys_oa_task + task_item) + List taskDetails = sysOaTaskMapper.selectPerformanceTaskList(userId, startDate, endDate); + vo.setTaskDetails(taskDetails); + + // 7) 汇总 + vo.setSummary(buildSummary(attendanceDetails, leaveDetails, responsibleProjects, mySteps, projectReports, taskDetails)); + + return vo; + } + + private String resolveNickName(Long userId) { + // 当前用户直接取缓存 + if (Objects.equals(userId, LoginHelper.getUserId())) { + return LoginHelper.getNickName(); + } + if (userId == null) { + return null; + } + com.ruoyi.common.core.domain.entity.SysUser u = sysUserMapper.selectById(userId); + return u != null ? u.getNickName() : null; + } + + private List buildAttendanceDetails(Long userId, Date startDate, Date endDate) { + // 先取原表记录 + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.eq(SysOaAttendance::getDelFlag, 0) + .eq(SysOaAttendance::getUserId, userId) + .ge(SysOaAttendance::getCreateTime, startDate) + .le(SysOaAttendance::getCreateTime, endDate) + .orderByAsc(SysOaAttendance::getCreateTime); + List rows = sysOaAttendanceMapper.selectList(lqw); + + // 组装 VO:补 projectName + Set projectIds = rows.stream() + .map(SysOaAttendance::getProjectId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + Map projMap = new HashMap<>(); + if (!projectIds.isEmpty()) { + LambdaQueryWrapper pq = Wrappers.lambdaQuery(); + pq.in(SysOaProject::getProjectId, projectIds); + projMap = sysOaProjectMapper.selectList(pq).stream().collect(Collectors.toMap(SysOaProject::getProjectId, p -> p, (a, b) -> a)); + } + + List vos = new ArrayList<>(); + for (SysOaAttendance r : rows) { + SysOaAttendanceVo a = new SysOaAttendanceVo(); + a.setId(r.getId()); + a.setUserId(r.getUserId()); + a.setAttendanceDay(r.getAttendanceDay()); + a.setProjectId(r.getProjectId()); + a.setDayLength(r.getDayLength()); + a.setHour(r.getHour()); + a.setRemark(r.getRemark()); + a.setCreateTime(r.getCreateTime()); + if (r.getProjectId() != null && projMap.containsKey(r.getProjectId())) { + a.setProjectName(projMap.get(r.getProjectId()).getProjectName()); + } + // 计算单条工时(dayLength*8 + hour) + BigDecimal hours = BigDecimal.ZERO; + if (r.getDayLength() != null) { + hours = hours.add(BigDecimal.valueOf(r.getDayLength()).multiply(BigDecimal.valueOf(8))); + } + if (r.getHour() != null) { + hours = hours.add(BigDecimal.valueOf(r.getHour())); + } + a.setHourWorkTimes(hours.doubleValue()); + // 加班:hour>8 的部分(仅作为展示口径) + if (r.getHour() != null) { + a.setOverTime(Math.max(0d, r.getHour() - 8d)); + } + vos.add(a); + } + return vos; + } + + private List buildScheduleStepsByProjects(List responsibleProjects, Date startDate, Date endDate) { + if (responsibleProjects == null || responsibleProjects.isEmpty()) { + return Collections.emptyList(); + } + List projectIds = responsibleProjects.stream() + .map(SysOaProjectVo::getProjectId) + .filter(Objects::nonNull) + .distinct() + .collect(Collectors.toList()); + if (projectIds.isEmpty()) { + return Collections.emptyList(); + } + + // 复用 OaProjectScheduleStepMapper.xml 里的 selectVoPageNew(它已 join project/supplier),这里用 Page 拉全量 + // 时间口径:尽量以“与区间有交集”为准(start/end/plan/actual/create 任一落在区间内即纳入) + com.baomidou.mybatisplus.extension.plugins.pagination.Page page = + new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>(1, 100000); + com.baomidou.mybatisplus.core.conditions.query.QueryWrapper qw = Wrappers.query(); + qw.eq("opss.del_flag", "0"); + qw.eq("opss.use_flag", 1); + qw.in("schedule.project_id", projectIds); + if (startDate != null && endDate != null) { + qw.and(w -> w + .between("opss.create_time", startDate, endDate) + .or().between("opss.start_time", startDate, endDate) + .or().between("opss.end_time", startDate, endDate) + .or().between("opss.plan_start", startDate, endDate) + .or().between("opss.plan_end", startDate, endDate) + .or().between("opss.actual_start", startDate, endDate) + .or().between("opss.actual_end", startDate, endDate) + ); + } + qw.orderByAsc("schedule.project_id", "opss.step_order"); + return oaProjectScheduleStepMapper.selectVoPageNew(page, qw).getRecords(); + } + + private MonthlyPerformanceSummaryVo buildSummary(List attendanceDetails, + List leaveDetails, + List responsibleProjects, + List mySteps, + List projectReports, + List tasks) { + MonthlyPerformanceSummaryVo s = new MonthlyPerformanceSummaryVo(); + + // 考勤 + s.setAttendanceRecordCount(attendanceDetails == null ? 0L : (long) attendanceDetails.size()); + BigDecimal workHours = BigDecimal.ZERO; + BigDecimal overtime = BigDecimal.ZERO; + if (attendanceDetails != null) { + for (SysOaAttendanceVo a : attendanceDetails) { + if (a.getHourWorkTimes() != null) { + workHours = workHours.add(BigDecimal.valueOf(a.getHourWorkTimes())); + } + if (a.getOverTime() != null) { + overtime = overtime.add(BigDecimal.valueOf(a.getOverTime())); + } + } + } + s.setAttendanceWorkHours(workHours.setScale(2, RoundingMode.HALF_UP)); + s.setAttendanceOvertimeHours(overtime.setScale(2, RoundingMode.HALF_UP)); + + // 请假 + s.setLeaveRequestCount(leaveDetails == null ? 0L : (long) leaveDetails.size()); + BigDecimal leaveHours = BigDecimal.ZERO; + if (leaveDetails != null) { + for (HrmLeaveReqVo lr : leaveDetails) { + if (lr.getHours() != null) { + leaveHours = leaveHours.add(lr.getHours()); + } + } + } + s.setLeaveHours(leaveHours.setScale(2, RoundingMode.HALF_UP)); + + // 项目 + s.setResponsibleProjectCount(responsibleProjects == null ? 0L : (long) responsibleProjects.size()); + + // 进度步骤(本人负责) + s.setMyScheduleStepCount(mySteps == null ? 0L : (long) mySteps.size()); + long myCompleted = 0; + if (mySteps != null) { + for (OaProjectScheduleStepVo step : mySteps) { + if (step.getStatus() != null && step.getStatus() == 1) { + myCompleted++; + } + } + } + s.setMyScheduleStepCompletedCount(myCompleted); + + // 报工 + s.setProjectReportCount(projectReports == null ? 0L : (long) projectReports.size()); + + // 任务 + s.setTaskCount(tasks == null ? 0L : (long) tasks.size()); + long taskCompleted = 0; + long taskItemCount = 0; + if (tasks != null) { + for (SysOaTaskVo t : tasks) { + if (t.getCompletedTime() != null) { + taskCompleted++; + } + if (t.getTaskItemVoList() != null) { + taskItemCount += t.getTaskItemVoList().size(); + } + } + } + s.setTaskCompletedCount(taskCompleted); + s.setTaskItemCount(taskItemCount); + return s; + } +} + + diff --git a/ruoyi-oa/src/main/resources/mapper/oa/PerformanceReportMapper.xml b/ruoyi-oa/src/main/resources/mapper/oa/PerformanceReportMapper.xml new file mode 100644 index 0000000..cd4fe6d --- /dev/null +++ b/ruoyi-oa/src/main/resources/mapper/oa/PerformanceReportMapper.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + diff --git a/ruoyi-oa/src/main/resources/mapper/oa/SysOaTaskMapper.xml b/ruoyi-oa/src/main/resources/mapper/oa/SysOaTaskMapper.xml index f7eedd9..74620c0 100644 --- a/ruoyi-oa/src/main/resources/mapper/oa/SysOaTaskMapper.xml +++ b/ruoyi-oa/src/main/resources/mapper/oa/SysOaTaskMapper.xml @@ -215,6 +215,95 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" ORDER BY soti.create_time DESC + + +