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.ParkingCouponRecord; 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.ParkingCouponRecordService; 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; private final ParkingCouponRecordService parkingCouponRecordService; @Value("${kym.domain}") private String DOMAIN; public WashOrderServiceImpl(AwoaraService awoaraService, AccountService accountService, @Lazy WashStationService washStationService, UserService userService, @Lazy OrderSettlementService orderSettlementService, ParkingCouponRecordService parkingCouponRecordService) { this.awoaraService = awoaraService; this.accountService = accountService; this.washStationService = washStationService; this.userService = userService; this.orderSettlementService = orderSettlementService; this.parkingCouponRecordService = parkingCouponRecordService; } /** * 创建订单(启动洗车机) * * @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() >= KymCache.INSTANCE.getParkingCouponMinAmountByStationId(orderList.get(0).getStationId())) { var parkingCouponUrl = KymCache.INSTANCE.getParkingQrCodeUrlByStationId(orderList.get(0).getStationId()); var code = UUID.randomUUID().toString(); KymCache.INSTANCE.setParkingCouponCode(code, parkingCouponUrl, 3600 * 2L); parkingCouponRecordService.save(new ParkingCouponRecord() .setCode(code) .setUserId(user.getId()) .setStationId(orderList.get(0).getStationId()) .setOrderId(orderList.get(0).getOrderId()) .setTargetUrl(parkingCouponUrl) .setStatus(ParkingCouponRecord.STATUS_未使用) .setExpireTime(LocalDateTime.now().plusHours(2))); 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() >= KymCache.INSTANCE.getParkingCouponMinAmountByStationId(orderList.get(0).getStationId())) { var parkingCouponUrl = KymCache.INSTANCE.getParkingQrCodeUrlByStationId(orderList.get(0).getStationId()); var code = UUID.randomUUID().toString(); KymCache.INSTANCE.setParkingCouponCode(code, parkingCouponUrl, 3600 * 2L); parkingCouponRecordService.save(new ParkingCouponRecord() .setCode(code) .setUserId(user.getId()) .setStationId(orderList.get(0).getStationId()) .setOrderId(orderList.get(0).getOrderId()) .setTargetUrl(parkingCouponUrl) .setStatus(ParkingCouponRecord.STATUS_未使用) .setExpireTime(LocalDateTime.now().plusHours(2))); return DOMAIN + "/api/parking-coupon?code=" + code; } } } throw new BusinessException("不符合优惠条件:订单完成超过2小时或洗车金额不足6元。"); } //endregion }