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