# -*- coding: utf-8 -*- # !/usr/bin/env python import datetime import logging import time from mongoengine import DoesNotExist from apilib.monetary import RMB, VirtualCoin, Ratio from apilib.utils_datetime import to_datetime from apilib.utils_string import make_title_from_dict from apilib.utils_sys import memcache_lock from apps.web.constant import Const, FAULT_LEVEL, DeviceCmdCode, DEALER_CONSUMPTION_AGG_KIND from apps.web.device.models import Group, Device from apps.web.eventer import EventBuilder from apps.web.eventer.base import FaultEvent, WorkEvent from apps.web.user.models import ServiceProgress, CardRechargeOrder, MyUser, UserVirtualCard 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 'level' in device_event: event_data.update({'level': device_event['level']}) if event_data["cmdCode"] in ["0A"]: return ChargingJNCarFaultEventer(self.deviceAdapter, event_data) else: return ChargingJNCarWorkEventer(self.deviceAdapter, event_data) class ChargingJNCarWorkEventer(WorkEvent): def support_playback(self): return self.event_data.get("cmdCode") == "2C" def do(self, **args): """ 主驱动函数 :param **args: :return: """ cmdCode = self.event_data.get("cmdCode") # 充电状态上报 if cmdCode == "26": self._do_port_report() # 结束事件上报 elif cmdCode == "2C": self._do_finished() # 在线卡启动后上报 elif cmdCode == "2D": self._do_card_consume() # 查询在线卡余额是否足够 elif cmdCode == "10": self._do_card_balance() else: logger.error("<{}> receive an invalid cmdCode <{}>".format(self.device.devNo, cmdCode)) def _do_port_report(self): """ 处理端口状态上报的事件, 端口状态一律不再这里面更新,防止主板误报造成订单异常 :return: """ self.event_data.pop("cmdCode", None) portNum = self.event_data.get("portNum", 0) devCache = Device.get_dev_control_cache(self.device.devNo) for i in xrange(portNum): portStr = str(i + 1) portCache = devCache.get(portStr, dict()) tempInfo = self.event_data.get(portStr) usedTime = tempInfo.get("usedTime", 0) power = tempInfo.get("power", 0) usedElec = tempInfo.get("usedElec", 0) voltage = tempInfo.get("voltage", 0) portCache.update({ "usedTime": usedTime, "power": power, "usedElec": usedElec, "voltage": voltage }) # devCache 中会有不是 dict 的项目 Device.update_dev_control_cache(self.device.devNo, {portStr:portCache}) # TODO zjl 端口上报记录 功率曲线绘制 def _do_finished(self): """ 处理结束事件 拿到sessionId 作为去重标准 :return: """ # TODO zjl 区别到底是 刷卡的结束还是扫码的结束 依赖缓存或者依赖主板上报? cardNo = self.event_data.get("cardNo") sessionId = self.event_data.get("sessionId") portStr = self.event_data.get("portStr") lock_id = "{}-{}-{}".format(self.device.devNo, portStr, sessionId) try: with memcache_lock(lock_id, value = '1', expire = 60) as acquired: if not acquired: return portCache = Device.clear_port_control_cache(self.device.devNo, portStr) if not portCache: return try: if int(cardNo): self._do_card_finished(portCache=portCache) else: self._do_net_pay_finished(portCache=portCache) except Exception as e: logger.exception(e) finally: openId = portCache.get("openId") user = MyUser.objects.filter(openId=openId, groupId=self.device.groupId).first() reason = self.event_data.get("desc", "") group = Group.get_group(self.device.groupId) # 通知用户充电结束 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", "") if group else "", ), service=u"充电服务", finishTime=to_datetime(self.recvTime).strftime('%Y-%m-%d %H:%M:%S'), remark = u'谢谢您的支持' ) finally: self.deviceAdapter._response_finished(portStr, sessionId) def _do_card_balance(self): """ ID 卡刷卡信号上报 用于检测是否是有效卡还是无效卡 卡消费金额默认大一些 防止出错 :return: """ cardNo = self.event_data.get("cardNo", "") cardCst = self.event_data.get("cardCst", 256) card = self.update_card_dealer_and_type(cardNo) if not card or not card.openId: return self.deviceAdapter._response_card_balance(0, "02") # 没有到账的卡给它充值, 应该用不上 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 float(card.balance) >= float(cardCst): res = "00" else: res = "01" return self.deviceAdapter._response_card_balance(float(card.balance), res) def _do_card_consume(self): """ 刷卡投币成功启动设备后上报 目前只有ID卡 到了此命令之后 设备已经启动 此处需要处理的就是扣费以及记录 :return: """ self.deviceAdapter._send_data("2D", "{:0>10}".format(self.event_data.get("sessionId", "")), cmd=DeviceCmdCode.OPERATE_DEV_NO_RESPONSE) portStr = self.event_data.get("portStr") needElec = self.event_data.get("needElec", 0) cardNo = self.event_data.get("cardNo") cardCst = self.event_data.get("cardCst") card = self.update_card_dealer_and_type(cardNo) if not card or not card.openId: logger.warning("error card, cardNo is {}".format(cardNo)) return res, cardBalance = self.consume_money_for_card(card, RMB(cardCst)) if res != 1: logger.warning("consume error!!!, cardNo is {}".format(cardNo)) return consumeDict = { "chargeIndex": portStr, "needElec": needElec } orderNo, cardOrderNo = self.record_consume_for_card(card, RMB(cardCst), servicedInfo=consumeDict) portCache = { "isStart": True, "status": Const.DEV_WORK_STATUS_WORKING, "openId": card.openId, "price": cardCst, "coins": cardCst, "needElec": needElec, "startTime": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "startTimeStamp": int(time.time()), "consumeType": "card", } Device.update_dev_control_cache(self.device.devNo, {portStr: portCache}) ServiceProgress.register_card_service( self.device, int(portStr), card, consumeOrder={ "orderNo": orderNo, "cardOrderNo": cardOrderNo, } ) def _do_card_finished(self, portCache): """ 处理刷卡结束的上报事件 卡的结束退费金额会在事件里面上报 这个地方不再需要去计算 :return: """ portStr = self.event_data.get("portStr") cardNo = self.event_data.get("cardNo") cardLeftBalance = self.event_data.get("cardLeftBalance") desc = self.event_data.get("desc") price = portCache.get("price", 0) if not price: return card = self.update_card_dealer_and_type(cardNo) consumeDict = { "chargeIndex": portStr, "cardId": str(card.id), "reason": desc } leftBalance = RMB(cardLeftBalance) # 处理退额超限问题 if leftBalance > RMB(price): leftBalance = RMB(0) if leftBalance > RMB(0): self.refund_money_for_card(leftBalance, str(card.id)) consumeDict.update({DEALER_CONSUMPTION_AGG_KIND.REFUND_CARD: leftBalance.mongo_amount}) self.notify_user( managerialOpenId=card.managerialOpenId, templateName="refund_coins", title=u"卡退款", backCount=u"{}".format(leftBalance.mongo_amount), finishTime=to_datetime(self.recvTime).strftime('%Y-%m-%d %H:%M:%S') ) ServiceProgress.update_progress_and_consume_rcd( self.device.ownerId, { "open_id": card.openId, "device_imei": self.device.devNo, "port": int(portStr), "cardId": str(card.id), "isFinished": False }, consumeDict ) def _do_net_pay_finished(self, portCache): """ 处理网络支付的结束事件 :return: """ portStr = self.event_data.get("portStr") openId = portCache.get("openId") # 没有 openId 的说明已经被处理过了 if not openId: logger.error("portCache has no openId , dev is <{}>, port is <{}>".format(self.device.devNo, portStr)) user = MyUser.objects.filter(openId=openId, groupId=self.device.groupId).first() # 消费相关信息的记录 consumeDict = { "chargeIndex": portStr, "reason": self.event_data.get("desc"), } billingType = portCache.get("billingType", "elec").capitalize() leftKey = "left{}".format(billingType) needKey = "need{}".format(billingType) consumeDict.update({ leftKey: self.event_data.get(leftKey) }) # 退费的相应处理 refundInfo = self._get_refund_money( self.event_data.get(leftKey, 0), portCache.get(needKey, 0), portCache ) refundMoney = refundInfo.get("money") logger.info("refund Money is <{}>".format(refundMoney.mongo_amount)) refundType = refundInfo.get("type") if refundType == 'vCard': vCardId = portCache.get('vCardId') logger.info("finished with vCard!") try: vCard = UserVirtualCard.objects.get(id=vCardId) except DoesNotExist: logger.info('can not find the vCard id = %s' % vCardId) return extra = [] extra.append({u'虚拟卡券': u'{}--{}'.format(vCard.cardName, vCard.cardNo)}) if billingType == 'time': extra.append({u'消费明细': u'消费{}分钟'.format(portCache.get(needKey, 0))}) else: extra.append({u'消耗明细': u'消费{}度'.format(portCache.get(needKey, 0))}) group = Group.get_group(self.device['groupId']) self.notify_user_service_complete( service_name='充电', openid=openId, port=self.event_data["port"], address=group["address"], reason=self.event_data.get('reason'), finished_time=datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), extra=extra ) else: if refundMoney and refundMoney > VirtualCoin(0): self.refund_net_pay(user, portCache, RMB(refundMoney.amount), refundMoney, consumeDict, (refundType == "cash")) if DEALER_CONSUMPTION_AGG_KIND.REFUNDED_CASH in consumeDict: refundTitle = u"退款" backCount = "{} 元".format(refundMoney.mongo_amount) elif DEALER_CONSUMPTION_AGG_KIND.REFUNDED_COINS in consumeDict: refundTitle = u"金币退款" backCount =u"金币: {}".format(refundMoney.mongo_amount) else: backCount = None if backCount: self.notify_user( managerialOpenId=user.managerialOpenId or "", templateName="refund_coins", title=refundTitle, backCount=backCount, finishTime=to_datetime(self.recvTime).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 ) def _get_refund_money(self, left, need, portCache, **kwargs): """ 获取结束时候需要退还的 金币 或者 现金 :param left: 剩余的 :param need: 订购的 :param portCache: 端口的缓存数据 :param kwargs: 预留的扩充字段 :return: """ if not self.device.is_auto_refund: return { "type": "coin", "money": VirtualCoin(0) } if 'vCardId' in portCache and portCache["vCardId"]: return { "type": 'vCard', "money": VirtualCoin(0) } refund_recharge_ids = [] if 'rechargeRcdId' in portCache: refund_recharge_ids.append(portCache['rechargeRcdId']) else: pay_info = portCache.get('payInfo', list()) for item in pay_info: if 'rechargeRcdId' in item: refund_recharge_ids.append(item['rechargeRcdId']) price = portCache.get("price", 0) coins = portCache.get("coins", 0) if len(refund_recharge_ids) > 0: _type = "cash" payMoney = VirtualCoin(price) else: _type = "coin" payMoney = VirtualCoin(coins) try: refundMoney = Ratio(float(left) / float(need)) * VirtualCoin(payMoney) except ZeroDivisionError: refundMoney = VirtualCoin(0) # 防止退大额度 if refundMoney > payMoney: refundMoney = VirtualCoin(0) return { "type": _type, "money": refundMoney } class ChargingJNCarFaultEventer(FaultEvent): def do(self, **args): group = Group.get_group(self.device['groupId']) if self.is_notify_dealer(): titleDictList = [ {u'告警名称': u'设备告警'}, {u'设备编号': self.device["logicalCode"]}, {u'地址名称': group["groupName"]} ] self.notify_dealer('device_fault', **{ 'title': make_title_from_dict(titleDictList), 'device': u'%s号设备\\n' % self.device['groupNumber'], 'faultType': u'设备告警\\n', 'notifyTime': u'%s\\n' % datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 'fault': u'%s(详细情况,建议您到管理后台的告警模块进行查看)\\n' % self.event_data.get("errorDesc"), }) self.record( faultCode=self.event_data.get("errorCode"), description=self.event_data.get("errorDesc"), title=u"设备告警", detail=self.event_data.get("errorDesc"), level=self.event_data.get('level', FAULT_LEVEL.NORMAL), portNo=self.event_data.get("portStr") )