EnNotifyServiceImpl.java 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. package com.kym.service.enplus.impl;
  2. import cn.hutool.extra.mail.MailUtil;
  3. import com.alibaba.fastjson2.JSONObject;
  4. import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
  5. import com.kym.entity.admin.ConnectorInfo;
  6. import com.kym.entity.admin.EquipmentInfo;
  7. import com.kym.entity.admin.MonitorLog;
  8. import com.kym.entity.common.RedisKeys;
  9. import com.kym.entity.enplus.EnConnectorStatusInfo;
  10. import com.kym.entity.miniapp.Account;
  11. import com.kym.entity.miniapp.ChargeOrder;
  12. import com.kym.entity.miniapp.WalletDetail;
  13. import com.kym.service.admin.ConnectorInfoService;
  14. import com.kym.service.admin.EquipmentInfoService;
  15. import com.kym.service.admin.MonitorLogService;
  16. import com.kym.service.cache.KymCache;
  17. import com.kym.service.enplus.EnNotifyService;
  18. import com.kym.service.enplus.EnPlusService;
  19. import com.kym.service.factory.DiscountStrategyFactory;
  20. import com.kym.service.miniapp.*;
  21. import jakarta.annotation.PostConstruct;
  22. import org.slf4j.Logger;
  23. import org.slf4j.LoggerFactory;
  24. import org.springframework.beans.factory.annotation.Value;
  25. import org.springframework.data.redis.core.StringRedisTemplate;
  26. import org.springframework.stereotype.Service;
  27. import org.springframework.transaction.annotation.Transactional;
  28. import java.math.BigDecimal;
  29. import java.time.LocalDateTime;
  30. import java.time.format.DateTimeFormatter;
  31. import java.util.Map;
  32. import java.util.stream.Collectors;
  33. /**
  34. * @author skyline
  35. * @description
  36. * @date 2023-08-04 14:26
  37. */
  38. @Service
  39. public class EnNotifyServiceImpl implements EnNotifyService {
  40. private static final Logger LOGGER = LoggerFactory.getLogger(EnNotifyServiceImpl.class);
  41. public final StringRedisTemplate redisTemplate;
  42. private final EnPlusService enPlusService;
  43. private final ChargeOrderService chargeOrderService;
  44. private final ChargeService chargeService;
  45. private final AccountService accountService;
  46. private final WalletDetailService walletDetailService;
  47. private final MonitorLogService monitorLogService;
  48. private final EquipmentInfoService equipmentInfoService;
  49. private final ConnectorInfoService connectorInfoService;
  50. private final UserStationService userStationService;
  51. @Value("${kym.notify-email}")
  52. private String notifyEmail;
  53. public EnNotifyServiceImpl(EnPlusService enPlusService, ChargeOrderService chargeOrderService,
  54. ChargeService chargeService, AccountService accountService, WalletDetailService walletDetailService,
  55. MonitorLogService monitorLogService, EquipmentInfoService equipmentInfoService,
  56. ConnectorInfoService connectorInfoService, StringRedisTemplate redisTemplate, UserStationService userStationService) {
  57. this.enPlusService = enPlusService;
  58. this.chargeOrderService = chargeOrderService;
  59. this.chargeService = chargeService;
  60. this.accountService = accountService;
  61. this.walletDetailService = walletDetailService;
  62. this.monitorLogService = monitorLogService;
  63. this.equipmentInfoService = equipmentInfoService;
  64. this.connectorInfoService = connectorInfoService;
  65. this.redisTemplate = redisTemplate;
  66. this.userStationService = userStationService;
  67. }
  68. @PostConstruct
  69. void init() {
  70. KymCache.INSTANCE.putConnectorId2Status(connectorInfoService.list().stream().collect(Collectors.toMap(item -> item.getConnectorId().concat("1"), ConnectorInfo::getStatus)));
  71. }
  72. /**
  73. * EN+ 充电站设备状态变化推送
  74. *
  75. * @param json
  76. * @return
  77. */
  78. @Override
  79. public String handleNotificationStationStatus(JSONObject json) {
  80. var data = enPlusService.signValidation(json);
  81. LOGGER.info("【EN+推送】收到充电桩设备状态变化推送:{},解密数据:{}", json, data);
  82. // 更新数据库,存入redis,发送邮件通知
  83. var connectorStatusInfo = JSONObject.parseObject(data).getJSONObject("ConnectorStatusInfo").toJavaObject(EnConnectorStatusInfo.class);
  84. var connectorId = connectorStatusInfo.getConnectorId();
  85. var equipmentId = connectorId.substring(0, 16);
  86. equipmentInfoService.lambdaUpdate()
  87. .eq(EquipmentInfo::getEquipmentId, equipmentId)
  88. .set(EquipmentInfo::getServiceStatus, connectorStatusInfo.getStatus())
  89. .update();
  90. connectorInfoService.lambdaUpdate()
  91. .eq(ConnectorInfo::getConnectorId, connectorId)
  92. .set(ConnectorInfo::getStatus, connectorStatusInfo.getStatus())
  93. .update();
  94. var connectorStatus = connectorStatusInfo.getStatus();
  95. if (connectorStatus == 0) {
  96. LOGGER.info("充电桩设备离线:{}", connectorStatusInfo.getConnectorId());
  97. // 如果设备离线,则存入redis
  98. var monitorLog = new MonitorLog()
  99. .setStationId(KymCache.INSTANCE.getStationIdByEquipmentIdOrConnectorId(connectorStatusInfo.getConnectorId()))
  100. .setSn(connectorStatusInfo.getConnectorId())
  101. .setOfflineTime(LocalDateTime.now())
  102. .setType(2)
  103. .setOfflineStatus(connectorStatusInfo.getStatus());
  104. monitorLogService.save(monitorLog);
  105. // 离线设备放入队列,60分钟之后如果还未恢复则放入长时间离线设备集合中并发送提醒,上线后发送提醒
  106. redisTemplate.opsForZSet().add(RedisKeys.OFFLINE, connectorStatusInfo.getConnectorId(), System.currentTimeMillis() + 60 * 60 * 1000);
  107. } else {
  108. // 先删除离线设备队列的记录,再删除离线超时队列中的记录
  109. var isDelete = redisTemplate.opsForZSet().remove(RedisKeys.OFFLINE, connectorStatusInfo.getConnectorId());
  110. var exist = redisTemplate.opsForSet().remove(RedisKeys.OFFLINE_EXPIRED, connectorStatusInfo.getConnectorId());
  111. if ((isDelete != null && isDelete > 0) || (exist != null && exist > 0)) {
  112. // 更新设备监控表
  113. monitorLogService.lambdaUpdate()
  114. .eq(MonitorLog::getSn, connectorStatusInfo.getConnectorId())
  115. .eq(MonitorLog::getIsRecover, MonitorLog.IS_RECOVER_未恢复) // 未恢复的记录
  116. .set(MonitorLog::getRecoverTime, LocalDateTime.now())
  117. .set(MonitorLog::getIsRecover, MonitorLog.IS_RECOVER_已恢复) // 设置为已恢复
  118. .update();
  119. }
  120. if (exist != null && exist > 0) {
  121. MailUtil.send(notifyEmail, "【设备上线通知】", "站点:%s,设备%s恢复上线"
  122. .formatted(KymCache.INSTANCE.getStationNameByConnectorId(connectorStatusInfo.getConnectorId()), KymCache.INSTANCE.getShortIdByEquipmentIdOrConnectorId(connectorStatusInfo.getConnectorId())), false);
  123. }
  124. }
  125. // 设备状态变为空闲,校验是否有预约订单,有则清除预约订单数据。
  126. if (connectorStatus == EquipmentInfo.SERVICE_STATUS_空闲 && KymCache.INSTANCE.getConnectorStatus(connectorId) != EquipmentInfo.SERVICE_STATUS_空闲) {
  127. LOGGER.info("设备:{}状态转为空闲,清除设备预约数据...", connectorId);
  128. chargeService.cancelBookingByConnector(connectorId);
  129. }
  130. KymCache.INSTANCE.putConnectorId2Status(Map.of(connectorId, connectorStatus));
  131. return """
  132. {
  133. "Status":%d
  134. }
  135. """.formatted(0);
  136. }
  137. /**
  138. * EN+ 推送启动充电结果
  139. *
  140. * @param json
  141. * @return
  142. */
  143. @Override
  144. @Transactional(rollbackFor = Exception.class)
  145. public String handleNotificationStartChargeResult(JSONObject json) {
  146. var data = enPlusService.signValidation(json);
  147. LOGGER.info("【EN+推送】收到启动充电结果推送:{},解密数据:{}", json, data);
  148. var obj = JSONObject.parseObject(data);
  149. var startChargeSeq = obj.getString("StartChargeSeq");
  150. var startChargeSeqStat = obj.getIntValue("StartChargeSeqStat");
  151. var connectorId = obj.getString("ConnectorID");
  152. var startTime = LocalDateTime.parse(obj.getString("StartTime"), DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
  153. // 更新订单状态
  154. UpdateWrapper<ChargeOrder> updateWrapper = new UpdateWrapper<>();
  155. updateWrapper.eq("connector_id", connectorId);
  156. updateWrapper.eq("start_charge_seq", startChargeSeq);
  157. updateWrapper.set("order_status", ChargeOrder.ORDER_STATUS_未知);
  158. updateWrapper.set("charge_status", startChargeSeqStat);
  159. updateWrapper.set("start_time", startTime);
  160. chargeOrderService.update(updateWrapper);
  161. return """
  162. {
  163. "StartChargeSeq":"%s",
  164. "SuccStat":%d,
  165. "FailReason":%d
  166. }
  167. """.formatted(startChargeSeq, 0, 0);
  168. }
  169. /**
  170. * 推送充电状态,1分钟推送一次
  171. *
  172. * @param json
  173. * @return
  174. * @see com.kym.miniapp.jobs.EquipmentChargeStatusJob#executeMpUserRelationJob()
  175. */
  176. @Override
  177. @Transactional(rollbackFor = Exception.class)
  178. public String handleNotificationEquipChargeStatus(JSONObject json) {
  179. var dataStr = enPlusService.signValidation(json);
  180. var data = JSONObject.parseObject(dataStr);
  181. LOGGER.info("【EN+推送】 :{},解密数据:{}", json, data);
  182. var startChargeSeq = data.getString("StartChargeSeq");
  183. var chargeOrder = chargeOrderService.getChargingOrderByStartChargeSeq(startChargeSeq);
  184. // 更新订单信息
  185. chargeOrder.setSoc(data.getDoubleValue("Soc"));
  186. chargeOrder.setTotalPower(data.getDoubleValue("TotalPower"));
  187. chargeOrder.setTotalMoney((data.getBigDecimal("TotalMoney").multiply(BigDecimal.valueOf(100))).intValue());
  188. chargeOrder.setElecMoney((data.getBigDecimal("ElecMoney").multiply(BigDecimal.valueOf(100))).intValue());
  189. chargeOrder.setServiceMoney((data.getBigDecimal("SeviceMoney").multiply(BigDecimal.valueOf(100))).intValue()); // 这里文档service单词错误,按文档填写
  190. chargeOrder.setSumPeriod(data.getIntValue("SumPeriod"));
  191. chargeOrder.setChargeDetail(data.getString("ChargeDetails"));
  192. chargeOrder.setChargeStatus(data.getIntValue("StartChargeSeqStat"));
  193. // 优化点 EN+一分钟推送一次,同时充电人数多的时候写入数据库过于频繁
  194. // redis保存(更新)订单信息
  195. redisTemplate.opsForHash().put(RedisKeys.CHARGE_ORDER_EQUIP_CHARGE_STATUS, startChargeSeq, JSONObject.toJSONString(chargeOrder));
  196. // 将数据库写入操作放到定时任务中
  197. // chargeOrderService.updateById(chargeOrder);
  198. return """
  199. {
  200. "StartChargeSeq":"%s",
  201. "SuccStat":%d
  202. }
  203. """.formatted(startChargeSeq, 0);
  204. }
  205. /**
  206. * 推送停止充电结果
  207. *
  208. * @param json
  209. * @return
  210. */
  211. @Override
  212. @Transactional(rollbackFor = Exception.class)
  213. public String handleNotificationStopChargeResult(JSONObject json) {
  214. var dataStr = enPlusService.signValidation(json);
  215. var data = JSONObject.parseObject(dataStr);
  216. LOGGER.info("【EN+推送】收到停止充电结果推送:{},解密数据:{}", json, data);
  217. var startChargeSeq = data.getString("StartChargeSeq");
  218. var chargeOrder = chargeOrderService.getChargingOrderByStartChargeSeq(startChargeSeq);
  219. if (data.containsKey("SuccStat") && data.getIntValue("SuccStat") == 0) {
  220. chargeOrder.setChargeStatus(data.getIntValue("StartChargeSeqStat"));
  221. chargeOrderService.updateById(chargeOrder);
  222. }
  223. return """
  224. {
  225. "StartChargeSeq":"%s",
  226. "SuccStat":%d,
  227. "FailReason":%d
  228. }
  229. """.formatted(startChargeSeq, 0, 0);
  230. }
  231. /**
  232. * 推送充电订单信息(订单结算)
  233. *
  234. * @param json
  235. * @return
  236. */
  237. @Override
  238. @Transactional(rollbackFor = Exception.class)
  239. public String handleNotificationChargeOrderInfo(JSONObject json) {
  240. var dataStr = enPlusService.signValidation(json);
  241. var data = JSONObject.parseObject(dataStr);
  242. LOGGER.info("【EN+推送】收到充电订单信息推送:{},解密数据:{}", json, data);
  243. var startChargeSeq = data.getString("StartChargeSeq");
  244. var chargeOrder = chargeOrderService.getChargingOrderByStartChargeSeq(startChargeSeq);
  245. // 账户
  246. var account = accountService.getAccountByUserId(chargeOrder.getUserId());
  247. // EN+平台推送重试策略是当天失败第二天再推送一次,仅此一次。EN+订单页面可以多次手动推送,所以这里要先判断订单状态,避免重复处理。
  248. if (chargeOrder.getChargeStatus() != ChargeOrder.CHARGE_STATUS_已结束 || chargeOrder.getOrderStatus() != ChargeOrder.ORDER_STATUS_成功) {
  249. // 更新订单信息
  250. chargeOrder
  251. .setStartTime(LocalDateTime.parse(data.getString("StartTime"), DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))
  252. .setEndTime(LocalDateTime.parse(data.getString("EndTime"), DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))
  253. .setTotalPower(data.getDoubleValue("TotalPower"))
  254. .setElecMoney((int) Math.round(data.getDouble("TotalElecMoney") * 100))
  255. .setServiceMoney((int) Math.round(data.getDoubleValue("TotalSeviceMoney") * 100)) // 这里文档service单词错误,按文档填写
  256. .setTotalMoney((int) Math.round(data.getDoubleValue("TotalMoney") * 100))
  257. .setStopReason(data.getIntValue("StopReason"))
  258. .setSumPeriod(data.getIntValue("SumPeriod"))
  259. .setChargeDetail(data.getString("ChargeDetails"))
  260. // 实付金额初始化为订单总金额
  261. .setPayAmount(chargeOrder.getTotalMoney());
  262. // 结束时间
  263. var endTime = LocalDateTime.parse(data.getString("EndTime"), DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
  264. // 处理充值权益优惠逻辑/优惠券优惠逻辑
  265. // 优惠金额
  266. DiscountStrategyFactory.getDiscountStrategy(chargeOrder.getDiscountType()).computeDiscount(chargeOrder, account);
  267. // 更新订单优惠金额
  268. chargeOrderService.updateById(chargeOrder);
  269. // 扣费等资金操作
  270. deductions(chargeOrder, account, endTime);
  271. // redis删除缓存订单信息(订单结算完调用)
  272. redisTemplate.opsForHash().delete(RedisKeys.CHARGE_ORDER_EQUIP_CHARGE_STATUS, startChargeSeq);
  273. // 更新用户站点数据
  274. userStationService.updateUserStation(chargeOrder);
  275. }
  276. return """
  277. {
  278. "StartChargeSeq":"%s",
  279. "ConnectorID":"%s",
  280. "ConfirmResult":%d
  281. }
  282. """.formatted(startChargeSeq, chargeOrder.getConnectorId(), 0);
  283. }
  284. /**
  285. * 扣费等资金操作
  286. *
  287. * @param chargeOrder
  288. * @param account
  289. * @param endTime
  290. */
  291. private void deductions(ChargeOrder chargeOrder, Account account, LocalDateTime endTime) {
  292. // 订单成功
  293. chargeOrder.setOrderStatus(ChargeOrder.ORDER_STATUS_成功);
  294. // 充电结束
  295. chargeOrder.setChargeStatus(ChargeOrder.CHARGE_STATUS_已结束);
  296. chargeOrderService.updateById(chargeOrder);
  297. // 账户扣费
  298. var beforeBalance = account.getBalance();
  299. account.setBalance(beforeBalance - chargeOrder.getPayAmount());
  300. accountService.updateById(account);
  301. // 记录资金流水
  302. var walletDetail = new WalletDetail();
  303. walletDetail.setUserId(chargeOrder.getUserId());
  304. walletDetail.setOrderNo(chargeOrder.getStartChargeSeq());
  305. // 消费
  306. walletDetail.setType(WalletDetail.TYPE_消费);
  307. walletDetail.setAmount(chargeOrder.getPayAmount());
  308. walletDetail.setBeforeBalance(beforeBalance);
  309. walletDetail.setAfterBalance(account.getBalance());
  310. walletDetail.setTransactionTime(endTime);
  311. // 已确认
  312. walletDetail.setStatus(WalletDetail.STATUS_已确认);
  313. walletDetailService.save(walletDetail);
  314. }
  315. }