# -*- coding: utf-8 -*- # !/usr/bin/env python import datetime import logging from mongoengine import DoesNotExist from apilib.monetary import RMB, VirtualCoin, Ratio from apilib.utils_sys import memcache_lock from apps.web.constant import Const, DEALER_CONSUMPTION_AGG_KIND from apps.web.core.device_define.baojia import CMD_CODE, CARD_STATUS, DEFAULT_PARAM, BILLING_TYPE from apps.web.device.models import Device, Group from apps.web.eventer import EventBuilder from apps.web.eventer.base import FaultEvent, WorkEvent from apps.web.eventer.errors import NoCommandHandlerAvailable from apps.web.user.models import VCardConsumeRecord, ServiceProgress, CardRechargeOrder, CardConsumeRecord, \ ConsumeRecord, MyUser, UserVirtualCard from apps.web.user.transaction_deprecated import refund_money logger = logging.getLogger(__name__) class builder(EventBuilder): def __getEvent__(self, device_event): event_data = self.deviceAdapter.analyze_event_data(device_event['data']) if event_data is None or 'cmdCode' not in event_data: return None if event_data['cmdCode'] in [CMD_CODE.DEVICE_SUBMIT_DEVICE_FAULT_0A]: return ChargingBAOJIAFaultEvent(self.deviceAdapter, event_data) elif event_data["cmdCode"] in [CMD_CODE.CARD_BALANCE_09, CMD_CODE.CARD_CONSUME_13, CMD_CODE.CARD_CONSUME_SURE_27]: return ChargingBJIDCardEvent(self.deviceAdapter, event_data) elif event_data["cmdCode"] in [CMD_CODE.CARD_REMOTE_CHARGE_12]: return ChargingBJICCardEvent(self.deviceAdapter, event_data) elif event_data["cmdCode"] in [CMD_CODE.DEVICE_SUBMIT_CHARGE_FINISHED_16]: return ChargingBJFinishEvent(self.deviceAdapter, event_data) elif event_data["cmdCode"] in [CMD_CODE.PORT_STATUS_21]: return ChargingBJReportEvent(self.deviceAdapter, event_data) else: raise NoCommandHandlerAvailable('[BJDZ] no command handler for cmd %s, curDevInfo=%s' % (event_data["cmdCode"], event_data)) class ChargingBJFinishEvent(WorkEvent): @staticmethod def cache_key(devNo, cmd): return "event-{}-{}".format(devNo, cmd) def do(self, **args): devNo = self.device['devNo'] logger.info('baojia charging event detected, devNo=%s,curInfo=%s' % (devNo, self.event_data)) cmdCode = self.event_data['cmdCode'] # 实际测试的时候会有多条结束事件一起报上来 cacheKey = self.cache_key(devNo, cmdCode) with memcache_lock(cacheKey, value = '1', expire = 1) as acquired: if not acquired: return self.do_finished() def do_finished(self): """ 网络支付的端口结束 :return: """ devCache = Device.get_dev_control_cache(self.device.devNo) portStr = self.event_data.get("portStr") portCache = devCache.get(portStr, dict()) billingType = portCache.get("billingType", BILLING_TYPE.TIME) cardNo = portCache.get("cardNo") refundProtection = bool(int(portCache.get("refundProtection", 0))) refundProtectionTime = portCache.get("refundProtectionTime") vCardId = portCache.get("vCardId") coins = portCache.get("coins") openId = portCache.get("openId") startTime = portCache.get("startTime") need = portCache.get("need{}".format(billingType.capitalize()), 0xFFFF) left = self.event_data.get("left{}".format(billingType.capitalize()), 0x0000) reason = self.event_data.get("reason") user = MyUser.objects.filter(openId=openId, groupId=self.device.groupId).first() group = Group.get_group(self.device.groupId) if not user: Device.clear_port_control_cache(self.device.devNo, portStr) return consumeDict = { "chargeIndex": portStr, "reason": reason, "need{}".format(billingType.capitalize()): need, "left{}".format(billingType.capitalize()): left } # 处理相应的退费 虚拟卡的退费和扫码的退费不一样的 刷卡的退费会在另一条指令处理 if cardNo: # 这个地方仅仅是更新一下 consumeDict.update({"cardNo": cardNo}) elif vCardId: # 尝试进行虚拟卡退费 refundCoins = self._get_refund_coins( coins, left, need, refundProtection, refundProtectionTime, startTime ) if refundCoins > VirtualCoin(0): vCard = UserVirtualCard.objects.get(id=vCardId) consumeRcdId = portCache.get("consumeRcdId", '') try: vCardConsumeRcd = VCardConsumeRecord.objects.get(id=consumeRcdId) except DoesNotExist: logger.info('can not find the consume rcd id = %s' % consumeRcdId) else: if billingType == BILLING_TYPE.TIME: vCard.refund_quota( consumeRcd=vCardConsumeRcd, usedTime=min(need-left, need), spendElec=0, backCoins=refundCoins ) else: vCard.refund_quota( consumeRcd=vCardConsumeRcd, spendElec=min(need - left, need), usedTime=0, backCoins=refundCoins ) else: refundCoins = self._get_refund_coins( coins, left, need, refundProtection, refundProtectionTime, startTime ) consumeDict.update({DEALER_CONSUMPTION_AGG_KIND.COIN: str(VirtualCoin(coins))}) if refundCoins > VirtualCoin(0): consumeDict.update({ DEALER_CONSUMPTION_AGG_KIND.COIN: str((VirtualCoin(coins) - refundCoins)), DEALER_CONSUMPTION_AGG_KIND.REFUNDED_COINS: str(refundCoins.mongo_amount) }) refund_money(self.device, refundCoins, openId) self.notify_user( managerialOpenId=user.managerialOpenId or "", templateName="refund_coins", title=u"金币退款", backCount=u"金币: {}".format(refundCoins), finishTime=datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') ) # 结束服务进程 通知用户服务结束 ServiceProgress.update_progress_and_consume_rcd( self.device.ownerId, { "open_id": openId, "device_imei": self.device.devNo, "port": int(portStr), "isFinished": False }, consumeDict ) self.notify_user( managerialOpenId=user.managerialOpenId if user else "", templateName="service_complete", title=u"\\n\\n结束原因:\\t\\t{reason}\\n\\n设备编号:\\t\\t{logicalCode}-{port}\\n\\n服务地址:\\t\\t{group}".format( reason=reason, logicalCode=self.device["logicalCode"], port=portStr, group=group.get("address", ""), ), service=u"充电服务", finishTime=datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), remark=u'谢谢您的支持' ) # 清空端口信息 Device.clear_port_control_cache(self.device.devNo, portStr) def _get_refund_coins(self, coins, left, need, refundProtection, refundProtectionTime, startTime): """ 获取退费的额度 FFFF 的情况已经在退费保护里面涉及到了 :param coins: :param left: :param need: :param refundProtection: :param refundProtectionTime: :param startTime: :return: """ # 首先检查退费保护 nowTime = datetime.datetime.now() startTime = datetime.datetime.strptime(startTime, "%Y-%m-%d %H:%M:%S") _time = (nowTime - startTime).total_seconds() / 60 if refundProtection and _time < int(refundProtectionTime): return VirtualCoin(coins) # 然后检查退费开关 if not self.device.is_auto_refund: return VirtualCoin(0) try: refundCoins = Ratio(float(left) / float(need)) * VirtualCoin(coins) except ZeroDivisionError: refundCoins = VirtualCoin(0) # 防止退大额度 if refundCoins > VirtualCoin(coins): refundCoins = VirtualCoin(0) return refundCoins class ChargingBAOJIAFaultEvent(FaultEvent): """ 基类的函数现在有问题,但是被太多接口引用, 先覆写吧,后续搞清楚了再修改基类函数 """ def do(self, **args): pass class ChargingBJIDCardEvent(WorkEvent): def do(self): funCode = self.event_data["cmdCode"] if funCode == CMD_CODE.CARD_BALANCE_09: self._do_query() elif funCode == CMD_CODE.CARD_CONSUME_13: self._do_settle() else: self._do_start_report() def _do_query(self): """ 查询 """ cardNo = self.event_data.get("cardNo", "") cardNoHex = self.event_data.get("cardNoHex", "") card = self.update_card_dealer_and_type(cardNo) # 没有查询到有效卡 if not card: balance = 0 else: balance = str(card.balance) self.deviceAdapter._response_card_balance(cardNoHex, balance) def _do_settle(self): """ 结算 """ cardNoHex = self.event_data.get("cardNoHex") cardNo = self.event_data.get("cardNo") payType = self.event_data.get("payType") money = self.event_data.get("money") cardType = self.event_data.get("cardType") params = [cardNo] if cardType == "01": params.append("ID") else: params.append("IC") card = self.update_card_dealer_and_type(*params) # 无效卡 if not card or not card.openId: logger.error("invalid card <{}>".format(cardNo)) self.deviceAdapter._response_card_consume(cardNoHex, 0, cardType, CARD_STATUS.STATUS_NO_CARD) return # 冻结卡 if card.frozen: logger.error("card <{}> be frozen".format(cardNo)) self.deviceAdapter._response_card_consume(cardNoHex, 0, cardType, CARD_STATUS.STATUS_FROZEN_CARD) return # 处理扣费事件 if payType == "01": status = self._do_settle_consume(card, money) # 处理退费事件 需要注意余额回收 和 设备的退费开关 else: status = self._do_settle_refund(card, money) return self.deviceAdapter._response_card_consume(cardNoHex, card.balance, cardType, status) pass def _do_start_report(self): """ 启动上报 """ portStr = self.event_data.get("portStr") cardNo = self.event_data.get("cardNo") money = self.event_data.get("money") _time = self.event_data.get("time") elec = self.event_data.get("elec") card = self.update_card_dealer_and_type(cardNo) record = CardConsumeRecord.objects.filter(cardId=str(card.id)).order_by("-dateTimeAdded").first() if not record: return consumeRecord = ConsumeRecord.objects.get(orderNo=record.linkedConsumeRcdOrderNo) attachParas = consumeRecord.attachParas attachParas.update({"chargeIndex": portStr}) consumeRecord.attachParas = attachParas consumeRecord.save() serviceDict = { 'orderNo': consumeRecord.orderNo, 'money': money, 'coin': money, 'cardOrderNo': record.orderNo } if _time: serviceDict.update({"needTime": str(_time)}) elif elec: serviceDict.update({"needElec": str(elec)}) ServiceProgress.register_card_service( self.device, int(portStr), card, serviceDict ) otherConf = self.device.get("otherConf", dict()) refundProtection = otherConf.get("refundProtection", DEFAULT_PARAM.DEFAULT_REFUND_PROTECTION) refundProtectionTime = otherConf.get("refundProtectionTime", DEFAULT_PARAM.DEFAULT_REFUND_PROTECTION_TIME) devCache = Device.get_dev_control_cache(self.device.devNo) portCache = devCache.get(portStr, {}) cacheDict = { "openId": card.openId, "status": Const.DEV_WORK_STATUS_WORKING, "coins": float(money), "consumeType": "card", "isStart": True, "startTime": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), # TODO 刷卡消费到底是什么模式? "billingType": BILLING_TYPE.TIME, "refundProtection": refundProtection, "refundProtectionTime": refundProtectionTime, "cardNo": cardNo, "needTime": _time } portCache.update(cacheDict) Device.update_dev_control_cache(self.device.devNo, {portStr: portCache}) pass def _do_settle_consume(self, card, money): """ 消费结算 """ balance = self.event_data.get("balance") payMoney = RMB(money) if card.cardType == "ID": # 检查有没有没有充值的订单 card_recharge_order = CardRechargeOrder.get_last_to_do_one(str(card.id)) self.recharge_id_card(card = card, rechargeType = 'append', order = card_recharge_order) card.reload() if VirtualCoin(card.balance) < VirtualCoin(money): logger.error("<{}> card balance not enough, balance is <{}>".format(card.cardNo, str(card.balance))) return CARD_STATUS.STATUS_CONSUME_FAIL self.record_consume_for_card(card, payMoney) self.consume_money_for_card(card, payMoney) self.notify_balance_has_consume_for_card(card, payMoney) return CARD_STATUS.STATUS_CONSUME_SUCCESS # IC卡需要写入刷卡后的金额 else: card.balance = RMB(balance) card.save() self.record_consume_for_card(card, payMoney) return CARD_STATUS.STATUS_CONSUME_SUCCESS def _do_settle_refund(self, card, money): """ 余额回收结算 """ # 对于 返回费用的 上限判断 refundMoney = RMB(money) # 没有开启退款的或者退款额度是0的直接失败 if card.cardType == "ID" and refundMoney <= RMB(0) or not self.device.is_auto_refund: return CARD_STATUS.STATUS_REFUND_FAIL # 退费金额高于了最后一单的消费金额 就不让退费 record = CardConsumeRecord.objects.filter(cardId = str(card.id)).order_by("-dateTimeAdded").first() if not record or record.money < refundMoney: logger.error("card <{}> has no consume record, refund money is <{}>".format(card.cardNo, refundMoney)) return CARD_STATUS.STATUS_REFUND_FAIL logger.info("card <{}> last ont consumeRecord is <{}>, consumeMoney is <{}>".format(card.cardNo, record.orderNo, record.money)) self.refund_money_for_card(refundMoney, str(card.id)) self.notify_user( card.managerialOpenId, 'refund_coins', **{ 'title': u'退币完成!您的卡号是%s,卡别名:%s' % (card.cardNo, card.nickName), 'backCount': u'金币:%s' % refundMoney, 'finishTime': datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') }) return CARD_STATUS.STATUS_REFUND_SUCCESS class ChargingBJICCardEvent(WorkEvent): def do(self): cardNo = self.event_data["cardNo"] money = self.event_data["money"] res = self.event_data.get("res") reason = self.event_data.get("reason") reasonCode = self.event_data.get("reasonCode") card = self.update_card_dealer_and_type(cardNo, "IC") # 在测试的时候发现,偶尔会出现IC卡卡号为 00000000 的情况 if not card or not card.openId: return # 充值不成功的 后续是否要处理现金退费 if res != "01" and reasonCode in ["03", "04"]: group = Group.get_group(self.device.groupId) self.notify_dealer( templateName = "device_fault", title = "注意!{}!".format(reason), device = u"{groupNumber}组-{logicalCode}".format(groupNumber = self.device["groupNumber"], logicalCode = self.device["logicalCode"]), location = u"{address}-{groupName}".format(address = group["address"], groupName = group["groupName"]), fault = u"客户离线卡充值失败,设备原因 <{}> 导致".format(reason), notifyTime = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") ) return card.balance += RMB(money) logger.info("card remote recharge success, card <{}> balance is <{}>".format(card.cardNo, card.balance)) card.save() class ChargingBJReportEvent(WorkEvent): def do(self): portInfo = self.event_data.get("portInfo", dict()) devCache = Device.get_dev_control_cache(self.device.devNo) for portStr, info in portInfo.items(): portCache = devCache.get(portStr, dict()) # 端口状态如果是空闲的情况下, 将isStart信息清除掉 if info.get("status", Const.DEV_WORK_STATUS_IDLE) == Const.DEV_WORK_STATUS_IDLE: portCache.pop("isStart", None) billingType = portCache.get("billingType", BILLING_TYPE.TIME) popType = BILLING_TYPE.ELEC if billingType == BILLING_TYPE.TIME else BILLING_TYPE.TIME info.pop("left".format(popType.capitalize()), None) portCache.update(info) Device.update_dev_control_cache(self.device.devNo, devCache)