rk
7 小时以前 cf1d82548a1bd8155ffe9b486df8167aa9e63a7d
server/services/src/main/java/com/doumee/service/business/impl/ReportServiceImpl.java
@@ -22,6 +22,8 @@
import com.doumee.dao.business.model.MemberRides;
import com.doumee.dao.business.model.MemberRidesTrack;
import com.doumee.dao.business.vo.BikeIncomeStatVO;
import com.doumee.dao.business.vo.BikeUsageStatVO;
import com.doumee.dao.business.vo.DashboardVO;
import com.doumee.dao.business.vo.IncomeDailyVO;
import com.doumee.dao.business.vo.IncomeStatVO;
import com.doumee.dao.business.vo.OperationCenterVO;
@@ -30,6 +32,7 @@
import com.doumee.dao.business.vo.OrderRideTrackVO;
import com.doumee.dao.business.vo.OrderRidesDetailVO;
import com.doumee.dao.business.vo.OverviewStatVO;
import com.doumee.dao.business.vo.PackageSourceStatVO;
import com.doumee.dao.business.web.request.BikeIncomeQueryDTO;
import com.doumee.dao.business.web.request.OperationOrderQueryDTO;
import com.doumee.service.business.ReportService;
@@ -41,6 +44,7 @@
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
@@ -89,6 +93,9 @@
    /** 百分比基数(增长率 = (本期 - 对比期) / 对比期 × 100) */
    private static final BigDecimal PERCENT_BASE = new BigDecimal("100");
    /** 支付方式:抖音券核销(套餐销售来源识别用) */
    private static final int PAY_WAY_DOUYIN = 2;
    @Override
    public OverviewStatVO overview() {
@@ -148,7 +155,23 @@
            incomeByParam.merge(o.getParamId(), amount, BigDecimal::add);
        }
        // 5. 组装结果:车型名 + 大类 + 收入(分→元,2位),按收入降序
        // 5. 按 paramId 统计每类车型的车辆数量(bikes 表未删除),一次分组查询避免 N 次 count
        Map<String, Long> bikeCountByParam;
        if (incomeByParam.isEmpty()) {
            // 无有收入的车型时跳过查询:空集合传给 in() 会生成 "IN ()",PostgreSQL 语法错误
            bikeCountByParam = Collections.emptyMap();
        } else {
            bikeCountByParam = bikesMapper.selectList(
                    new QueryWrapper<Bikes>().lambda()
                            .select(Bikes::getParamId)
                            .eq(Bikes::getIsdeleted, Constants.ZERO)
                            .in(Bikes::getParamId, incomeByParam.keySet()))
                    .stream()
                    .filter(b -> b.getParamId() != null)
                    .collect(Collectors.groupingBy(Bikes::getParamId, Collectors.counting()));
        }
        // 6. 组装结果:车型名 + 大类 + 收入(分→元,2位)+ 车辆数,按收入降序
        List<BikeIncomeStatVO> result = new ArrayList<>();
        for (Map.Entry<String, BigDecimal> e : incomeByParam.entrySet()) {
            BaseParam param = paramMap.get(e.getKey());
@@ -157,6 +180,7 @@
            vo.setParamName(param == null ? "未知车型" : param.getName());
            vo.setCategory(param == null ? "未知" : categoryOf(param.getType()));
            vo.setIncome(e.getValue().divide(CENT_PER_YUAN, 2, BigDecimal.ROUND_HALF_UP));
            vo.setBikeCount(bikeCountByParam.getOrDefault(e.getKey(), 0L));
            result.add(vo);
        }
        result.sort(Comparator.comparing(BikeIncomeStatVO::getIncome).reversed());
@@ -255,7 +279,7 @@
        BigDecimal totalCents = BigDecimal.ZERO;
        for (Goodsorder o : orders) {
            BigDecimal amount = o.getCloseMoney() == null ? BigDecimal.ZERO : o.getCloseMoney();
            sumByDay.merge(DateUtil.getShortDateStr(o.getPayDate()), amount, BigDecimal::add);
            sumByDay.merge(DateUtil.getDateLong(o.getPayDate()), amount, BigDecimal::add);
            totalCents = totalCents.add(amount);
        }
@@ -265,7 +289,7 @@
        List<IncomeDailyVO> dailyList = new ArrayList<>();
        for (Date d : DateUtil.getDateList(DateUtil.getStartOfDay(start), DateUtil.getStartOfDay(end))) {
            IncomeDailyVO vo = new IncomeDailyVO();
            vo.setDate(DateUtil.getShortDateStr(d));
            vo.setDate(DateUtil.getDateLong(d));
            BigDecimal daySum = sumByDay.getOrDefault(vo.getDate(), BigDecimal.ZERO);
            // 分→元,2位小数
            vo.setIncome(daySum.divide(CENT_PER_YUAN, 2, BigDecimal.ROUND_HALF_UP));
@@ -617,4 +641,170 @@
        }
        return "未知";
    }
    @Override
    public IncomeStatVO incomeStat30() {
        // 近30天(含今天共30天),复用 incomeStat 既有实现与口径
        BikeIncomeQueryDTO query = new BikeIncomeQueryDTO();
        query.setDateType(3);
        return incomeStat(query);
    }
    @Override
    public BikeUsageStatVO bikeUsageStat() {
        // status:0 空闲 / 1 使用中;禁用(3)不计入;type:0 自行车 / 1 电动车
        BikeUsageStatVO vo = new BikeUsageStatVO();
        vo.setBikeIdle(countBikeByStatus(Constants.ZERO, Constants.ZERO));
        vo.setBikeInUse(countBikeByStatus(Constants.ZERO, Constants.ONE));
        vo.setEleBikeIdle(countBikeByStatus(Constants.ONE, Constants.ZERO));
        vo.setEleBikeInUse(countBikeByStatus(Constants.ONE, Constants.ONE));
        return vo;
    }
    /**
     * 按车辆类型 + 状态统计未删除车辆数量。
     *
     * @param type   车辆类型 0 自行车 / 1 电动车
     * @param status 车辆状态 0 空闲 / 1 使用中
     * @return 满足条件的车辆数
     */
    private long countBikeByStatus(Integer type, Integer status) {
        return bikesMapper.selectCount(
                new QueryWrapper<Bikes>().lambda()
                        .eq(Bikes::getType, type)
                        .eq(Bikes::getStatus, status)
                        .eq(Bikes::getIsdeleted, Constants.ZERO));
    }
    @Override
    public PackageSourceStatVO packageSourceStat() {
        // 本月/本年起止(pay_date 落在区间内);goodsorder type=1 套餐卡购买 + payStatus=1 已支付
        Calendar cal = Calendar.getInstance();
        Date monthStart = DateUtil.getStartOfDay(getFirstMs(cal, Calendar.MONTH));
        Date monthEnd = DateUtil.getEndOfDay(new Date());
        Date yearStart = DateUtil.getStartOfDay(getFirstMs(cal, Calendar.YEAR));
        Date yearEnd = DateUtil.getEndOfDay(new Date());
        PackageSourceStatVO vo = new PackageSourceStatVO();
        vo.setMonth(countPackageSource(monthStart, monthEnd));
        vo.setYear(countPackageSource(yearStart, yearEnd));
        return vo;
    }
    /**
     * 统计指定时段内套餐销售来源数量。
     * <p>抖音兑换 = payWay 2,小程序购买 = payWay 0(微信)。
     *
     * @param start 起始时间(含)
     * @param end   结束时间(含)
     * @return 抖音/小程序套餐数量
     */
    private PackageSourceStatVO.PeriodCount countPackageSource(Date start, Date end) {
        List<Goodsorder> orders = goodsorderMapper.selectList(
                new QueryWrapper<Goodsorder>().lambda()
                        .select(Goodsorder::getPayWay)
                        .eq(Goodsorder::getType, Constants.ONE)
                        .eq(Goodsorder::getPayStatus, Constants.ONE)
                        .eq(Goodsorder::getIsdeleted, Constants.ZERO)
                        .ge(Goodsorder::getPayDate, start)
                        .le(Goodsorder::getPayDate, end));
        long douyin = 0L;
        long mini = 0L;
        for (Goodsorder o : orders) {
            if (o.getPayWay() == null) {
                continue;
            }
            if (o.getPayWay() == PAY_WAY_DOUYIN) {
                douyin++;
            } else if (o.getPayWay() == Constants.ZERO) {
                mini++;
            }
        }
        PackageSourceStatVO.PeriodCount pc = new PackageSourceStatVO.PeriodCount();
        pc.setDouyinCount(douyin);
        pc.setMiniCount(mini);
        return pc;
    }
    @Override
    public DashboardVO dashboard() {
        DashboardVO vo = new DashboardVO();
        // 各时段起止:本月/昨日/今日
        Date now = new Date();
        Calendar cal = Calendar.getInstance();
        Date monthStart = DateUtil.getStartOfDay(getFirstMs(cal, Calendar.MONTH));
        Date monthEnd = DateUtil.getEndOfDay(now);
        Date yesterdayStart = DateUtil.getStartOfDay(DateUtil.increaseDay(now, -1));
        Date yesterdayEnd = DateUtil.getEndOfDay(DateUtil.increaseDay(now, -1));
        Date todayStart = DateUtil.getStartOfDay(now);
        Date todayEnd = DateUtil.getEndOfDay(now);
        // 收益(口径同 incomeStat:type=0 押金 + status=4 已结算 的 closeMoney),复用 sumClosedMoney
        vo.setMonthIncome(sumClosedMoney(monthStart, monthEnd));
        vo.setYesterdayIncome(sumClosedMoney(yesterdayStart, yesterdayEnd));
        vo.setTodayIncome(sumClosedMoney(todayStart, todayEnd));
        // 订单数:已支付(payStatus=1)订单计数
        vo.setMonthOrderCount(countPaidOrders(monthStart, monthEnd));
        vo.setYesterdayOrderCount(countPaidOrders(yesterdayStart, yesterdayEnd));
        vo.setTodayOrderCount(countPaidOrders(todayStart, todayEnd));
        // 车辆:仅返回未删除总数
        vo.setTotalBikeCount((long) bikesMapper.selectCount(
                new QueryWrapper<Bikes>().lambda().eq(Bikes::getIsdeleted, Constants.ZERO)));
        // 客户数:总会员=未删除全部;今日/昨日新增按 create_date 落在对应区间(与 overview 口径一致)
        vo.setTotalMemberCount((long) memberMapper.selectCount(
                new QueryWrapper<Member>().lambda().eq(Member::getIsdeleted, Constants.ZERO)));
        vo.setYesterdayNewMember((long) memberMapper.selectCount(
                new QueryWrapper<Member>().lambda()
                        .eq(Member::getIsdeleted, Constants.ZERO)
                        .ge(Member::getCreateDate, yesterdayStart)
                        .le(Member::getCreateDate, yesterdayEnd)));
        vo.setTodayNewMember((long) memberMapper.selectCount(
                new QueryWrapper<Member>().lambda()
                        .eq(Member::getIsdeleted, Constants.ZERO)
                        .ge(Member::getCreateDate, todayStart)));
        return vo;
    }
    /**
     * 统计指定时段内已支付订单数量(payStatus=1)。
     *
     * @param start 起始时间(含)
     * @param end   结束时间(含)
     * @return 已支付订单数
     */
    private long countPaidOrders(Date start, Date end) {
        return goodsorderMapper.selectCount(
                new QueryWrapper<Goodsorder>().lambda()
                        .eq(Goodsorder::getPayStatus, Constants.ONE)
                        .eq(Goodsorder::getIsdeleted, Constants.ZERO)
                        .ge(Goodsorder::getPayDate, start)
                        .le(Goodsorder::getPayDate, end));
    }
    /**
     * 取本月/本年第一天的时间原值(时分秒归零前的毫秒),用于构造区间起始。
     * <p>用 Calendar 把「当月1日 00:00:00.000」或「当年1月1日 00:00:00.000」取出。
     *
     * @param cal   日历(会被修改)
     * @param field Calendar.MONTH 本月第一天 / Calendar.YEAR 本年第一天
     * @return 区间起始时间
     */
    private Date getFirstMs(Calendar cal, int field) {
        cal.setTime(new Date());
        if (field == Calendar.MONTH) {
            cal.set(Calendar.DAY_OF_MONTH, 1);
        } else {
            cal.set(Calendar.MONTH, Calendar.JANUARY);
            cal.set(Calendar.DAY_OF_MONTH, 1);
        }
        cal.set(Calendar.HOUR_OF_DAY, 0);
        cal.set(Calendar.MINUTE, 0);
        cal.set(Calendar.SECOND, 0);
        cal.set(Calendar.MILLISECOND, 0);
        return cal.getTime();
    }
}