cxjz2.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. # coding=utf-8
  2. import datetime
  3. import logging
  4. from arrow import Arrow
  5. from django.conf import settings
  6. from typing import TYPE_CHECKING
  7. from apilib.monetary import VirtualCoin, Ratio
  8. from apps.web.constant import START_DEVICE_STATUS, DEALER_CONSUMPTION_AGG_KIND
  9. from apps.web.core.device_define.cxjz import ChargeMode, REASON_MAP, DEFAULT_REFUND_PROTECTION_TIME, DEFAULT_REFUND_CAL_BASE, DEFAULT_SERVICE_FEE
  10. from apps.web.device.models import Device
  11. from apps.web.eventer import EventBuilder
  12. from apps.web.eventer.base import ComNetPayAckEvent, WorkEvent
  13. from apps.web.user.utils import clear_frozen_user_balance
  14. from apps.web.utils import set_start_key_status
  15. if TYPE_CHECKING:
  16. from apps.web.user.models import ConsumeRecord
  17. logger = logging.getLogger(__name__)
  18. class builder(EventBuilder):
  19. def __getEvent__(self, device_event):
  20. if 'order_id' in device_event:
  21. if device_event["order_type"] == "com_start":
  22. return MyComNetPayAckEvent(self.deviceAdapter, device_event)
  23. # 暂时没有卡
  24. event_data = self.deviceAdapter.analyze_event_data(device_event['data'])
  25. if event_data is None or 'cmdCode' not in event_data:
  26. return None
  27. return CXJZWorkEvent(self.deviceAdapter, event_data)
  28. class CXJZWorkEvent(WorkEvent):
  29. def do(self, **args):
  30. pass
  31. class MyComNetPayAckEvent(ComNetPayAckEvent):
  32. def do_running_order(self, order, result): # type: (ConsumeRecord, dict) -> None
  33. """
  34. 处理运行订单
  35. :param order: 用户服务器订单
  36. :param result: device_event 设备侧订单
  37. :return:
  38. """
  39. # 订单消息已经被回复过
  40. logger.debug('[{} do_running_order] order = {}, result = {}'.format(self.__class__, repr(order), result))
  41. if order.status in ["running", "finished"]:
  42. order.isNormal = True
  43. order.save()
  44. logger.debug('order<{}> no need to deal. this has done.'.format(repr(order)))
  45. return
  46. # 启动设备的时候 设备实际启动成功 但是订单串口超时 不知道订单的明确状态 后面启动时间又重新上报
  47. if order.status == "unknown":
  48. errorDesc = u"设备信号恢复,订单正常运行"
  49. logger.info("order <{}> timeout to running")
  50. # 正常运行的订单
  51. else:
  52. errorDesc = u""
  53. if 'master' in result:
  54. order.association = {
  55. 'master': result['master']
  56. }
  57. order.servicedInfo.update({'masterOrderNo': result['master']})
  58. order.errorDesc = errorDesc
  59. order.isNormal = True
  60. order.status = 'running'
  61. order.startTime = datetime.datetime.fromtimestamp(result['sts'])
  62. order.save()
  63. set_start_key_status(start_key=order.startKey, state=START_DEVICE_STATUS.FINISHED, order_id=str(order.id))
  64. def do_finished_order(self, order, result): # type: (ConsumeRecord, dict) -> None
  65. """
  66. 处理结束运行订单
  67. :param order: 用户服务器订单
  68. :param result: device_event 设备侧订单
  69. :return:
  70. """
  71. portCache = Device.get_port_control_cache(self.device.devNo, str(order.used_port))
  72. self.event_data["portCache"] = portCache
  73. # 子单归并主单
  74. if 'sub' in result:
  75. order.association = {
  76. 'sub': [item['order_id'] for item in self.event_data['sub']]
  77. }
  78. order.servicedInfo.update(
  79. {'subOrderNo': '{}'.format(', '.join([item['order_id'] for item in self.event_data['sub']]))})
  80. # 主单归并自身
  81. elif 'master' in result:
  82. order.association = {
  83. 'master': result['master']
  84. }
  85. order.servicedInfo.update({'masterOrderNo': result['master']})
  86. # 此时理论上服务器订单状态有三种可能(finished在上层已经被排除)
  87. # 正常的状态 相当于订单由运行状态 即将切换为finished状态
  88. if order.status == "running":
  89. order.isNormal = True
  90. order.status = "finished"
  91. order.errorDesc = u""
  92. order.finishedTime = datetime.datetime.fromtimestamp(result['fts'])
  93. # 非正常状态 相当于订单最开始串口超时 然后直接变为结束
  94. elif order.status == "unknown":
  95. order.isNormal = True
  96. order.status = "finished"
  97. order.errorDesc = u"设备信号恢复,订单正常结束(0001)"
  98. order.startTime = datetime.datetime.fromtimestamp(result['sts'])
  99. order.finishedTime = datetime.datetime.fromtimestamp(result['fts'])
  100. # 正常状态 相当于订单启动失败或者是中间running单没有上来
  101. elif order.status == "created":
  102. order.isNormal = True
  103. order.status = "finished"
  104. order.errorDesc = u"设备信号恢复,订单正常结束(0002)"
  105. order.startTime = datetime.datetime.fromtimestamp(result['sts'])
  106. order.finishedTime = datetime.datetime.fromtimestamp(result['fts'])
  107. else:
  108. logger.warning('order<{}> status = <{}> to finished. no deal with'.format(repr(order), order.status))
  109. order.save()
  110. set_start_key_status(start_key=order.startKey, state=START_DEVICE_STATUS.FINISHED, order_id=str(order.id))
  111. def do_finished_event(self, order, sub_orders, merge_order_info): # type:(ConsumeRecord, list, dict) -> None
  112. """
  113. 订单的状态已经完成 进一步事件 扣费等等
  114. :param order: 处理完毕的订单(主订单)
  115. :param sub_orders: 子订单
  116. :param merge_order_info: 合并单的信息
  117. :return:
  118. """
  119. order.reload()
  120. # 解析event的参数 这个left是转换后的时间
  121. need = merge_order_info["needValue"]
  122. left = merge_order_info["leftValue"]
  123. coins = merge_order_info["coins"]
  124. billingType = merge_order_info["billingType"]
  125. # 获取端口的缓存
  126. # 算电量 算时间
  127. if billingType == ChargeMode.TIME:
  128. backCoins = self._cal_power_refund_money(coins, left, need)
  129. duration = need - left
  130. spendElec = 0
  131. extra = [
  132. {u"本次订购时长": "{}分钟".format(need)},
  133. {u"本次实际使用时长": "{}分钟".format(need-left)},
  134. {u"消费金额": "{}(金币)".format((VirtualCoin(coins) - VirtualCoin(backCoins)).amount)},
  135. ]
  136. else:
  137. # 电量模式下 时间有可能不太准 考虑断电的情况
  138. backCoins = self._cal_elec_refund_money(coins, left, need)
  139. duration = (self.event_data["fts"] - self.event_data["sts"]) / 60
  140. spendElec = (need - left) / 100.0
  141. extra = [
  142. {u"本次订购电量": u"{} 度".format(need/100.0)},
  143. {u"消费金额": u"{}(金币)".format((VirtualCoin(coins) - VirtualCoin(backCoins)).amount)}
  144. ]
  145. if backCoins > VirtualCoin(0):
  146. extra.append({u"退费金额": u"{}(金币)".format(VirtualCoin(backCoins).amount)})
  147. clear_frozen_user_balance(self.device, order, duration, spendElec=spendElec, backCoins=backCoins, user=order.user)
  148. # 组织消费信息
  149. consumeDict = {
  150. "reason": self._get_finish_reason(),
  151. "billingType": u"时间计费" if billingType != ChargeMode.ELEC else u"电量计费",
  152. DEALER_CONSUMPTION_AGG_KIND.DURATION: duration,
  153. DEALER_CONSUMPTION_AGG_KIND.SPEND_MONEY: (VirtualCoin(coins) - VirtualCoin(backCoins)).mongo_amount,
  154. }
  155. order.update_service_info(consumeDict)
  156. self.notify_user_service_complete(
  157. service_name='充电',
  158. openid=order.user.managerialOpenId,
  159. port=str(order.used_port),
  160. address=order.address,
  161. reason=consumeDict["reason"],
  162. finished_time=order.finishedTime.strftime('%Y-%m-%d %H:%M:%S'),
  163. extra=extra
  164. )
  165. def merge_order(self, master_order, sub_orders): # type:(ConsumeRecord, list)->dict
  166. """
  167. 主板暂时不支持续充 如果续充的话 时间不准
  168. :param master_order:
  169. :param sub_orders:
  170. :return:
  171. """
  172. start_time = Arrow.fromdatetime(master_order.startTime, tzinfo=settings.TIME_ZONE)
  173. # 首先获取充电的方式
  174. chargeMode = self.event_data["charge_mode"]
  175. if chargeMode == ChargeMode.TIME:
  176. needKind = "needTime"
  177. leftValue = self.event_data["left_time"]
  178. # 功率分档模式之下 有二次检测的时间 这个时候 需要及时变更时间
  179. if "second_time" in self.event_data:
  180. needValue = self.event_data["second_time"]
  181. else:
  182. needValue = self.event_data["need_time"]
  183. else:
  184. needKind = "needElec"
  185. needValue = self.event_data["need_elec"]
  186. leftValue = self.event_data["left_elec"]
  187. coins = master_order.package["coins"]
  188. price = master_order.package["price"]
  189. portCache = {
  190. "openId": master_order.openId,
  191. "consumeType": "mobile",
  192. "needKind": needKind,
  193. "estimatedTs": int(start_time.timestamp + 720 * 60 * 60),
  194. "needValue": needValue,
  195. "billingType": chargeMode,
  196. "leftValue": leftValue
  197. }
  198. for _sub in sub_orders:
  199. coins += _sub.package["coins"]
  200. price += _sub.package["price"]
  201. portCache["coins"] = coins
  202. portCache["price"] = price
  203. return portCache
  204. def _cal_power_refund_money(self, coins, left, need): # type:(VirtualCoin, float, float) -> VirtualCoin
  205. """
  206. 按照闪灵科技 的退费规则进行退费
  207. 退费这块主要是两种情况,
  208. 1.非正常情况(机器不启动,空载,过载),开始几分钟的时候,全额退费。
  209. 2.正常情况:例如,经销商设置最低消费0.6元 充电标准4h/元。某用户支付1元,充电1小时,消费0.25元,正常情况应当退还0.75元,此时由于设置了最低消费为0.6元,则此1-0.6=0.4,0.4/4=0.1,只退还0.4-0.1=0.3元
  210. 最低消费金额可设置,退费计算单位可设置
  211. 向上取整 数学公式 UP(A/B) = (A+B-1)/B
  212. """
  213. # 首先判断是退费开关
  214. if not self.device.is_auto_refund:
  215. return VirtualCoin(0)
  216. # 然后判断是否到达退费保护时间
  217. refundProtectionTime = self.device.get("otherConf", dict()).get("refundProtectionTime", DEFAULT_REFUND_PROTECTION_TIME)
  218. serviceFee = self.device.get("otherConf", dict()).get("serviceFee", DEFAULT_SERVICE_FEE)
  219. refundCalBase = self.device.get("otherConf", dict()).get("refundCalBase", DEFAULT_REFUND_CAL_BASE)
  220. left = min(need, left)
  221. if (need - left) <= refundProtectionTime:
  222. return VirtualCoin(coins)
  223. # 按照进制计算时间
  224. upper = lambda x, y: int(x+y-1) // y
  225. usedBase = upper(int(need - left), int(refundCalBase))
  226. needBase = upper(int(need), int(refundCalBase))
  227. # 使用进制转换后的时间进行计算退费
  228. leftMoney = max(VirtualCoin(0), VirtualCoin(coins) - VirtualCoin(serviceFee)) # 减去服务费用的剩余的钱
  229. refundMoney = leftMoney * Ratio((needBase - usedBase) * 1.0 / needBase)
  230. return refundMoney
  231. def _cal_elec_refund_money(self, coins, left, need): # type:(VirtualCoin, float, float) -> VirtualCoin
  232. """
  233. 电量计费 不涉及进制转换的问题
  234. """
  235. if not self.device.is_auto_refund:
  236. return VirtualCoin(0)
  237. # 然后判断是否到达退费保护时间
  238. refundProtectionTime = self.device.get("otherConf", dict()).get("refundProtectionTime", DEFAULT_REFUND_PROTECTION_TIME)
  239. serviceFee = self.device.get("otherConf", dict()).get("serviceFee", DEFAULT_SERVICE_FEE)
  240. # 由于电量计费 只能使用订单时间来计算
  241. if self.event_data["fts"] - self.event_data["sts"] <= refundProtectionTime * 60:
  242. return VirtualCoin(coins)
  243. # 使用进制转换后的时间进行计算退费
  244. leftMoney = max(VirtualCoin(0), VirtualCoin(coins) - VirtualCoin(serviceFee)) # 减去服务费用的剩余的钱
  245. refundMoney = leftMoney * Ratio(left * 1.0 / need)
  246. return refundMoney
  247. def _get_finish_reason(self):
  248. if "reason" in self.event_data:
  249. return REASON_MAP.get(self.event_data["reason"], u"未知原因")
  250. return u"未知原因"