feat(oa): 新增用户活跃统计功能并完善个人报告统计

- 新增用户活跃统计模块,包括实体类、业务接口和实现、控制器及Mapper
- 实现用户登录时记录当日活跃数据
- 在个人报告中增加活跃天数、报工信息、出差信息、项目信息等统计维度
- 添加工程异常统计和关键采购任务统计功能
- 完善任务信息统计,包括发放任务、承担任务及其状态分析
-优化个人报告接口,支持更全面的工作数据展示
This commit is contained in:
2025-10-30 15:33:39 +08:00
parent 29340d323f
commit 4f0ebc1a4e
15 changed files with 1229 additions and 3 deletions

View File

@@ -15,6 +15,7 @@ import com.ruoyi.fadapp.domain.vo.NickDeptVo;
import com.ruoyi.fadapp.service.IFadAppAuthService;
import com.ruoyi.system.service.ISysUserService;
import com.ruoyi.system.service.SysPermissionService;
import com.ruoyi.oa.service.IOaUserActiveService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@@ -35,6 +36,7 @@ public class FadAppAuthServiceImpl implements IFadAppAuthService {
private final ISysUserService userService;
private final SysPermissionService permissionService;
private final IOaUserActiveService userActiveService;
/**
* 验证码缓存前缀
@@ -106,6 +108,11 @@ public class FadAppAuthServiceImpl implements IFadAppAuthService {
LoginHelper.loginByDevice(loginUser, DeviceType.APP);
String token = StpUtil.getTokenValue();
// 记录用户当日活跃(登录)
if (user.getUserId() != null) {
userActiveService.recordTodayLogin(user.getUserId());
}
// 构建登录结果
LoginResultVo result = new LoginResultVo();
result.setToken(token);

View File

@@ -14,6 +14,7 @@ import com.ruoyi.system.domain.vo.RouterVo;
import com.ruoyi.system.service.ISysMenuService;
import com.ruoyi.system.service.ISysUserService;
import com.ruoyi.system.service.SysLoginService;
import com.ruoyi.oa.service.IOaUserActiveService;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
@@ -39,6 +40,7 @@ public class SysLoginController {
private final SysLoginService loginService;
private final ISysMenuService menuService;
private final ISysUserService userService;
private final IOaUserActiveService userActiveService;
/**
* 登录方法
@@ -54,6 +56,10 @@ public class SysLoginController {
String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
loginBody.getUuid());
ajax.put(Constants.TOKEN, token);
Long userId = LoginHelper.getUserId();
if (userId != null) {
userActiveService.recordTodayLogin(userId);
}
return R.ok(ajax);
}

View File

@@ -135,4 +135,5 @@ public class OaProjectScheduleStepController extends BaseController {
) {
return R.ok(iOaProjectScheduleStepService.personalReport(poolId, nickName));
}
}

View File

@@ -0,0 +1,101 @@
package com.ruoyi.oa.controller;
import java.util.List;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import lombok.RequiredArgsConstructor;
import javax.servlet.http.HttpServletResponse;
import javax.validation.constraints.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.validation.annotation.Validated;
import com.ruoyi.common.annotation.RepeatSubmit;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.PageQuery;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.validate.AddGroup;
import com.ruoyi.common.core.validate.EditGroup;
import com.ruoyi.common.core.validate.QueryGroup;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.oa.domain.vo.OaUserActiveVo;
import com.ruoyi.oa.domain.bo.OaUserActiveBo;
import com.ruoyi.oa.service.IOaUserActiveService;
import com.ruoyi.common.core.page.TableDataInfo;
/**
* 用户活跃统计
*
* @author ruoyi
* @date 2025-10-30
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/oa/userActive")
public class OaUserActiveController extends BaseController {
private final IOaUserActiveService iOaUserActiveService;
/**
* 查询用户活跃统计列表
*/
@GetMapping("/list")
public TableDataInfo<OaUserActiveVo> list(OaUserActiveBo bo, PageQuery pageQuery) {
return iOaUserActiveService.queryPageList(bo, pageQuery);
}
/**
* 导出用户活跃统计列表
*/
@Log(title = "用户活跃统计", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(OaUserActiveBo bo, HttpServletResponse response) {
List<OaUserActiveVo> list = iOaUserActiveService.queryList(bo);
ExcelUtil.exportExcel(list, "用户活跃统计", OaUserActiveVo.class, response);
}
/**
* 获取用户活跃统计详细信息
*
* @param activeId 主键
*/
@GetMapping("/{activeId}")
public R<OaUserActiveVo> getInfo(@NotNull(message = "主键不能为空")
@PathVariable Long activeId) {
return R.ok(iOaUserActiveService.queryById(activeId));
}
/**
* 新增用户活跃统计
*/
@Log(title = "用户活跃统计", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping()
public R<Void> add(@Validated(AddGroup.class) @RequestBody OaUserActiveBo bo) {
return toAjax(iOaUserActiveService.insertByBo(bo));
}
/**
* 修改用户活跃统计
*/
@Log(title = "用户活跃统计", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping()
public R<Void> edit(@Validated(EditGroup.class) @RequestBody OaUserActiveBo bo) {
return toAjax(iOaUserActiveService.updateByBo(bo));
}
/**
* 删除用户活跃统计
*
* @param activeIds 主键串
*/
@Log(title = "用户活跃统计", businessType = BusinessType.DELETE)
@DeleteMapping("/{activeIds}")
public R<Void> remove(@NotEmpty(message = "主键不能为空")
@PathVariable Long[] activeIds) {
return toAjax(iOaUserActiveService.deleteWithValidByIds(Arrays.asList(activeIds), true));
}
}

View File

@@ -0,0 +1,54 @@
package com.ruoyi.oa.domain;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.util.Date;
import java.math.BigDecimal;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.common.core.domain.BaseEntity;
/**
* 用户活跃统计对象 oa_user_active
*
* @author ruoyi
* @date 2025-10-30
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("oa_user_active")
public class OaUserActive extends BaseEntity {
private static final long serialVersionUID=1L;
/**
* 主键ID
*/
@TableId(value = "active_id")
private Long activeId;
/**
* 用户ID
*/
private Long userId;
/**
* 活跃日期
*/
private Date activeDate;
/**
* 当日登录次数
*/
private Long loginCount;
/**
* 备注
*/
private String remark;
/**
* 删除标志0=正常1=已删除
*/
@TableLogic
private Integer delFlag;
}

View File

@@ -0,0 +1,52 @@
package com.ruoyi.oa.domain.bo;
import com.ruoyi.common.core.validate.AddGroup;
import com.ruoyi.common.core.validate.EditGroup;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.*;
import java.util.Date;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.common.core.domain.BaseEntity;
/**
* 用户活跃统计业务对象 oa_user_active
*
* @author ruoyi
* @date 2025-10-30
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class OaUserActiveBo extends BaseEntity {
/**
* 主键ID
*/
private Long activeId;
/**
* 用户ID
*/
private Long userId;
/**
* 活跃日期
*/
private Date activeDate;
/**
* 当日登录次数
*/
private Long loginCount;
/**
* 备注
*/
private String remark;
}

View File

@@ -5,6 +5,8 @@ import com.ruoyi.oa.domain.OaProjectScheduleStep;
import com.ruoyi.oa.domain.SysOaProject;
import com.ruoyi.oa.domain.SysOaTask;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
/**
@@ -20,10 +22,35 @@ public class PersonalReportDTO {
/** 进度统计信息 */
private ProgressStats progressStats;
/** 用户负责的进度步骤列表 */
private List<OaProjectScheduleStep> userSteps;
//nickname和projectId关联的任务
private List<SysOaTask> tasks;
/** 活跃天数 */
private Integer activeDays;
/** 报工统计 */
private PersonalReportDTO.WorkReportStats workReportStats;
/** 出差统计 */
private PersonalReportDTO.BusinessTripStats businessTripStats;
/** 项目统计 */
private PersonalReportDTO.ProjectStats projectStats;
// /** 进度统计 */
// private PersonalReportDTO.ProgressStats progressStats;
/** 任务统计 */
private PersonalReportDTO.TaskStats taskStats;
/** 工程异常统计 */
private PersonalReportDTO.EngineeringExceptionStats exceptionStats;
/** 关键采购任务统计 */
private PersonalReportDTO.ProcurementTaskStats procurementStats;
/**
* 进度统计内部类封装total、completed等字段
*/
@@ -38,4 +65,206 @@ public class PersonalReportDTO {
/** 延期进度数 */
private Long delayed;
}
/**
* 报工统计
*/
@Data
public static class WorkReportStats {
/** 总报工天数 */
private BigDecimal totalWorkDays;
/** 有效报工天数 */
private BigDecimal validWorkDays;
/** 报工项目数量 */
private Integer reportProjectCount;
}
/**
* 出差统计
*/
@Data
public static class BusinessTripStats {
/** 总出差天数 */
private BigDecimal totalTripDays;
/** 国内出差天数 */
private BigDecimal domesticTripDays;
/** 国外出差天数 */
private BigDecimal foreignTripDays;
/** 出差项目数量 */
private Integer tripProjectCount;
}
/**
* 项目统计
*/
@Data
public static class ProjectStats {
/** 涉及项目总数 */
private Integer totalProjects;
/** 负责项目数量 */
private Integer responsibleProjects;
/** 参与项目数量 */
private Integer participatedProjects;
/** 项目清单 */
private List<PersonalReportDTO.ProjectSummary> projectList;
}
/**
* 项目摘要
*/
@Data
public static class ProjectSummary {
/** 项目ID */
private Long projectId;
/** 项目名称 */
private String projectName;
/** 项目编号 */
private String projectNum;
/** 参与角色 */
private String role;
/** 参与天数 */
private BigDecimal participationDays;
}
// /**
// * 进度统计
// */
// @Data
// public static class ProgressStats {
// /** 涉及进度总数 */
// private Integer totalProgress;
// /** 已完成进度数 */
// private Integer completedProgress;
// /** 进行中进度数 */
// private Integer inProgressCount;
// /** 延期进度数 */
// private Integer delayedProgress;
// /** 进度清单 */
// private List<PersonalReportDTO.ProgressSummary> progressList;
// }
/**
* 进度摘要
*/
@Data
public static class ProgressSummary {
/** 进度ID */
private Long trackId;
/** 步骤名称 */
private String stepName;
/** 项目名称 */
private String projectName;
/** 进度状态 */
private Long status;
/** 是否延期 */
private Boolean isDelayed;
}
/**
* 任务统计
*/
@Data
public static class TaskStats {
/** 发放任务数量 */
private Integer assignedTasks;
/** 承担任务数量 */
private Integer undertakenTasks;
/** 已完成任务数量 */
private Integer completedTasks;
/** 设置进行中任务数量 */
private Integer underwayTasks;
/** 待完成任务数量 */
private Integer unfinishedTasks;
/** 延期任务数量 */
private Integer delayedTasks;
/** 发放任务清单 */
private List<PersonalReportDTO.TaskSummary> assignedTaskList;
/** 承担任务清单 */
private List<PersonalReportDTO.TaskSummary> undertakenTaskList;
}
/**
* 任务摘要
*/
@Data
public static class TaskSummary {
/** 任务ID */
private Long taskId;
/** 任务主题 */
private String taskTitle;
/** 项目名称 */
private String projectName;
/** 任务状态 */
private Long state;
/** 是否延期 */
private Boolean isDelayed;
/** 延期次数 */
private Long postponements;
}
/**
* 工程异常统计
*/
@Data
public static class EngineeringExceptionStats {
/** 承担异常数量 */
private Integer totalExceptions;
/** 延期异常数量 */
private Integer delayedExceptions;
/** 已解决异常数量 */
private Integer resolvedExceptions;
/** 异常清单 */
private List<PersonalReportDTO.ExceptionSummary> exceptionList;
}
/**
* 异常摘要
*/
@Data
public static class ExceptionSummary {
/** 异常ID */
private Long exceptionId;
/** 异常描述 */
private String description;
/** 项目名称 */
private String projectName;
/** 异常状态 */
private String status;
/** 是否延期 */
private Boolean isDelayed;
}
/**
* 关键采购任务统计
*/
@Data
public static class ProcurementTaskStats {
/** 关键采购任务总数 */
private Integer totalProcurementTasks;
/** 已完成采购任务数 */
private Integer completedProcurementTasks;
/** 延期采购任务数 */
private Integer delayedProcurementTasks;
/** 采购任务清单 */
private List<PersonalReportDTO.ProcurementTaskSummary> procurementTaskList;
}
/**
* 采购任务摘要
*/
@Data
public static class ProcurementTaskSummary {
/** 采购任务ID */
private Long procurementId;
/** 采购内容 */
private String procurementContent;
/** 项目名称 */
private String projectName;
/** 采购状态 */
private String status;
/** 是否延期 */
private Boolean isDelayed;
}
}

View File

@@ -0,0 +1,57 @@
package com.ruoyi.oa.domain.vo;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty;
import com.ruoyi.common.annotation.ExcelDictFormat;
import com.ruoyi.common.convert.ExcelDictConvert;
import lombok.Data;
import java.util.Date;
/**
* 用户活跃统计视图对象 oa_user_active
*
* @author ruoyi
* @date 2025-10-30
*/
@Data
@ExcelIgnoreUnannotated
public class OaUserActiveVo {
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@ExcelProperty(value = "主键ID")
private Long activeId;
/**
* 用户ID
*/
@ExcelProperty(value = "用户ID")
private Long userId;
/**
* 活跃日期
*/
@ExcelProperty(value = "活跃日期")
private Date activeDate;
/**
* 当日登录次数
*/
@ExcelProperty(value = "当日登录次数")
private Long loginCount;
/**
* 备注
*/
@ExcelProperty(value = "备注")
private String remark;
}

View File

@@ -0,0 +1,15 @@
package com.ruoyi.oa.mapper;
import com.ruoyi.oa.domain.OaUserActive;
import com.ruoyi.oa.domain.vo.OaUserActiveVo;
import com.ruoyi.common.core.mapper.BaseMapperPlus;
/**
* 用户活跃统计Mapper接口
*
* @author ruoyi
* @date 2025-10-30
*/
public interface OaUserActiveMapper extends BaseMapperPlus<OaUserActiveMapper, OaUserActive, OaUserActiveVo> {
}

View File

@@ -73,4 +73,5 @@ public interface IOaProjectScheduleStepService{
List<Map<String, Object>> queryProgressByHeader();
PersonalReportDTO personalReport(Long poolId, String nickName);
}

View File

@@ -0,0 +1,53 @@
package com.ruoyi.oa.service;
import com.ruoyi.oa.domain.OaUserActive;
import com.ruoyi.oa.domain.vo.OaUserActiveVo;
import com.ruoyi.oa.domain.bo.OaUserActiveBo;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.core.domain.PageQuery;
import java.util.Collection;
import java.util.List;
/**
* 用户活跃统计Service接口
*
* @author ruoyi
* @date 2025-10-30
*/
public interface IOaUserActiveService {
/**
* 查询用户活跃统计
*/
OaUserActiveVo queryById(Long activeId);
/**
* 查询用户活跃统计列表
*/
TableDataInfo<OaUserActiveVo> queryPageList(OaUserActiveBo bo, PageQuery pageQuery);
/**
* 查询用户活跃统计列表
*/
List<OaUserActiveVo> queryList(OaUserActiveBo bo);
/**
* 新增用户活跃统计
*/
Boolean insertByBo(OaUserActiveBo bo);
/**
* 修改用户活跃统计
*/
Boolean updateByBo(OaUserActiveBo bo);
/**
* 校验并批量删除用户活跃统计信息
*/
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
boolean hasActiveToday(Long userId);
void recordTodayLogin(Long userId);
}

View File

@@ -4,9 +4,7 @@ import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.mapper.BaseMapperPlus;
import com.ruoyi.common.helper.LoginHelper;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.core.page.TableDataInfo;
@@ -17,11 +15,20 @@ import com.ruoyi.oa.domain.*;
import com.ruoyi.oa.domain.bo.BatchBo;
import com.ruoyi.oa.domain.dto.NodeDTO;
import com.ruoyi.oa.domain.dto.PersonalReportDTO;
import com.ruoyi.oa.domain.vo.OaExpressVo;
import com.ruoyi.oa.mapper.*;
import com.ruoyi.oa.mapper.OaProjectReportMapper;
import com.ruoyi.oa.mapper.SysOaTaskMapper;
import com.ruoyi.oa.mapper.OaUserActiveMapper;
import com.ruoyi.oa.mapper.OaExpressQuestionMapper;
import com.ruoyi.oa.mapper.OaRequirementsMapper;
import com.ruoyi.oa.service.IOaExpressService;
import com.ruoyi.oa.service.ISysOaTaskService;
import com.ruoyi.system.mapper.SysUserMapper;
import java.math.BigDecimal;
import java.util.Date;
import lombok.RequiredArgsConstructor;
import org.flowable.job.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import com.ruoyi.oa.domain.bo.OaProjectScheduleStepBo;
import com.ruoyi.oa.domain.vo.OaProjectScheduleStepVo;
@@ -59,6 +66,18 @@ public class OaProjectScheduleStepServiceImpl implements IOaProjectScheduleStepS
private final OaProjectScheduleStepMapper projectScheduleStepMapper;
private final ISysOaTaskService sysOaTaskService;
private final OaProjectReportMapper projectReportMapper;
private final SysOaTaskMapper taskMapper;
private final OaUserActiveMapper userActiveMapper;
private final OaExpressQuestionMapper expressQuestionMapper;
private final OaRequirementsMapper requirementsMapper;
private final IOaExpressService oaExpressService;
@@ -358,6 +377,7 @@ public class OaProjectScheduleStepServiceImpl implements IOaProjectScheduleStepS
if (user == null) {
throw new RuntimeException("未找到用户:" + nickName);
}
Long userId = user.getUserId();
// 查询奖金池关联的项目ID
List<OaBonusProjectRel> rels = bonusProjectRelMapper.selectList(
@@ -444,9 +464,474 @@ public class OaProjectScheduleStepServiceImpl implements IOaProjectScheduleStepS
result.setProgressStats(progressStats);
result.setUserSteps(userSteps); // 此时userSteps已包含每个步骤对应的projectId
result.setTasks(tasks);
// 统计活跃天数
result.setActiveDays(getActiveDays(userId, startTime, endTime));
// 1. 统计报工信息
result.setWorkReportStats(getWorkReportStats(userId, startTime, endTime));
// 2. 统计出差信息
result.setBusinessTripStats(getBusinessTripStats(userId, startTime, endTime));
// 3. 统计项目信息
result.setProjectStats(getProjectStats(userId, nickName, startTime, endTime));
// // 4. 统计进度信息
// result.setProgressStats(getProgressStats(nickName, startDate, endDate));
// 5. 统计任务信息
result.setTaskStats(getTaskStats(userId, startTime, endTime));
// 6. 统计工程异常信息
result.setExceptionStats(getExceptionStats(nickName, startTime, endTime));
// 7. 统计关键采购任务信息
result.setProcurementStats(getProcurementStats(userId, startTime, endTime));
return result;
}
/**
* 统计用户活跃天数
* @param userId 用户ID
* @param startDate 开始日期
* @param endDate 结束日期
* @return 活跃天数
*/
private Integer getActiveDays(Long userId, Date startDate, Date endDate) {
// 构建查询条件
LambdaQueryWrapper<OaUserActive> wrapper = Wrappers.<OaUserActive>lambdaQuery()
.eq(OaUserActive::getUserId, userId)
.eq(OaUserActive::getDelFlag, 0);
// 添加时间范围条件
if (startDate != null && endDate != null) {
wrapper.between(OaUserActive::getActiveDate, startDate, endDate);
}
// 查询活跃记录数量
Long count = userActiveMapper.selectCount(wrapper);
return count != null ? count.intValue() : 0;
}
/**
* 统计报工信息
*/
private PersonalReportDTO.WorkReportStats getWorkReportStats(Long userId, Date startDate, Date endDate) {
PersonalReportDTO.WorkReportStats stats = new PersonalReportDTO.WorkReportStats();
LambdaQueryWrapper<OaProjectReport> wrapper = Wrappers.<OaProjectReport>lambdaQuery()
.eq(OaProjectReport::getDelFlag, 0)
.eq(OaProjectReport::getUserId, userId);
// 添加时间范围条件
if (startDate != null && endDate != null) {
wrapper.between(OaProjectReport::getCreateTime, startDate, endDate);
}
List<OaProjectReport> reports = projectReportMapper.selectList(wrapper);
// 计算报工天数(这里简化处理,实际可能需要按日期去重)
stats.setTotalWorkDays(BigDecimal.valueOf(reports.size()));
stats.setValidWorkDays(BigDecimal.valueOf(reports.size()));
// 统计涉及的项目数量
Set<Long> projectIds = reports.stream()
.map(OaProjectReport::getProjectId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
stats.setReportProjectCount(projectIds.size());
return stats;
}
/**
* 统计出差信息
*/
private PersonalReportDTO.BusinessTripStats getBusinessTripStats(Long userId, Date startDate, Date endDate) {
PersonalReportDTO.BusinessTripStats stats = new PersonalReportDTO.BusinessTripStats();
LambdaQueryWrapper<OaProjectReport> wrapper = Wrappers.<OaProjectReport>lambdaQuery()
.eq(OaProjectReport::getDelFlag, 0)
.eq(OaProjectReport::getIsTrip, 1) // 出差标记
.eq(OaProjectReport::getUserId, userId);
// 添加时间范围条件
if (startDate != null && endDate != null) {
wrapper.between(OaProjectReport::getCreateTime, startDate, endDate);
}
// 1. 统计总出差天数(按记录数统计,假设一条记录对应一天)
Long totalTripCount = projectReportMapper.selectCount(wrapper);
stats.setTotalTripDays(BigDecimal.valueOf(totalTripCount));
// 国外
LambdaQueryWrapper<OaProjectReport> foreignWrapper = Wrappers.<OaProjectReport>lambdaQuery()
.eq(OaProjectReport::getDelFlag, 0)
.eq(OaProjectReport::getIsTrip, 1)
.eq(OaProjectReport::getUserId, userId)
.eq(OaProjectReport::getWorkType, 1); // 国外标志
if (StringUtils.isNull(startDate) && StringUtils.isNull(endDate)) {
foreignWrapper.between(OaProjectReport::getCreateTime, startDate, endDate);
}
Long foreignTripCount = projectReportMapper.selectCount(foreignWrapper);
stats.setForeignTripDays(BigDecimal.valueOf(foreignTripCount));
// 3. 国内出差天数 = 总出差天数 - 国外出差天数
stats.setDomesticTripDays(BigDecimal.valueOf(totalTripCount - foreignTripCount));
// 统计出差项目数量
Set<Long> tripProjectIds = projectReportMapper.selectObjs(wrapper
.select(OaProjectReport::getProjectId)) // 只查询project_id字段
.stream()
.filter(Objects::nonNull)
.map(obj -> (Long) obj)
.collect(Collectors.toSet());
stats.setTripProjectCount(tripProjectIds.size());
return stats;
}
/**
* 统计项目信息
*/
private PersonalReportDTO.ProjectStats getProjectStats(Long userId, String nickName, Date startDate, Date endDate) {
PersonalReportDTO.ProjectStats stats = new PersonalReportDTO.ProjectStats();
// 通过报工记录查询参与的项目
LambdaQueryWrapper<OaProjectReport> reportWrapper = Wrappers.<OaProjectReport>lambdaQuery()
.eq(OaProjectReport::getUserId, userId)
.eq(OaProjectReport::getDelFlag, 0);
// if (startDate != null && endDate != null) {
// reportWrapper.between(OaProjectReport::getCreateTime, startDate, endDate);
// }
List<OaProjectReport> reports = projectReportMapper.selectList(reportWrapper);
Set<Long> participatedProjectIds = reports.stream()
.map(OaProjectReport::getProjectId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
// 查询负责的项目
LambdaQueryWrapper<SysOaProject> projectWrapper = Wrappers.<SysOaProject>lambdaQuery()
.eq(SysOaProject::getFunctionary, nickName);
List<SysOaProject> responsibleProjectList = projectMapper.selectList(projectWrapper);
stats.setTotalProjects(participatedProjectIds.size());
stats.setResponsibleProjects(responsibleProjectList.size());
stats.setParticipatedProjects(participatedProjectIds.size());
// 构建项目清单
List<PersonalReportDTO.ProjectSummary> projectList = new ArrayList<>();
// 添加负责的项目
for (SysOaProject project : responsibleProjectList) {
PersonalReportDTO.ProjectSummary summary = new PersonalReportDTO.ProjectSummary();
summary.setProjectId(project.getProjectId());
summary.setProjectName(project.getProjectName());
summary.setProjectNum(project.getProjectNum());
summary.setRole("负责人");
// 计算参与天数
//TODO 项目负责人是不报工的
long participationDays = reports.stream()
.filter(report -> project.getProjectId().equals(report.getProjectId()))
.count();
summary.setParticipationDays(BigDecimal.valueOf(participationDays));
projectList.add(summary);
}
// 添加参与的其他项目
for (Long projectId : participatedProjectIds) {
if (responsibleProjectList.stream().noneMatch(p -> p.getProjectId().equals(projectId))) {
SysOaProject project = projectMapper.selectById(projectId);
if (project != null) {
PersonalReportDTO.ProjectSummary summary = new PersonalReportDTO.ProjectSummary();
summary.setProjectId(project.getProjectId());
summary.setProjectName(project.getProjectName());
summary.setProjectNum(project.getProjectNum());
summary.setRole("参与者");
// 计算参与天数
long participationDays = reports.stream()
.filter(report -> projectId.equals(report.getProjectId()))
.count();
summary.setParticipationDays(BigDecimal.valueOf(participationDays));
projectList.add(summary);
}
}
}
stats.setProjectList(projectList);
return stats;
}
/**
* 统计任务信息
*/
private PersonalReportDTO.TaskStats getTaskStats(Long userId, Date startDate, Date endDate) {
PersonalReportDTO.TaskStats stats = new PersonalReportDTO.TaskStats();
// 查询发放的任务(创建的任务)
LambdaQueryWrapper<SysOaTask> assignedWrapper = Wrappers.<SysOaTask>lambdaQuery()
.eq(SysOaTask::getCreateUserId, userId);
if (startDate != null && endDate != null) {
assignedWrapper.between(SysOaTask::getBeginTime, startDate, endDate);
}
List<SysOaTask> assignedTasks = taskMapper.selectList(assignedWrapper);
// 查询承担的任务(被分配的任务)
LambdaQueryWrapper<SysOaTask> undertakenWrapper = Wrappers.<SysOaTask>lambdaQuery()
.eq(SysOaTask::getWorkerId, userId);
if (startDate != null && endDate != null) {
undertakenWrapper.between(SysOaTask::getBeginTime, startDate, endDate);
}
List<SysOaTask> undertakenTasks = taskMapper.selectList(undertakenWrapper);
// 统计数量
stats.setAssignedTasks(assignedTasks.size());
stats.setUndertakenTasks(undertakenTasks.size());
// 统计已完成任务数量(状态为完成)
long completedAssigned = assignedTasks.stream().filter(task -> Long.valueOf(2).equals(task.getState())).count();
long completedUndertaken = undertakenTasks.stream().filter(task -> Long.valueOf(2).equals(task.getState())).count();
stats.setCompletedTasks((int) (completedAssigned + completedUndertaken));
// 统计进行中任务数量(状态为 1 等待审核)
long underwayAssigned = assignedTasks.stream().filter(task -> Long.valueOf(1).equals(task.getState())).count();
long underwayUndertaken = undertakenTasks.stream().filter(task -> Long.valueOf(1).equals(task.getState())).count();
stats.setUnderwayTasks((int) (underwayAssigned + underwayUndertaken));
//未完成的数量
long unfinishedAssigned = assignedTasks.stream().filter(task -> Long.valueOf(0).equals(task.getState())).count();
long unfinishedUndertaken = undertakenTasks.stream().filter(task -> Long.valueOf(0).equals(task.getState())).count();
stats.setUnfinishedTasks((int) (unfinishedAssigned + unfinishedUndertaken));
// 统计延期任务数量
long delayedAssigned = assignedTasks.stream().filter(task -> Long.valueOf(15).equals(task.getState())).count();
long delayedUndertaken = undertakenTasks.stream().filter(task -> Long.valueOf(15).equals(task.getState())).count();
stats.setDelayedTasks((int) (delayedAssigned + delayedUndertaken));
// 构建任务清单
List<PersonalReportDTO.TaskSummary> assignedTaskList = assignedTasks.stream()
.map(this::buildTaskSummary)
.collect(Collectors.toList());
List<PersonalReportDTO.TaskSummary> undertakenTaskList = undertakenTasks.stream()
.map(this::buildTaskSummary)
.collect(Collectors.toList());
stats.setAssignedTaskList(assignedTaskList);
stats.setUndertakenTaskList(undertakenTaskList);
return stats;
}
/**
* 构建任务摘要
*/
private PersonalReportDTO.TaskSummary buildTaskSummary(SysOaTask task) {
PersonalReportDTO.TaskSummary summary = new PersonalReportDTO.TaskSummary();
summary.setTaskId(task.getTaskId());
summary.setTaskTitle(task.getTaskTitle());
summary.setState(task.getState());
summary.setIsDelayed(task.getPostponements() != null && task.getPostponements() > 0);
summary.setPostponements(task.getPostponements());
// 获取项目名称
if (task.getProjectId() != null) {
SysOaProject project = projectMapper.selectById(task.getProjectId());
if (project != null) {
summary.setProjectName(project.getProjectName());
}
}
return summary;
}
/**
* 统计工程异常信息基于oa_express_question表
*/
private PersonalReportDTO.EngineeringExceptionStats getExceptionStats(String nickName, Date startDate, Date endDate) {
PersonalReportDTO.EngineeringExceptionStats stats = new PersonalReportDTO.EngineeringExceptionStats();
// 查询快递问题表,通过汇报人匹配
LambdaQueryWrapper<OaExpressQuestion> wrapper = Wrappers.<OaExpressQuestion>lambdaQuery()
.eq(OaExpressQuestion::getReportBy, nickName)
.eq(OaExpressQuestion::getDelFlag, 0);
// 添加时间范围条件(基于汇报时间)
if (startDate != null && endDate != null) {
wrapper.between(OaExpressQuestion::getReportTime, startDate, endDate);
}
List<OaExpressQuestion> questions = expressQuestionMapper.selectList(wrapper);
stats.setTotalExceptions(questions.size());
// 统计已解决和未解决的异常
long resolvedCount = questions.stream().filter(q -> Long.valueOf(1).equals(q.getStatus())).count();
long unresolvedCount = questions.stream().filter(q -> Long.valueOf(0).equals(q.getStatus())).count();
stats.setResolvedExceptions((int) resolvedCount);
stats.setDelayedExceptions((int) unresolvedCount); // 未解决的视为延期异常
// 构建异常清单
List<PersonalReportDTO.ExceptionSummary> exceptionList = questions.stream()
.map(question -> {
PersonalReportDTO.ExceptionSummary summary = new PersonalReportDTO.ExceptionSummary();
summary.setExceptionId(question.getQuestionId());
summary.setDescription(question.getDescription());
summary.setStatus(Long.valueOf(1).equals(question.getStatus()) ? "已解决" : "未解决");
summary.setIsDelayed(Long.valueOf(0).equals(question.getStatus())); // 未解决的视为延期
// 这里可以根据expressId关联获取项目信息
summary.setProjectName(oaExpressService.queryById(question.getExpressId()).getProjectName());
return summary;
})
.collect(Collectors.toList());
stats.setExceptionList(exceptionList);
return stats;
}
/**
* 统计关键采购任务信息基于oa_requirements表
*/
private PersonalReportDTO.ProcurementTaskStats getProcurementStats(Long userId, Date startDate, Date endDate) {
PersonalReportDTO.ProcurementTaskStats stats = new PersonalReportDTO.ProcurementTaskStats();
// 查询需求表通过负责人ID匹配
LambdaQueryWrapper<OaRequirements> wrapper = Wrappers.<OaRequirements>lambdaQuery()
.eq(OaRequirements::getOwnerId, userId)
.eq(OaRequirements::getDelFlag, 0);
// 添加时间范围条件
if (startDate != null && endDate != null) {
wrapper.between(OaRequirements::getCreateTime, startDate, endDate);
}
List<OaRequirements> requirements = requirementsMapper.selectList(wrapper);
stats.setTotalProcurementTasks(requirements.size());
// 统计已完成和延期的需求
long completedCount = requirements.stream().filter(req -> Integer.valueOf(1).equals(req.getStatus())).count();
stats.setCompletedProcurementTasks((int) completedCount);
// 计算延期需求(截止日期已过但未完成的)
long delayedCount = requirements.stream()
.filter(req -> Integer.valueOf(0).equals(req.getStatus())) // 未完成
.filter(req -> req.getDeadline() != null && req.getDeadline().before(new Date())) // 已过期
.count();
stats.setDelayedProcurementTasks((int) delayedCount);
// 构建采购任务清单
List<PersonalReportDTO.ProcurementTaskSummary> procurementList = requirements.stream()
.map(req -> {
PersonalReportDTO.ProcurementTaskSummary summary = new PersonalReportDTO.ProcurementTaskSummary();
summary.setProcurementId(req.getRequirementId());
summary.setProcurementContent(req.getTitle());
summary.setStatus(Integer.valueOf(1).equals(req.getStatus()) ? "已完成" : "进行中");
// 判断是否延期
boolean isDelayed = Integer.valueOf(0).equals(req.getStatus()) &&
req.getDeadline() != null && req.getDeadline().before(new Date());
summary.setIsDelayed(isDelayed);
// 获取项目名称
if (req.getProjectId() != null) {
SysOaProject project = projectMapper.selectById(req.getProjectId());
if (project != null) {
summary.setProjectName(project.getProjectName());
}
} else {
summary.setProjectName("无关联项目");
}
return summary;
})
.collect(Collectors.toList());
stats.setProcurementTaskList(procurementList);
return stats;
}
// /**
// * 统计进度信息
// */
// private PersonalReportDTO.ProgressStats getProgressStats(String nickName, String startDate, String endDate) {
// PersonalReportDTO.ProgressStats stats = new PersonalReportDTO.ProgressStats();
//
// // 查询用户负责的进度
// LambdaQueryWrapper<OaProjectScheduleStep> wrapper = Wrappers.<OaProjectScheduleStep>lambdaQuery()
// .eq(OaProjectScheduleStep::getNodeHeader, nickName)
// .eq(OaProjectScheduleStep::getDelFlag, "0");
//
// if (startDate != null && endDate != null) {
// wrapper.between(OaProjectScheduleStep::getCreateTime, startDate, endDate);
// }
//
// List<OaProjectScheduleStep> steps = baseMapper.selectList(wrapper);
//
// stats.setTotalProgress(steps.size());
//
// // 统计各状态的进度数量
// long completedCount = steps.stream().filter(step -> Long.valueOf(2).equals(step.getStatus())).count();
// long inProgressCount = steps.stream().filter(step -> Long.valueOf(1).equals(step.getStatus())).count();
//
// // 计算延期进度(实际结束时间晚于计划结束时间)
// long delayedCount = steps.stream()
// .filter(step -> step.getActualEnd() != null && step.getPlanEnd() != null)
// .filter(step -> step.getActualEnd().after(step.getPlanEnd()))
// .count();
//
// stats.setCompletedProgress((int) completedCount);
// stats.setInProgressCount((int) inProgressCount);
// stats.setDelayedProgress((int) delayedCount);
//
// // 构建进度清单
// List<PersonalReportDTO.ProgressSummary> progressList = steps.stream()
// .map(step -> {
// PersonalReportDTO.ProgressSummary summary = new PersonalReportDTO.ProgressSummary();
// summary.setTrackId(step.getTrackId());
// summary.setStepName(step.getStepName());
// summary.setStatus(step.getStatus());
//
// // 判断是否延期
// boolean isDelayed = step.getActualEnd() != null && step.getPlanEnd() != null &&
// step.getActualEnd().after(step.getPlanEnd());
// summary.setIsDelayed(isDelayed);
//
// // 获取项目名称
// if (step.getScheduleId() != null) {
// OaProjectSchedule schedule = scheduleMapper.selectById(step.getScheduleId());
// if (schedule != null && schedule.getProjectId() != null) {
// SysOaProject project = projectMapper.selectById(schedule.getProjectId());
// if (project != null) {
// summary.setProjectName(project.getProjectName());
// }
// }
// }
//
// return summary;
// })
// .collect(Collectors.toList());
//
// stats.setProgressList(progressList);
// return stats;
// }
}

View File

@@ -0,0 +1,141 @@
package com.ruoyi.oa.service.impl;
import cn.hutool.core.bean.BeanUtil;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.core.domain.PageQuery;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import com.ruoyi.oa.domain.bo.OaUserActiveBo;
import com.ruoyi.oa.domain.vo.OaUserActiveVo;
import com.ruoyi.oa.domain.OaUserActive;
import com.ruoyi.oa.mapper.OaUserActiveMapper;
import com.ruoyi.oa.service.IOaUserActiveService;
import java.util.List;
import java.util.Map;
import java.util.Collection;
import java.time.LocalDate;
import java.sql.Date;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
/**
* 用户活跃统计Service业务层处理
*
* @author ruoyi
* @date 2025-10-30
*/
@RequiredArgsConstructor
@Service
public class OaUserActiveServiceImpl implements IOaUserActiveService {
private final OaUserActiveMapper baseMapper;
/**
* 查询用户活跃统计
*/
@Override
public OaUserActiveVo queryById(Long activeId){
return baseMapper.selectVoById(activeId);
}
/**
* 查询用户活跃统计列表
*/
@Override
public TableDataInfo<OaUserActiveVo> queryPageList(OaUserActiveBo bo, PageQuery pageQuery) {
LambdaQueryWrapper<OaUserActive> lqw = buildQueryWrapper(bo);
Page<OaUserActiveVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
return TableDataInfo.build(result);
}
/**
* 查询用户活跃统计列表
*/
@Override
public List<OaUserActiveVo> queryList(OaUserActiveBo bo) {
LambdaQueryWrapper<OaUserActive> lqw = buildQueryWrapper(bo);
return baseMapper.selectVoList(lqw);
}
private LambdaQueryWrapper<OaUserActive> buildQueryWrapper(OaUserActiveBo bo) {
Map<String, Object> params = bo.getParams();
LambdaQueryWrapper<OaUserActive> lqw = Wrappers.lambdaQuery();
lqw.eq(bo.getUserId() != null, OaUserActive::getUserId, bo.getUserId());
lqw.eq(bo.getActiveDate() != null, OaUserActive::getActiveDate, bo.getActiveDate());
lqw.eq(bo.getLoginCount() != null, OaUserActive::getLoginCount, bo.getLoginCount());
lqw.orderByDesc(OaUserActive::getCreateTime);
return lqw;
}
/**
* 新增用户活跃统计
*/
@Override
public Boolean insertByBo(OaUserActiveBo bo) {
OaUserActive add = BeanUtil.toBean(bo, OaUserActive.class);
validEntityBeforeSave(add);
boolean flag = baseMapper.insert(add) > 0;
if (flag) {
bo.setActiveId(add.getActiveId());
}
return flag;
}
/**
* 修改用户活跃统计
*/
@Override
public Boolean updateByBo(OaUserActiveBo bo) {
OaUserActive update = BeanUtil.toBean(bo, OaUserActive.class);
validEntityBeforeSave(update);
return baseMapper.updateById(update) > 0;
}
/**
* 保存前的数据校验
*/
private void validEntityBeforeSave(OaUserActive entity){
//TODO 做一些数据校验,如唯一约束
}
/**
* 批量删除用户活跃统计
*/
@Override
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
if(isValid){
//TODO 做一些业务上的校验,判断是否需要校验
}
return baseMapper.deleteBatchIds(ids) > 0;
}
@Override
public boolean hasActiveToday(Long userId) {
Date today = Date.valueOf(LocalDate.now());
LambdaQueryWrapper<OaUserActive> lqw = Wrappers.lambdaQuery();
lqw.eq(OaUserActive::getUserId, userId)
.eq(OaUserActive::getActiveDate, today);
return baseMapper.selectCount(lqw) > 0;
}
@Override
public void recordTodayLogin(Long userId) {
Date today = Date.valueOf(LocalDate.now());
LambdaUpdateWrapper<OaUserActive> uw = Wrappers.lambdaUpdate();
uw.eq(OaUserActive::getUserId, userId)
.eq(OaUserActive::getActiveDate, today)
.setSql("login_count = login_count + 1");
int rows = baseMapper.update(null, uw);
if (rows == 0) {
OaUserActive entity = new OaUserActive();
entity.setUserId(userId);
entity.setActiveDate(today);
entity.setLoginCount(1L);
baseMapper.insert(entity);
}
}
}

View File

@@ -24,6 +24,8 @@ import com.ruoyi.oa.domain.vo.SysOaTaskVo;
import com.ruoyi.oa.mapper.SysOaTaskMapper;
import com.ruoyi.oa.service.ISysOaTaskService;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*;
@@ -323,6 +325,7 @@ public class SysOaTaskServiceImpl implements ISysOaTaskService {
return Collections.emptyList();
}
// 增加任务开始时间在奖金池时间范围内的筛选
return baseMapper.selectList(Wrappers.<SysOaTask>lambdaQuery()
.eq(SysOaTask::getWorkerId, user.getUserId())

View File

@@ -0,0 +1,21 @@
<?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.OaUserActiveMapper">
<resultMap type="com.ruoyi.oa.domain.OaUserActive" id="OaUserActiveResult">
<result property="activeId" column="active_id"/>
<result property="userId" column="user_id"/>
<result property="activeDate" column="active_date"/>
<result property="loginCount" column="login_count"/>
<result property="remark" column="remark"/>
<result property="createBy" column="create_by"/>
<result property="createTime" column="create_time"/>
<result property="updateBy" column="update_by"/>
<result property="updateTime" column="update_time"/>
<result property="delFlag" column="del_flag"/>
</resultMap>
</mapper>