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 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().lambda().eq(Member::getIsdeleted, Constants.ZERO))); // 今日新增:创建时间 ≥ 今日0点 Date todayStart = DateUtil.getStartOfDay(new Date()); vo.setTodayMembers((long) memberMapper.selectCount( new QueryWrapper().lambda() .eq(Member::getIsdeleted, Constants.ZERO) .ge(Member::getCreateDate, todayStart))); // 自行车数量(type=0),电动车数量(type=1);均含全部未删除车辆(含禁用) vo.setBikeCount((long) bikesMapper.selectCount( new QueryWrapper().lambda() .eq(Bikes::getType, Constants.ZERO) .eq(Bikes::getIsdeleted, Constants.ZERO))); vo.setEleBikeCount((long) bikesMapper.selectCount( new QueryWrapper().lambda() .eq(Bikes::getType, Constants.ONE) .eq(Bikes::getIsdeleted, Constants.ZERO))); return vo; } @Override public List 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 paramList = baseParamMapper.selectList( new QueryWrapper().lambda() .eq(BaseParam::getIsdeleted, Constants.ZERO) .in(BaseParam::getType, Constants.THREE, Constants.FOUR)); Map paramMap = paramList.stream() .collect(Collectors.toMap(BaseParam::getId, p -> p, (a, b) -> a)); // 3. 时段内已结算的租车押金订单(查询模式参考后台 getBikeIncomeReportVOList: // type=0 押金类、status=4 已结算、paramId 非空、payDate 落在区间内) List orders = goodsorderMapper.selectList( new QueryWrapper().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 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 result = new ArrayList<>(); for (Map.Entry 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 → 自定义起止(均含)。 *

{@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 orders = goodsorderMapper.selectList( new QueryWrapper().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 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 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 之和,分→元)。 *

供累计收入、环比、同比复用,统一收入口径;只 select closeMoney 列以减少数据传输。 * * @param start 起始时间(含) * @param end 结束时间(含) * @return 区间结算收入(元,2位小数);无数据返回 0 */ private BigDecimal sumClosedMoney(Date start, Date end) { List orders = goodsorderMapper.selectList( new QueryWrapper().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().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().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 之和,分→元)。 *

供运营中心"今日套餐收入"等按 type 统计支付金额使用。 * * @param type 订单类型(0 租车押金 / 1 套餐卡购买) * @param start 起始时间(含) * @param end 结束时间(含) * @return 区间支付金额(元,2位小数);无数据返回 0 */ private BigDecimal sumMoney(Integer type, Date start, Date end) { List orders = goodsorderMapper.selectList( new QueryWrapper().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 operationOrderPage(PageWrap pageWrap) { // 分页对象 IPage 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 wrapper = new MPJLambdaWrapper() // 主表字段:订单主键、编号、结算时间 .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 result = goodsorderMapper.selectJoinPage(page, OperationOrderVO.class, wrapper); List records = result.getRecords(); // 无数据直接返回,避免空 in() 查询 if (records.isEmpty()) { return PageData.from(result); } // ── 步骤2:收集当前页订单 id ── List 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 rides = memberRidesJoinMapper.selectJoinList(MemberRides.class, new MPJLambdaWrapper() .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 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 ridesList = memberRidesJoinMapper.selectList( new QueryWrapper().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> trackByRide = new HashMap<>(); if (isEbike) { List ridesIds = ridesList.stream() .map(MemberRides::getId).collect(Collectors.toList()); List tracks = memberRidesTrackMapper.selectList( new QueryWrapper().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 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; } /** * 骑行状态 → 中文名。 *

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 "未知"; } } /** * 车辆类型 → 中文名。 *

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().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; } /** * 统计指定时段内套餐销售来源数量。 *

抖音兑换 = payWay 2,小程序购买 = payWay 0(微信)。 * * @param start 起始时间(含) * @param end 结束时间(含) * @return 抖音/小程序套餐数量 */ private PackageSourceStatVO.PeriodCount countPackageSource(Date start, Date end) { List orders = goodsorderMapper.selectList( new QueryWrapper().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().lambda().eq(Bikes::getIsdeleted, Constants.ZERO))); // 客户数:总会员=未删除全部;今日/昨日新增按 create_date 落在对应区间(与 overview 口径一致) vo.setTotalMemberCount((long) memberMapper.selectCount( new QueryWrapper().lambda().eq(Member::getIsdeleted, Constants.ZERO))); vo.setYesterdayNewMember((long) memberMapper.selectCount( new QueryWrapper().lambda() .eq(Member::getIsdeleted, Constants.ZERO) .ge(Member::getCreateDate, yesterdayStart) .le(Member::getCreateDate, yesterdayEnd))); vo.setTodayNewMember((long) memberMapper.selectCount( new QueryWrapper().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().lambda() .eq(Goodsorder::getPayStatus, Constants.ONE) .eq(Goodsorder::getIsdeleted, Constants.ZERO) .ge(Goodsorder::getPayDate, start) .le(Goodsorder::getPayDate, end)); } /** * 取本月/本年第一天的时间原值(时分秒归零前的毫秒),用于构造区间起始。 *

用 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(); } }