baojia.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485
  1. # -*- coding: utf-8 -*-
  2. # !/usr/bin/env python
  3. import datetime
  4. import logging
  5. from mongoengine import DoesNotExist
  6. from apilib.monetary import RMB, VirtualCoin, Ratio
  7. from apilib.utils_sys import memcache_lock
  8. from apps.web.constant import Const, DEALER_CONSUMPTION_AGG_KIND
  9. from apps.web.core.device_define.baojia import CMD_CODE, CARD_STATUS, DEFAULT_PARAM, BILLING_TYPE
  10. from apps.web.device.models import Device, Group
  11. from apps.web.eventer import EventBuilder
  12. from apps.web.eventer.base import FaultEvent, WorkEvent
  13. from apps.web.eventer.errors import NoCommandHandlerAvailable
  14. from apps.web.user.models import VCardConsumeRecord, ServiceProgress, CardRechargeOrder, CardConsumeRecord, \
  15. ConsumeRecord, MyUser, UserVirtualCard
  16. from apps.web.user.transaction_deprecated import refund_money
  17. logger = logging.getLogger(__name__)
  18. class builder(EventBuilder):
  19. def __getEvent__(self, device_event):
  20. event_data = self.deviceAdapter.analyze_event_data(device_event['data'])
  21. if event_data is None or 'cmdCode' not in event_data:
  22. return None
  23. if event_data['cmdCode'] in [CMD_CODE.DEVICE_SUBMIT_DEVICE_FAULT_0A]:
  24. return ChargingBAOJIAFaultEvent(self.deviceAdapter, event_data)
  25. elif event_data["cmdCode"] in [CMD_CODE.CARD_BALANCE_09, CMD_CODE.CARD_CONSUME_13, CMD_CODE.CARD_CONSUME_SURE_27]:
  26. return ChargingBJIDCardEvent(self.deviceAdapter, event_data)
  27. elif event_data["cmdCode"] in [CMD_CODE.CARD_REMOTE_CHARGE_12]:
  28. return ChargingBJICCardEvent(self.deviceAdapter, event_data)
  29. elif event_data["cmdCode"] in [CMD_CODE.DEVICE_SUBMIT_CHARGE_FINISHED_16]:
  30. return ChargingBJFinishEvent(self.deviceAdapter, event_data)
  31. elif event_data["cmdCode"] in [CMD_CODE.PORT_STATUS_21]:
  32. return ChargingBJReportEvent(self.deviceAdapter, event_data)
  33. else:
  34. raise NoCommandHandlerAvailable('[BJDZ] no command handler for cmd %s, curDevInfo=%s' % (event_data["cmdCode"], event_data))
  35. class ChargingBJFinishEvent(WorkEvent):
  36. @staticmethod
  37. def cache_key(devNo, cmd):
  38. return "event-{}-{}".format(devNo, cmd)
  39. def do(self, **args):
  40. devNo = self.device['devNo']
  41. logger.info('baojia charging event detected, devNo=%s,curInfo=%s' % (devNo, self.event_data))
  42. cmdCode = self.event_data['cmdCode']
  43. # 实际测试的时候会有多条结束事件一起报上来
  44. cacheKey = self.cache_key(devNo, cmdCode)
  45. with memcache_lock(cacheKey, value = '1', expire = 1) as acquired:
  46. if not acquired:
  47. return
  48. self.do_finished()
  49. def do_finished(self):
  50. """
  51. 网络支付的端口结束
  52. :return:
  53. """
  54. devCache = Device.get_dev_control_cache(self.device.devNo)
  55. portStr = self.event_data.get("portStr")
  56. portCache = devCache.get(portStr, dict())
  57. billingType = portCache.get("billingType", BILLING_TYPE.TIME)
  58. cardNo = portCache.get("cardNo")
  59. refundProtection = bool(int(portCache.get("refundProtection", 0)))
  60. refundProtectionTime = portCache.get("refundProtectionTime")
  61. vCardId = portCache.get("vCardId")
  62. coins = portCache.get("coins")
  63. openId = portCache.get("openId")
  64. startTime = portCache.get("startTime")
  65. need = portCache.get("need{}".format(billingType.capitalize()), 0xFFFF)
  66. left = self.event_data.get("left{}".format(billingType.capitalize()), 0x0000)
  67. reason = self.event_data.get("reason")
  68. user = MyUser.objects.filter(openId=openId, groupId=self.device.groupId).first()
  69. group = Group.get_group(self.device.groupId)
  70. if not user:
  71. Device.clear_port_control_cache(self.device.devNo, portStr)
  72. return
  73. consumeDict = {
  74. "chargeIndex": portStr,
  75. "reason": reason,
  76. "need{}".format(billingType.capitalize()): need,
  77. "left{}".format(billingType.capitalize()): left
  78. }
  79. # 处理相应的退费 虚拟卡的退费和扫码的退费不一样的 刷卡的退费会在另一条指令处理
  80. if cardNo:
  81. # 这个地方仅仅是更新一下
  82. consumeDict.update({"cardNo": cardNo})
  83. elif vCardId:
  84. # 尝试进行虚拟卡退费
  85. refundCoins = self._get_refund_coins(
  86. coins,
  87. left,
  88. need,
  89. refundProtection,
  90. refundProtectionTime,
  91. startTime
  92. )
  93. if refundCoins > VirtualCoin(0):
  94. vCard = UserVirtualCard.objects.get(id=vCardId)
  95. consumeRcdId = portCache.get("consumeRcdId", '')
  96. try:
  97. vCardConsumeRcd = VCardConsumeRecord.objects.get(id=consumeRcdId)
  98. except DoesNotExist:
  99. logger.info('can not find the consume rcd id = %s' % consumeRcdId)
  100. else:
  101. if billingType == BILLING_TYPE.TIME:
  102. vCard.refund_quota(
  103. consumeRcd=vCardConsumeRcd,
  104. usedTime=min(need-left, need),
  105. spendElec=0,
  106. backCoins=refundCoins
  107. )
  108. else:
  109. vCard.refund_quota(
  110. consumeRcd=vCardConsumeRcd,
  111. spendElec=min(need - left, need),
  112. usedTime=0,
  113. backCoins=refundCoins
  114. )
  115. else:
  116. refundCoins = self._get_refund_coins(
  117. coins,
  118. left,
  119. need,
  120. refundProtection,
  121. refundProtectionTime,
  122. startTime
  123. )
  124. consumeDict.update({DEALER_CONSUMPTION_AGG_KIND.COIN: str(VirtualCoin(coins))})
  125. if refundCoins > VirtualCoin(0):
  126. consumeDict.update({
  127. DEALER_CONSUMPTION_AGG_KIND.COIN: str((VirtualCoin(coins) - refundCoins)),
  128. DEALER_CONSUMPTION_AGG_KIND.REFUNDED_COINS: str(refundCoins.mongo_amount)
  129. })
  130. refund_money(self.device, refundCoins, openId)
  131. self.notify_user(
  132. managerialOpenId=user.managerialOpenId or "",
  133. templateName="refund_coins",
  134. title=u"金币退款",
  135. backCount=u"金币: {}".format(refundCoins),
  136. finishTime=datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
  137. )
  138. # 结束服务进程 通知用户服务结束
  139. ServiceProgress.update_progress_and_consume_rcd(
  140. self.device.ownerId,
  141. {
  142. "open_id": openId,
  143. "device_imei": self.device.devNo,
  144. "port": int(portStr),
  145. "isFinished": False
  146. },
  147. consumeDict
  148. )
  149. self.notify_user(
  150. managerialOpenId=user.managerialOpenId if user else "",
  151. templateName="service_complete",
  152. title=u"\\n\\n结束原因:\\t\\t{reason}\\n\\n设备编号:\\t\\t{logicalCode}-{port}\\n\\n服务地址:\\t\\t{group}".format(
  153. reason=reason,
  154. logicalCode=self.device["logicalCode"],
  155. port=portStr,
  156. group=group.get("address", ""),
  157. ),
  158. service=u"充电服务",
  159. finishTime=datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
  160. remark=u'谢谢您的支持'
  161. )
  162. # 清空端口信息
  163. Device.clear_port_control_cache(self.device.devNo, portStr)
  164. def _get_refund_coins(self, coins, left, need, refundProtection, refundProtectionTime, startTime):
  165. """
  166. 获取退费的额度 FFFF 的情况已经在退费保护里面涉及到了
  167. :param coins:
  168. :param left:
  169. :param need:
  170. :param refundProtection:
  171. :param refundProtectionTime:
  172. :param startTime:
  173. :return:
  174. """
  175. # 首先检查退费保护
  176. nowTime = datetime.datetime.now()
  177. startTime = datetime.datetime.strptime(startTime, "%Y-%m-%d %H:%M:%S")
  178. _time = (nowTime - startTime).total_seconds() / 60
  179. if refundProtection and _time < int(refundProtectionTime):
  180. return VirtualCoin(coins)
  181. # 然后检查退费开关
  182. if not self.device.is_auto_refund:
  183. return VirtualCoin(0)
  184. try:
  185. refundCoins = Ratio(float(left) / float(need)) * VirtualCoin(coins)
  186. except ZeroDivisionError:
  187. refundCoins = VirtualCoin(0)
  188. # 防止退大额度
  189. if refundCoins > VirtualCoin(coins):
  190. refundCoins = VirtualCoin(0)
  191. return refundCoins
  192. class ChargingBAOJIAFaultEvent(FaultEvent):
  193. """
  194. 基类的函数现在有问题,但是被太多接口引用, 先覆写吧,后续搞清楚了再修改基类函数
  195. """
  196. def do(self, **args):
  197. pass
  198. class ChargingBJIDCardEvent(WorkEvent):
  199. def do(self):
  200. funCode = self.event_data["cmdCode"]
  201. if funCode == CMD_CODE.CARD_BALANCE_09:
  202. self._do_query()
  203. elif funCode == CMD_CODE.CARD_CONSUME_13:
  204. self._do_settle()
  205. else:
  206. self._do_start_report()
  207. def _do_query(self):
  208. """ 查询 """
  209. cardNo = self.event_data.get("cardNo", "")
  210. cardNoHex = self.event_data.get("cardNoHex", "")
  211. card = self.update_card_dealer_and_type(cardNo)
  212. # 没有查询到有效卡
  213. if not card:
  214. balance = 0
  215. else:
  216. balance = str(card.balance)
  217. self.deviceAdapter._response_card_balance(cardNoHex, balance)
  218. def _do_settle(self):
  219. """ 结算 """
  220. cardNoHex = self.event_data.get("cardNoHex")
  221. cardNo = self.event_data.get("cardNo")
  222. payType = self.event_data.get("payType")
  223. money = self.event_data.get("money")
  224. cardType = self.event_data.get("cardType")
  225. params = [cardNo]
  226. if cardType == "01":
  227. params.append("ID")
  228. else:
  229. params.append("IC")
  230. card = self.update_card_dealer_and_type(*params)
  231. # 无效卡
  232. if not card or not card.openId:
  233. logger.error("invalid card <{}>".format(cardNo))
  234. self.deviceAdapter._response_card_consume(cardNoHex, 0, cardType, CARD_STATUS.STATUS_NO_CARD)
  235. return
  236. # 冻结卡
  237. if card.frozen:
  238. logger.error("card <{}> be frozen".format(cardNo))
  239. self.deviceAdapter._response_card_consume(cardNoHex, 0, cardType, CARD_STATUS.STATUS_FROZEN_CARD)
  240. return
  241. # 处理扣费事件
  242. if payType == "01":
  243. status = self._do_settle_consume(card, money)
  244. # 处理退费事件 需要注意余额回收 和 设备的退费开关
  245. else:
  246. status = self._do_settle_refund(card, money)
  247. return self.deviceAdapter._response_card_consume(cardNoHex, card.balance, cardType, status)
  248. pass
  249. def _do_start_report(self):
  250. """ 启动上报 """
  251. portStr = self.event_data.get("portStr")
  252. cardNo = self.event_data.get("cardNo")
  253. money = self.event_data.get("money")
  254. _time = self.event_data.get("time")
  255. elec = self.event_data.get("elec")
  256. card = self.update_card_dealer_and_type(cardNo)
  257. record = CardConsumeRecord.objects.filter(cardId=str(card.id)).order_by("-dateTimeAdded").first()
  258. if not record:
  259. return
  260. consumeRecord = ConsumeRecord.objects.get(orderNo=record.linkedConsumeRcdOrderNo)
  261. attachParas = consumeRecord.attachParas
  262. attachParas.update({"chargeIndex": portStr})
  263. consumeRecord.attachParas = attachParas
  264. consumeRecord.save()
  265. serviceDict = {
  266. 'orderNo': consumeRecord.orderNo,
  267. 'money': money,
  268. 'coin': money,
  269. 'cardOrderNo': record.orderNo
  270. }
  271. if _time:
  272. serviceDict.update({"needTime": str(_time)})
  273. elif elec:
  274. serviceDict.update({"needElec": str(elec)})
  275. ServiceProgress.register_card_service(
  276. self.device,
  277. int(portStr),
  278. card,
  279. serviceDict
  280. )
  281. otherConf = self.device.get("otherConf", dict())
  282. refundProtection = otherConf.get("refundProtection", DEFAULT_PARAM.DEFAULT_REFUND_PROTECTION)
  283. refundProtectionTime = otherConf.get("refundProtectionTime", DEFAULT_PARAM.DEFAULT_REFUND_PROTECTION_TIME)
  284. devCache = Device.get_dev_control_cache(self.device.devNo)
  285. portCache = devCache.get(portStr, {})
  286. cacheDict = {
  287. "openId": card.openId,
  288. "status": Const.DEV_WORK_STATUS_WORKING,
  289. "coins": float(money),
  290. "consumeType": "card",
  291. "isStart": True,
  292. "startTime": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
  293. # TODO 刷卡消费到底是什么模式?
  294. "billingType": BILLING_TYPE.TIME,
  295. "refundProtection": refundProtection,
  296. "refundProtectionTime": refundProtectionTime,
  297. "cardNo": cardNo,
  298. "needTime": _time
  299. }
  300. portCache.update(cacheDict)
  301. Device.update_dev_control_cache(self.device.devNo, {portStr: portCache})
  302. pass
  303. def _do_settle_consume(self, card, money):
  304. """ 消费结算 """
  305. balance = self.event_data.get("balance")
  306. payMoney = RMB(money)
  307. if card.cardType == "ID":
  308. # 检查有没有没有充值的订单
  309. card_recharge_order = CardRechargeOrder.get_last_to_do_one(str(card.id))
  310. self.recharge_id_card(card = card, rechargeType = 'append', order = card_recharge_order)
  311. card.reload()
  312. if VirtualCoin(card.balance) < VirtualCoin(money):
  313. logger.error("<{}> card balance not enough, balance is <{}>".format(card.cardNo, str(card.balance)))
  314. return CARD_STATUS.STATUS_CONSUME_FAIL
  315. self.record_consume_for_card(card, payMoney)
  316. self.consume_money_for_card(card, payMoney)
  317. self.notify_balance_has_consume_for_card(card, payMoney)
  318. return CARD_STATUS.STATUS_CONSUME_SUCCESS
  319. # IC卡需要写入刷卡后的金额
  320. else:
  321. card.balance = RMB(balance)
  322. card.save()
  323. self.record_consume_for_card(card, payMoney)
  324. return CARD_STATUS.STATUS_CONSUME_SUCCESS
  325. def _do_settle_refund(self, card, money):
  326. """ 余额回收结算 """
  327. # 对于 返回费用的 上限判断
  328. refundMoney = RMB(money)
  329. # 没有开启退款的或者退款额度是0的直接失败
  330. if card.cardType == "ID" and refundMoney <= RMB(0) or not self.device.is_auto_refund:
  331. return CARD_STATUS.STATUS_REFUND_FAIL
  332. # 退费金额高于了最后一单的消费金额 就不让退费
  333. record = CardConsumeRecord.objects.filter(cardId = str(card.id)).order_by("-dateTimeAdded").first()
  334. if not record or record.money < refundMoney:
  335. logger.error("card <{}> has no consume record, refund money is <{}>".format(card.cardNo, refundMoney))
  336. return CARD_STATUS.STATUS_REFUND_FAIL
  337. logger.info("card <{}> last ont consumeRecord is <{}>, consumeMoney is <{}>".format(card.cardNo, record.orderNo,
  338. record.money))
  339. self.refund_money_for_card(refundMoney, str(card.id))
  340. self.notify_user(
  341. card.managerialOpenId,
  342. 'refund_coins',
  343. **{
  344. 'title': u'退币完成!您的卡号是%s,卡别名:%s' % (card.cardNo, card.nickName),
  345. 'backCount': u'金币:%s' % refundMoney,
  346. 'finishTime': datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
  347. })
  348. return CARD_STATUS.STATUS_REFUND_SUCCESS
  349. class ChargingBJICCardEvent(WorkEvent):
  350. def do(self):
  351. cardNo = self.event_data["cardNo"]
  352. money = self.event_data["money"]
  353. res = self.event_data.get("res")
  354. reason = self.event_data.get("reason")
  355. reasonCode = self.event_data.get("reasonCode")
  356. card = self.update_card_dealer_and_type(cardNo, "IC")
  357. # 在测试的时候发现,偶尔会出现IC卡卡号为 00000000 的情况
  358. if not card or not card.openId:
  359. return
  360. # 充值不成功的 后续是否要处理现金退费
  361. if res != "01" and reasonCode in ["03", "04"]:
  362. group = Group.get_group(self.device.groupId)
  363. self.notify_dealer(
  364. templateName = "device_fault",
  365. title = "注意!{}!".format(reason),
  366. device = u"{groupNumber}组-{logicalCode}".format(groupNumber = self.device["groupNumber"],
  367. logicalCode = self.device["logicalCode"]),
  368. location = u"{address}-{groupName}".format(address = group["address"],
  369. groupName = group["groupName"]),
  370. fault = u"客户离线卡充值失败,设备原因 <{}> 导致".format(reason),
  371. notifyTime = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
  372. )
  373. return
  374. card.balance += RMB(money)
  375. logger.info("card remote recharge success, card <{}> balance is <{}>".format(card.cardNo, card.balance))
  376. card.save()
  377. class ChargingBJReportEvent(WorkEvent):
  378. def do(self):
  379. portInfo = self.event_data.get("portInfo", dict())
  380. devCache = Device.get_dev_control_cache(self.device.devNo)
  381. for portStr, info in portInfo.items():
  382. portCache = devCache.get(portStr, dict())
  383. # 端口状态如果是空闲的情况下, 将isStart信息清除掉
  384. if info.get("status", Const.DEV_WORK_STATUS_IDLE) == Const.DEV_WORK_STATUS_IDLE:
  385. portCache.pop("isStart", None)
  386. billingType = portCache.get("billingType", BILLING_TYPE.TIME)
  387. popType = BILLING_TYPE.ELEC if billingType == BILLING_TYPE.TIME else BILLING_TYPE.TIME
  388. info.pop("left".format(popType.capitalize()), None)
  389. portCache.update(info)
  390. Device.update_dev_control_cache(self.device.devNo, devCache)