feat(oa): 添加仪表板功能模块

- 新增仪表板控制器、服务接口及其实现类- 创建仪表板相关的数据传输对象(DTO)
- 设计并实现仪表板数据的获取和统计逻辑
- 添加仪表板数据的数据库访问接口和映射文件
This commit is contained in:
2025-09-17 16:46:18 +08:00
parent 0346e09dd3
commit c3812b6c51
11 changed files with 904 additions and 0 deletions

View File

@@ -0,0 +1,35 @@
package com.gear.oa.controller;
import com.gear.common.core.controller.BaseController;
import com.gear.common.core.domain.R;
import com.gear.oa.domain.vo.dashboard.DashboardOverviewVO;
import com.gear.oa.service.IGearDashboardService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 仪表板控制器
*
* @author Joshi
* @date 2025-09-17
*/
@RequiredArgsConstructor
@RestController
@RequestMapping("/oa/dashboard")
public class GearDashboardController extends BaseController {
private final IGearDashboardService dashboardService;
/**
* 获取仪表板总览数据
*
* @return 仪表板总览数据
*/
@GetMapping("/overview")
public R<DashboardOverviewVO> getDashboardOverview() {
DashboardOverviewVO overview = dashboardService.getDashboardOverview();
return R.ok(overview);
}
}

View File

@@ -0,0 +1,43 @@
package com.gear.oa.domain.vo.dashboard;
import lombok.Data;
import java.math.BigDecimal;
/**
* 日趋势数据VO
*
* @author Joshi
* @date 2025-09-17
*/
@Data
public class DailyTrendVO {
/**
* 日期格式MM-dd
*/
private String date;
/**
* 数值(订单数量或薪资金额)
*/
private BigDecimal value;
/**
* 标签(用于显示)
*/
private String label;
public DailyTrendVO() {}
public DailyTrendVO(String date, BigDecimal value) {
this.date = date;
this.value = value;
this.label = date;
}
public DailyTrendVO(String date, BigDecimal value, String label) {
this.date = date;
this.value = value;
this.label = label;
}
}

View File

@@ -0,0 +1,35 @@
package com.gear.oa.domain.vo.dashboard;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
/**
* 仪表板总览VO
*
* @author Joshi
* @date 2025-09-17
*/
@Data
public class DashboardOverviewVO {
/**
* 订单统计数据
*/
private OrderStatisticsVO orderStatistics;
/**
* 薪资支出统计数据
*/
private SalaryStatisticsVO salaryStatistics;
/**
* 库存排行数据
*/
private List<StockRankingVO> stockRanking;
/**
* 其他统计数据
*/
private OtherStatisticsVO otherStatistics;
}

View File

@@ -0,0 +1,45 @@
package com.gear.oa.domain.vo.dashboard;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
/**
* 订单统计VO
*
* @author Joshi
* @date 2025-09-17
*/
@Data
public class OrderStatisticsVO {
/**
* 今日订单数
*/
private Integer todayOrderCount;
/**
* 本周订单数
*/
private Integer weekOrderCount;
/**
* 本月订单数
*/
private Integer monthOrderCount;
/**
* 近一周订单数量趋势7天数据
*/
private List<DailyTrendVO> weeklyTrend;
/**
* 订单总金额(本周)
*/
private BigDecimal weekTotalAmount;
/**
* 与上周对比增长率
*/
private BigDecimal growthRate;
}

View File

@@ -0,0 +1,54 @@
package com.gear.oa.domain.vo.dashboard;
import lombok.Data;
import java.math.BigDecimal;
/**
* 其他统计数据VO
*
* @author Joshi
* @date 2025-09-17
*/
@Data
public class OtherStatisticsVO {
/**
* 活跃客户数
*/
private Integer activeCustomerCount;
/**
* 待处理订单数
*/
private Integer pendingOrderCount;
/**
* 库存预警数量
*/
private Integer lowStockCount;
/**
* 本月营收
*/
private BigDecimal monthlyRevenue;
/**
* 员工总数
*/
private Integer totalEmployeeCount;
/**
* 今日出勤率
*/
private BigDecimal todayAttendanceRate;
/**
* 产品总数
*/
private Integer totalProductCount;
/**
* 供应商总数
*/
private Integer totalSupplierCount;
}

View File

@@ -0,0 +1,45 @@
package com.gear.oa.domain.vo.dashboard;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
/**
* 薪资统计VO
*
* @author Joshi
* @date 2025-09-17
*/
@Data
public class SalaryStatisticsVO {
/**
* 今日薪资支出
*/
private BigDecimal todaySalary;
/**
* 本周薪资支出
*/
private BigDecimal weekSalary;
/**
* 本月薪资支出
*/
private BigDecimal monthSalary;
/**
* 近一周薪资支出趋势7天数据
*/
private List<DailyTrendVO> weeklyTrend;
/**
* 与上周对比增长率
*/
private BigDecimal growthRate;
/**
* 平均日薪资支出
*/
private BigDecimal avgDailySalary;
}

View File

@@ -0,0 +1,69 @@
package com.gear.oa.domain.vo.dashboard;
import lombok.Data;
import java.math.BigDecimal;
/**
* 库存排行VO
*
* @author Joshi
* @date 2025-09-17
*/
@Data
public class StockRankingVO {
/**
* 排名
*/
private Integer rank;
/**
* 物品ID
*/
private Long itemId;
/**
* 物品名称
*/
private String itemName;
/**
* 物品编号
*/
private String itemCode;
/**
* 物品类型raw_material/product
*/
private String itemType;
/**
* 物品类型名称
*/
private String itemTypeName;
/**
* 库存数量
*/
private BigDecimal quantity;
/**
* 单位
*/
private String unit;
/**
* 仓库名称
*/
private String warehouseName;
/**
* 产品分类(如果是产品)
*/
private String categoryName;
/**
* 负责人
*/
private String owner;
}

View File

@@ -0,0 +1,118 @@
package com.gear.oa.mapper;
import com.gear.oa.domain.vo.dashboard.StockRankingVO;
import org.apache.ibatis.annotations.Mapper;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
/**
* 仪表板数据访问层
*
* @author Joshi
* @date 2025-09-17
*/
@Mapper
public interface GearDashboardMapper {
/**
* 获取今日订单数
*/
Integer getTodayOrderCount();
/**
* 获取本周订单数
*/
Integer getWeekOrderCount();
/**
* 获取本月订单数
*/
Integer getMonthOrderCount();
/**
* 获取上周订单数
*/
Integer getLastWeekOrderCount();
/**
* 获取本周订单总金额
*/
BigDecimal getWeekOrderAmount();
/**
* 获取近一周订单趋势
*/
List<Map<String, Object>> getWeeklyOrderTrend();
/**
* 获取今日薪资支出
*/
BigDecimal getTodaySalary();
/**
* 获取本周薪资支出
*/
BigDecimal getWeekSalary();
/**
* 获取本月薪资支出
*/
BigDecimal getMonthSalary();
/**
* 获取上周薪资支出
*/
BigDecimal getLastWeekSalary();
/**
* 获取近一周薪资趋势
*/
List<Map<String, Object>> getWeeklySalaryTrend();
/**
* 获取库存排行前10
*/
List<StockRankingVO> getStockRanking();
/**
* 获取活跃客户数
*/
Integer getActiveCustomerCount();
/**
* 获取待处理订单数
*/
Integer getPendingOrderCount();
/**
* 获取库存预警数量
*/
Integer getLowStockCount();
/**
* 获取本月营收
*/
BigDecimal getMonthlyRevenue();
/**
* 获取员工总数
*/
Integer getTotalEmployeeCount();
/**
* 获取今日出勤率
*/
BigDecimal getTodayAttendanceRate();
/**
* 获取产品总数
*/
Integer getTotalProductCount();
/**
* 获取供应商总数
*/
Integer getTotalSupplierCount();
}

View File

@@ -0,0 +1,19 @@
package com.gear.oa.service;
import com.gear.oa.domain.vo.dashboard.DashboardOverviewVO;
/**
* 仪表板服务接口
*
* @author Joshi
* @date 2025-09-17
*/
public interface IGearDashboardService {
/**
* 获取仪表板总览数据
*
* @return 仪表板总览数据
*/
DashboardOverviewVO getDashboardOverview();
}

View File

@@ -0,0 +1,211 @@
package com.gear.oa.service.impl;
import com.gear.oa.domain.vo.dashboard.*;
import com.gear.oa.mapper.GearDashboardMapper;
import com.gear.oa.service.IGearDashboardService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* 仪表板服务实现类
*
* @author Joshi
* @date 2025-09-17
*/
@Service
@RequiredArgsConstructor
public class GearDashboardServiceImpl implements IGearDashboardService {
private final GearDashboardMapper dashboardMapper;
@Override
public DashboardOverviewVO getDashboardOverview() {
DashboardOverviewVO overview = new DashboardOverviewVO();
// 获取订单统计数据
overview.setOrderStatistics(getOrderStatistics());
// 获取薪资统计数据
overview.setSalaryStatistics(getSalaryStatistics());
// 获取库存排行数据
overview.setStockRanking(getStockRanking());
// 获取其他统计数据
overview.setOtherStatistics(getOtherStatistics());
return overview;
}
/**
* 获取订单统计数据
*/
private OrderStatisticsVO getOrderStatistics() {
OrderStatisticsVO statistics = new OrderStatisticsVO();
// 获取今日、本周、本月订单数
statistics.setTodayOrderCount(dashboardMapper.getTodayOrderCount());
statistics.setWeekOrderCount(dashboardMapper.getWeekOrderCount());
statistics.setMonthOrderCount(dashboardMapper.getMonthOrderCount());
// 获取本周订单总金额
statistics.setWeekTotalAmount(dashboardMapper.getWeekOrderAmount());
// 获取近一周订单趋势
List<Map<String, Object>> weeklyData = dashboardMapper.getWeeklyOrderTrend();
statistics.setWeeklyTrend(convertToTrendList(weeklyData));
// 计算增长率
Integer lastWeekCount = dashboardMapper.getLastWeekOrderCount();
statistics.setGrowthRate(calculateGrowthRate(statistics.getWeekOrderCount(), lastWeekCount));
return statistics;
}
/**
* 获取薪资统计数据
*/
private SalaryStatisticsVO getSalaryStatistics() {
SalaryStatisticsVO statistics = new SalaryStatisticsVO();
// 获取今日、本周、本月薪资支出
statistics.setTodaySalary(dashboardMapper.getTodaySalary());
statistics.setWeekSalary(dashboardMapper.getWeekSalary());
statistics.setMonthSalary(dashboardMapper.getMonthSalary());
// 获取近一周薪资趋势
List<Map<String, Object>> weeklyData = dashboardMapper.getWeeklySalaryTrend();
statistics.setWeeklyTrend(convertToTrendList(weeklyData));
// 计算增长率
BigDecimal lastWeekSalary = dashboardMapper.getLastWeekSalary();
statistics.setGrowthRate(calculateGrowthRate(statistics.getWeekSalary(), lastWeekSalary));
// 计算平均日薪资
if (statistics.getWeekSalary() != null) {
statistics.setAvgDailySalary(statistics.getWeekSalary().divide(new BigDecimal(7), 2, RoundingMode.HALF_UP));
}
return statistics;
}
/**
* 获取库存排行数据
*/
private List<StockRankingVO> getStockRanking() {
List<StockRankingVO> rankings = dashboardMapper.getStockRanking();
// 设置排名
for (int i = 0; i < rankings.size(); i++) {
rankings.get(i).setRank(i + 1);
// 设置物品类型名称
String itemType = rankings.get(i).getItemType();
if ("product".equals(itemType)) {
rankings.get(i).setItemTypeName("产品");
} else if ("raw_material".equals(itemType)) {
rankings.get(i).setItemTypeName("原材料");
}
}
return rankings;
}
/**
* 获取其他统计数据
*/
private OtherStatisticsVO getOtherStatistics() {
OtherStatisticsVO statistics = new OtherStatisticsVO();
statistics.setActiveCustomerCount(dashboardMapper.getActiveCustomerCount());
statistics.setPendingOrderCount(dashboardMapper.getPendingOrderCount());
statistics.setLowStockCount(dashboardMapper.getLowStockCount());
statistics.setMonthlyRevenue(dashboardMapper.getMonthlyRevenue());
statistics.setTotalEmployeeCount(dashboardMapper.getTotalEmployeeCount());
statistics.setTodayAttendanceRate(dashboardMapper.getTodayAttendanceRate());
statistics.setTotalProductCount(dashboardMapper.getTotalProductCount());
statistics.setTotalSupplierCount(dashboardMapper.getTotalSupplierCount());
return statistics;
}
/**
* 转换趋势数据
*/
private List<DailyTrendVO> convertToTrendList(List<Map<String, Object>> data) {
List<DailyTrendVO> trends = new ArrayList<>();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM-dd");
for (Map<String, Object> item : data) {
DailyTrendVO trend = new DailyTrendVO();
// 处理日期
Object dateObj = item.get("date");
if (dateObj instanceof LocalDate) {
trend.setDate(((LocalDate) dateObj).format(formatter));
} else if (dateObj instanceof String) {
trend.setDate((String) dateObj);
}
// 处理数值
Object valueObj = item.get("value");
if (valueObj instanceof BigDecimal) {
trend.setValue((BigDecimal) valueObj);
} else if (valueObj instanceof Integer) {
trend.setValue(new BigDecimal((Integer) valueObj));
} else if (valueObj instanceof Long) {
trend.setValue(new BigDecimal((Long) valueObj));
}
trend.setLabel(trend.getDate());
trends.add(trend);
}
return trends;
}
/**
* 计算增长率
*/
private BigDecimal calculateGrowthRate(Object current, Object previous) {
if (current == null || previous == null) {
return BigDecimal.ZERO;
}
BigDecimal currentValue = convertToBigDecimal(current);
BigDecimal previousValue = convertToBigDecimal(previous);
if (previousValue.compareTo(BigDecimal.ZERO) == 0) {
return currentValue.compareTo(BigDecimal.ZERO) > 0 ? new BigDecimal(100) : BigDecimal.ZERO;
}
return currentValue.subtract(previousValue)
.divide(previousValue, 4, RoundingMode.HALF_UP)
.multiply(new BigDecimal(100))
.setScale(2, RoundingMode.HALF_UP);
}
/**
* 转换为BigDecimal
*/
private BigDecimal convertToBigDecimal(Object value) {
if (value instanceof BigDecimal) {
return (BigDecimal) value;
} else if (value instanceof Integer) {
return new BigDecimal((Integer) value);
} else if (value instanceof Long) {
return new BigDecimal((Long) value);
} else if (value instanceof String) {
return new BigDecimal((String) value);
}
return BigDecimal.ZERO;
}
}

View File

@@ -0,0 +1,230 @@
<?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.gear.oa.mapper.GearDashboardMapper">
<!-- 获取今日订单数 -->
<select id="getTodayOrderCount" resultType="java.lang.Integer">
SELECT COUNT(*)
FROM gear_order
WHERE DATE(create_time) = CURDATE()
AND del_flag = 0
</select>
<!-- 获取本周订单数 -->
<select id="getWeekOrderCount" resultType="java.lang.Integer">
SELECT COUNT(*)
FROM gear_order
WHERE YEARWEEK(create_time, 1) = YEARWEEK(CURDATE(), 1)
AND del_flag = 0
</select>
<!-- 获取本月订单数 -->
<select id="getMonthOrderCount" resultType="java.lang.Integer">
SELECT COUNT(*)
FROM gear_order
WHERE YEAR(create_time) = YEAR(CURDATE())
AND MONTH(create_time) = MONTH(CURDATE())
AND del_flag = 0
</select>
<!-- 获取上周订单数 -->
<select id="getLastWeekOrderCount" resultType="java.lang.Integer">
SELECT COUNT(*)
FROM gear_order
WHERE YEARWEEK(create_time, 1) = YEARWEEK(CURDATE(), 1) - 1
AND del_flag = 0
</select>
<!-- 获取本周订单总金额 -->
<select id="getWeekOrderAmount" resultType="java.math.BigDecimal">
SELECT COALESCE(SUM(tax_amount), 0)
FROM gear_order
WHERE YEARWEEK(create_time, 1) = YEARWEEK(CURDATE(), 1)
AND del_flag = 0
</select>
<!-- 获取近一周订单趋势 -->
<select id="getWeeklyOrderTrend" resultType="java.util.Map">
SELECT
DATE(create_time) as date,
COUNT(*) as value
FROM gear_order
WHERE create_time >= DATE_SUB(CURDATE(), INTERVAL 6 DAY)
AND create_time &lt; DATE_ADD(CURDATE(), INTERVAL 1 DAY)
AND del_flag = 0
GROUP BY DATE(create_time)
ORDER BY DATE(create_time)
</select>
<!-- 获取今日薪资支出 -->
<select id="getTodaySalary" resultType="java.math.BigDecimal">
SELECT COALESCE(SUM(wage), 0)
FROM gear_attendance_record
WHERE DATE(record_date) = CURDATE()
AND del_flag = 0
AND wage IS NOT NULL
</select>
<!-- 获取本周薪资支出 -->
<select id="getWeekSalary" resultType="java.math.BigDecimal">
SELECT COALESCE(SUM(wage), 0)
FROM gear_attendance_record
WHERE YEARWEEK(record_date, 1) = YEARWEEK(CURDATE(), 1)
AND del_flag = 0
AND wage IS NOT NULL
</select>
<!-- 获取本月薪资支出 -->
<select id="getMonthSalary" resultType="java.math.BigDecimal">
SELECT COALESCE(SUM(wage), 0)
FROM gear_attendance_record
WHERE YEAR(record_date) = YEAR(CURDATE())
AND MONTH(record_date) = MONTH(CURDATE())
AND del_flag = 0
AND wage IS NOT NULL
</select>
<!-- 获取上周薪资支出 -->
<select id="getLastWeekSalary" resultType="java.math.BigDecimal">
SELECT COALESCE(SUM(wage), 0)
FROM gear_attendance_record
WHERE YEARWEEK(record_date, 1) = YEARWEEK(CURDATE(), 1) - 1
AND del_flag = 0
AND wage IS NOT NULL
</select>
<!-- 获取近一周薪资趋势 -->
<select id="getWeeklySalaryTrend" resultType="java.util.Map">
SELECT
DATE(record_date) as date,
COALESCE(SUM(wage), 0) as value
FROM gear_attendance_record
WHERE record_date >= DATE_SUB(CURDATE(), INTERVAL 6 DAY)
AND record_date &lt; DATE_ADD(CURDATE(), INTERVAL 1 DAY)
AND del_flag = 0
GROUP BY DATE(record_date)
ORDER BY DATE(record_date)
</select>
<!-- 获取库存排行前10 -->
<select id="getStockRanking" resultType="com.gear.oa.domain.vo.dashboard.StockRankingVO">
SELECT
s.item_id,
s.item_type,
s.quantity,
s.unit,
w.warehouse_name,
CASE
WHEN s.item_type = 'product' THEN p.product_name
WHEN s.item_type = 'raw_material' THEN rm.material_name
ELSE '未知'
END as item_name,
CASE
WHEN s.item_type = 'product' THEN p.product_code
WHEN s.item_type = 'raw_material' THEN rm.material_code
ELSE ''
END as item_code,
CASE
WHEN s.item_type = 'product' THEN pc.category_name
ELSE NULL
END as category_name,
CASE
WHEN s.item_type = 'product' THEN p.owner
ELSE NULL
END as owner
FROM gear_stock s
LEFT JOIN gear_warehouse w ON s.warehouse_id = w.warehouse_id
LEFT JOIN gear_product p ON s.item_type = 'product' AND s.item_id = p.product_id
LEFT JOIN gear_product_category pc ON p.category_id = pc.category_id
LEFT JOIN (
SELECT product_id as material_id, product_name as material_name, product_code as material_code
FROM gear_product
WHERE type = 'raw'
) rm ON s.item_type = 'raw_material' AND s.item_id = rm.material_id
WHERE s.del_flag = 0
AND s.quantity > 0
ORDER BY s.quantity DESC
LIMIT 10
</select>
<!-- 获取活跃客户数(本月有订单的客户) -->
<select id="getActiveCustomerCount" resultType="java.lang.Integer">
SELECT COUNT(DISTINCT customer_id)
FROM gear_order
WHERE YEAR(create_time) = YEAR(CURDATE())
AND MONTH(create_time) = MONTH(CURDATE())
AND del_flag = 0
</select>
<!-- 获取待处理订单数 -->
<select id="getPendingOrderCount" resultType="java.lang.Integer">
SELECT COUNT(*)
FROM gear_order
WHERE order_status IN (0, 1)
AND del_flag = 0
</select>
<!-- 获取库存预警数量假设库存小于10为预警 -->
<select id="getLowStockCount" resultType="java.lang.Integer">
SELECT COUNT(*)
FROM gear_stock
WHERE quantity &lt; 10
AND del_flag = 0
</select>
<!-- 获取本月营收 -->
<select id="getMonthlyRevenue" resultType="java.math.BigDecimal">
SELECT COALESCE(SUM(tax_amount), 0)
FROM gear_order
WHERE YEAR(create_time) = YEAR(CURDATE())
AND MONTH(create_time) = MONTH(CURDATE())
AND order_status = 2
AND del_flag = 0
</select>
<!-- 获取员工总数(从考勤记录中统计活跃用户) -->
<select id="getTotalEmployeeCount" resultType="java.lang.Integer">
SELECT COUNT(DISTINCT user_id)
FROM gear_attendance_record
WHERE record_date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
AND del_flag = 0
</select>
<!-- 获取今日出勤率 -->
<select id="getTodayAttendanceRate" resultType="java.math.BigDecimal">
SELECT
CASE
WHEN total_employees.total = 0 THEN 0
ELSE ROUND((attended_employees.attended * 100.0 / total_employees.total), 2)
END as attendance_rate
FROM
(SELECT COUNT(DISTINCT user_id) as total
FROM gear_attendance_record
WHERE record_date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
AND del_flag = 0) total_employees
CROSS JOIN
(SELECT COUNT(DISTINCT user_id) as attended
FROM gear_attendance_record
WHERE DATE(record_date) = CURDATE()
AND record_type = 'attendance'
AND del_flag = 0) attended_employees
</select>
<!-- 获取产品总数 -->
<select id="getTotalProductCount" resultType="java.lang.Integer">
SELECT COUNT(*)
FROM gear_product
WHERE del_flag = 0
AND is_enabled = 1
</select>
<!-- 获取供应商总数 -->
<select id="getTotalSupplierCount" resultType="java.lang.Integer">
SELECT COUNT(*)
FROM gear_supplier
WHERE del_flag = 0
</select>
</mapper>