“mitao”
2025-12-23 822f2b39af31e8290803146824165e7e1a7f3f4c
feat:管理后台财务管理汇总统计
10个文件已添加
15个文件已修改
776 ■■■■■ 已修改文件
ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/DashboardStatisticsController.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/TIntegralSettlementController.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/agent/AgentCompanyController.java 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-applet/src/main/java/com/ruoyi/web/controller/api/AppUserVisitController.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-applet/src/main/resources/application-test.yml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/export/IntegralSettlementImport.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/mapper/TIntegralSettlementMapper.java 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/model/TAppUserVisit.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/model/TBigGiftPackageOrder.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/query/DashboardStatisticsQuery.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/query/TIntegralSettlementDetailQuery.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/DashboardStatisticsService.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/TAppUserVisitService.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/DashboardStatisticsServiceImpl.java 374 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/ICBCNotifyServiceImpl.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TAppUserVisitServiceImpl.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TIntegralSettlementServiceImpl.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/vo/DashboardStatisticsVO.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/vo/MonthlyOrderStatisticsVO.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/vo/StatisticsItemVO.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/vo/TIntegralSettlementDetailVO.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/vo/TIntegralSettlementVO.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/vo/TrendDataVO.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/resources/mapper/system/TIntegralSettlementMapper.xml 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/DashboardStatisticsController.java
New file
@@ -0,0 +1,36 @@
package com.ruoyi.web.controller.api;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.system.query.DashboardStatisticsQuery;
import com.ruoyi.system.service.DashboardStatisticsService;
import com.ruoyi.system.vo.DashboardStatisticsVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Lazy;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
 * 数据统计面板 前端控制器
 *
 * @author xiaochen
 * @since 2025-12-23
 */
@RestController
@RequestMapping("/dashboard-statistics")
@Api(tags = "(嘉视达)数据统计面板")
@RequiredArgsConstructor(onConstructor_ = @Lazy)
public class DashboardStatisticsController {
    private final DashboardStatisticsService dashboardStatisticsService;
    @ApiOperation(value = "获取数据统计面板数据")
    @PostMapping("/overview")
    public R<DashboardStatisticsVO> getOverview(@RequestBody DashboardStatisticsQuery query) {
        return R.ok(dashboardStatisticsService.getStatistics(query));
    }
}
ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/TIntegralSettlementController.java
@@ -61,8 +61,8 @@
            response.setHeader("content-Type", "application/vnd.ms-excel");
            response.setHeader("Pragma", "no-cache");
            response.setHeader("Cache-Control", "no-cache");
            workbook.write(outputStream);
            outputStream = response.getOutputStream();
            workbook.write(outputStream);
        } catch (IOException e) {
            log.error("积分结算导入模板下载失败", e);
        } finally {
ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/agent/AgentCompanyController.java
New file
@@ -0,0 +1,56 @@
package com.ruoyi.web.controller.api.agent;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.system.model.TCompany;
import com.ruoyi.system.service.TCompanyService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Lazy;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
 * 经销商管理后台 - 公司信息 前端控制器
 *
 * @author xiaochen
 * @since 2025-12-23
 */
@RestController
@RequestMapping("/agent/company")
@Api(tags = "(经销商后台)公司信息")
@RequiredArgsConstructor(onConstructor_ = @Lazy)
public class AgentCompanyController extends BaseController {
    private final TCompanyService companyService;
    @ApiOperation(value = "获取当前用户所属公司详情")
    @GetMapping("/detail")
    public R<TCompany> getDetail() {
        // 获取当前登录用户
        LoginUser loginUser = SecurityUtils.getLoginUser();
        SysUser user = loginUser.getUser();
        if (!user.getRoleType().equals(2)) {
            return R.fail("请使用经销商账号访问");
        }
        // 获取用户所属公司ID
        Integer companyId = user.getCompanyId();
        if (companyId == null) {
            return R.fail("当前用户未关联公司");
        }
        // 查询公司详情
        TCompany company = companyService.getById(companyId);
        if (company == null) {
            return R.fail("公司信息不存在");
        }
        return R.ok(company);
    }
}
ruoyi-applet/src/main/java/com/ruoyi/web/controller/api/AppUserVisitController.java
New file
@@ -0,0 +1,35 @@
package com.ruoyi.web.controller.api;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.framework.web.service.TokenService;
import com.ruoyi.system.service.TAppUserVisitService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Lazy;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
 * 用户访问记录
 *
 * @author xiaochen
 */
@RestController
@RequestMapping("/appUserVisit")
@Api(tags = "用户访问记录")
@RequiredArgsConstructor(onConstructor_ = {@Lazy})
public class AppUserVisitController {
    private final TokenService tokenService;
    private final TAppUserVisitService appUserVisitService;
    @ApiOperation(value = "记录用户访问")
    @PostMapping("/record")
    public R<Void> recordVisit() {
        Integer userId = tokenService.getLoginUserApplet().getUserId();
        appUserVisitService.recordVisit(userId);
        return R.ok();
    }
}
ruoyi-applet/src/main/resources/application-test.yml
@@ -240,7 +240,7 @@
    order-refund-url: "https://gw.open.icbc.com.cn/api/cardbusiness/aggregatepay/b2c/online/refundqry/V1"
    # 完整的回调通知地址,支付完成后通知商户的URL
    notify-url: "http://jiashida.free.idcfengye.com/icbc-notify/"
    notify-url: "http://221.182.45.100:7890/icbc-notify/"
    # 回调方法的路径
    notify-url-method: "/icbc-notify/"
ruoyi-system/src/main/java/com/ruoyi/system/export/IntegralSettlementImport.java
@@ -14,7 +14,7 @@
    private String code;
    @Excel(name = "结算积分", width = 20)
    private BigDecimal totalSettlementIntegral;
    private Integer totalSettlementIntegral;
    @Excel(name = "备注", width = 20)
    private String remark;
ruoyi-system/src/main/java/com/ruoyi/system/mapper/TIntegralSettlementMapper.java
@@ -25,6 +25,7 @@
     * 分页查询积分结算列表(按结算周期分组汇总)
     */
    List<TIntegralSettlementVO> selectSettlementPageList(Page<TIntegralSettlementVO> page, @Param("query") TIntegralSettlementQuery query);
    /**
     * 分页查询积分结算详情列表
     */
ruoyi-system/src/main/java/com/ruoyi/system/model/TAppUserVisit.java
@@ -6,6 +6,8 @@
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableField;
import java.io.Serializable;
import java.time.LocalDateTime;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@@ -39,5 +41,9 @@
    @TableField("visit_date")
    private LocalDate visitDate;
    @ApiModelProperty(value = "创建时间")
    @TableField("create_time")
    private LocalDateTime createTime;
}
ruoyi-system/src/main/java/com/ruoyi/system/model/TBigGiftPackageOrder.java
@@ -79,7 +79,7 @@
    @TableField("payment_price")
    private BigDecimal paymentPrice;
    @ApiModelProperty(value = "订单状态(1=待付款,2=已付订金,3=完成支付,4=确认收货,5=已退款)")
    @ApiModelProperty(value = "订单状态(1=待付款,2=已付订金,3=完成支付,4=确认收货,5=已退款,6=已取消)")
    @TableField("status")
    private Integer status;
ruoyi-system/src/main/java/com/ruoyi/system/query/DashboardStatisticsQuery.java
New file
@@ -0,0 +1,31 @@
package com.ruoyi.system.query;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.time.LocalDate;
/**
 * 数据统计面板查询条件
 *
 * @author xiaochen
 * @since 2025-12-23
 */
@Data
@ApiModel(value = "数据统计面板查询条件")
public class DashboardStatisticsQuery implements Serializable {
    private static final long serialVersionUID = 1L;
    @ApiModelProperty(value = "开始日期")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate startDate;
    @ApiModelProperty(value = "结束日期")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate endDate;
}
ruoyi-system/src/main/java/com/ruoyi/system/query/TIntegralSettlementDetailQuery.java
@@ -9,8 +9,11 @@
 * 积分结算列表查询条件
 */
@Data
@ApiModel(value = "积分结算列表查询条件")
@ApiModel(value = "积分结算详情列表查询条件")
public class TIntegralSettlementDetailQuery extends BasePage {
    @ApiModelProperty("主键ID")
    private Integer id;
    @ApiModelProperty(value = "用户名")
    private String name;
@@ -20,4 +23,7 @@
    @ApiModelProperty(value = "推广号")
    private String code;
    @ApiModelProperty(value = "结算周期(前端不传)")
    private String settlementInterval;
}
ruoyi-system/src/main/java/com/ruoyi/system/service/DashboardStatisticsService.java
New file
@@ -0,0 +1,22 @@
package com.ruoyi.system.service;
import com.ruoyi.system.query.DashboardStatisticsQuery;
import com.ruoyi.system.vo.DashboardStatisticsVO;
/**
 * 数据统计面板 服务类
 *
 * @author xiaochen
 * @since 2025-12-23
 */
public interface DashboardStatisticsService {
    /**
     * 获取数据统计面板数据
     *
     * @param query 查询条件
     * @return 统计数据
     */
    DashboardStatisticsVO getStatistics(DashboardStatisticsQuery query);
}
ruoyi-system/src/main/java/com/ruoyi/system/service/TAppUserVisitService.java
@@ -13,4 +13,8 @@
 */
public interface TAppUserVisitService extends IService<TAppUserVisit> {
    /**
     * 记录用户访问(每日去重)
     */
    void recordVisit(Integer userId);
}
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/DashboardStatisticsServiceImpl.java
New file
@@ -0,0 +1,374 @@
package com.ruoyi.system.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ruoyi.system.model.TAppUserVisit;
import com.ruoyi.system.model.TBigGiftPackageOrder;
import com.ruoyi.system.model.TPhysicalGoodsOrder;
import com.ruoyi.system.query.DashboardStatisticsQuery;
import com.ruoyi.system.service.DashboardStatisticsService;
import com.ruoyi.system.service.TAppUserVisitService;
import com.ruoyi.system.service.TBigGiftPackageOrderService;
import com.ruoyi.system.service.TPhysicalGoodsOrderService;
import com.ruoyi.system.vo.*;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.stream.Collectors;
/**
 * 数据统计面板 服务实现类
 *
 * @author xiaochen
 * @since 2025-12-23
 */
@Service
@RequiredArgsConstructor(onConstructor_ = @Lazy)
public class DashboardStatisticsServiceImpl implements DashboardStatisticsService {
    private final TAppUserVisitService appUserVisitService;
    private final TPhysicalGoodsOrderService physicalGoodsOrderService;
    private final TBigGiftPackageOrderService bigGiftPackageOrderService;
    /**
     * 实物商品订单有效状态:完成支付(2)、确认收货(3)
     */
    private static final List<Integer> PHYSICAL_ORDER_VALID_STATUS = Arrays.asList(2, 3);
    /**
     * 大礼包订单有效状态:已付订金(2)、完成支付(3)、确认收货(4)
     */
    private static final List<Integer> BIG_GIFT_ORDER_VALID_STATUS = Arrays.asList(2, 3, 4);
    @Override
    public DashboardStatisticsVO getStatistics(DashboardStatisticsQuery query) {
        // 处理日期范围,默认为当月
        LocalDate endDate = query.getEndDate() != null ? query.getEndDate() : LocalDate.now();
        LocalDate startDate = query.getStartDate() != null ? query.getStartDate() : endDate.withDayOfMonth(1);
        // 计算上一周期的日期范围(用于环比计算)
        long daysBetween = ChronoUnit.DAYS.between(startDate, endDate) + 1;
        LocalDate lastPeriodEnd = startDate.minusDays(1);
        LocalDate lastPeriodStart = lastPeriodEnd.minusDays(daysBetween - 1);
        return DashboardStatisticsVO.builder()
                .visitStats(buildVisitStats(startDate, endDate, lastPeriodStart, lastPeriodEnd))
                .orderCountStats(buildOrderCountStats(startDate, endDate, lastPeriodStart, lastPeriodEnd))
                .orderAmountStats(buildOrderAmountStats(startDate, endDate, lastPeriodStart, lastPeriodEnd))
                .monthlyStats(buildMonthlyStats(startDate, endDate))
                .build();
    }
    /**
     * 构建客户访问量统计
     */
    private StatisticsItemVO buildVisitStats(LocalDate startDate, LocalDate endDate,
                                              LocalDate lastPeriodStart, LocalDate lastPeriodEnd) {
        // 当前周期访问量
        long currentTotal = appUserVisitService.count(new LambdaQueryWrapper<TAppUserVisit>()
                .between(TAppUserVisit::getVisitDate, startDate, endDate));
        // 上一周期访问量
        long lastTotal = appUserVisitService.count(new LambdaQueryWrapper<TAppUserVisit>()
                .between(TAppUserVisit::getVisitDate, lastPeriodStart, lastPeriodEnd));
        // 趋势数据(按日统计)
        List<TAppUserVisit> visitList = appUserVisitService.list(new LambdaQueryWrapper<TAppUserVisit>()
                .between(TAppUserVisit::getVisitDate, startDate, endDate));
        Map<LocalDate, Long> visitByDate = visitList.stream()
                .collect(Collectors.groupingBy(TAppUserVisit::getVisitDate, Collectors.counting()));
        List<TrendDataVO> trend = buildDailyTrend(startDate, endDate, visitByDate);
        return StatisticsItemVO.builder()
                .total(BigDecimal.valueOf(currentTotal))
                .rate(calculateRate(currentTotal, lastTotal))
                .trend(trend)
                .build();
    }
    /**
     * 构建订单数统计
     */
    private StatisticsItemVO buildOrderCountStats(LocalDate startDate, LocalDate endDate,
                                                   LocalDate lastPeriodStart, LocalDate lastPeriodEnd) {
        LocalDateTime startDateTime = startDate.atStartOfDay();
        LocalDateTime endDateTime = endDate.atTime(LocalTime.MAX);
        LocalDateTime lastStartDateTime = lastPeriodStart.atStartOfDay();
        LocalDateTime lastEndDateTime = lastPeriodEnd.atTime(LocalTime.MAX);
        // 当前周期订单数
        long physicalCount = physicalGoodsOrderService.count(new LambdaQueryWrapper<TPhysicalGoodsOrder>()
                .in(TPhysicalGoodsOrder::getStatus, PHYSICAL_ORDER_VALID_STATUS)
                .between(TPhysicalGoodsOrder::getCreateTime, startDateTime, endDateTime));
        long bigGiftCount = bigGiftPackageOrderService.count(new LambdaQueryWrapper<TBigGiftPackageOrder>()
                .in(TBigGiftPackageOrder::getStatus, BIG_GIFT_ORDER_VALID_STATUS)
                .between(TBigGiftPackageOrder::getCreateTime, startDateTime, endDateTime));
        long currentTotal = physicalCount + bigGiftCount;
        // 上一周期订单数
        long lastPhysicalCount = physicalGoodsOrderService.count(new LambdaQueryWrapper<TPhysicalGoodsOrder>()
                .in(TPhysicalGoodsOrder::getStatus, PHYSICAL_ORDER_VALID_STATUS)
                .between(TPhysicalGoodsOrder::getCreateTime, lastStartDateTime, lastEndDateTime));
        long lastBigGiftCount = bigGiftPackageOrderService.count(new LambdaQueryWrapper<TBigGiftPackageOrder>()
                .in(TBigGiftPackageOrder::getStatus, BIG_GIFT_ORDER_VALID_STATUS)
                .between(TBigGiftPackageOrder::getCreateTime, lastStartDateTime, lastEndDateTime));
        long lastTotal = lastPhysicalCount + lastBigGiftCount;
        // 趋势数据
        List<TrendDataVO> trend = buildOrderCountTrend(startDate, endDate);
        return StatisticsItemVO.builder()
                .total(BigDecimal.valueOf(currentTotal))
                .rate(calculateRate(currentTotal, lastTotal))
                .trend(trend)
                .build();
    }
    /**
     * 构建订单金额统计
     */
    private StatisticsItemVO buildOrderAmountStats(LocalDate startDate, LocalDate endDate,
                                                    LocalDate lastPeriodStart, LocalDate lastPeriodEnd) {
        LocalDateTime startDateTime = startDate.atStartOfDay();
        LocalDateTime endDateTime = endDate.atTime(LocalTime.MAX);
        LocalDateTime lastStartDateTime = lastPeriodStart.atStartOfDay();
        LocalDateTime lastEndDateTime = lastPeriodEnd.atTime(LocalTime.MAX);
        // 当前周期订单金额
        BigDecimal currentTotal = calculateTotalOrderAmount(startDateTime, endDateTime);
        // 上一周期订单金额
        BigDecimal lastTotal = calculateTotalOrderAmount(lastStartDateTime, lastEndDateTime);
        // 趋势数据
        List<TrendDataVO> trend = buildOrderAmountTrend(startDate, endDate);
        return StatisticsItemVO.builder()
                .total(currentTotal)
                .rate(calculateRate(currentTotal, lastTotal))
                .trend(trend)
                .build();
    }
    /**
     * 计算指定时间范围内的订单总金额
     */
    private BigDecimal calculateTotalOrderAmount(LocalDateTime startDateTime, LocalDateTime endDateTime) {
        // 实物商品订单金额
        List<TPhysicalGoodsOrder> physicalOrders = physicalGoodsOrderService.list(
                new LambdaQueryWrapper<TPhysicalGoodsOrder>()
                        .in(TPhysicalGoodsOrder::getStatus, PHYSICAL_ORDER_VALID_STATUS)
                        .between(TPhysicalGoodsOrder::getCreateTime, startDateTime, endDateTime)
                        .select(TPhysicalGoodsOrder::getPaymentPrice));
        BigDecimal physicalAmount = physicalOrders.stream()
                .map(o -> o.getPaymentPrice() != null ? o.getPaymentPrice() : BigDecimal.ZERO)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        // 大礼包订单金额
        List<TBigGiftPackageOrder> bigGiftOrders = bigGiftPackageOrderService.list(
                new LambdaQueryWrapper<TBigGiftPackageOrder>()
                        .in(TBigGiftPackageOrder::getStatus, BIG_GIFT_ORDER_VALID_STATUS)
                        .between(TBigGiftPackageOrder::getCreateTime, startDateTime, endDateTime)
                        .select(TBigGiftPackageOrder::getPaymentPrice));
        BigDecimal bigGiftAmount = bigGiftOrders.stream()
                .map(o -> o.getPaymentPrice() != null ? o.getPaymentPrice() : BigDecimal.ZERO)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        return physicalAmount.add(bigGiftAmount);
    }
    /**
     * 构建月度订单统计
     */
    private List<MonthlyOrderStatisticsVO> buildMonthlyStats(LocalDate startDate, LocalDate endDate) {
        LocalDateTime startDateTime = startDate.atStartOfDay();
        LocalDateTime endDateTime = endDate.atTime(LocalTime.MAX);
        // 获取实物商品订单
        List<TPhysicalGoodsOrder> physicalOrders = physicalGoodsOrderService.list(
                new LambdaQueryWrapper<TPhysicalGoodsOrder>()
                        .in(TPhysicalGoodsOrder::getStatus, PHYSICAL_ORDER_VALID_STATUS)
                        .between(TPhysicalGoodsOrder::getCreateTime, startDateTime, endDateTime));
        // 获取大礼包订单
        List<TBigGiftPackageOrder> bigGiftOrders = bigGiftPackageOrderService.list(
                new LambdaQueryWrapper<TBigGiftPackageOrder>()
                        .in(TBigGiftPackageOrder::getStatus, BIG_GIFT_ORDER_VALID_STATUS)
                        .between(TBigGiftPackageOrder::getCreateTime, startDateTime, endDateTime));
        // 按月份分组统计
        Map<String, Long> countByMonth = new TreeMap<>();
        Map<String, BigDecimal> amountByMonth = new TreeMap<>();
        // 统计实物商品订单
        for (TPhysicalGoodsOrder order : physicalOrders) {
            String month = order.getCreateTime().getMonthValue() + "月";
            countByMonth.merge(month, 1L, Long::sum);
            BigDecimal amount = order.getPaymentPrice() != null ? order.getPaymentPrice() : BigDecimal.ZERO;
            amountByMonth.merge(month, amount, BigDecimal::add);
        }
        // 统计大礼包订单
        for (TBigGiftPackageOrder order : bigGiftOrders) {
            String month = order.getCreateTime().getMonthValue() + "月";
            countByMonth.merge(month, 1L, Long::sum);
            BigDecimal amount = order.getPaymentPrice() != null ? order.getPaymentPrice() : BigDecimal.ZERO;
            amountByMonth.merge(month, amount, BigDecimal::add);
        }
        // 构建返回结果
        List<MonthlyOrderStatisticsVO> result = new ArrayList<>();
        for (String month : countByMonth.keySet()) {
            result.add(new MonthlyOrderStatisticsVO(
                    month,
                    countByMonth.getOrDefault(month, 0L),
                    amountByMonth.getOrDefault(month, BigDecimal.ZERO)
            ));
        }
        // 按月份数字排序
        result.sort(Comparator.comparingInt(vo -> Integer.parseInt(vo.getMonth().replace("月", ""))));
        return result;
    }
    /**
     * 构建每日趋势数据
     */
    private List<TrendDataVO> buildDailyTrend(LocalDate startDate, LocalDate endDate,
                                               Map<LocalDate, Long> dataByDate) {
        List<TrendDataVO> trend = new ArrayList<>();
        LocalDate current = startDate;
        while (!current.isAfter(endDate)) {
            Long value = dataByDate.getOrDefault(current, 0L);
            trend.add(new TrendDataVO(current.toString(), value));
            current = current.plusDays(1);
        }
        return trend;
    }
    /**
     * 构建订单数趋势数据(优化:一次查询,内存分组)
     */
    private List<TrendDataVO> buildOrderCountTrend(LocalDate startDate, LocalDate endDate) {
        LocalDateTime startDateTime = startDate.atStartOfDay();
        LocalDateTime endDateTime = endDate.atTime(LocalTime.MAX);
        // 一次性查询所有实物商品订单
        List<TPhysicalGoodsOrder> physicalOrders = physicalGoodsOrderService.list(
                new LambdaQueryWrapper<TPhysicalGoodsOrder>()
                        .in(TPhysicalGoodsOrder::getStatus, PHYSICAL_ORDER_VALID_STATUS)
                        .between(TPhysicalGoodsOrder::getCreateTime, startDateTime, endDateTime)
                        .select(TPhysicalGoodsOrder::getCreateTime));
        // 一次性查询所有大礼包订单
        List<TBigGiftPackageOrder> bigGiftOrders = bigGiftPackageOrderService.list(
                new LambdaQueryWrapper<TBigGiftPackageOrder>()
                        .in(TBigGiftPackageOrder::getStatus, BIG_GIFT_ORDER_VALID_STATUS)
                        .between(TBigGiftPackageOrder::getCreateTime, startDateTime, endDateTime)
                        .select(TBigGiftPackageOrder::getCreateTime));
        // 按日期分组统计
        Map<LocalDate, Long> countByDate = new HashMap<>();
        physicalOrders.forEach(o -> {
            LocalDate date = o.getCreateTime().toLocalDate();
            countByDate.merge(date, 1L, Long::sum);
        });
        bigGiftOrders.forEach(o -> {
            LocalDate date = o.getCreateTime().toLocalDate();
            countByDate.merge(date, 1L, Long::sum);
        });
        // 构建趋势数据
        return buildDailyTrend(startDate, endDate, countByDate);
    }
    /**
     * 构建订单金额趋势数据(优化:一次查询,内存分组)
     */
    private List<TrendDataVO> buildOrderAmountTrend(LocalDate startDate, LocalDate endDate) {
        LocalDateTime startDateTime = startDate.atStartOfDay();
        LocalDateTime endDateTime = endDate.atTime(LocalTime.MAX);
        // 一次性查询所有实物商品订单
        List<TPhysicalGoodsOrder> physicalOrders = physicalGoodsOrderService.list(
                new LambdaQueryWrapper<TPhysicalGoodsOrder>()
                        .in(TPhysicalGoodsOrder::getStatus, PHYSICAL_ORDER_VALID_STATUS)
                        .between(TPhysicalGoodsOrder::getCreateTime, startDateTime, endDateTime)
                        .select(TPhysicalGoodsOrder::getCreateTime, TPhysicalGoodsOrder::getPaymentPrice));
        // 一次性查询所有大礼包订单
        List<TBigGiftPackageOrder> bigGiftOrders = bigGiftPackageOrderService.list(
                new LambdaQueryWrapper<TBigGiftPackageOrder>()
                        .in(TBigGiftPackageOrder::getStatus, BIG_GIFT_ORDER_VALID_STATUS)
                        .between(TBigGiftPackageOrder::getCreateTime, startDateTime, endDateTime)
                        .select(TBigGiftPackageOrder::getCreateTime, TBigGiftPackageOrder::getPaymentPrice));
        // 按日期分组统计金额
        Map<LocalDate, BigDecimal> amountByDate = new HashMap<>();
        physicalOrders.forEach(o -> {
            LocalDate date = o.getCreateTime().toLocalDate();
            BigDecimal amount = o.getPaymentPrice() != null ? o.getPaymentPrice() : BigDecimal.ZERO;
            amountByDate.merge(date, amount, BigDecimal::add);
        });
        bigGiftOrders.forEach(o -> {
            LocalDate date = o.getCreateTime().toLocalDate();
            BigDecimal amount = o.getPaymentPrice() != null ? o.getPaymentPrice() : BigDecimal.ZERO;
            amountByDate.merge(date, amount, BigDecimal::add);
        });
        // 构建趋势数据
        List<TrendDataVO> trend = new ArrayList<>();
        LocalDate current = startDate;
        while (!current.isAfter(endDate)) {
            BigDecimal value = amountByDate.getOrDefault(current, BigDecimal.ZERO);
            trend.add(new TrendDataVO(current.toString(), value));
            current = current.plusDays(1);
        }
        return trend;
    }
    /**
     * 计算环比增长率
     *
     * @param current 当前值
     * @param last    上期值
     * @return 环比增长率(百分比)
     */
    private BigDecimal calculateRate(long current, long last) {
        if (last == 0) {
            return current > 0 ? BigDecimal.valueOf(100) : BigDecimal.ZERO;
        }
        return BigDecimal.valueOf(current - last)
                .multiply(BigDecimal.valueOf(100))
                .divide(BigDecimal.valueOf(last), 2, RoundingMode.HALF_UP);
    }
    /**
     * 计算环比增长率(BigDecimal版本)
     */
    private BigDecimal calculateRate(BigDecimal current, BigDecimal last) {
        if (last.compareTo(BigDecimal.ZERO) == 0) {
            return current.compareTo(BigDecimal.ZERO) > 0 ? BigDecimal.valueOf(100) : BigDecimal.ZERO;
        }
        return current.subtract(last)
                .multiply(BigDecimal.valueOf(100))
                .divide(last, 2, RoundingMode.HALF_UP);
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/ICBCNotifyServiceImpl.java
@@ -119,7 +119,7 @@
            String finalPayOrderCode = bizContent.getString("out_trade_no");
            String returnCode = bizContent.getString("return_code");
            msgId = bizContent.getString("msg_id");
            String orderId = bizContent.getString("order_id");
            // 验签
            if (validateSignature(request, icbcConfig.getNotifyUrlMethod() + "/big-gift/final-pay-notify")) {
                signResult = true;
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TAppUserVisitServiceImpl.java
@@ -6,6 +6,8 @@
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
/**
 * <p>
 * 用户访问记录 服务实现类
@@ -17,4 +19,18 @@
@Service
public class TAppUserVisitServiceImpl extends ServiceImpl<TAppUserVisitMapper, TAppUserVisit> implements TAppUserVisitService {
    @Override
    public void recordVisit(Integer userId) {
        LocalDate today = LocalDate.now();
        Long count = this.lambdaQuery()
                .eq(TAppUserVisit::getAppUserId, userId)
                .eq(TAppUserVisit::getVisitDate, today)
                .count();
        if (count == 0) {
            TAppUserVisit visit = new TAppUserVisit();
            visit.setAppUserId(userId);
            visit.setVisitDate(today);
            this.save(visit);
        }
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TIntegralSettlementServiceImpl.java
@@ -8,6 +8,7 @@
import com.google.common.collect.Sets;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.system.dto.TIntegralSettlementDTO;
import com.ruoyi.system.export.IntegralSettlementImport;
import com.ruoyi.system.model.TAppUser;
@@ -68,8 +69,12 @@
            Map<String, Integer> allAppUserIdMap = getAllAppUserIdMap(list);
            List<TIntegralSettlement> saveList = list.stream().map(item -> {
                TIntegralSettlement model = new TIntegralSettlement();
                model.setSettlementInterval(dto.getSettlementInterval());
                model.setSettlementStartTime(dto.getSettlementStartTime());
                model.setSettlementEndTime(dto.getSettlementEndTime());
                model.setAppUserId(allAppUserIdMap.get(item.getCode()));
                model.setTotalSettlementIntegral(item.getTotalSettlementIntegral());
                model.setCreateUserId(SecurityUtils.getUserId().intValue());
                model.setRemark(item.getRemark());
                return model;
            }).collect(Collectors.toList());
@@ -102,7 +107,7 @@
        TIntegralSettlementVO tIntegralSettlementVO = BeanUtil.copyProperties(integralSettlement, TIntegralSettlementVO.class);
        if (Objects.nonNull(integralSettlement)) {
            List<TIntegralSettlement> list = this.lambdaQuery().eq(TIntegralSettlement::getSettlementInterval, integralSettlement.getSettlementInterval()).list();
            BigDecimal total = list.stream().map(TIntegralSettlement::getTotalSettlementIntegral).reduce(BigDecimal.ZERO, BigDecimal::add);
            Integer total = list.stream().mapToInt(TIntegralSettlement::getTotalSettlementIntegral).sum();
            tIntegralSettlementVO.setTotalSettlementIntegral(total);
            SysUser sysUser = sysUserService.selectUserById(Long.valueOf(integralSettlement.getCreateUserId()));
            tIntegralSettlementVO.setCreateUserName(Objects.nonNull(sysUser) ? sysUser.getNickName() : "");
@@ -112,6 +117,11 @@
    @Override
    public Page<TIntegralSettlementDetailVO> getSettlementDetailPageList(TIntegralSettlementDetailQuery query) {
        TIntegralSettlement integralSettlement = getById(query.getId());
        if (Objects.isNull(integralSettlement)) {
            throw new ServiceException("积分结算记录不存在");
        }
        query.setSettlementInterval(integralSettlement.getSettlementInterval());
        Page<TIntegralSettlementDetailVO> page = new Page<>(query.getPageNum(), query.getPageSize());
        page.setRecords(baseMapper.getSettlementDetailPageList(page, query));
        return page;
ruoyi-system/src/main/java/com/ruoyi/system/vo/DashboardStatisticsVO.java
New file
@@ -0,0 +1,40 @@
package com.ruoyi.system.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.List;
/**
 * 数据统计面板返回对象
 *
 * @author xiaochen
 * @since 2025-12-23
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ApiModel(value = "数据统计面板返回对象")
public class DashboardStatisticsVO implements Serializable {
    private static final long serialVersionUID = 1L;
    @ApiModelProperty(value = "客户访问量统计")
    private StatisticsItemVO visitStats;
    @ApiModelProperty(value = "订单数统计")
    private StatisticsItemVO orderCountStats;
    @ApiModelProperty(value = "订单金额统计")
    private StatisticsItemVO orderAmountStats;
    @ApiModelProperty(value = "月度订单统计")
    private List<MonthlyOrderStatisticsVO> monthlyStats;
}
ruoyi-system/src/main/java/com/ruoyi/system/vo/MonthlyOrderStatisticsVO.java
New file
@@ -0,0 +1,35 @@
package com.ruoyi.system.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.math.BigDecimal;
/**
 * 月度订单统计
 *
 * @author xiaochen
 * @since 2025-12-23
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
@ApiModel(value = "月度订单统计")
public class MonthlyOrderStatisticsVO implements Serializable {
    private static final long serialVersionUID = 1L;
    @ApiModelProperty(value = "月份")
    private String month;
    @ApiModelProperty(value = "订单数")
    private Long orderCount;
    @ApiModelProperty(value = "订单金额")
    private BigDecimal orderAmount;
}
ruoyi-system/src/main/java/com/ruoyi/system/vo/StatisticsItemVO.java
New file
@@ -0,0 +1,38 @@
package com.ruoyi.system.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.List;
/**
 * 统计项(总量+环比+趋势)
 *
 * @author xiaochen
 * @since 2025-12-23
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ApiModel(value = "统计项")
public class StatisticsItemVO implements Serializable {
    private static final long serialVersionUID = 1L;
    @ApiModelProperty(value = "总量")
    private BigDecimal total;
    @ApiModelProperty(value = "环比增长率(百分比)")
    private BigDecimal rate;
    @ApiModelProperty(value = "趋势数据")
    private List<TrendDataVO> trend;
}
ruoyi-system/src/main/java/com/ruoyi/system/vo/TIntegralSettlementDetailVO.java
@@ -30,7 +30,7 @@
    private String code;
    @ApiModelProperty(value = "结算积分")
    private BigDecimal totalSettlementIntegral;
    private Integer totalSettlementIntegral;
    @ApiModelProperty("备注")
    private String remark;
ruoyi-system/src/main/java/com/ruoyi/system/vo/TIntegralSettlementVO.java
@@ -38,6 +38,6 @@
    private LocalDateTime createTime;
    @ApiModelProperty(value = "合计结算积分")
    private BigDecimal totalSettlementIntegral;
    private Integer totalSettlementIntegral;
}
ruoyi-system/src/main/java/com/ruoyi/system/vo/TrendDataVO.java
New file
@@ -0,0 +1,37 @@
package com.ruoyi.system.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.math.BigDecimal;
/**
 * 趋势数据点
 *
 * @author xiaochen
 * @since 2025-12-23
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
@ApiModel(value = "趋势数据点")
public class TrendDataVO implements Serializable {
    private static final long serialVersionUID = 1L;
    @ApiModelProperty(value = "日期")
    private String date;
    @ApiModelProperty(value = "数值")
    private BigDecimal value;
    public TrendDataVO(String date, Long value) {
        this.date = date;
        this.value = value != null ? BigDecimal.valueOf(value) : BigDecimal.ZERO;
    }
}
ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml
@@ -23,6 +23,8 @@
        <result property="updateBy"     column="update_by"    />
        <result property="updateTime"   column="update_time"  />
        <result property="remark"       column="remark"       />
        <result property="companyId"       column="company_id"       />
        <result property="roleType"       column="role_type"       />
        <association property="dept"    javaType="SysDept"         resultMap="deptResult" />
        <collection  property="roles"   javaType="java.util.List"  resultMap="RoleResult" />
    </resultMap>
@@ -50,7 +52,7 @@
        select u.user_id, u.dept_id, u.user_name, u.nick_name, u.email, u.avatar, u.phonenumber, u.password,
               u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time, u.remark,
        d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.status as dept_status,
        r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.status as role_status
        r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.status as role_status,u.company_id,role_type
        from sys_user u
            left join sys_dept d on u.dept_id = d.dept_id
            left join sys_user_role ur on u.user_id = ur.user_id
ruoyi-system/src/main/resources/mapper/system/TIntegralSettlementMapper.xml
@@ -35,11 +35,11 @@
        LEFT JOIN sys_user su ON t.create_user_id = su.user_id
        <where>
            <if test="query.settlementInterval != null">
                AND t.settlement_interval = #{query.settlementInterval}
                AND t.settlement_interval LIKE CONCAT('%', #{query.settlementInterval},'%')
            </if>
        </where>
        GROUP BY t.settlement_interval, t.create_user_id, su.nick_name
        ORDER BY t.settlement_interval DESC, MAX(t.create_time) DESC
        ORDER BY MAX(t.create_time) DESC
    </select>
    <select id="getSettlementDetailPageList" resultType="com.ruoyi.system.vo.TIntegralSettlementDetailVO">
@@ -53,6 +53,9 @@
        FROM t_integral_settlement t
        LEFT JOIN t_app_user tau ON tau.id = t.app_user_id
        <where>
            <if test="query.settlementInterval != null and query.settlementInterval != ''">
                AND t.settlement_interval = #{query.settlementInterval}
            </if>
            <if test="query.name != null and query.name != ''">
                AND tau.name LIKE CONCAT('%', #{query.name}, '%')
            </if>