package com.kym.service.impl;
import cn.dev33.satoken.stp.StpUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.github.yulichang.toolkit.JoinWrappers;
import com.github.yulichang.wrapper.MPJLambdaWrapper;
import com.kym.common.exception.BusinessException;
import com.kym.common.utils.CommUtil;
import com.kym.common.utils.OrderUtils;
import com.kym.entity.Account;
import com.kym.entity.User;
import com.kym.entity.WashOrder;
import com.kym.entity.WashStation;
import com.kym.entity.awoara.OrderInfo;
import com.kym.entity.common.PageBean;
import com.kym.entity.common.PageParams;
import com.kym.entity.queryParams.DeviceQueryParams;
import com.kym.entity.queryParams.StatQueryParam;
import com.kym.entity.queryParams.WashOrderQueryParams;
import com.kym.entity.vo.StationTrendVo;
import com.kym.entity.vo.WashOrderVo;
import com.kym.mapper.WashOrderMapper;
import com.kym.service.AccountService;
import com.kym.service.OrderSettlementService;
import com.kym.service.UserService;
import com.kym.service.WashOrderService;
import com.kym.service.WashStationService;
import com.kym.service.awoara.AwoaraService;
import com.kym.service.cache.KymCache;
import com.kym.service.mybatisplus.MyBaseServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.temporal.TemporalAdjusters;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
/**
*
* 洗车订单表 服务实现类
*
*
* @author skyline
* @since 2024-09-11
*/
@Slf4j
@Service
public class WashOrderServiceImpl extends MyBaseServiceImpl implements WashOrderService {
private final AwoaraService awoaraService;
private final AccountService accountService;
private final WashStationService washStationService;
private final UserService userService;
private final OrderSettlementService orderSettlementService;
@Value("${kym.domain}")
private String DOMAIN;
public WashOrderServiceImpl(AwoaraService awoaraService, AccountService accountService,
@Lazy WashStationService washStationService, UserService userService,
@Lazy OrderSettlementService orderSettlementService) {
this.awoaraService = awoaraService;
this.accountService = accountService;
this.washStationService = washStationService;
this.userService = userService;
this.orderSettlementService = orderSettlementService;
}
/**
* 创建订单(启动洗车机)
*
* @param params
* @return
*/
@Override
public String createOrder(DeviceQueryParams params) {
// 校验余额
var account = accountService.getAccountByUserId(StpUtil.getLoginIdAsLong());
if (account.getBalance() < Account.MIN_BALANCE) {
throw new BusinessException("余额不足,请保持余额不低于2元!");
}
// 校验用户是否有未完结的订单
var unfinishedOrder = lambdaQuery()
.eq(WashOrder::getUserId, StpUtil.getLoginIdAsLong())
.and(wrapper -> wrapper
.eq(WashOrder::getOrderStatus, WashOrder.ORDER_STATUS_开机)
.or()
.eq(WashOrder::getPayStatus, WashOrder.PAY_STATUS_未支付))
.list();
if (!unfinishedOrder.isEmpty()) {
throw new BusinessException("您有未完结的订单!");
}
// 检查设备是否有僵死订单(设备显示忙碌但订单实际已超时)
checkAndResolveStaleOrder(params.getProductKey(), params.getDeviceName());
var memberName = StpUtil.getSession().getString(User.ST_SESSION_KEY_MOBILE);
var orderId = OrderUtils.getOrderNo();
// 请求阿里云lot
var createOrder = awoaraService.createOrder(params.getProductKey(), params.getDeviceName(),
orderId,
memberName,
account.getBalance(),
Account.NO_DISCOUNT,
// 本次开机最大消费金额
account.getBalance());
// 首次消费:无归属站点的用户自动归属到当前设备所在站点
long userId = StpUtil.getLoginIdAsLong();
var user = userService.getById(userId);
var userStationId = user != null ? user.getStationId() : null;
if (CommUtil.isEmptyOrNull(userStationId)) {
userService.lambdaUpdate()
.set(User::getStationId, params.getStationId())
.eq(User::getId, userId)
.update();
StpUtil.getSession().set("stationId", params.getStationId());
KymCache.INSTANCE.putUserId2StationId(Map.of(userId, params.getStationId()));
userStationId = params.getStationId();
}
var washOrder = new WashOrder()
.setUserId(StpUtil.getLoginIdAsLong())
.setStationId(params.getStationId())
.setProductKey(params.getProductKey())
.setDeviceName(params.getDeviceName())
.setShortId(KymCache.INSTANCE.getShortIdByProductKeyAndDeviceName(params.getProductKey(), params.getDeviceName()))
.setOpenType(WashOrder.START_CLOSE_TYPE_网络)
.setOrderId(orderId)
.setOrderIdLocal(createOrder.getOrder_id_local())
.setMemberDiscount(Account.NO_DISCOUNT)
.setPrepayMoney(account.getBalance())
.setStartTime(LocalDateTime.now())
.setOrderStatus(WashOrder.ORDER_STATUS_开机)
.setPayStatus(WashOrder.PAY_STATUS_未支付)
.setIsCross(!params.getStationId().equals(userStationId));
save(washOrder);
return orderId;
}
@Override
public void closeOrder(DeviceQueryParams params) {
var order = lambdaQuery()
.eq(WashOrder::getProductKey, params.getProductKey())
.eq(WashOrder::getDeviceName, params.getDeviceName())
.eq(WashOrder::getOrderStatus, WashOrder.ORDER_STATUS_开机)
.eq(WashOrder::getPayStatus, WashOrder.PAY_STATUS_未支付)
.one();
if (order == null) {
return;
}
if (order.getUserId() != StpUtil.getLoginIdAsLong()) {
throw new BusinessException("您没有权限关闭该订单!");
}
// 尝试发送关闭命令,失败后重试1次
boolean sent = sendCloseOrderWithRetry(params.getProductKey(), params.getDeviceName(), order.getOrderId(), 1);
if (sent) {
return;
}
// RRpc 失败,主动查询设备订单状态
log.warn("订单 {} 关闭命令发送失败,尝试主动查询设备状态", order.getOrderId());
try {
OrderInfo orderInfo = awoaraService.queryOrder(params.getProductKey(), params.getDeviceName(), order.getOrderId());
if (orderInfo != null && orderInfo.getClose_type() != null && !orderInfo.getClose_type().isEmpty()) {
log.info("订单 {} 设备已关闭,执行结算", order.getOrderId());
orderSettlementService.settleOrder(order, orderInfo);
return;
}
if (orderInfo != null && (orderInfo.getClose_type() == null || orderInfo.getClose_type().isEmpty())) {
// 设备还在运行,尝试强制关闭
log.info("订单 {} 设备仍在运行,尝试强制关闭", order.getOrderId());
try {
awoaraService.forceCloseOrder(params.getProductKey(), params.getDeviceName());
} catch (Exception fe) {
log.error("订单 {} 强制关闭也失败", order.getOrderId(), fe);
}
}
} catch (Exception e) {
log.error("订单 {} 主动查询设备状态也失败", order.getOrderId(), e);
}
throw new BusinessException("关闭订单失败,请稍后重试或联系管理员");
}
@Override
public void forceSettleOrder(String orderId) {
var order = lambdaQuery()
.eq(WashOrder::getOrderId, orderId)
.eq(WashOrder::getOrderStatus, WashOrder.ORDER_STATUS_开机)
.eq(WashOrder::getPayStatus, WashOrder.PAY_STATUS_未支付)
.one();
if (order == null) {
throw new BusinessException("未找到该订单或订单已完结");
}
// 尝试通过 RRpc 发送关闭命令
sendCloseOrderWithRetry(order.getProductKey(), order.getDeviceName(), order.getOrderId(), 1);
// 查询设备订单状态进行结算
log.info("订单 {} 已发送关闭命令,查询设备订单状态进行结算", order.getOrderId());
try {
OrderInfo orderInfo = awoaraService.queryOrder(order.getProductKey(), order.getDeviceName(), order.getOrderId());
if (orderInfo != null && orderInfo.getClose_type() != null && !orderInfo.getClose_type().isEmpty()) {
log.info("订单 {} 设备已关闭,执行结算", order.getOrderId());
orderSettlementService.settleOrder(order, orderInfo);
return;
}
if (orderInfo != null && (orderInfo.getClose_type() == null || orderInfo.getClose_type().isEmpty())) {
// 设备还在运行,尝试强制关闭
log.info("订单 {} 设备仍在运行,尝试强制关闭", order.getOrderId());
try {
awoaraService.forceCloseOrder(order.getProductKey(), order.getDeviceName());
} catch (Exception fe) {
log.error("订单 {} 强制关闭也失败", order.getOrderId(), fe);
}
}
} catch (Exception e) {
log.error("订单 {} 主动查询设备状态也失败", order.getOrderId(), e);
}
throw new BusinessException("强制结算失败,请稍后重试或联系管理员");
}
private boolean sendCloseOrderWithRetry(String productKey, String deviceName, String orderId, int maxRetries) {
for (int i = 0; i <= maxRetries; i++) {
try {
awoaraService.closeOrder(productKey, deviceName, orderId);
return true;
} catch (Exception e) {
if (i < maxRetries) {
log.warn("订单 {} 关闭命令第{}次失败,准备重试", orderId, i + 1, e);
} else {
log.error("订单 {} 关闭命令{}次均失败", orderId, maxRetries + 1, e);
}
}
}
return false;
}
/**
* 检查设备是否有僵死订单,有则尝试清理
*/
private void checkAndResolveStaleOrder(String productKey, String deviceName) {
var staleOrder = lambdaQuery()
.eq(WashOrder::getProductKey, productKey)
.eq(WashOrder::getDeviceName, deviceName)
.eq(WashOrder::getOrderStatus, WashOrder.ORDER_STATUS_开机)
.eq(WashOrder::getPayStatus, WashOrder.PAY_STATUS_未支付)
.one();
if (staleOrder == null) {
return;
}
log.warn("设备 {}/{} 存在未完结订单 {},尝试查询设备实际状态", productKey, deviceName, staleOrder.getOrderId());
try {
OrderInfo orderInfo = awoaraService.queryOrder(productKey, deviceName, staleOrder.getOrderId());
if (orderInfo != null && orderInfo.getClose_type() != null && !orderInfo.getClose_type().isEmpty()) {
log.info("设备实际已关闭订单 {},执行结算", staleOrder.getOrderId());
orderSettlementService.settleOrder(staleOrder, orderInfo);
}
} catch (Exception e) {
log.warn("查询设备订单状态失败,跳过清理:{}", e.getMessage());
}
}
/**
* 查询订单详情
*
* @param params
* @return
*/
@Override
public WashOrder queryOrder(WashOrderQueryParams params) {
// 非实时数据
WashOrder order = lambdaQuery()
.eq(WashOrder::getOrderId, params.getOrderId())
.one();
if (null != params.getUserId()) {
CommUtil.asserts(null != order && order.getUserId().equals(StpUtil.getLoginIdAsLong()),
"订单不存在或您没有权限查看该订单!");
}
return order;
}
/**
* 查询停车减免订单
*
* @param unionid
* @return
*/
@Override
public String getParkingDiscounts(String unionid) {
var user = userService.lambdaQuery().eq(User::getUnionid, unionid).one();
CommUtil.asserts(null != user, "用户信息异常:无此用户");
// 查询用户24小时内的订单
var orders = lambdaQuery()
.eq(WashOrder::getUserId, user.getId())
.ge(WashOrder::getStartTime, LocalDateTime.now().minusHours(24))
.eq(WashOrder::getPayStatus, WashOrder.PAY_STATUS_已支付)
.orderByDesc(WashOrder::getId)
.list();
CommUtil.asserts(CommUtil.isEmptyOrNull(orders) && (orders.stream().mapToInt(WashOrder::getAmount).sum() >= 0),
"抱歉:无停车场洗车记录");
return washStationService.lambdaQuery().eq(WashStation::getStationId, orders.get(0).getStationId()).one().getParkingQrCode();
}
/**
* 当前用户订单列表
*
* @param params
* @return
*/
@Override
public PageBean listMyWashOrder(PageParams params) {
PageHelper.startPage(params.getPageNum(), params.getPageSize());
var res = lambdaQuery()
.eq(WashOrder::getUserId, StpUtil.getLoginIdAsLong())
.orderByDesc(WashOrder::getId)
.list();
PageInfo pages = new PageInfo<>(res);
var voList = pages.getList().stream().map(order -> {
var vo = new WashOrderVo();
BeanUtils.copyProperties(order, vo);
vo.setStationName(KymCache.INSTANCE.getStationNameById(order.getStationId()));
return vo;
}).toList();
PageBean bean = new PageBean<>(voList);
bean.setPages(pages.getPages());
bean.setPageNum(pages.getPageNum());
bean.setPageSize(pages.getPageSize());
bean.setTotal(pages.getTotal());
return bean;
}
//region 管理后台
@Override
public PageBean list(WashOrderQueryParams params) {
PageHelper.startPage(params.getPageNum(), params.getPageSize());
MPJLambdaWrapper wrapper = JoinWrappers.lambda(WashOrder.class)
.selectAsClass(WashOrder.class, WashOrderVo.class)
.selectAs(User::getMobilePhone, WashOrderVo::getMobilePhone)
.leftJoin(User.class, User::getId, WashOrder::getUserId)
.eq(CommUtil.isNotEmptyAndNull(params.getUserId()), WashOrder::getUserId, params.getUserId())
.eq(CommUtil.isNotEmptyAndNull(params.getMobilePhone()), User::getMobilePhone, params.getMobilePhone())
.eq(CommUtil.isNotEmptyAndNull(params.getOrderStatus()), WashOrder::getOrderStatus, params.getOrderStatus())
.eq(CommUtil.isNotEmptyAndNull(params.getPayStatus()), WashOrder::getPayStatus, params.getPayStatus())
.orderByDesc(WashOrder::getId);
//连表查询 返回自定义ResultType
List list = selectJoinList(WashOrderVo.class, wrapper);
list.forEach(item -> {
item.setStationName(KymCache.INSTANCE.getStationNameById(item.getStationId()));
item.setUserStationId(KymCache.INSTANCE.getUserStationId(item.getUserId()));
item.setUserStationName(KymCache.INSTANCE.getStationNameById(item.getUserStationId()));
});
return new PageBean<>(list);
}
/**
* 获取订单详情
* 根据订单号查询,而不是主键ID
*
* @param orderId 订单号
* @return 订单详情
*/
@Override
public WashOrderVo detail(String orderId) {
MPJLambdaWrapper wrapper = JoinWrappers.lambda(WashOrder.class)
.selectAsClass(WashOrder.class, WashOrderVo.class)
.selectAs(User::getMobilePhone, WashOrderVo::getMobilePhone)
.leftJoin(User.class, User::getId, WashOrder::getUserId)
.eq(WashOrder::getOrderId, orderId);
WashOrderVo washOrderVo = selectJoinOne(WashOrderVo.class, wrapper);
if (washOrderVo != null) {
washOrderVo.setStationName(KymCache.INSTANCE.getStationNameById(washOrderVo.getStationId()));
washOrderVo.setUserStationId(KymCache.INSTANCE.getUserStationId(washOrderVo.getUserId()));
washOrderVo.setUserStationName(KymCache.INSTANCE.getStationNameById(washOrderVo.getUserStationId()));
}
return washOrderVo;
}
/**
* 统计指定日期各站点消费金额
*
* @param statDay
* @return
*/
@Override
public Map sumAmountByDate(LocalDate statDay) {
LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
wrapper.select(WashOrder::getAmount, WashOrder::getStationId);
wrapper.ge(WashOrder::getStartTime, LocalDateTime.of(statDay, LocalTime.MIN));
wrapper.gt(WashOrder::getStartTime, LocalDateTime.of(statDay, LocalTime.MAX));
var list = list(wrapper);
// 按照站点分组统计金额
return list.stream().collect(Collectors.groupingBy(WashOrder::getStationId, Collectors.summingInt(WashOrder::getAmount)));
}
/**
* 统计指定日期当月各站点总消费金额
*
* @param statDay
* @return
*/
@Override
public Map sumMonthAmount(LocalDate statDay) {
var startTime = statDay.with(TemporalAdjusters.firstDayOfMonth()).atTime(LocalTime.MIN);
var endTime = statDay.with(TemporalAdjusters.lastDayOfMonth()).atTime(LocalTime.MAX);
LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
wrapper.select(WashOrder::getAmount, WashOrder::getStationId);
wrapper.ge(WashOrder::getStartTime, startTime);
wrapper.lt(WashOrder::getStartTime, endTime);
var list = list(wrapper);
// 按照站点分组统计金额
return list.stream().collect(Collectors.groupingBy(WashOrder::getStationId, Collectors.summingInt(WashOrder::getAmount)));
}
/**
* 统计指定日期订单数量
*
* @param statDay
* @return
*/
@Override
public Map countDailyOrders(LocalDate statDay) {
LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
wrapper.ge(WashOrder::getStartTime, LocalDateTime.of(statDay, LocalTime.MIN));
wrapper.lt(WashOrder::getStartTime, LocalDateTime.of(statDay, LocalTime.MAX));
// 按照站点分组统计订单数量
return list(wrapper).stream().collect(Collectors.groupingBy(WashOrder::getStationId, Collectors.summingInt(o -> 1)));
}
/**
* 统计指定日期订单数量
*
* @param statDay
* @return
*/
@Override
public Map countMonthOrders(LocalDate statDay) {
var startTime = statDay.with(TemporalAdjusters.firstDayOfMonth()).atTime(LocalTime.MIN);
var endTime = statDay.with(TemporalAdjusters.lastDayOfMonth()).atTime(LocalTime.MAX);
LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
wrapper.select(WashOrder::getStationId);
wrapper.ge(WashOrder::getStartTime, startTime);
wrapper.lt(WashOrder::getStartTime, endTime);
// 按照站点分组统计订单数量
return list(wrapper).stream().collect(Collectors.groupingBy(WashOrder::getStationId, Collectors.summingInt(o -> 1)));
}
@Override
public Map countDailyActiveUsers(LocalDate statDay) {
LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
wrapper.select(WashOrder::getUserId, WashOrder::getStationId);
wrapper.ge(WashOrder::getStartTime, LocalDateTime.of(statDay, LocalTime.MIN));
wrapper.lt(WashOrder::getStartTime, LocalDateTime.of(statDay, LocalTime.MAX));
wrapper.groupBy(WashOrder::getStationId, WashOrder::getUserId);
return list(wrapper).stream()
.collect(Collectors.groupingBy(WashOrder::getStationId, Collectors.summingInt(o -> 1)));
}
@Override
public Map countMonthActiveUsers(LocalDate statDay) {
var startTime = statDay.with(TemporalAdjusters.firstDayOfMonth()).atTime(LocalTime.MIN);
var endTime = statDay.with(TemporalAdjusters.lastDayOfMonth()).atTime(LocalTime.MAX);
LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
wrapper.select(WashOrder::getUserId, WashOrder::getStationId);
wrapper.ge(WashOrder::getStartTime, startTime);
wrapper.lt(WashOrder::getStartTime, endTime);
wrapper.groupBy(WashOrder::getStationId, WashOrder::getUserId);
return list(wrapper).stream()
.collect(Collectors.groupingBy(WashOrder::getStationId, Collectors.summingInt(o -> 1)));
}
@Override
public WashOrder getOrderInProgressByUserId(long userId) {
return lambdaQuery()
.eq(WashOrder::getUserId, userId)
.eq(WashOrder::getPayStatus, WashOrder.PAY_STATUS_未支付)
.one();
}
@Override
public List stationTrend(StatQueryParam params) {
return baseMapper.StationTrend(params);
}
@Override
public String checkParkingCoupon(String mobilePhone) {
CommUtil.asserts(CommUtil.isNotEmptyAndNull(mobilePhone),"查询手机号不能为空");
var user = userService.getOne(new LambdaQueryWrapper().eq(User::getMobilePhone, mobilePhone));
if (user == null) {
throw new BusinessException("请输入注册洗车小程序使用的手机号码!");
}
// 优惠券有效期2小时,先查已支付订单
var orderList = lambdaQuery()
.eq(WashOrder::getUserId, user.getId())
.ge(WashOrder::getEndTime, LocalDateTime.now().minusHours(2))
.eq(WashOrder::getPayStatus, WashOrder.PAY_STATUS_已支付)
.orderByDesc(WashOrder::getId).list();
if (!orderList.isEmpty() && orderList.stream().collect(Collectors.summarizingInt(WashOrder::getAmount)).getSum() >= WashOrder.PARKING_COUPON_MIN_AMOUNT) {
var parkingCouponUrl = KymCache.INSTANCE.getParkingQrCodeUrlByStationId(orderList.get(0).getStationId());
var code = UUID.randomUUID().toString();
KymCache.INSTANCE.setParkingCouponCode(code, parkingCouponUrl, 3600 * 2L);
return DOMAIN + "/api/parking-coupon?code=" + code;
}
// 未找到符合条件的已支付订单,尝试查找僵死订单并主动对账结算
var staleOrders = lambdaQuery()
.eq(WashOrder::getUserId, user.getId())
.ge(WashOrder::getStartTime, LocalDateTime.now().minusHours(2))
.eq(WashOrder::getOrderStatus, WashOrder.ORDER_STATUS_开机)
.eq(WashOrder::getPayStatus, WashOrder.PAY_STATUS_未支付)
.orderByDesc(WashOrder::getId).list();
if (!staleOrders.isEmpty()) {
boolean reconciled = false;
for (WashOrder staleOrder : staleOrders) {
try {
OrderInfo orderInfo = awoaraService.queryOrder(staleOrder.getProductKey(), staleOrder.getDeviceName(), staleOrder.getOrderId());
if (orderInfo != null && orderInfo.getClose_type() != null && !orderInfo.getClose_type().isEmpty()) {
log.info("停车券查询-订单 {} 设备已关闭,主动结算", staleOrder.getOrderId());
orderSettlementService.settleOrder(staleOrder, orderInfo);
reconciled = true;
}
} catch (Exception e) {
log.warn("停车券查询-订单 {} 查询设备失败:{}", staleOrder.getOrderId(), e.getMessage());
}
}
if (reconciled) {
// 结算后重新查询已支付订单
orderList = lambdaQuery()
.eq(WashOrder::getUserId, user.getId())
.ge(WashOrder::getEndTime, LocalDateTime.now().minusHours(2))
.eq(WashOrder::getPayStatus, WashOrder.PAY_STATUS_已支付)
.orderByDesc(WashOrder::getId).list();
if (!orderList.isEmpty() && orderList.stream().collect(Collectors.summarizingInt(WashOrder::getAmount)).getSum() >= WashOrder.PARKING_COUPON_MIN_AMOUNT) {
var parkingCouponUrl = KymCache.INSTANCE.getParkingQrCodeUrlByStationId(orderList.get(0).getStationId());
var code = UUID.randomUUID().toString();
KymCache.INSTANCE.setParkingCouponCode(code, parkingCouponUrl, 3600 * 2L);
return DOMAIN + "/api/parking-coupon?code=" + code;
}
}
}
throw new BusinessException("不符合优惠条件:订单完成超过2小时或洗车金额不足6元。");
}
//endregion
}