全量提交

This commit is contained in:
2026-02-28 15:20:47 +08:00
parent cb41bcf367
commit 697fe67780
10 changed files with 630 additions and 0 deletions

View File

@@ -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<MonthlyPerformanceReportVo> 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));
}
}

View File

@@ -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;
}

View File

@@ -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<SysOaAttendanceVo> attendanceDetails;
/** 明细请假hrm_leave_req */
private List<HrmLeaveReqVo> leaveDetails;
/** 明细负责人项目sys_oa_project.functionary */
private List<SysOaProjectVo> responsibleProjects;
/** 明细项目进度步骤oa_project_schedule_step */
private List<OaProjectScheduleStepVo> scheduleStepDetails;
/** 明细本人负责的进度步骤node_header=本人) */
private List<OaProjectScheduleStepVo> myScheduleStepDetails;
/** 明细报工oa_project_report */
private List<OaProjectReportVo> projectReportDetails;
/** 明细任务sys_oa_task含 task_item 子项) */
private List<SysOaTaskVo> taskDetails;
}

View File

@@ -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;
}

View File

@@ -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<HrmLeaveReqVo> selectLeaveDetails(@Param("userId") Long userId,
@Param("startDate") Date startDate,
@Param("endDate") Date endDate);
/**
* 负责人项目sys_oa_project.functionary 包含 nickName
*/
List<SysOaProjectVo> selectResponsibleProjects(@Param("nickName") String nickName);
}

View File

@@ -56,4 +56,11 @@ public interface SysOaTaskMapper extends BaseMapperPlus<SysOaTaskMapper, SysOaTa
*/
List<SysOaTaskItemVo> selectTaskItemsByTaskId(Long taskId);
/**
* 绩效报告:按用户 + 时间范围查询任务明细(包含 task_item 子项)
*/
List<SysOaTaskVo> selectPerformanceTaskList(@Param("userId") Long userId,
@Param("startDate") Date startDate,
@Param("endDate") Date endDate);
}

View File

@@ -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);
}

View File

@@ -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<SysOaAttendanceVo> attendanceDetails = buildAttendanceDetails(userId, startDate, endDate);
vo.setAttendanceDetails(attendanceDetails);
// 2) 请假明细hrm_leave_req
List<HrmLeaveReqVo> leaveDetails = performanceReportMapper.selectLeaveDetails(userId, startDate, endDate);
vo.setLeaveDetails(leaveDetails);
// 3) 负责人项目functionary
List<SysOaProjectVo> responsibleProjects = performanceReportMapper.selectResponsibleProjects(vo.getNickName());
vo.setResponsibleProjects(responsibleProjects);
// 4) 项目进度步骤(按负责项目汇总出所有步骤;另给出本人负责步骤)
List<OaProjectScheduleStepVo> scheduleSteps = buildScheduleStepsByProjects(responsibleProjects, startDate, endDate);
vo.setScheduleStepDetails(scheduleSteps);
List<OaProjectScheduleStepVo> 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<OaProjectReportVo> projectReports = oaProjectReportMapper.selectAll(reportBo);
vo.setProjectReportDetails(projectReports);
// 6) 任务明细sys_oa_task + task_item
List<SysOaTaskVo> 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<SysOaAttendanceVo> buildAttendanceDetails(Long userId, Date startDate, Date endDate) {
// 先取原表记录
LambdaQueryWrapper<SysOaAttendance> lqw = Wrappers.lambdaQuery();
lqw.eq(SysOaAttendance::getDelFlag, 0)
.eq(SysOaAttendance::getUserId, userId)
.ge(SysOaAttendance::getCreateTime, startDate)
.le(SysOaAttendance::getCreateTime, endDate)
.orderByAsc(SysOaAttendance::getCreateTime);
List<SysOaAttendance> rows = sysOaAttendanceMapper.selectList(lqw);
// 组装 VO补 projectName
Set<Long> projectIds = rows.stream()
.map(SysOaAttendance::getProjectId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
Map<Long, SysOaProject> projMap = new HashMap<>();
if (!projectIds.isEmpty()) {
LambdaQueryWrapper<SysOaProject> pq = Wrappers.lambdaQuery();
pq.in(SysOaProject::getProjectId, projectIds);
projMap = sysOaProjectMapper.selectList(pq).stream().collect(Collectors.toMap(SysOaProject::getProjectId, p -> p, (a, b) -> a));
}
List<SysOaAttendanceVo> 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<OaProjectScheduleStepVo> buildScheduleStepsByProjects(List<SysOaProjectVo> responsibleProjects, Date startDate, Date endDate) {
if (responsibleProjects == null || responsibleProjects.isEmpty()) {
return Collections.emptyList();
}
List<Long> 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<Object> page =
new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>(1, 100000);
com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<com.ruoyi.oa.domain.OaProjectScheduleStep> 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<SysOaAttendanceVo> attendanceDetails,
List<HrmLeaveReqVo> leaveDetails,
List<SysOaProjectVo> responsibleProjects,
List<OaProjectScheduleStepVo> mySteps,
List<OaProjectReportVo> projectReports,
List<SysOaTaskVo> 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;
}
}

View File

@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.oa.mapper.PerformanceReportMapper">
<select id="selectLeaveDetails" resultType="com.ruoyi.oa.domain.vo.performance.HrmLeaveReqVo">
SELECT
lr.biz_id AS bizId,
lr.emp_id AS empId,
he.user_id AS userId,
lr.project_id AS projectId,
lr.leave_type AS leaveType,
lr.start_time AS startTime,
lr.end_time AS endTime,
lr.hours AS hours,
lr.reason AS reason,
lr.status AS status,
lr.accessory_apply_ids AS accessoryApplyIds,
lr.accessory_receipt_ids AS accessoryReceiptIds,
lr.remark AS remark,
lr.create_time AS createTime,
lr.create_by AS createBy
FROM hrm_leave_req lr
INNER JOIN hrm_employee he ON lr.emp_id = he.emp_id AND he.del_flag = 0
WHERE lr.del_flag = 0
AND he.user_id = #{userId}
AND lr.start_time &lt;= #{endDate}
AND lr.end_time &gt;= #{startDate}
ORDER BY lr.start_time ASC
</select>
<select id="selectResponsibleProjects" resultType="com.ruoyi.oa.domain.vo.SysOaProjectVo">
SELECT
project_id AS projectId,
project_name AS projectName,
project_num AS projectNum,
project_type AS projectType,
address AS address,
funds AS funds,
functionary AS functionary,
begin_time AS beginTime,
finish_time AS finishTime,
introduction AS introduction,
project_grade AS projectGrade,
project_status AS projectStatus,
trade_type AS tradeType,
project_code AS projectCode,
pre_pay AS prePay,
is_top AS isTop,
remark AS remark,
create_by AS createBy,
create_time AS createTime,
update_by AS updateBy,
update_time AS updateTime
FROM sys_oa_project
WHERE del_flag = 0
AND functionary IS NOT NULL
AND functionary LIKE CONCAT('%', #{nickName}, '%')
ORDER BY update_time DESC, create_time DESC
</select>
</mapper>

View File

@@ -215,6 +215,95 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
ORDER BY soti.create_time DESC
</select>
<!-- 绩效报告:按用户 + 时间范围查询任务明细(包含 task_item 子项) -->
<select id="selectPerformanceTaskList" resultMap="SysOaTaskResult">
SELECT sot.task_id,
sot.project_id,
sot.create_user_id,
sot.worker_id,
sot.task_title,
sot.task_type,
sot.task_grade,
sot.collaborator,
sot.finish_time,
sot.temp_time,
sot.begin_time,
sot.origin_finish_time,
sot.postponements,
sot.completed_time,
sot.rank_number,
sot.remark,
sot.task_rank,
sot.state,
sot.time_gap,
sot.status,
sot.accessory,
sot.create_by,
sot.create_time,
sot.update_by,
sot.update_time,
sot.del_flag,
sot.own_rank,
sot.track_id,
stepAgg.tab_node AS tabNode,
stepAgg.first_level_node AS firstLevelNode,
stepAgg.second_level_node AS secondLevelNode,
sop.project_name,
sop.project_num,
sop.project_code,
su1.nick_name AS createUserNickName,
su2.nick_name AS workerNickName,
sd.dept_name AS deptName,
IF(
sot.completed_time IS NULL,
IF(
(
SELECT a.end_time
FROM sys_oa_task_item a
WHERE a.task_id = sot.task_id
AND a.completed_time IS NULL
LIMIT 1
) IS NOT NULL,
DATEDIFF(
NOW(),
(
SELECT a.end_time
FROM sys_oa_task_item a
WHERE a.task_id = sot.task_id
AND a.completed_time IS NULL
LIMIT 1
)
),
DATEDIFF(NOW(), sot.finish_time)
),
0
) AS overDays
FROM sys_oa_task sot
LEFT JOIN sys_user su1 ON su1.user_id = sot.create_user_id
LEFT JOIN sys_user su2 ON su2.user_id = sot.worker_id
LEFT JOIN sys_oa_project sop ON sop.project_id = sot.project_id
LEFT JOIN sys_dept sd ON sd.dept_id = su2.dept_id
LEFT JOIN oa_project_schedule_step stepAgg ON stepAgg.track_id = sot.track_id
WHERE sot.del_flag = '0'
AND (
sot.create_user_id = #{userId}
OR sot.worker_id = #{userId}
OR EXISTS (
SELECT 1 FROM sys_oa_task_user sotu
WHERE sotu.task_id = sot.task_id
AND sotu.user_id = #{userId}
AND sotu.del_flag = 0
)
)
AND (
(COALESCE(sot.begin_time, sot.create_time) &lt;= #{endDate}
AND COALESCE(sot.finish_time, sot.temp_time, sot.completed_time, sot.create_time) &gt;= #{startDate})
OR (sot.create_time BETWEEN #{startDate} AND #{endDate})
OR (sot.completed_time BETWEEN #{startDate} AND #{endDate})
)
ORDER BY sot.create_time DESC
</select>
<select id="getMonthlyData"
parameterType="java.lang.String"
resultType="com.ruoyi.oa.domain.UserMonthlyData">