feat(oa): 新增奖金池分配与个人报表功能

- 在 IOaProjectScheduleStepService 接口中新增 queryProgressByHeader 和 personalReport 方法- OaBonusAllocationController 中添加注释说明新增接口用于传入 nickname 和 allocation_id
- OaBonusAllocationMapper 添加 List 引用并更新 XML 配置文件
- OaBonusPoolServiceImpl 实现批量添加奖金池逻辑,包括项目和进度负责人的收集、去重及分配记录插入
- OaProjectScheduleStepController 新增 /personal 接口以获取个人报表信息Project
- OaScheduleStepServiceImpl 实现 queryProgressByHeader 和 personalReport 方法,支持进度统计和个人报表数据查询
- SysOaProject 类新增 delFlag 字段
- SysOaWarehouseMasterServiceImpl 的 updateRemark 方法优化参数校验和更新逻辑,并增加日志记录
This commit is contained in:
2025-10-24 14:55:44 +08:00
parent 40e56ab051
commit 3dbe097772
9 changed files with 336 additions and 32 deletions

View File

@@ -98,4 +98,6 @@ public class OaBonusAllocationController extends BaseController {
@PathVariable Long[] allocationIds) {
return toAjax(iOaBonusAllocationService.deleteWithValidByIds(Arrays.asList(allocationIds), true));
}
//新增一个接口用来传入nickname和allocation_id
}

View File

@@ -2,12 +2,16 @@ package com.ruoyi.oa.controller;
import java.util.List;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import com.ruoyi.common.core.AjaxResult;
import com.ruoyi.oa.domain.bo.BatchBo;
import lombok.RequiredArgsConstructor;
import javax.servlet.http.HttpServletResponse;
import javax.validation.constraints.*;
import cn.dev33.satoken.annotation.SaCheckPermission;
import org.springframework.web.bind.annotation.*;
import org.springframework.validation.annotation.Validated;
@@ -47,6 +51,7 @@ public class OaProjectScheduleStepController extends BaseController {
public TableDataInfo<OaProjectScheduleStepVo> list(OaProjectScheduleStepBo bo, PageQuery pageQuery) {
return iOaProjectScheduleStepService.queryPageList(bo, pageQuery);
}
/**
* 查询项目进度步骤跟踪列表
*/
@@ -114,4 +119,19 @@ public class OaProjectScheduleStepController extends BaseController {
@PathVariable Long[] trackIds) {
return toAjax(iOaProjectScheduleStepService.deleteWithValidByIds(Arrays.asList(trackIds), true));
}
/**
* 获取个人报表信息
*
* @param nickName 用户名(昵称)
* @param poolId 奖金池ID
* @return 报表数据
*/
@GetMapping("/personal")
public R<Map<String, Object>> getPersonalReport(
@RequestParam Long poolId,
@RequestParam String nickName
) {
return R.ok(iOaProjectScheduleStepService.personalReport(poolId,nickName));
}
}

View File

@@ -167,4 +167,6 @@ public class SysOaProject extends BaseEntity {
//是否置顶
private Integer isTop;
//是否删除
private Integer delFlag;
}

View File

@@ -4,6 +4,8 @@ import com.ruoyi.oa.domain.OaBonusAllocation;
import com.ruoyi.oa.domain.vo.OaBonusAllocationVo;
import com.ruoyi.common.core.mapper.BaseMapperPlus;
import java.util.List;
/**
* 奖金池分配记录奖金池向用户的分配比例及金额Mapper接口
*
@@ -12,4 +14,5 @@ import com.ruoyi.common.core.mapper.BaseMapperPlus;
*/
public interface OaBonusAllocationMapper extends BaseMapperPlus<OaBonusAllocationMapper, OaBonusAllocation, OaBonusAllocationVo> {
}

View File

@@ -11,6 +11,7 @@ import com.ruoyi.common.core.domain.PageQuery;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* 项目进度步骤跟踪Service接口
@@ -66,4 +67,8 @@ public interface IOaProjectScheduleStepService{
void batchInsertNodes(List<NodeDTO> nodeList, Long scheduleId);
TableDataInfo<OaProjectScheduleStepVo> queryPageListPage(OaProjectScheduleStepBo bo, PageQuery pageQuery);
List<Map<String, Object>> queryProgressByHeader();
Map<String, Object> personalReport(Long poolId, String nickName);
}

View File

@@ -2,28 +2,30 @@ package com.ruoyi.oa.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.esotericsoftware.minlog.Log;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.helper.LoginHelper;
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 com.ruoyi.oa.domain.OaBonusProjectRel;
import com.ruoyi.oa.domain.*;
import com.ruoyi.oa.domain.bo.OaBonusProjectRelBo;
import com.ruoyi.oa.mapper.OaBonusProjectRelMapper;
import com.ruoyi.oa.mapper.*;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ruoyi.oa.domain.bo.OaBonusPoolBo;
import com.ruoyi.oa.domain.vo.OaBonusPoolVo;
import com.ruoyi.oa.domain.OaBonusPool;
import com.ruoyi.oa.mapper.OaBonusPoolMapper;
import com.ruoyi.oa.service.IOaBonusPoolService;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Map;
import java.util.Collection;
import java.math.BigDecimal;
import java.util.*;
import java.util.stream.Collectors;
/**
* 奖金池Service业务层处理
@@ -40,11 +42,21 @@ public class OaBonusPoolServiceImpl implements IOaBonusPoolService {
@Autowired
private OaBonusProjectRelMapper bonusProjectRelMapper;
private final OaProjectScheduleMapper projectScheduleMapper;
private final SysOaProjectMapper projectMapper;
@Autowired
private OaProjectScheduleStepMapper projectScheduleStepMapper;
@Autowired
private OaBonusAllocationMapper bonusAllocationMapper;
/**
* 查询奖金池
*/
@Override
public OaBonusPoolVo queryById(Long poolId){
public OaBonusPoolVo queryById(Long poolId) {
return baseMapper.selectVoById(poolId);
}
@@ -103,7 +115,7 @@ public class OaBonusPoolServiceImpl implements IOaBonusPoolService {
/**
* 保存前的数据校验
*/
private void validEntityBeforeSave(OaBonusPool entity){
private void validEntityBeforeSave(OaBonusPool entity) {
//TODO 做一些数据校验,如唯一约束
}
@@ -112,41 +124,124 @@ public class OaBonusPoolServiceImpl implements IOaBonusPoolService {
*/
@Override
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
if(isValid){
if (isValid) {
//TODO 做一些业务上的校验,判断是否需要校验
}
return baseMapper.deleteBatchIds(ids) > 0;
}
//现在需要知道项目id查询对应的项目负责人然后在进度里面查找对应项目的进度负责人
//总金额是绑定项目的时候填每个项目对应的金额 填入到奖金池与项目的绑定表中的分配给该项目的金额 然后统计这个金额字段
//所以此处是一个项目负责人和n个进度负责人 然后绑定奖金池id和进度负责人的nickname和项目负责人的nickname 作为分配比例的依据
//看他完成的任务情况也就是status的值 查项目负责人负责了多少项目,进度负责人负责了多少进度 进度完成的咋样 是否有延期或者未完成的 是多少
//然后就是每个项目负责人和进度负责人的分配比例手填 填他们的金额分配比例 拿到对应的金额
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean batchAdd(OaBonusPoolBo bo) {
// 插入主表信息
// 插入奖金池主表
OaBonusPool pool = BeanUtil.toBean(bo, OaBonusPool.class);
validEntityBeforeSave(pool);
boolean flag = baseMapper.insert(pool) > 0;
if (!flag) {
Log.warn("奖金池主表插入失败");
return false;
}
// 获取插入后的主键
Long poolId = pool.getPoolId();
//批量插入奖金池id和项目id属于一对多的关系
// 校验项目ID列表
if (CollUtil.isEmpty(bo.getProjectIds())) {
Log.warn("批量添加奖金池失败项目ID列表为空");
return false;
}
boolean success = true;
// 收集所有负责人昵称(项目负责人 + 进度负责人通过Set自动去重
Set<String> allNickNames = new HashSet<>();
// 获取项目负责人昵称sys_oa_project.functionary
List<SysOaProject> projects = projectMapper.selectList(
Wrappers.<SysOaProject>lambdaQuery()
.in(SysOaProject::getProjectId, bo.getProjectIds())
.select(SysOaProject::getFunctionary)
);
if (CollUtil.isEmpty(projects)) {
Log.warn("批量添加奖金池失败未找到项目ID对应的项目记录");
return false;
}
projects.stream()
.map(SysOaProject::getFunctionary)
.filter(StrUtil::isNotBlank) // 过滤空值
.forEach(allNickNames::add); // 添加到Set自动去重
// 2.2 通过项目ID查询进度主表获取schedule_id
List<OaProjectSchedule> schedules = projectScheduleMapper.selectList(
Wrappers.<OaProjectSchedule>lambdaQuery()
.in(OaProjectSchedule::getProjectId, bo.getProjectIds())
.eq(OaProjectSchedule::getDelFlag, "0")
.select(OaProjectSchedule::getScheduleId)
);
//收集进度负责人oa_project_schedule_step.header
if (CollUtil.isEmpty(schedules)) {
Log.warn("项目ID对应的进度主表记录为空仅收集项目负责人");
} else {
List<Long> scheduleIds = schedules.stream()
.map(OaProjectSchedule::getScheduleId)
.collect(Collectors.toList());
List<OaProjectScheduleStep> scheduleSteps = projectScheduleStepMapper.selectList(
Wrappers.<OaProjectScheduleStep>lambdaQuery()
.in(OaProjectScheduleStep::getScheduleId, scheduleIds)
.eq(OaProjectScheduleStep::getDelFlag, "0")
.select(OaProjectScheduleStep::getHeader)
);
scheduleSteps.stream()
.map(OaProjectScheduleStep::getHeader)
.filter(StrUtil::isNotBlank) // 过滤空值
.forEach(allNickNames::add); // 添加到Set自动去重
}
// 校验去重后的昵称集合
if (allNickNames.isEmpty()) {
Log.warn("批量添加奖金池失败:未找到任何有效负责人昵称");
return false;
}
// 拼接所有昵称(逗号分隔)
String mergedNickNames = String.join(",", allNickNames);
// 插入奖金池分配表(单条记录)
OaBonusAllocation allocation = new OaBonusAllocation();
allocation.setPoolId(poolId);
allocation.setNickName(mergedNickNames);
allocation.setAllocationRatio(new BigDecimal("0.00"));
allocation.setCreateBy(LoginHelper.getUsername());
allocation.setCreateTime(new Date());
allocation.setDelFlag("0");
int insertResult = bonusAllocationMapper.insert(allocation);
if (insertResult <= 0) {
Log.error("插入奖金池分配记录失败poolId={}");
throw new RuntimeException("分配记录插入失败,触发回滚");
}
// 插入奖金池与项目关联表
for (Long projectId : bo.getProjectIds()) {
OaBonusProjectRel rel = new OaBonusProjectRel();
rel.setPoolId(poolId);
rel.setProjectId(projectId);
rel.setCreateBy(LoginHelper.getUsername());
rel.setCreateTime(new Date());
rel.setDelFlag("0");
int result = bonusProjectRelMapper.insert(rel);
if (result <= 0) {
success = false;
break;
Log.error("插入奖金池-项目关联记录失败projectId={}");
throw new RuntimeException("关联记录插入失败,触发回滚");
}
}
return success;
Log.info("奖金池批量创建成功poolId={},负责人:{}");
return true;
}
}

View File

@@ -1,9 +1,11 @@
package com.ruoyi.oa.service.impl;
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;
@@ -11,10 +13,15 @@ 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.toolkit.Wrappers;
import com.ruoyi.oa.domain.OaBonusProjectRel;
import com.ruoyi.oa.domain.OaProjectSchedule;
import com.ruoyi.oa.domain.SysOaProject;
import com.ruoyi.oa.domain.bo.BatchBo;
import com.ruoyi.oa.domain.dto.NodeDTO;
import com.ruoyi.oa.mapper.OaBonusProjectRelMapper;
import com.ruoyi.oa.mapper.OaProjectScheduleMapper;
import com.ruoyi.oa.mapper.SysOaProjectMapper;
import com.ruoyi.system.mapper.SysUserMapper;
import kotlin.jvm.internal.Lambda;
import lombok.RequiredArgsConstructor;
import org.flowable.job.service.impl.ServiceImpl;
@@ -44,6 +51,18 @@ public class OaProjectScheduleStepServiceImpl implements IOaProjectScheduleStepS
private final OaProjectScheduleMapper scheduleMapper;
private final SysUserMapper sysUserMapper;
private final OaBonusProjectRelMapper bonusProjectRelMapper;
private final SysOaProjectMapper projectMapper;
private final OaProjectScheduleMapper projectScheduleMapper;
private final OaProjectScheduleStepMapper projectScheduleStepMapper;
/**
* 查询项目进度步骤跟踪
*/
@@ -264,6 +283,147 @@ public class OaProjectScheduleStepServiceImpl implements IOaProjectScheduleStepS
// 批量插入数据库
baseMapper.saveBatch(entities);
}
//查询进度负责人负责了多少进度以及完成度是多少0进行中 1待验收 2已完成 是否延期用比较end_time和original_end_time的大小
//如果前者大于后者则表示延期
@Override
public List<Map<String, Object>> queryProgressByHeader() {
// 构建查询条件:只查询未删除且正在使用的记录
LambdaQueryWrapper<OaProjectScheduleStep> wrapper = Wrappers.lambdaQuery();
wrapper.eq(OaProjectScheduleStep::getDelFlag, 0)
.eq(OaProjectScheduleStep::getUseFlag, 1);
// 查询所有相关记录
List<OaProjectScheduleStep> steps = baseMapper.selectList(wrapper);
// 按负责人(header)分组统计
Map<String, List<OaProjectScheduleStep>> headerGrouped = steps.stream()
.filter(step -> step.getHeader() != null && !step.getHeader().isEmpty())
.collect(Collectors.groupingBy(OaProjectScheduleStep::getHeader));
// 构造返回结果
List<Map<String, Object>> result = new ArrayList<>();
for (Map.Entry<String, List<OaProjectScheduleStep>> entry : headerGrouped.entrySet()) {
String header = entry.getKey();
List<OaProjectScheduleStep> headerSteps = entry.getValue();
long total = headerSteps.size(); // 总进度数
long completed = headerSteps.stream()
.mapToLong(step -> step.getStatus() == null ? 0L : step.getStatus())
.filter(status -> status == 2) // 状态为"已完成"
.count();
// 对待验收状态的统计
long pendingAcceptance = headerSteps.stream()
.mapToLong(step -> step.getStatus() == null ? 0L : step.getStatus())
.filter(status -> status == 1) // 状态为"待验收"
.count();
long delayed = headerSteps.stream()
.filter(step -> step.getEndTime() != null && step.getOriginalEndTime() != null)
.filter(step -> step.getEndTime().isAfter(step.getOriginalEndTime()))
.count();
Map<String, Object> item = new HashMap<>();
item.put("header", header);
item.put("total", total);
item.put("completed", completed);
item.put("pendingAcceptance", pendingAcceptance);
item.put("delayed", delayed);
result.add(item);
}
return result;
}
/**
* 个人报表接口根据昵称和奖金池ID查询个人信息、负责项目及进度统计
* @param nickName 用户名(昵称)
* @param poolId 奖金池ID
* @return 包含个人信息、负责项目、进度统计的结果
*/
@Override
public Map<String, Object> personalReport(Long poolId,String nickName) {
Map<String, Object> result = new HashMap<>();
//查询用户基本信息sys_user表
SysUser user = sysUserMapper.selectOne(
Wrappers.<SysUser>lambdaQuery()
.eq(SysUser::getNickName, nickName)
.eq(SysUser::getDelFlag, "0") // 未删除
);
if (user == null) {
throw new RuntimeException("未找到用户:" + nickName);
}
result.put("userInfo", user); // 个人基本信息(可按需筛选字段)
// 根据奖金池ID查询关联的项目IDoa_bonus_project_rel表
List<OaBonusProjectRel> rels = bonusProjectRelMapper.selectList(
Wrappers.<OaBonusProjectRel>lambdaQuery()
.eq(OaBonusProjectRel::getPoolId, poolId)
.eq(OaBonusProjectRel::getDelFlag, "0")
);
if (CollUtil.isEmpty(rels)) {
result.put("responsibleProjects", Collections.emptyList());
result.put("progressStats", Collections.emptyMap());
return result;
}
List<Long> projectIds = rels.stream()
.map(OaBonusProjectRel::getProjectId)
.collect(Collectors.toList());
// 筛选用户负责的项目sys_oa_project表functionary = nickName
List<SysOaProject> responsibleProjects = projectMapper.selectList(
Wrappers.<SysOaProject>lambdaQuery()
.in(SysOaProject::getProjectId, projectIds)
.eq(SysOaProject::getFunctionary, nickName) // 项目负责人匹配
.eq(SysOaProject::getDelFlag, "0")
);
result.put("responsibleProjects", responsibleProjects); // 负责的项目列表
// 查询用户负责的进度及统计(复用进度统计逻辑,限定关联项目)
// 先查询关联项目的进度主表IDoa_project_schedule
List<OaProjectSchedule> schedules = projectScheduleMapper.selectList(
Wrappers.<OaProjectSchedule>lambdaQuery()
.in(OaProjectSchedule::getProjectId, projectIds)
.eq(OaProjectSchedule::getDelFlag, "0")
);
if (CollUtil.isEmpty(schedules)) {
result.put("progressStats", Collections.emptyMap());
return result;
}
List<Long> scheduleIds = schedules.stream()
.map(OaProjectSchedule::getScheduleId)
.collect(Collectors.toList());
//查询这些进度下的步骤,且负责人是当前用户
LambdaQueryWrapper<OaProjectScheduleStep> stepWrapper = Wrappers.lambdaQuery();
stepWrapper.in(OaProjectScheduleStep::getScheduleId, scheduleIds)
.eq(OaProjectScheduleStep::getHeader, nickName) // 进度负责人匹配
.eq(OaProjectScheduleStep::getDelFlag, "0")
.eq(OaProjectScheduleStep::getUseFlag, 1);
List<OaProjectScheduleStep> userSteps = projectScheduleStepMapper.selectList(stepWrapper);
// 统计进度数据(总数量、完成数、待验收数、延期数)
Map<String, Object> progressStats = new HashMap<>();
long total = userSteps.size();
long completed = userSteps.stream()
.filter(step -> step.getStatus() != null && step.getStatus() == 2) // 已完成
.count();
long pendingAcceptance = userSteps.stream()
.filter(step -> step.getStatus() != null && step.getStatus() == 1) // 待验收
.count();
long delayed = userSteps.stream()
.filter(step -> step.getEndTime() != null && step.getOriginalEndTime() != null)
.filter(step -> step.getEndTime().isAfter(step.getOriginalEndTime())) // 实际结束>原定结束
.count();
progressStats.put("total", total);
progressStats.put("completed", completed);
progressStats.put("pendingAcceptance", pendingAcceptance);
progressStats.put("delayed", delayed);
result.put("progressStats", progressStats);
return result;
}
}

View File

@@ -2,6 +2,8 @@ package com.ruoyi.oa.service.impl;
import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.esotericsoftware.minlog.Log;
import com.ruoyi.common.helper.LoginHelper;
import com.ruoyi.common.utils.BeanCopyUtils;
import com.ruoyi.common.utils.StringUtils;
@@ -319,23 +321,37 @@ public class SysOaWarehouseMasterServiceImpl implements ISysOaWarehouseMasterSer
@Override
public Boolean updateRemark(Long masterId, String remark, Long requirementId, String masterNum) {
if (masterId == null) return false;
if (requirementId == 0) {
return baseMapper.update(null,
Wrappers.<SysOaWarehouseMaster>lambdaUpdate()
.set(SysOaWarehouseMaster::getRequirementId, null)
.eq(SysOaWarehouseMaster::getMasterId, masterId)
) > 0;
} else {
return baseMapper.update(null,
Wrappers.<SysOaWarehouseMaster>lambdaUpdate()
.set(SysOaWarehouseMaster::getRemark, remark)
.set(SysOaWarehouseMaster::getRequirementId, requirementId)
.set(SysOaWarehouseMaster::getMasterNum, masterNum)
.eq(SysOaWarehouseMaster::getMasterId, masterId)
) > 0;
// 1. 核心参数校验,补充日志
if (masterId == null) {
Log.warn("更新仓库备注失败masterId不能为空");
return false;
}
// 2. 构建更新条件,过滤已删除记录
LambdaUpdateWrapper<SysOaWarehouseMaster> updateWrapper = Wrappers.<SysOaWarehouseMaster>lambdaUpdate()
.eq(SysOaWarehouseMaster::getMasterId, masterId)
.eq(SysOaWarehouseMaster::getDelFlag, "0");
// 3. 统一处理需要更新的字段(根据业务场景调整)
updateWrapper.set(SysOaWarehouseMaster::getMasterNum, masterNum)
.set(SysOaWarehouseMaster::getRemark, remark);
// 4. 处理requirementIdnull或0时清除否则设置为传入值
if (requirementId == null || requirementId == 0) {
updateWrapper.set(SysOaWarehouseMaster::getRequirementId, null);
} else {
updateWrapper.set(SysOaWarehouseMaster::getRequirementId, requirementId);
}
// 5. 执行更新并记录日志
int rows = baseMapper.update(null, updateWrapper);
boolean success = rows > 0;
if (success) {
Log.info("更新仓库备注成功masterId={}, requirementId={}");
} else {
Log.warn("更新仓库备注失败未找到有效记录masterId={})或无变更");
}
return success;
}
// 2. 修改returnType

View File

@@ -18,4 +18,5 @@
</resultMap>
</mapper>