WashOrderServiceImpl.java 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618
  1. package com.kym.service.impl;
  2. import cn.dev33.satoken.stp.StpUtil;
  3. import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  4. import com.github.pagehelper.PageHelper;
  5. import com.github.pagehelper.PageInfo;
  6. import com.github.yulichang.toolkit.JoinWrappers;
  7. import com.github.yulichang.wrapper.MPJLambdaWrapper;
  8. import com.kym.common.constant.ResponseEnum;
  9. import com.kym.common.exception.BusinessException;
  10. import com.kym.common.utils.CommUtil;
  11. import com.kym.common.utils.OrderUtils;
  12. import com.kym.entity.Account;
  13. import com.kym.entity.ParkingCouponRecord;
  14. import com.kym.entity.User;
  15. import com.kym.entity.WashOrder;
  16. import com.kym.entity.WashStation;
  17. import com.kym.entity.awoara.OrderInfo;
  18. import com.kym.entity.common.PageBean;
  19. import com.kym.entity.common.PageParams;
  20. import com.kym.entity.queryParams.DeviceQueryParams;
  21. import com.kym.entity.queryParams.StatQueryParam;
  22. import com.kym.entity.queryParams.WashOrderQueryParams;
  23. import com.kym.entity.vo.StationTrendVo;
  24. import com.kym.entity.vo.WashOrderVo;
  25. import com.kym.mapper.WashOrderMapper;
  26. import com.kym.service.AccountService;
  27. import com.kym.service.OrderSettlementService;
  28. import com.kym.service.ParkingCouponRecordService;
  29. import com.kym.service.UserService;
  30. import com.kym.service.WashOrderService;
  31. import com.kym.service.WashStationService;
  32. import com.kym.service.awoara.AwoaraService;
  33. import com.kym.service.cache.KymCache;
  34. import com.kym.service.mybatisplus.MyBaseServiceImpl;
  35. import lombok.extern.slf4j.Slf4j;
  36. import org.springframework.beans.BeanUtils;
  37. import org.springframework.beans.factory.annotation.Value;
  38. import org.springframework.context.annotation.Lazy;
  39. import org.springframework.stereotype.Service;
  40. import java.time.LocalDate;
  41. import java.time.LocalDateTime;
  42. import java.time.LocalTime;
  43. import java.time.temporal.TemporalAdjusters;
  44. import java.util.List;
  45. import java.util.Map;
  46. import java.util.UUID;
  47. import java.util.stream.Collectors;
  48. /**
  49. * <p>
  50. * 洗车订单表 服务实现类
  51. * </p>
  52. *
  53. * @author skyline
  54. * @since 2024-09-11
  55. */
  56. @Slf4j
  57. @Service
  58. public class WashOrderServiceImpl extends MyBaseServiceImpl<WashOrderMapper, WashOrder> implements WashOrderService {
  59. private final AwoaraService awoaraService;
  60. private final AccountService accountService;
  61. private final WashStationService washStationService;
  62. private final UserService userService;
  63. private final OrderSettlementService orderSettlementService;
  64. private final ParkingCouponRecordService parkingCouponRecordService;
  65. @Value("${kym.domain}")
  66. private String DOMAIN;
  67. public WashOrderServiceImpl(AwoaraService awoaraService, AccountService accountService,
  68. @Lazy WashStationService washStationService, UserService userService,
  69. @Lazy OrderSettlementService orderSettlementService,
  70. ParkingCouponRecordService parkingCouponRecordService) {
  71. this.awoaraService = awoaraService;
  72. this.accountService = accountService;
  73. this.washStationService = washStationService;
  74. this.userService = userService;
  75. this.orderSettlementService = orderSettlementService;
  76. this.parkingCouponRecordService = parkingCouponRecordService;
  77. }
  78. /**
  79. * 创建订单(启动洗车机)
  80. *
  81. * @param params
  82. * @return
  83. */
  84. @Override
  85. public String createOrder(DeviceQueryParams params) {
  86. // 校验用户是否被锁定(锁定用户禁止启动设备,提示内容透传给用户端强制弹窗展示)
  87. var lockCheckUser = userService.getById(StpUtil.getLoginIdAsLong());
  88. if (lockCheckUser != null && lockCheckUser.getStatus() != null && lockCheckUser.getStatus() == User.STATUS_禁用) {
  89. throw new BusinessException(ResponseEnum.USER_LOCKED.getCode(),
  90. CommUtil.isNotEmptyAndNull(lockCheckUser.getLockNotice()) ? lockCheckUser.getLockNotice()
  91. : "您的账户已被锁定,如有疑问请联系平台客服");
  92. }
  93. // 校验余额
  94. var account = accountService.getAccountByUserId(StpUtil.getLoginIdAsLong());
  95. if (account.getBalance() < Account.MIN_BALANCE) {
  96. throw new BusinessException("余额不足,请保持余额不低于2元!");
  97. }
  98. // 校验用户是否有未完结的订单
  99. var unfinishedOrder = lambdaQuery()
  100. .eq(WashOrder::getUserId, StpUtil.getLoginIdAsLong())
  101. .and(wrapper -> wrapper
  102. .eq(WashOrder::getOrderStatus, WashOrder.ORDER_STATUS_开机)
  103. .or()
  104. .eq(WashOrder::getPayStatus, WashOrder.PAY_STATUS_未支付))
  105. .list();
  106. if (!unfinishedOrder.isEmpty()) {
  107. throw new BusinessException("您有未完结的订单!");
  108. }
  109. // 检查设备是否有僵死订单(设备显示忙碌但订单实际已超时)
  110. checkAndResolveStaleOrder(params.getProductKey(), params.getDeviceName());
  111. // 设备占用检查:僵死单清理后设备仍有他人未完结订单,说明设备真实使用中,拒绝开单
  112. var occupiedOrder = lambdaQuery()
  113. .eq(WashOrder::getProductKey, params.getProductKey())
  114. .eq(WashOrder::getDeviceName, params.getDeviceName())
  115. .eq(WashOrder::getOrderStatus, WashOrder.ORDER_STATUS_开机)
  116. .eq(WashOrder::getPayStatus, WashOrder.PAY_STATUS_未支付)
  117. .one();
  118. if (occupiedOrder != null && !occupiedOrder.getUserId().equals(StpUtil.getLoginIdAsLong())) {
  119. throw new BusinessException("设备他人使用中!");
  120. }
  121. var memberName = StpUtil.getSession().getString(User.ST_SESSION_KEY_MOBILE);
  122. var orderId = OrderUtils.getOrderNo();
  123. // 请求阿里云lot
  124. var createOrder = awoaraService.createOrder(params.getProductKey(), params.getDeviceName(),
  125. orderId,
  126. memberName,
  127. account.getBalance(),
  128. Account.NO_DISCOUNT,
  129. // 本次开机最大消费金额
  130. account.getBalance());
  131. // 首次消费:无归属站点的用户自动归属到当前设备所在站点
  132. long userId = StpUtil.getLoginIdAsLong();
  133. var user = userService.getById(userId);
  134. var userStationId = user != null ? user.getStationId() : null;
  135. if (CommUtil.isEmptyOrNull(userStationId)) {
  136. userService.lambdaUpdate()
  137. .set(User::getStationId, params.getStationId())
  138. .eq(User::getId, userId)
  139. .update();
  140. StpUtil.getSession().set("stationId", params.getStationId());
  141. KymCache.INSTANCE.putUserId2StationId(Map.of(userId, params.getStationId()));
  142. userStationId = params.getStationId();
  143. }
  144. var washOrder = new WashOrder()
  145. .setUserId(StpUtil.getLoginIdAsLong())
  146. .setStationId(params.getStationId())
  147. .setProductKey(params.getProductKey())
  148. .setDeviceName(params.getDeviceName())
  149. .setShortId(KymCache.INSTANCE.getShortIdByProductKeyAndDeviceName(params.getProductKey(), params.getDeviceName()))
  150. .setOpenType(WashOrder.START_CLOSE_TYPE_网络)
  151. .setOrderId(orderId)
  152. .setOrderIdLocal(createOrder.getOrder_id_local())
  153. .setMemberDiscount(Account.NO_DISCOUNT)
  154. .setPrepayMoney(account.getBalance())
  155. .setStartTime(LocalDateTime.now())
  156. .setOrderStatus(WashOrder.ORDER_STATUS_开机)
  157. .setPayStatus(WashOrder.PAY_STATUS_未支付)
  158. .setIsCross(!params.getStationId().equals(userStationId));
  159. save(washOrder);
  160. return orderId;
  161. }
  162. @Override
  163. public void closeOrder(DeviceQueryParams params) {
  164. var order = lambdaQuery()
  165. .eq(WashOrder::getProductKey, params.getProductKey())
  166. .eq(WashOrder::getDeviceName, params.getDeviceName())
  167. .eq(WashOrder::getOrderStatus, WashOrder.ORDER_STATUS_开机)
  168. .eq(WashOrder::getPayStatus, WashOrder.PAY_STATUS_未支付)
  169. .one();
  170. if (order == null) {
  171. return;
  172. }
  173. if (order.getUserId() != StpUtil.getLoginIdAsLong()) {
  174. throw new BusinessException("您没有权限关闭该订单!");
  175. }
  176. // 尝试发送关闭命令,失败后重试1次
  177. boolean sent = sendCloseOrderWithRetry(params.getProductKey(), params.getDeviceName(), order.getOrderId(), 1);
  178. if (sent) {
  179. return;
  180. }
  181. // RRpc 失败,主动查询设备订单状态
  182. log.warn("订单 {} 关闭命令发送失败,尝试主动查询设备状态", order.getOrderId());
  183. try {
  184. OrderInfo orderInfo = awoaraService.queryOrder(params.getProductKey(), params.getDeviceName(), order.getOrderId());
  185. if (orderInfo != null && orderInfo.getClose_type() != null && !orderInfo.getClose_type().isEmpty()) {
  186. log.info("订单 {} 设备已关闭,执行结算", order.getOrderId());
  187. orderSettlementService.settleOrder(order, orderInfo);
  188. return;
  189. }
  190. if (orderInfo != null && (orderInfo.getClose_type() == null || orderInfo.getClose_type().isEmpty())) {
  191. // 设备还在运行,尝试强制关闭
  192. log.info("订单 {} 设备仍在运行,尝试强制关闭", order.getOrderId());
  193. try {
  194. awoaraService.forceCloseOrder(params.getProductKey(), params.getDeviceName());
  195. } catch (Exception fe) {
  196. log.error("订单 {} 强制关闭也失败", order.getOrderId(), fe);
  197. }
  198. }
  199. } catch (Exception e) {
  200. log.error("订单 {} 主动查询设备状态也失败", order.getOrderId(), e);
  201. }
  202. throw new BusinessException("关闭订单失败,请稍后重试或联系管理员");
  203. }
  204. @Override
  205. public void forceSettleOrder(String orderId) {
  206. var order = lambdaQuery()
  207. .eq(WashOrder::getOrderId, orderId)
  208. .eq(WashOrder::getOrderStatus, WashOrder.ORDER_STATUS_开机)
  209. .eq(WashOrder::getPayStatus, WashOrder.PAY_STATUS_未支付)
  210. .one();
  211. if (order == null) {
  212. throw new BusinessException("未找到该订单或订单已完结");
  213. }
  214. // 尝试通过 RRpc 发送关闭命令
  215. sendCloseOrderWithRetry(order.getProductKey(), order.getDeviceName(), order.getOrderId(), 1);
  216. // 查询设备订单状态进行结算
  217. log.info("订单 {} 已发送关闭命令,查询设备订单状态进行结算", order.getOrderId());
  218. try {
  219. OrderInfo orderInfo = awoaraService.queryOrder(order.getProductKey(), order.getDeviceName(), order.getOrderId());
  220. if (orderInfo != null && orderInfo.getClose_type() != null && !orderInfo.getClose_type().isEmpty()) {
  221. log.info("订单 {} 设备已关闭,执行结算", order.getOrderId());
  222. orderSettlementService.settleOrder(order, orderInfo);
  223. return;
  224. }
  225. if (orderInfo != null && (orderInfo.getClose_type() == null || orderInfo.getClose_type().isEmpty())) {
  226. // 设备还在运行,尝试强制关闭
  227. log.info("订单 {} 设备仍在运行,尝试强制关闭", order.getOrderId());
  228. try {
  229. awoaraService.forceCloseOrder(order.getProductKey(), order.getDeviceName());
  230. } catch (Exception fe) {
  231. log.error("订单 {} 强制关闭也失败", order.getOrderId(), fe);
  232. }
  233. }
  234. } catch (Exception e) {
  235. log.error("订单 {} 主动查询设备状态也失败", order.getOrderId(), e);
  236. }
  237. throw new BusinessException("强制结算失败,请稍后重试或联系管理员");
  238. }
  239. private boolean sendCloseOrderWithRetry(String productKey, String deviceName, String orderId, int maxRetries) {
  240. for (int i = 0; i <= maxRetries; i++) {
  241. try {
  242. awoaraService.closeOrder(productKey, deviceName, orderId);
  243. return true;
  244. } catch (Exception e) {
  245. if (i < maxRetries) {
  246. log.warn("订单 {} 关闭命令第{}次失败,准备重试", orderId, i + 1, e);
  247. } else {
  248. log.error("订单 {} 关闭命令{}次均失败", orderId, maxRetries + 1, e);
  249. }
  250. }
  251. }
  252. return false;
  253. }
  254. /**
  255. * 检查设备是否有僵死订单,有则尝试清理
  256. */
  257. private void checkAndResolveStaleOrder(String productKey, String deviceName) {
  258. var staleOrder = lambdaQuery()
  259. .eq(WashOrder::getProductKey, productKey)
  260. .eq(WashOrder::getDeviceName, deviceName)
  261. .eq(WashOrder::getOrderStatus, WashOrder.ORDER_STATUS_开机)
  262. .eq(WashOrder::getPayStatus, WashOrder.PAY_STATUS_未支付)
  263. .one();
  264. if (staleOrder == null) {
  265. return;
  266. }
  267. log.warn("设备 {}/{} 存在未完结订单 {},尝试查询设备实际状态", productKey, deviceName, staleOrder.getOrderId());
  268. try {
  269. OrderInfo orderInfo = awoaraService.queryOrder(productKey, deviceName, staleOrder.getOrderId());
  270. if (orderInfo != null && orderInfo.getClose_type() != null && !orderInfo.getClose_type().isEmpty()) {
  271. log.info("设备实际已关闭订单 {},执行结算", staleOrder.getOrderId());
  272. orderSettlementService.settleOrder(staleOrder, orderInfo);
  273. }
  274. } catch (Exception e) {
  275. log.warn("查询设备订单状态失败,跳过清理:{}", e.getMessage());
  276. }
  277. }
  278. /**
  279. * 查询订单详情
  280. *
  281. * @param params
  282. * @return
  283. */
  284. @Override
  285. public WashOrder queryOrder(WashOrderQueryParams params) {
  286. // C 端订单详情:强制按当前登录用户过滤,防止通过订单号越权查看他人订单
  287. WashOrder order = lambdaQuery()
  288. .eq(WashOrder::getOrderId, params.getOrderId())
  289. .eq(WashOrder::getUserId, StpUtil.getLoginIdAsLong())
  290. .one();
  291. CommUtil.asserts(null != order, "订单不存在或您没有权限查看该订单!");
  292. return order;
  293. }
  294. /**
  295. * 查询停车减免订单
  296. *
  297. * @param unionid
  298. * @return
  299. */
  300. @Override
  301. public String getParkingDiscounts(String unionid) {
  302. var user = userService.lambdaQuery().eq(User::getUnionid, unionid).one();
  303. CommUtil.asserts(null != user, "用户信息异常:无此用户");
  304. // 查询用户24小时内的订单
  305. var orders = lambdaQuery()
  306. .eq(WashOrder::getUserId, user.getId())
  307. .ge(WashOrder::getStartTime, LocalDateTime.now().minusHours(24))
  308. .eq(WashOrder::getPayStatus, WashOrder.PAY_STATUS_已支付)
  309. .orderByDesc(WashOrder::getId)
  310. .list();
  311. // 原断言写反:orders 非空时抛异常、为空时走到 orders.get(0) NPE,此处修正
  312. CommUtil.asserts(!CommUtil.isEmptyOrNull(orders), "抱歉:无停车场洗车记录");
  313. return washStationService.lambdaQuery().eq(WashStation::getStationId, orders.get(0).getStationId()).one().getParkingQrCode();
  314. }
  315. /**
  316. * 当前用户订单列表
  317. *
  318. * @param params
  319. * @return
  320. */
  321. @Override
  322. public PageBean<WashOrderVo> listMyWashOrder(PageParams params) {
  323. PageHelper.startPage(params.getPageNum(), params.getPageSize());
  324. var res = lambdaQuery()
  325. .eq(WashOrder::getUserId, StpUtil.getLoginIdAsLong())
  326. .orderByDesc(WashOrder::getId)
  327. .list();
  328. PageInfo<WashOrder> pages = new PageInfo<>(res);
  329. var voList = pages.getList().stream().map(order -> {
  330. var vo = new WashOrderVo();
  331. BeanUtils.copyProperties(order, vo);
  332. vo.setStationName(KymCache.INSTANCE.getStationNameById(order.getStationId()));
  333. return vo;
  334. }).toList();
  335. PageBean<WashOrderVo> bean = new PageBean<>(voList);
  336. bean.setPages(pages.getPages());
  337. bean.setPageNum(pages.getPageNum());
  338. bean.setPageSize(pages.getPageSize());
  339. bean.setTotal(pages.getTotal());
  340. return bean;
  341. }
  342. //region 管理后台
  343. @Override
  344. public PageBean<WashOrderVo> list(WashOrderQueryParams params) {
  345. PageHelper.startPage(params.getPageNum(), params.getPageSize());
  346. MPJLambdaWrapper<WashOrder> wrapper = JoinWrappers.lambda(WashOrder.class)
  347. .selectAsClass(WashOrder.class, WashOrderVo.class)
  348. .selectAs(User::getMobilePhone, WashOrderVo::getMobilePhone)
  349. .leftJoin(User.class, User::getId, WashOrder::getUserId)
  350. .eq(CommUtil.isNotEmptyAndNull(params.getUserId()), WashOrder::getUserId, params.getUserId())
  351. .eq(CommUtil.isNotEmptyAndNull(params.getMobilePhone()), User::getMobilePhone, params.getMobilePhone())
  352. .eq(CommUtil.isNotEmptyAndNull(params.getOrderStatus()), WashOrder::getOrderStatus, params.getOrderStatus())
  353. .eq(CommUtil.isNotEmptyAndNull(params.getPayStatus()), WashOrder::getPayStatus, params.getPayStatus())
  354. .orderByDesc(WashOrder::getId);
  355. //连表查询 返回自定义ResultType
  356. List<WashOrderVo> list = selectJoinList(WashOrderVo.class, wrapper);
  357. list.forEach(item -> {
  358. item.setStationName(KymCache.INSTANCE.getStationNameById(item.getStationId()));
  359. item.setUserStationId(KymCache.INSTANCE.getUserStationId(item.getUserId()));
  360. item.setUserStationName(KymCache.INSTANCE.getStationNameById(item.getUserStationId()));
  361. });
  362. return new PageBean<>(list);
  363. }
  364. /**
  365. * 获取订单详情
  366. * 根据订单号查询,而不是主键ID
  367. *
  368. * @param orderId 订单号
  369. * @return 订单详情
  370. */
  371. @Override
  372. public WashOrderVo detail(String orderId) {
  373. MPJLambdaWrapper<WashOrder> wrapper = JoinWrappers.lambda(WashOrder.class)
  374. .selectAsClass(WashOrder.class, WashOrderVo.class)
  375. .selectAs(User::getMobilePhone, WashOrderVo::getMobilePhone)
  376. .leftJoin(User.class, User::getId, WashOrder::getUserId)
  377. .eq(WashOrder::getOrderId, orderId);
  378. WashOrderVo washOrderVo = selectJoinOne(WashOrderVo.class, wrapper);
  379. if (washOrderVo != null) {
  380. washOrderVo.setStationName(KymCache.INSTANCE.getStationNameById(washOrderVo.getStationId()));
  381. washOrderVo.setUserStationId(KymCache.INSTANCE.getUserStationId(washOrderVo.getUserId()));
  382. washOrderVo.setUserStationName(KymCache.INSTANCE.getStationNameById(washOrderVo.getUserStationId()));
  383. }
  384. return washOrderVo;
  385. }
  386. /**
  387. * 统计指定日期各站点消费金额
  388. *
  389. * @param statDay
  390. * @return
  391. */
  392. @Override
  393. public Map<String, Integer> sumAmountByDate(LocalDate statDay) {
  394. LambdaQueryWrapper<WashOrder> wrapper = new LambdaQueryWrapper<>();
  395. wrapper.select(WashOrder::getAmount, WashOrder::getStationId);
  396. wrapper.ge(WashOrder::getStartTime, LocalDateTime.of(statDay, LocalTime.MIN));
  397. wrapper.lt(WashOrder::getStartTime, LocalDateTime.of(statDay.plusDays(1), LocalTime.MIN));
  398. var list = list(wrapper);
  399. // 按照站点分组统计金额
  400. return list.stream().collect(Collectors.groupingBy(WashOrder::getStationId, Collectors.summingInt(WashOrder::getAmount)));
  401. }
  402. /**
  403. * 统计指定日期当月各站点总消费金额
  404. *
  405. * @param statDay
  406. * @return
  407. */
  408. @Override
  409. public Map<String, Integer> sumMonthAmount(LocalDate statDay) {
  410. var startTime = statDay.with(TemporalAdjusters.firstDayOfMonth()).atTime(LocalTime.MIN);
  411. var endTime = statDay.with(TemporalAdjusters.firstDayOfMonth()).plusMonths(1).atTime(LocalTime.MIN);
  412. LambdaQueryWrapper<WashOrder> wrapper = new LambdaQueryWrapper<>();
  413. wrapper.select(WashOrder::getAmount, WashOrder::getStationId);
  414. wrapper.ge(WashOrder::getStartTime, startTime);
  415. wrapper.lt(WashOrder::getStartTime, endTime);
  416. var list = list(wrapper);
  417. // 按照站点分组统计金额
  418. return list.stream().collect(Collectors.groupingBy(WashOrder::getStationId, Collectors.summingInt(WashOrder::getAmount)));
  419. }
  420. /**
  421. * 统计指定日期订单数量
  422. *
  423. * @param statDay
  424. * @return
  425. */
  426. @Override
  427. public Map<String, Integer> countDailyOrders(LocalDate statDay) {
  428. LambdaQueryWrapper<WashOrder> wrapper = new LambdaQueryWrapper<>();
  429. wrapper.ge(WashOrder::getStartTime, LocalDateTime.of(statDay, LocalTime.MIN));
  430. wrapper.lt(WashOrder::getStartTime, LocalDateTime.of(statDay.plusDays(1), LocalTime.MIN));
  431. // 按照站点分组统计订单数量
  432. return list(wrapper).stream().collect(Collectors.groupingBy(WashOrder::getStationId, Collectors.summingInt(o -> 1)));
  433. }
  434. /**
  435. * 统计指定日期订单数量
  436. *
  437. * @param statDay
  438. * @return
  439. */
  440. @Override
  441. public Map<String, Integer> countMonthOrders(LocalDate statDay) {
  442. var startTime = statDay.with(TemporalAdjusters.firstDayOfMonth()).atTime(LocalTime.MIN);
  443. var endTime = statDay.with(TemporalAdjusters.firstDayOfMonth()).plusMonths(1).atTime(LocalTime.MIN);
  444. LambdaQueryWrapper<WashOrder> wrapper = new LambdaQueryWrapper<>();
  445. wrapper.select(WashOrder::getStationId);
  446. wrapper.ge(WashOrder::getStartTime, startTime);
  447. wrapper.lt(WashOrder::getStartTime, endTime);
  448. // 按照站点分组统计订单数量
  449. return list(wrapper).stream().collect(Collectors.groupingBy(WashOrder::getStationId, Collectors.summingInt(o -> 1)));
  450. }
  451. @Override
  452. public Map<String, Integer> countDailyActiveUsers(LocalDate statDay) {
  453. LambdaQueryWrapper<WashOrder> wrapper = new LambdaQueryWrapper<>();
  454. wrapper.select(WashOrder::getUserId, WashOrder::getStationId);
  455. wrapper.ge(WashOrder::getStartTime, LocalDateTime.of(statDay, LocalTime.MIN));
  456. wrapper.lt(WashOrder::getStartTime, LocalDateTime.of(statDay.plusDays(1), LocalTime.MIN));
  457. wrapper.groupBy(WashOrder::getStationId, WashOrder::getUserId);
  458. return list(wrapper).stream()
  459. .collect(Collectors.groupingBy(WashOrder::getStationId, Collectors.summingInt(o -> 1)));
  460. }
  461. @Override
  462. public Map<String, Integer> countMonthActiveUsers(LocalDate statDay) {
  463. var startTime = statDay.with(TemporalAdjusters.firstDayOfMonth()).atTime(LocalTime.MIN);
  464. var endTime = statDay.with(TemporalAdjusters.firstDayOfMonth()).plusMonths(1).atTime(LocalTime.MIN);
  465. LambdaQueryWrapper<WashOrder> wrapper = new LambdaQueryWrapper<>();
  466. wrapper.select(WashOrder::getUserId, WashOrder::getStationId);
  467. wrapper.ge(WashOrder::getStartTime, startTime);
  468. wrapper.lt(WashOrder::getStartTime, endTime);
  469. wrapper.groupBy(WashOrder::getStationId, WashOrder::getUserId);
  470. return list(wrapper).stream()
  471. .collect(Collectors.groupingBy(WashOrder::getStationId, Collectors.summingInt(o -> 1)));
  472. }
  473. @Override
  474. public Map<String, Integer> countDistinctActiveUsers(LocalDate startDate, LocalDate endDate) {
  475. LambdaQueryWrapper<WashOrder> wrapper = new LambdaQueryWrapper<>();
  476. wrapper.select(WashOrder::getUserId, WashOrder::getStationId);
  477. wrapper.ge(WashOrder::getStartTime, LocalDateTime.of(startDate, LocalTime.MIN));
  478. wrapper.lt(WashOrder::getStartTime, LocalDateTime.of(endDate.plusDays(1), LocalTime.MIN));
  479. wrapper.groupBy(WashOrder::getStationId, WashOrder::getUserId);
  480. return list(wrapper).stream()
  481. .collect(Collectors.groupingBy(WashOrder::getStationId, Collectors.summingInt(o -> 1)));
  482. }
  483. @Override
  484. public WashOrder getOrderInProgressByUserId(long userId) {
  485. return lambdaQuery()
  486. .eq(WashOrder::getUserId, userId)
  487. .eq(WashOrder::getPayStatus, WashOrder.PAY_STATUS_未支付)
  488. .one();
  489. }
  490. @Override
  491. public List<StationTrendVo> stationTrend(StatQueryParam params) {
  492. return baseMapper.StationTrend(params);
  493. }
  494. @Override
  495. public String checkParkingCoupon(String mobilePhone) {
  496. CommUtil.asserts(CommUtil.isNotEmptyAndNull(mobilePhone),"查询手机号不能为空");
  497. var user = userService.getOne(new LambdaQueryWrapper<User>().eq(User::getMobilePhone, mobilePhone));
  498. if (user == null) {
  499. throw new BusinessException("请输入注册洗车小程序使用的手机号码!");
  500. }
  501. // 优惠券有效期2小时,先查已支付订单
  502. var orderList = lambdaQuery()
  503. .eq(WashOrder::getUserId, user.getId())
  504. .ge(WashOrder::getEndTime, LocalDateTime.now().minusHours(2))
  505. .eq(WashOrder::getPayStatus, WashOrder.PAY_STATUS_已支付)
  506. .orderByDesc(WashOrder::getId).list();
  507. if (!orderList.isEmpty() && orderList.stream().collect(Collectors.summarizingInt(WashOrder::getAmount)).getSum() >= KymCache.INSTANCE.getParkingCouponMinAmountByStationId(orderList.get(0).getStationId())) {
  508. var parkingCouponUrl = KymCache.INSTANCE.getParkingQrCodeUrlByStationId(orderList.get(0).getStationId());
  509. var code = UUID.randomUUID().toString();
  510. KymCache.INSTANCE.setParkingCouponCode(code, parkingCouponUrl, 3600 * 2L);
  511. parkingCouponRecordService.save(new ParkingCouponRecord()
  512. .setCode(code)
  513. .setUserId(user.getId())
  514. .setStationId(orderList.get(0).getStationId())
  515. .setOrderId(orderList.get(0).getOrderId())
  516. .setTargetUrl(parkingCouponUrl)
  517. .setStatus(ParkingCouponRecord.STATUS_未使用)
  518. .setExpireTime(LocalDateTime.now().plusHours(2)));
  519. return DOMAIN + "/api/parking-coupon?code=" + code;
  520. }
  521. // 未找到符合条件的已支付订单,尝试查找僵死订单并主动对账结算
  522. var staleOrders = lambdaQuery()
  523. .eq(WashOrder::getUserId, user.getId())
  524. .ge(WashOrder::getStartTime, LocalDateTime.now().minusHours(2))
  525. .eq(WashOrder::getOrderStatus, WashOrder.ORDER_STATUS_开机)
  526. .eq(WashOrder::getPayStatus, WashOrder.PAY_STATUS_未支付)
  527. .orderByDesc(WashOrder::getId).list();
  528. if (!staleOrders.isEmpty()) {
  529. boolean reconciled = false;
  530. for (WashOrder staleOrder : staleOrders) {
  531. try {
  532. OrderInfo orderInfo = awoaraService.queryOrder(staleOrder.getProductKey(), staleOrder.getDeviceName(), staleOrder.getOrderId());
  533. if (orderInfo != null && orderInfo.getClose_type() != null && !orderInfo.getClose_type().isEmpty()) {
  534. log.info("停车券查询-订单 {} 设备已关闭,主动结算", staleOrder.getOrderId());
  535. orderSettlementService.settleOrder(staleOrder, orderInfo);
  536. reconciled = true;
  537. }
  538. } catch (Exception e) {
  539. log.warn("停车券查询-订单 {} 查询设备失败:{}", staleOrder.getOrderId(), e.getMessage());
  540. }
  541. }
  542. if (reconciled) {
  543. // 结算后重新查询已支付订单
  544. orderList = lambdaQuery()
  545. .eq(WashOrder::getUserId, user.getId())
  546. .ge(WashOrder::getEndTime, LocalDateTime.now().minusHours(2))
  547. .eq(WashOrder::getPayStatus, WashOrder.PAY_STATUS_已支付)
  548. .orderByDesc(WashOrder::getId).list();
  549. if (!orderList.isEmpty() && orderList.stream().collect(Collectors.summarizingInt(WashOrder::getAmount)).getSum() >= KymCache.INSTANCE.getParkingCouponMinAmountByStationId(orderList.get(0).getStationId())) {
  550. var parkingCouponUrl = KymCache.INSTANCE.getParkingQrCodeUrlByStationId(orderList.get(0).getStationId());
  551. var code = UUID.randomUUID().toString();
  552. KymCache.INSTANCE.setParkingCouponCode(code, parkingCouponUrl, 3600 * 2L);
  553. parkingCouponRecordService.save(new ParkingCouponRecord()
  554. .setCode(code)
  555. .setUserId(user.getId())
  556. .setStationId(orderList.get(0).getStationId())
  557. .setOrderId(orderList.get(0).getOrderId())
  558. .setTargetUrl(parkingCouponUrl)
  559. .setStatus(ParkingCouponRecord.STATUS_未使用)
  560. .setExpireTime(LocalDateTime.now().plusHours(2)));
  561. return DOMAIN + "/api/parking-coupon?code=" + code;
  562. }
  563. }
  564. }
  565. throw new BusinessException("不符合优惠条件:订单完成超过2小时或洗车金额不足6元。");
  566. }
  567. //endregion
  568. }