薪资分析

This commit is contained in:
2025-06-24 17:20:19 +08:00
parent 9995425f28
commit 3ed61a8a00
17 changed files with 495 additions and 0 deletions

View File

@@ -0,0 +1,31 @@
package com.ruoyi.oa.controller;
import com.ruoyi.common.core.AjaxResult;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.oa.domain.vo.SalaryDashboardVo;
import com.ruoyi.oa.service.ISalaryDashboardService;
import org.springframework.beans.factory.annotation.Autowired;
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;
@RestController
@RequestMapping("/oa/salary/dashboard")
public class SalaryDashboardController extends BaseController {
@Autowired
private ISalaryDashboardService salaryDashboardService;
@GetMapping
public R<SalaryDashboardVo>getDashboardData(
@RequestParam(required = true) Long payYear,
@RequestParam(required = true) Long payMonth
) {
return R.ok(
salaryDashboardService.getDashboardData(payYear, payMonth)
);
}
}

View File

@@ -31,6 +31,10 @@ public class OaEmployee extends BaseEntity {
* 姓名
*/
private String employeeName;
/**
* 部门ID
*/
private Long deptId;
/**
* 公司
*/

View File

@@ -0,0 +1,16 @@
package com.ruoyi.oa.domain.vo;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class CardDataVo {
private BigDecimal totalSalaryExpenditure;
private BigDecimal lastMonthTotalSalaryExpenditureRate;
private BigDecimal avgDepartmentExpenditure;
private BigDecimal lastMonthAvgDepartmentExpenditureRate;
private BigDecimal avgPersonSalary;
private BigDecimal lastMonthAvgPersonSalaryRate;
private BigDecimal yearOnYearGrowthRate;
}

View File

@@ -0,0 +1,13 @@
package com.ruoyi.oa.domain.vo;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class CardMetricsVo {
private BigDecimal totalSalaryExpenditure;
private BigDecimal totalCompanyExpenditure;
private Integer employeeCount;
private Integer departmentCount;
}

View File

@@ -0,0 +1,12 @@
package com.ruoyi.oa.domain.vo;
import lombok.Data;
import java.util.List;
@Data
public class ChartDataVo {
private List<MonthlyExpenditureVo> monthlyExpenditures;
private List<SalaryComponentVo> salaryComponents;
private LineChartVo lineChartData;
}

View File

@@ -0,0 +1,15 @@
package com.ruoyi.oa.domain.vo;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class DepartmentStatVo {
private Long deptId;
private String deptName;
private BigDecimal totalExpenditure;
private BigDecimal avgSalary;
private Integer employeeCount;
private BigDecimal yearOnYearGrowthRate;
}

View File

@@ -0,0 +1,12 @@
package com.ruoyi.oa.domain.vo;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class LineChartPointVo {
private Integer month;
private BigDecimal totalExpenditure;
private BigDecimal avgSalary;
}

View File

@@ -0,0 +1,26 @@
package com.ruoyi.oa.domain.vo;
import lombok.Data;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
@Data
public class LineChartVo {
private List<Integer> months;
private List<BigDecimal> totalExpenditures;
private List<BigDecimal> avgSalaries;
public LineChartVo(List<LineChartPointVo> points) {
this.months = new ArrayList<>();
this.totalExpenditures = new ArrayList<>();
this.avgSalaries = new ArrayList<>();
for (LineChartPointVo point : points) {
this.months.add(point.getMonth());
this.totalExpenditures.add(point.getTotalExpenditure());
this.avgSalaries.add(point.getAvgSalary());
}
}
}

View File

@@ -0,0 +1,11 @@
package com.ruoyi.oa.domain.vo;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class MonthlyExpenditureVo {
private Integer month;
private BigDecimal totalExpenditure;
}

View File

@@ -44,6 +44,16 @@ public class OaEmployeeVo {
*/
@ExcelProperty(value = "备注")
private String remark;
/**
* 部门ID
*/
@ExcelProperty(value = "部门ID")
private Long deptId;
/**
* 部门名称
*/
@ExcelProperty(value = "部门名称")
private String deptName;
}

View File

@@ -0,0 +1,11 @@
package com.ruoyi.oa.domain.vo;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class SalaryComponentVo {
private String itemName;
private BigDecimal totalAmount;
}

View File

@@ -0,0 +1,11 @@
package com.ruoyi.oa.domain.vo;
import com.ruoyi.common.core.page.TableDataInfo;
import lombok.Data;
@Data
public class SalaryDashboardVo {
private CardDataVo cardData;
private ChartDataVo chartData;
private TableDataInfo<DepartmentStatVo> departmentStats;
}

View File

@@ -0,0 +1,21 @@
package com.ruoyi.oa.mapper;
import com.ruoyi.oa.domain.vo.*;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface SalaryDashboardMapper {
CardMetricsVo queryCardMetrics(@Param("year") Long year, @Param("month") Long month);
List<MonthlyExpenditureVo> queryMonthlyExpenditures(@Param("year") Long year);
List<SalaryComponentVo> querySalaryComponents(@Param("year") Long year, @Param("month") Long month);
List<LineChartPointVo> queryLineChartData(@Param("year") Long year);
List<DepartmentStatVo> queryDepartmentStats(@Param("year") Long year, @Param("month") Long month);
}

View File

@@ -0,0 +1,8 @@
package com.ruoyi.oa.service;
import com.ruoyi.common.core.domain.PageQuery;
import com.ruoyi.oa.domain.vo.SalaryDashboardVo;
public interface ISalaryDashboardService {
SalaryDashboardVo getDashboardData(Long payYear, Long payMonth);
}

View File

@@ -1,12 +1,15 @@
package com.ruoyi.oa.service.impl;
import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.ruoyi.common.core.domain.entity.SysDept;
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.system.mapper.SysDeptMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import com.ruoyi.oa.domain.bo.OaEmployeeBo;
@@ -18,6 +21,8 @@ import com.ruoyi.oa.service.IOaEmployeeService;
import java.util.List;
import java.util.Map;
import java.util.Collection;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* 员工基础信息Service业务层处理
@@ -30,6 +35,7 @@ import java.util.Collection;
public class OaEmployeeServiceImpl implements IOaEmployeeService {
private final OaEmployeeMapper baseMapper;
private final SysDeptMapper deptMapper;
/**
* 查询员工基础信息
@@ -46,6 +52,35 @@ public class OaEmployeeServiceImpl implements IOaEmployeeService {
public TableDataInfo<OaEmployeeVo> queryPageList(OaEmployeeBo bo, PageQuery pageQuery) {
LambdaQueryWrapper<OaEmployee> lqw = buildQueryWrapper(bo);
Page<OaEmployeeVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
// 根据部门id查询部门名称
if (CollectionUtils.isNotEmpty(result.getRecords())) {
// 提取所有部门ID
List<Long> deptIds = result.getRecords().stream()
.map(OaEmployeeVo::getDeptId)
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
// 批量查询部门信息
if (!deptIds.isEmpty()) {
LambdaQueryWrapper<SysDept> queryWrapper = Wrappers.lambdaQuery();
queryWrapper.in(SysDept::getDeptId, deptIds)
.eq(SysDept::getDelFlag, '0'); // 只查询未删除的部门
List<SysDept> deptList = deptMapper.selectList(queryWrapper);
Map<Long, String> deptNameMap = deptList.stream()
.collect(Collectors.toMap(SysDept::getDeptId, SysDept::getDeptName, (k1, k2) -> k1));
// 将部门名称设置到对应的VO对象中
for (OaEmployeeVo vo : result.getRecords()) {
if (vo.getDeptId() != null) {
vo.setDeptName(deptNameMap.getOrDefault(vo.getDeptId(), "未知部门"));
}
}
}
}
return TableDataInfo.build(result);
}

View File

@@ -0,0 +1,116 @@
package com.ruoyi.oa.service.impl;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.oa.domain.vo.*;
import com.ruoyi.oa.mapper.SalaryDashboardMapper;
import com.ruoyi.oa.service.ISalaryDashboardService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.List;
@Service
public class SalaryDashboardServiceImpl implements ISalaryDashboardService {
@Autowired
private SalaryDashboardMapper dashboardMapper;
@Override
public SalaryDashboardVo getDashboardData(Long payYear, Long payMonth) {
SalaryDashboardVo dashboardVo = new SalaryDashboardVo();
// 1. 获取卡片数据
dashboardVo.setCardData(getCardData(payYear, payMonth));
// 2. 获取图表数据
dashboardVo.setChartData(getChartData(payYear, payMonth));
// 3. 获取部门统计表格数据 (带分页)
// PageHelper.startPage(pageNum, pageSize);
List<DepartmentStatVo> deptStats = dashboardMapper.queryDepartmentStats(payYear, payMonth);
TableDataInfo<DepartmentStatVo> tableData = new TableDataInfo<>(deptStats, deptStats.size());
dashboardVo.setDepartmentStats(tableData);
return dashboardVo;
}
private CardDataVo getCardData(Long payYear, Long payMonth) {
CardDataVo cardData = new CardDataVo();
// 获取当月数据
CardMetricsVo currentMonthMetrics = dashboardMapper.queryCardMetrics(payYear, payMonth);
// 获取上月数据 (处理1月的情况)
Long lastMonth = payMonth == 1 ? 12 : payMonth - 1;
Long lastYear = payMonth == 1 ? payYear - 1 : payYear;
CardMetricsVo lastMonthMetrics = dashboardMapper.queryCardMetrics(lastYear, lastMonth);
// 获取去年同期数据
CardMetricsVo lastYearMetrics = dashboardMapper.queryCardMetrics(payYear - 1, payMonth);
// 计算各项指标
cardData.setTotalSalaryExpenditure(currentMonthMetrics.getTotalSalaryExpenditure());
// 计算较上月增长率
if (lastMonthMetrics.getTotalSalaryExpenditure().compareTo(BigDecimal.ZERO) != 0) {
BigDecimal monthGrowth = currentMonthMetrics.getTotalSalaryExpenditure()
.subtract(lastMonthMetrics.getTotalSalaryExpenditure())
.divide(lastMonthMetrics.getTotalSalaryExpenditure(), 4, RoundingMode.HALF_UP)
.multiply(new BigDecimal("100"));
cardData.setLastMonthTotalSalaryExpenditureRate(monthGrowth);
}
// 计算部门平均支出
if (currentMonthMetrics.getDepartmentCount() > 0) {
cardData.setAvgDepartmentExpenditure(
currentMonthMetrics.getTotalCompanyExpenditure()
.divide(new BigDecimal(currentMonthMetrics.getDepartmentCount()), 2, RoundingMode.HALF_UP)
);
}
// 计算人均实发工资
if (currentMonthMetrics.getEmployeeCount() > 0) {
cardData.setAvgPersonSalary(
currentMonthMetrics.getTotalSalaryExpenditure()
.divide(new BigDecimal(currentMonthMetrics.getEmployeeCount()), 2, RoundingMode.HALF_UP)
);
}
// 计算同比增长率
if (lastYearMetrics.getEmployeeCount() > 0) {
BigDecimal lastYearAvgSalary = lastYearMetrics.getTotalSalaryExpenditure()
.divide(new BigDecimal(lastYearMetrics.getEmployeeCount()), 2, RoundingMode.HALF_UP);
if (lastYearAvgSalary.compareTo(BigDecimal.ZERO) != 0) {
BigDecimal yearGrowth = cardData.getAvgPersonSalary()
.subtract(lastYearAvgSalary)
.divide(lastYearAvgSalary, 4, RoundingMode.HALF_UP)
.multiply(new BigDecimal("100"));
cardData.setYearOnYearGrowthRate(yearGrowth);
}
}
return cardData;
}
private ChartDataVo getChartData(Long payYear, Long payMonth) {
ChartDataVo chartData = new ChartDataVo();
// 1. 获取柱状图数据 (年度每月总支出)
List<MonthlyExpenditureVo> monthlyExpenditures =
dashboardMapper.queryMonthlyExpenditures(payYear);
chartData.setMonthlyExpenditures(monthlyExpenditures);
// 2. 获取饼图数据 (薪资构成)
List<SalaryComponentVo> salaryComponents =
dashboardMapper.querySalaryComponents(payYear, payMonth);
chartData.setSalaryComponents(salaryComponents);
// 3. 获取折线图数据 (总支出 vs 平均工资)
List<LineChartPointVo> lineChartData =
dashboardMapper.queryLineChartData(payYear);
chartData.setLineChartData(new LineChartVo(lineChartData));
return chartData;
}
}

View File

@@ -0,0 +1,143 @@
<?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.SalaryDashboardMapper">
<!-- 查询卡片指标数据 -->
<select id="queryCardMetrics" resultType="com.ruoyi.oa.domain.vo.CardMetricsVo">
SELECT
-- 薪资支出总额只计算salary类型的实际支出
SUM(CASE WHEN d.template_type = 'salary' THEN d.paid_amount ELSE 0 END) AS totalSalaryExpenditure,
-- 公司总支出:包括薪资和社保的所有实际支出
SUM(d.paid_amount) AS totalCompanyExpenditure,
COUNT(DISTINCT b.employee_id) AS employeeCount,
COUNT(DISTINCT e.dept_id) AS departmentCount
FROM
oa_employee_template_binding b
JOIN oa_binding_item_detail d ON b.binding_id = d.binding_id
JOIN oa_employee e ON b.employee_id = e.employee_id
WHERE
b.pay_year = #{year}
AND b.pay_month = #{month}
AND b.del_flag = 0
AND e.del_flag = 0
AND d.del_flag = 0
</select>
<!-- 查询年度每月总支出 -->
<select id="queryMonthlyExpenditures" resultType="com.ruoyi.oa.domain.vo.MonthlyExpenditureVo">
SELECT
b.pay_month AS month,
SUM(d.paid_amount) AS totalExpenditure
FROM
oa_employee_template_binding b
JOIN oa_binding_item_detail d ON b.binding_id = d.binding_id
WHERE
b.pay_year = #{year}
AND b.del_flag = 0
AND d.del_flag = 0
GROUP BY
b.pay_month
ORDER BY
b.pay_month
</select>
<!-- 查询薪资构成 -->
<select id="querySalaryComponents" resultType="com.ruoyi.oa.domain.vo.SalaryComponentVo">
SELECT
sdt.salary_item AS itemName,
SUM(bid.paid_amount) AS totalAmount
FROM
oa_binding_item_detail bid
JOIN oa_employee_template_binding betb ON bid.binding_id = betb.binding_id
JOIN oa_salary_template_detail sdt ON bid.item_detail_id = sdt.salary_detail_id
WHERE
betb.pay_year = #{year}
AND betb.pay_month = #{month}
AND bid.template_type = 'salary'
AND betb.del_flag = 0
AND bid.del_flag = 0
GROUP BY
sdt.salary_item
</select>
<!-- 查询折线图数据 -->
<select id="queryLineChartData" resultType="com.ruoyi.oa.domain.vo.LineChartPointVo">
SELECT
b.pay_month AS month,
-- 总支出:所有实际支出(包括薪资和社保)
SUM(d.paid_amount) AS totalExpenditure,
-- 平均工资只计算salary类型的实际支出的平均值
AVG(CASE WHEN d.template_type = 'salary' THEN d.paid_amount ELSE 0 END) AS avgSalary
FROM
oa_employee_template_binding b
JOIN oa_binding_item_detail d ON b.binding_id = d.binding_id
WHERE
b.pay_year = #{year}
AND b.del_flag = 0
AND d.del_flag = 0
GROUP BY
b.pay_month
ORDER BY
b.pay_month
</select>
<!-- 查询部门统计数据 -->
<select id="queryDepartmentStats" resultType="com.ruoyi.oa.domain.vo.DepartmentStatVo">
WITH CurrentYearStats AS (
SELECT
e.dept_id,
-- 部门总支出:该部门所有员工的所有实际支出
SUM(d.paid_amount) AS totalExpenditure,
-- 部门平均工资该部门所有员工的salary类型实际支出的平均值
AVG(CASE WHEN d.template_type = 'salary' THEN d.paid_amount ELSE 0 END) AS avgSalary,
COUNT(DISTINCT e.employee_id) AS employeeCount
FROM
oa_employee_template_binding b
JOIN oa_employee e ON b.employee_id = e.employee_id
JOIN oa_binding_item_detail d ON b.binding_id = d.binding_id
WHERE
b.pay_year = #{year}
AND b.pay_month = #{month}
AND b.del_flag = 0
AND d.del_flag = 0
GROUP BY
e.dept_id
),
LastYearStats AS (
SELECT
e.dept_id,
-- 去年同期部门平均工资只计算salary类型的实际支出
AVG(CASE WHEN d.template_type = 'salary' THEN d.paid_amount ELSE 0 END) AS lastYearAvgSalary
FROM
oa_employee_template_binding b
JOIN oa_employee e ON b.employee_id = e.employee_id
JOIN oa_binding_item_detail d ON b.binding_id = d.binding_id
WHERE
b.pay_year = #{year} - 1
AND b.pay_month = #{month}
AND b.del_flag = 0
AND d.del_flag = 0
GROUP BY
e.dept_id
)
SELECT
d.dept_id,
d.dept_name,
COALESCE(cys.totalExpenditure, 0) AS totalExpenditure,
COALESCE(cys.avgSalary, 0) AS avgSalary,
COALESCE(cys.employeeCount, 0) AS employeeCount,
CASE
WHEN lys.lastYearAvgSalary IS NULL OR lys.lastYearAvgSalary = 0 THEN 0
ELSE ((COALESCE(cys.avgSalary, 0) - lys.lastYearAvgSalary) / lys.lastYearAvgSalary) * 100
END AS yearOnYearGrowthRate
FROM
sys_dept d
LEFT JOIN CurrentYearStats cys ON d.dept_id = cys.dept_id
LEFT JOIN LastYearStats lys ON d.dept_id = lys.dept_id
WHERE
d.del_flag = '0'
AND cys.employeeCount > 0
ORDER BY
totalExpenditure DESC
</select>
</mapper>