MrShi
17 小时以前 9eeb62c02a7b3c7b95c20678b6a9c74e7f12f943
server/services/src/main/java/com/doumee/service/business/impl/ReportServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,793 @@
package com.doumee.service.business.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.doumee.core.constants.Constants;
import com.doumee.core.constants.ResponseStatus;
import com.doumee.core.exception.BusinessException;
import com.doumee.core.model.PageData;
import com.doumee.core.model.PageWrap;
import com.doumee.core.utils.DateUtil;
import com.doumee.dao.business.BaseParamMapper;
import com.doumee.dao.business.BikesMapper;
import com.doumee.dao.business.GoodsorderMapper;
import com.doumee.dao.business.MemberMapper;
import com.doumee.dao.business.MemberRidesTrackMapper;
import com.doumee.dao.business.join.MemberRidesJoinMapper;
import com.doumee.dao.business.model.BaseParam;
import com.doumee.dao.business.model.Bikes;
import com.doumee.dao.business.model.Goodsorder;
import com.doumee.dao.business.model.Member;
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;
import com.doumee.dao.business.vo.OperationOrderVO;
import com.doumee.dao.business.vo.OrderRideItemVO;
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;
import com.github.yulichang.wrapper.MPJLambdaWrapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
/**
 * æ•°æ®æŠ¥è¡¨ Service å®žçް(web ç«¯:概览统计 + æ”¶å…¥è½¦åž‹åˆ†æž)。
 *
 * @author rk
 * @date 2026/06/26
 */
@Slf4j
@Service
public class ReportServiceImpl implements ReportService {
    @Autowired
    private MemberMapper memberMapper;
    @Autowired
    private MemberRidesJoinMapper memberRidesJoinMapper;
    @Autowired
    private BikesMapper bikesMapper;
    @Autowired
    private GoodsorderMapper goodsorderMapper;
    @Autowired
    private BaseParamMapper baseParamMapper;
    /** ç”µè½¦éª‘行轨迹 Mapper(自行车走 MQTT æ— è½¨è¿¹,仅电车有数据) */
    @Autowired
    private MemberRidesTrackMapper memberRidesTrackMapper;
    /** æ—¶æ®µå¿«æ·ç±»åž‹ â†’ è¿‘ N å¤©:dateType 1→7、2→15、3→30(均含今天) */
    private static final Map<Integer, Integer> RECENT_DAYS = new LinkedHashMap<>();
    static {
        RECENT_DAYS.put(1, 7);
        RECENT_DAYS.put(2, 15);
        RECENT_DAYS.put(3, 30);
    }
    /** ç»“算金额分→元换算除数 */
    private static final BigDecimal CENT_PER_YUAN = new BigDecimal("100");
    /** ç™¾åˆ†æ¯”基数(增长率 = (本期 - å¯¹æ¯”期) / å¯¹æ¯”期 Ã— 100) */
    private static final BigDecimal PERCENT_BASE = new BigDecimal("100");
    /** æ”¯ä»˜æ–¹å¼:抖音券核销(套餐销售来源识别用) */
    private static final int PAY_WAY_DOUYIN = 2;
    @Override
    public OverviewStatVO overview() {
        OverviewStatVO vo = new OverviewStatVO();
        // æ€»æ³¨å†Œç”¨æˆ·:未删除的全部用户
        vo.setTotalMembers((long) memberMapper.selectCount(
                new QueryWrapper<Member>().lambda().eq(Member::getIsdeleted, Constants.ZERO)));
        // ä»Šæ—¥æ–°å¢ž:创建时间 â‰¥ ä»Šæ—¥0点
        Date todayStart = DateUtil.getStartOfDay(new Date());
        vo.setTodayMembers((long) memberMapper.selectCount(
                new QueryWrapper<Member>().lambda()
                        .eq(Member::getIsdeleted, Constants.ZERO)
                        .ge(Member::getCreateDate, todayStart)));
        // è‡ªè¡Œè½¦æ•°é‡(type=0),电动车数量(type=1);均含全部未删除车辆(含禁用)
        vo.setBikeCount((long) bikesMapper.selectCount(
                new QueryWrapper<Bikes>().lambda()
                        .eq(Bikes::getType, Constants.ZERO)
                        .eq(Bikes::getIsdeleted, Constants.ZERO)));
        vo.setEleBikeCount((long) bikesMapper.selectCount(
                new QueryWrapper<Bikes>().lambda()
                        .eq(Bikes::getType, Constants.ONE)
                        .eq(Bikes::getIsdeleted, Constants.ZERO)));
        return vo;
    }
    @Override
    public List<BikeIncomeStatVO> bikeIncome(BikeIncomeQueryDTO query) {
        // 1. è§£æžæ—¶æ®µ:1/2/3 è¿‘ N å¤©(含今天共 N å¤©),4 è‡ªå®šä¹‰
        DateRange range = resolveRange(query);
        Date start = range.start;
        Date end = range.end;
        // 2. è½¦åž‹å­—å…¸:base_param type=3 å•车 / 4 ç”µè½¦,供车型名 + å¤§ç±»å½’ç±»
        List<BaseParam> paramList = baseParamMapper.selectList(
                new QueryWrapper<BaseParam>().lambda()
                        .eq(BaseParam::getIsdeleted, Constants.ZERO)
                        .in(BaseParam::getType, Constants.THREE, Constants.FOUR));
        Map<String, BaseParam> paramMap = paramList.stream()
                .collect(Collectors.toMap(BaseParam::getId, p -> p, (a, b) -> a));
        // 3. æ—¶æ®µå†…已结算的租车押金订单(查询模式参考后台 getBikeIncomeReportVOList:
        //    type=0 æŠ¼é‡‘类、status=4 å·²ç»“算、paramId éžç©ºã€payDate è½åœ¨åŒºé—´å†…)
        List<Goodsorder> orders = goodsorderMapper.selectList(
                new QueryWrapper<Goodsorder>().lambda()
                        .eq(Goodsorder::getType, Constants.ZERO)
                        .eq(Goodsorder::getStatus, Constants.FOUR)
                        .eq(Goodsorder::getIsdeleted, Constants.ZERO)
                        .isNotNull(Goodsorder::getParamId)
                        .ne(Goodsorder::getParamId, StringUtils.EMPTY)
                        .ge(Goodsorder::getPayDate, start)
                        .le(Goodsorder::getPayDate, end));
        // 4. æŒ‰ paramId(车型)分组合计结算金额 closeMoney(单位:分)
        Map<String, BigDecimal> incomeByParam = new LinkedHashMap<>();
        for (Goodsorder o : orders) {
            BigDecimal amount = o.getCloseMoney() == null ? BigDecimal.ZERO : o.getCloseMoney();
            incomeByParam.merge(o.getParamId(), amount, BigDecimal::add);
        }
        // 5. ç»„装结果:车型名 + å¤§ç±» + æ”¶å…¥(分→元,2位),按收入降序
        List<BikeIncomeStatVO> result = new ArrayList<>();
        for (Map.Entry<String, BigDecimal> e : incomeByParam.entrySet()) {
            BaseParam param = paramMap.get(e.getKey());
            BikeIncomeStatVO vo = new BikeIncomeStatVO();
            vo.setParamId(e.getKey());
            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));
            result.add(vo);
        }
        result.sort(Comparator.comparing(BikeIncomeStatVO::getIncome).reversed());
        return result;
    }
    /**
     * æŒ‰ base_param.type æ´¾ç”Ÿè½¦è¾†å¤§ç±»:3 è‡ªè¡Œè½¦ / 4 ç”µåŠ¨è½¦,其余归"未知"。
     *
     * @param type base_param.type(3 å•车类型 / 4 ç”µè½¦ç±»åž‹)
     * @return å¤§ç±»ä¸­æ–‡å
     */
    private String categoryOf(Integer type) {
        if (type == null) {
            return "未知";
        }
        // æ³¨æ„:Constants.THREE æ˜¯ Integer、Constants.FOUR æ˜¯ int(类型不一致),
        // ç»Ÿä¸€æ‹†æˆ int æ¯”较,避开对基本类型调用 equals / åŒ…装类引用比较的坑
        int t = type;
        if (t == Constants.THREE) {
            return "自行车";
        }
        if (t == Constants.FOUR) {
            return "电动车";
        }
        return "未知";
    }
    /**
     * è§£æžåŽçš„æŸ¥è¯¢æ—¶æ®µ(均含端点)。
     */
    private static final class DateRange {
        /** èµ·å§‹æ—¶é—´(含) */
        final Date start;
        /** ç»“束时间(含) */
        final Date end;
        DateRange(Date start, Date end) {
            this.start = start;
            this.end = end;
        }
    }
    /**
     * è§£æžæŸ¥è¯¢æ—¶æ®µ:dateType 1/2/3 â†’ è¿‘ N å¤©(含今天共 N å¤©),4 â†’ è‡ªå®šä¹‰èµ·æ­¢(均含)。
     * <p>{@link #bikeIncome} ä¸Ž {@link #incomeStat} å…±ç”¨,保证两端时段口径一致。
     *
     * @param query æ—¶æ®µæŸ¥è¯¢å…¥å‚
     * @return è§£æžåŽçš„ [start, end] åŒºé—´
     */
    private DateRange resolveRange(BikeIncomeQueryDTO query) {
        if (query == null) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST);
        }
        Integer dateType = query.getDateType();
        Date start;
        Date end;
        if (dateType != null && RECENT_DAYS.containsKey(dateType)) {
            // å¿«æ·:近 N å¤©,含今天共 N å¤©,起始 = ä»Šå¤©å¾€å‰ N-1 å¤©çš„0点
            int days = RECENT_DAYS.get(dateType);
            end = DateUtil.getEndOfDay(new Date());
            start = DateUtil.getStartOfDay(DateUtil.increaseDay(new Date(), -(days - 1)));
        } else if (dateType != null && dateType == 4) {
            // è‡ªå®šä¹‰:起止均含,校验非空且 start<=end
            if (query.getStartDate() == null || query.getEndDate() == null
                    || query.getStartDate().getTime() > query.getEndDate().getTime()) {
                throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "自定义时段起止日期不合法");
            }
            start = query.getStartDate();
            end = query.getEndDate();
        } else {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "时段类型不合法");
        }
        return new DateRange(start, end);
    }
    @Override
    public IncomeStatVO incomeStat(BikeIncomeQueryDTO query) {
        // 1. è§£æžæ—¶æ®µ
        DateRange range = resolveRange(query);
        Date start = range.start;
        Date end = range.end;
        // 2. æŸ¥è¯¢æœ¬æœŸå·²ç»“算的租车押金订单(查询模式参考后台 getTotalData / getBikeIncomeReportVOList:
        //    type=0 æŠ¼é‡‘类、status=4 å·²ç»“算、payDate è½åœ¨åŒºé—´å†…)。收入统计不限车型,故不约束 paramId
        List<Goodsorder> orders = goodsorderMapper.selectList(
                new QueryWrapper<Goodsorder>().lambda()
                        .eq(Goodsorder::getType, Constants.ZERO)
                        .eq(Goodsorder::getStatus, Constants.FOUR)
                        .eq(Goodsorder::getIsdeleted, Constants.ZERO)
                        .ge(Goodsorder::getPayDate, start)
                        .le(Goodsorder::getPayDate, end));
        // 3. æŒ‰ payDate çš„æ—¥æœŸ(yyyy-MM-dd)分组汇总 closeMoney(单位:分),同时累计区间总额
        Map<String, BigDecimal> sumByDay = new LinkedHashMap<>();
        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);
            totalCents = totalCents.add(amount);
        }
        // 4. ç”Ÿæˆå®Œæ•´æ—¥æœŸåºåˆ—(柱状图横轴连续),无数据日补 0。
        //    æ³¨:DateUtil.getDateList ç”¨ dEnd.after(begin) æ¯”较,若两端时分秒不一致会多算一天,
        //    æ•…统一规整到当天 0 ç‚¹å†ç”Ÿæˆåºåˆ—
        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));
            BigDecimal daySum = sumByDay.getOrDefault(vo.getDate(), BigDecimal.ZERO);
            // åˆ†â†’å…ƒ,2位小数
            vo.setIncome(daySum.divide(CENT_PER_YUAN, 2, BigDecimal.ROUND_HALF_UP));
            dailyList.add(vo);
        }
        // 5. åŒºé—´ç´¯è®¡æ”¶å…¥(复用已查的本期数据,分→元,避免重复查询)
        BigDecimal totalIncome = totalCents.divide(CENT_PER_YUAN, 2, BigDecimal.ROUND_HALF_UP);
        IncomeStatVO result = new IncomeStatVO();
        result.setDailyList(dailyList);
        result.setTotalIncome(totalIncome);
        // 6. çŽ¯æ¯”:紧邻前一等长区间(整体平移 -N å¤©,N=区间天数)。对比期只汇总金额,不需每日明细
        int spanDays = DateUtil.daysBetweenDates(start, end) + 1;
        BigDecimal chainAmount = sumClosedMoney(
                DateUtil.increaseDay(start, -spanDays), DateUtil.increaseDay(end, -spanDays));
        result.setChainAmount(chainAmount);
        result.setChainRate(growthRate(totalIncome, chainAmount));
        // 7. åŒæ¯”:去年同期同长度区间(平移 -1 å¹´,increaseYear æŒ‰ Calendar ç²¾ç¡®å¹³ç§»,闰年不失真)
        BigDecimal yearOnYearAmount = sumClosedMoney(
                DateUtil.increaseYear(start, -1), DateUtil.increaseYear(end, -1));
        result.setYearOnYearAmount(yearOnYearAmount);
        result.setYearOnYearRate(growthRate(totalIncome, yearOnYearAmount));
        return result;
    }
    /**
     * æ±‡æ€»æŒ‡å®šæ—¶æ®µå†…已结算租车押金订单的结算收入(closeMoney ä¹‹å’Œ,分→元)。
     * <p>供累计收入、环比、同比复用,统一收入口径;只 select closeMoney åˆ—以减少数据传输。
     *
     * @param start èµ·å§‹æ—¶é—´(含)
     * @param end   ç»“束时间(含)
     * @return åŒºé—´ç»“ç®—æ”¶å…¥(元,2位小数);无数据返回 0
     */
    private BigDecimal sumClosedMoney(Date start, Date end) {
        List<Goodsorder> orders = goodsorderMapper.selectList(
                new QueryWrapper<Goodsorder>().lambda()
                        .select(Goodsorder::getCloseMoney)
                        .eq(Goodsorder::getType, Constants.ZERO)
                        .eq(Goodsorder::getStatus, Constants.FOUR)
                        .eq(Goodsorder::getIsdeleted, Constants.ZERO)
                        .ge(Goodsorder::getPayDate, start)
                        .le(Goodsorder::getPayDate, end));
        BigDecimal sum = BigDecimal.ZERO;
        for (Goodsorder o : orders) {
            if (o.getCloseMoney() != null) {
                sum = sum.add(o.getCloseMoney());
            }
        }
        // åˆ†â†’å…ƒ,2位小数
        return sum.divide(CENT_PER_YUAN, 2, BigDecimal.ROUND_HALF_UP);
    }
    /**
     * è®¡ç®—增长率:(current - base) / base Ã— 100,保留2位小数。
     *
     * @param current æœ¬æœŸå€¼
     * @param base    å¯¹æ¯”期值
     * @return å¢žé•¿çއ(%);base ä¸º 0 æˆ– null æ—¶è¿”回 null(无法计算,前端显示"-")
     */
    private BigDecimal growthRate(BigDecimal current, BigDecimal base) {
        if (base == null || base.compareTo(BigDecimal.ZERO) == 0) {
            // å¯¹æ¯”期无收入,增长率无意义
            return null;
        }
        BigDecimal current0 = current == null ? BigDecimal.ZERO : current;
        return current0.subtract(base)
                .multiply(PERCENT_BASE)
                .divide(base, 2, BigDecimal.ROUND_HALF_UP);
    }
    @Override
    public OperationCenterVO operationCenter() {
        Date now = new Date();
        // ä»Šæ—¥èµ·æ­¢(含端点),用于"今日*"系列统计
        Date todayStart = DateUtil.getStartOfDay(now);
        Date todayEnd = DateUtil.getEndOfDay(now);
        OperationCenterVO vo = new OperationCenterVO();
        // ä»Šæ—¥æ—¥æœŸ + æ˜ŸæœŸå‡ 
        vo.setToday(DateUtil.getShortDateStr(now));
        vo.setWeekDay(DateUtil.getWeekOfDate(now));
        // ä»Šæ—¥è®¢å•总数:今日已支付订单(payStatus=1),含骑行押金(type=0)与套餐卡(type=1)
        vo.setTodayOrderCount((long) goodsorderMapper.selectCount(
                new QueryWrapper<Goodsorder>().lambda()
                        .eq(Goodsorder::getPayStatus, Constants.ONE)
                        .eq(Goodsorder::getIsdeleted, Constants.ZERO)
                        .ge(Goodsorder::getPayDate, todayStart)
                        .le(Goodsorder::getPayDate, todayEnd)));
        // è¿›è¡Œä¸­è®¢å•数量:骑行中(type=0 æŠ¼é‡‘、已支付未结算 status=1),实时在途,不限日期
        vo.setOngoingOrderCount((long) goodsorderMapper.selectCount(
                new QueryWrapper<Goodsorder>().lambda()
                        .eq(Goodsorder::getType, Constants.ZERO)
                        .eq(Goodsorder::getStatus, Constants.ONE)
                        .eq(Goodsorder::getIsdeleted, Constants.ZERO)));
        // ä»Šæ—¥å¥—餐收入(元):今日套餐卡购买(type=1、已支付)的 money ä¹‹å’Œ
        vo.setPackageIncome(sumMoney(Constants.ONE, todayStart, todayEnd));
        // ä»Šæ—¥æ€»æ”¶å…¥(元):与收入统计同口径(type=0 æŠ¼é‡‘ + status=4 å·²ç»“ç®— çš„ closeMoney),复用
        vo.setTotalIncome(sumClosedMoney(todayStart, todayEnd));
        return vo;
    }
    /**
     * æ±‡æ€»æŒ‡å®šè®¢å•类型在时段内已支付订单的支付金额(money ä¹‹å’Œ,分→元)。
     * <p>供运营中心"今日套餐收入"等按 type ç»Ÿè®¡æ”¯ä»˜é‡‘额使用。
     *
     * @param type  è®¢å•类型(0 ç§Ÿè½¦æŠ¼é‡‘ / 1 å¥—餐卡购买)
     * @param start èµ·å§‹æ—¶é—´(含)
     * @param end   ç»“束时间(含)
     * @return åŒºé—´æ”¯ä»˜é‡‘额(元,2位小数);无数据返回 0
     */
    private BigDecimal sumMoney(Integer type, Date start, Date end) {
        List<Goodsorder> orders = goodsorderMapper.selectList(
                new QueryWrapper<Goodsorder>().lambda()
                        .select(Goodsorder::getMoney)
                        .eq(Goodsorder::getType, type)
                        .eq(Goodsorder::getPayStatus, Constants.ONE)
                        .eq(Goodsorder::getIsdeleted, Constants.ZERO)
                        .ge(Goodsorder::getPayDate, start)
                        .le(Goodsorder::getPayDate, end));
        BigDecimal sum = BigDecimal.ZERO;
        for (Goodsorder o : orders) {
            if (o.getMoney() != null) {
                sum = sum.add(o.getMoney());
            }
        }
        // åˆ†â†’å…ƒ,2位小数
        return sum.divide(CENT_PER_YUAN, 2, BigDecimal.ROUND_HALF_UP);
    }
    @Override
    public PageData<OperationOrderVO> operationOrderPage(PageWrap<OperationOrderQueryDTO> pageWrap) {
        // åˆ†é¡µå¯¹è±¡
        IPage<OperationOrderVO> page = new Page<>(pageWrap.getPage(), pageWrap.getCapacity());
        OperationOrderQueryDTO model = pageWrap.getModel() == null
                ? new OperationOrderQueryDTO() : pageWrap.getModel();
        // â”€â”€ æ­¥éª¤1:分页主查询 â”€â”€
        // ä¸»è¡¨ goodsorder left join member(取手机号)+ left join base_param(取结算车型名);
        // è®¢å•状态(status)映射到 VO å†…部承载字段,供回填区分取数分支;不再带逐行取数子查询。
        // æ³¨:bikeType ç­›é€‰ä»ç”¨ inSql å­æŸ¥è¯¢â€”—它只作 WHERE æ¡ä»¶ã€æ•´é¡µæ‰§è¡Œä¸€æ¬¡,不是逐行投影,可保留。
        MPJLambdaWrapper<Goodsorder> wrapper = new MPJLambdaWrapper<Goodsorder>()
                // ä¸»è¡¨å­—段:订单主键、编号、结算时间
                .select(Goodsorder::getId, Goodsorder::getCode, Goodsorder::getCloseDate)
                // è®¢å•状态:内部承载字段(@JsonIgnore,不返回前端),用于回填分支判断
                .selectAs(Goodsorder::getStatus, OperationOrderVO::getOrderStatus)
                // ç»“算车型名:已结算订单直接 left join base_param å–(goodsorder.param_id→base_param.name)
                .selectAs(BaseParam::getName, OperationOrderVO::getSettleParamName)
                // ç”¨æˆ·æ‰‹æœºå·:left join member
                .selectAs(Member::getPhone, OperationOrderVO::getPhone)
                .leftJoin(Member.class, Member::getId, Goodsorder::getMemberId)
                .leftJoin(BaseParam.class, BaseParam::getId, Goodsorder::getParamId)
                // å›ºå®šæ¡ä»¶:默认只查押金订单(type=0)
                .eq(Goodsorder::getType, Constants.ZERO)
                .eq(Goodsorder::getIsdeleted, Constants.ZERO)
                // è®¢å•状态:1进行中 / 4已完结(可选)
                .eq(Objects.nonNull(model.getStatus()), Goodsorder::getStatus, model.getStatus())
                // ç”¨æˆ·æ‰‹æœºå·:模糊匹配(可选)
                .like(StringUtils.isNotBlank(model.getPhone()), Member::getPhone, model.getPhone())
                // è®¢å•类型:按骑行记录 member_rides.type ç­›é€‰(可选)
                .inSql(Objects.nonNull(model.getBikeType()), Goodsorder::getId,
                        "select ordre_id from member_rides where isdeleted = 0 and type = " + model.getBikeType())
                .orderByDesc(Goodsorder::getPayDate);
        IPage<OperationOrderVO> result = goodsorderMapper.selectJoinPage(page, OperationOrderVO.class, wrapper);
        List<OperationOrderVO> records = result.getRecords();
        // æ— æ•°æ®ç›´æŽ¥è¿”回,避免空 in() æŸ¥è¯¢
        if (records.isEmpty()) {
            return PageData.from(result);
        }
        // â”€â”€ æ­¥éª¤2:收集当前页订单 id â”€â”€
        List<String> orderIds = records.stream().map(OperationOrderVO::getId).collect(Collectors.toList());
        // â”€â”€ æ­¥éª¤3:一次性批量查骑行记录(含骑行车型名) â”€â”€
        // left join base_param ç›´æŽ¥å¸¦å‡ºéª‘行车型名(member_rides.param_id→base_param.name→MemberRides.paramName);
        // æŒ‰ create_date desc æŽ’序,内存按订单分组取每组第一条即"最近一条骑行记录"
        List<MemberRides> rides = memberRidesJoinMapper.selectJoinList(MemberRides.class,
                new MPJLambdaWrapper<MemberRides>()
                        .select(MemberRides::getOrdreId, MemberRides::getType, MemberRides::getRentDate,
                                MemberRides::getBikeCode)
                        .selectAs(BaseParam::getName, MemberRides::getParamName)
                        .leftJoin(BaseParam.class, BaseParam::getId, MemberRides::getParamId)
                        .eq(MemberRides::getIsdeleted, Constants.ZERO)
                        .in(MemberRides::getOrdreId, orderIds)
                        .orderByDesc(MemberRides::getCreateDate)
                        .orderByDesc(MemberRides::getRentDate));
        Map<String, MemberRides> latestRideByOrder = new LinkedHashMap<>();
        for (MemberRides r : rides) {
            // putIfAbsent:已按 create_date desc æŽ’序,首次出现即该订单最近一条
            latestRideByOrder.putIfAbsent(r.getOrdreId(), r);
        }
        // â”€â”€ å›žå¡« VO(全程仅 2 æ¬¡æŸ¥è¯¢:分页 / éª‘行;车型名均由 join å¸¦å‡º,无需单独查车型字典) â”€â”€
        for (OperationOrderVO vo : records) {
            MemberRides latest = latestRideByOrder.get(vo.getId());
            // è½¦åž‹ååˆ†æ”¯:进行中(status=1)取最近骑行的车型名,否则(含已结算)取订单结算车型名
            Integer orderStatus = vo.getOrderStatus();
            boolean inProgress = orderStatus != null && orderStatus.equals(Constants.ONE);
            if (latest != null) {
                // è®¢å•类型、骑行开始时间、车辆编号:统一取最近一条骑行记录
                // (进行中即"当前骑行车辆",已完结即"最后骑行车辆";bike_code = bikes.code)
                vo.setBikeType(latest.getType());
                vo.setRentDate(latest.getRentDate());
                if (inProgress) {
                    // è¿›è¡Œä¸­:骑行车型名(join å·²å¸¦å‡º,存于 MemberRides.paramName)
                    vo.setParamName(latest.getParamName());
                    vo.setBikeCode(latest.getBikeCode());
                }
            }
            if (!inProgress) {
                // å·²ç»“ç®—:订单结算车型名(分页 join å·²å¸¦å‡º,存于 settleParamName)
                vo.setParamName(vo.getSettleParamName());
            }
        }
        return PageData.from(result);
    }
    @Override
    public OrderRidesDetailVO orderRidesDetail(String orderId) {
        OrderRidesDetailVO result = new OrderRidesDetailVO();
        // è®¢å•号为空:直接返回空结果(不抛异常,前端按 hasTrack=false å…œåº•)
        if (StringUtils.isBlank(orderId)) {
            result.setHasTrack(false);
            result.setRides(Collections.emptyList());
            return result;
        }
        // 1. æŸ¥è¯¥è®¢å•下全部骑行记录(按创建时间升序,还原同一订单多次骑行的先后)
        List<MemberRides> ridesList = memberRidesJoinMapper.selectList(
                new QueryWrapper<MemberRides>().lambda()
                        .eq(MemberRides::getOrdreId, orderId)
                        .eq(MemberRides::getIsdeleted, Constants.ZERO)
                        .orderByAsc(MemberRides::getCreateDate));
        if (ridesList.isEmpty()) {
            // è®¢å•下无骑行记录(理论不应出现,兜底返回空)
            result.setHasTrack(false);
            result.setRides(Collections.emptyList());
            return result;
        }
        // 2. è½¦è¾†ç±»åž‹å–首条骑行 type(0自行车/1电车):决定是否查轨迹
        Integer bikeType = ridesList.get(0).getType();
        result.setBikeType(bikeType);
        result.setBikeTypeName(bikeTypeNameOf(bikeType));
        boolean isEbike = bikeType != null && bikeType.equals(Constants.ONE);
        // 3. è½¨è¿¹é¢„è½½(仅电车):一次性查出该订单所有骑行轨迹,按 rides_id åˆ†ç»„、按上报时间升序,
        //    é¿å…é€æ¡éª‘行 N æ¬¡æŸ¥è½¨è¿¹;自行车(type=0)èµ° MQTT æ—  GPS,跳过。
        Map<String, List<OrderRideTrackVO>> trackByRide = new HashMap<>();
        if (isEbike) {
            List<String> ridesIds = ridesList.stream()
                    .map(MemberRides::getId).collect(Collectors.toList());
            List<MemberRidesTrack> tracks = memberRidesTrackMapper.selectList(
                    new QueryWrapper<MemberRidesTrack>().lambda()
                            .select(MemberRidesTrack::getRidesId, MemberRidesTrack::getLongitude,
                                    MemberRidesTrack::getLatitude, MemberRidesTrack::getReportTime)
                            .eq(MemberRidesTrack::getIsdeleted, Constants.ZERO)
                            .in(MemberRidesTrack::getRidesId, ridesIds)
                            .orderByAsc(MemberRidesTrack::getReportTime));
            for (MemberRidesTrack t : tracks) {
                OrderRideTrackVO tv = new OrderRideTrackVO();
                tv.setLongitude(t.getLongitude());
                tv.setLatitude(t.getLatitude());
                tv.setReportTime(t.getReportTime());
                trackByRide.computeIfAbsent(t.getRidesId(), k -> new ArrayList<>()).add(tv);
            }
        }
        // 4. ç»„装骑行记录列表(每条挂对应轨迹点;自行车一律空轨迹)
        List<OrderRideItemVO> rides = new ArrayList<>(ridesList.size());
        for (MemberRides r : ridesList) {
            OrderRideItemVO item = new OrderRideItemVO();
            item.setRidesId(r.getId());
            item.setStatus(r.getStatus());
            item.setStatusName(rideStatusNameOf(r.getStatus()));
            item.setRentDate(r.getRentDate());
            item.setBackDate(r.getBackDate());
            item.setBikeCode(r.getBikeCode());
            item.setDuration(r.getDuration());
            item.setBikeTypeName(bikeTypeNameOf(r.getType()));
            item.setTracks(isEbike
                    ? trackByRide.getOrDefault(r.getId(), Collections.emptyList())
                    : Collections.emptyList());
            rides.add(item);
        }
        result.setRides(rides);
        // 5. è½¨è¿¹å¯ç”¨æ€§ + è‡ªè¡Œè½¦æ— è½¨è¿¹æç¤º
        if (isEbike) {
            result.setHasTrack(true);
        } else {
            result.setHasTrack(false);
            result.setNoTrackMessage("该订单为自行车订单,无车辆轨迹");
        }
        return result;
    }
    /**
     * éª‘行状态 â†’ ä¸­æ–‡åã€‚
     * <p>member_rides.status:0请求开锁中 / 1骑行中 / 2已还车 / 3开锁失败 / 4临时锁车。
     *
     * @param status éª‘行状态原文(可能为 null)
     * @return çŠ¶æ€ä¸­æ–‡å(空值/未知取值返回"未知")
     */
    private String rideStatusNameOf(Integer status) {
        if (status == null) {
            return "未知";
        }
        switch (status) {
            case 0:
                return "请求开锁中";
            case 1:
                return "骑行中";
            case 2:
                return "已还车";
            case 3:
                return "开锁失败";
            case 4:
                return "临时锁车";
            default:
                return "未知";
        }
    }
    /**
     * è½¦è¾†ç±»åž‹ â†’ ä¸­æ–‡åã€‚
     * <p>member_rides.type:0自行车 / 1电动车。
     *
     * @param type è½¦è¾†ç±»åž‹(可能为 null)
     * @return ç±»åž‹ä¸­æ–‡å(未知取值返回"未知")
     */
    private String bikeTypeNameOf(Integer type) {
        if (type == null) {
            return "未知";
        }
        if (type.equals(Constants.ZERO)) {
            return "自行车";
        }
        if (type.equals(Constants.ONE)) {
            return "电动车";
        }
        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();
    }
}