# coding=utf-8 import datetime import logging from arrow import Arrow from django.conf import settings from typing import TYPE_CHECKING, Iterable from apilib.monetary import VirtualCoin, RMB from apps.web.common.models import TempValues from apps.web.constant import START_DEVICE_STATUS, DEALER_CONSUMPTION_AGG_KIND, Const from apps.web.core.device_define.kehang import BillingType, REASON_MAP, CARD_STATUS, CARD_OPT from apps.web.device.models import Device, Group from apps.web.eventer import EventBuilder from apps.web.eventer.base import ComNetPayAckEvent, IdStartAckEvent, AckEventProcessorIntf, WorkEvent from apps.web.user.models import Card, ServiceProgress, CardRechargeOrder, CardRechargeRecord from apps.web.user.utils import clear_frozen_user_balance from apps.web.utils import set_start_key_status if TYPE_CHECKING: from apps.web.user.models import ConsumeRecord logger = logging.getLogger(__name__) class builder(EventBuilder): def __getEvent__(self, device_event): if 'order_id' in device_event: if device_event["order_type"] == "com_start": return MyComNetPayAckEvent(self.deviceAdapter, device_event) elif device_event["order_type"] == "id_start": return IDPayAckEvent(self.deviceAdapter, 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 ["22", "11", "12", "16"]: return KeHangWorkEvent(self.deviceAdapter, event_data) class KeHangWorkEvent(WorkEvent): def _do_query_card(self): logger.info("device <{}> do query id card balance, event = {}".format(self.device.devNo, self.event_data)) cardNo = self.event_data["cardNo"] card = self.update_card_dealer_and_type(cardNo) # type: Card # 非法卡 (经销商绑卡会有 openId = any......) if not card or not card.openId: logger.info("device <{}> receive query card, error card = <{}>".format(self.device.devNo, cardNo)) return self.deviceAdapter._response_card(int(cardNo), CARD_STATUS.ILLEGAL, float(VirtualCoin(0)), openId="") # 冻结卡 if card.frozen: logger.info("device <{}> receive query card, frozen card = <{}>".format(self.device.devNo, cardNo)) return self.deviceAdapter._response_card(int(cardNo), CARD_STATUS.FROZEN, float(VirtualCoin(card.balance)) * 10, openId=str(card.id)) # 正常的卡余额回复 不需要管余额是否足够 交由模块判断 logger.info("device <{}> receive query card success, card = <{}>, balance = <{}>".format(self.device.devNo, cardNo, card.balance)) return self.deviceAdapter._response_card(int(cardNo), CARD_STATUS.SUCCESS, float(VirtualCoin(card.balance)) * 10, openId=str(card.id)) def _do_ic_card(self): logger.info("device <{}> do card start event, event = {}".format(self.device.devNo, self.event_data)) cardNo = self.event_data["cardNo"] card = self.update_card_dealer_and_type(cardNo, "IC") # 卡不存在的地方 直接退出 if not card: return # 卡存在的情况下 分为退费 和 扣费两种 if self.event_data["opt"] == CARD_OPT.DEDUCT: orderNo, cardOrderNo = self.record_consume_for_card(card, RMB(self.event_data["cost"]), desc=u"离线卡启动") consumeDict = { "orderNo": "orderNo", "cardOrderNo": cardOrderNo } ServiceProgress.register_card_service( self.device, int(self.event_data["port"]), card, consumeDict ) Device.update_dev_control_cache(self.device.devNo, {str(self.event_data["port"]): {"status": Const.DEV_WORK_STATUS_WORKING}}) else: self.record_refund_money_for_card(RMB(self.event_data["cost"]), str(card.id)) Device.clear_port_control_cache(self.device.devNo, str(self.event_data["port"])) def _do_ic_card_sync_balance(self): logger.info("device <{}> do ic card sync balance, event = {}".format(self.device.devNo, self.event_data)) cardNo = self.event_data["cardNo"] cardType = self.event_data["type"] card = self.update_card_dealer_and_type(cardNo, "IC") if not card: logger.info("device <{}> do ic card <{}> sync balance, card not find".format(self.device.devNo, cardNo)) return money, coins, orderNos, cardOrderNos = CardRechargeOrder.get_to_do_list(str(card.id)) if not orderNos: logger.info("device <{}> do ic card <{}> sync balance, charge orders not find".format(self.device.devNo, cardNo)) return # 订单号保存到缓存里 TempValues.set('%s-%s' % (self.device.devNo, cardNo), value=cardOrderNos) asyncMoney = (RMB(self.event_data["balance"]) + RMB(coins)) * 10 try: CardRechargeOrder.update_card_order_has_finished(str(card.id)) except Exception as e: logger.exception('%s' % e) else: self.deviceAdapter._response_sync_card_balance(cardNo, asyncMoney) def _do_ic_card_sync_response(self): logger.info("device <{}> do ic card sync balance response, event = {}".format(self.device.devNo, self.event_data)) cardNo = self.event_data["cardNo"] card = self.update_card_dealer_and_type(cardNo, "IC") balance = self.event_data["balance"] success = self.event_data["success"] if not success: logger.info("device <{}> do ic card sync balance response not success, event = {}".format(self.device.devNo, self.event_data)) return cardOrderNos = TempValues.get('%s-%s' % (self.device.devNo, cardNo)) cardOrders = CardRechargeOrder.objects.filter(orderNo__in=cardOrderNos) for _order in cardOrders: # type: CardRechargeOrder _order.update_after_recharge_ic_card( device=self.device, sendMoney=RMB(_order.coins), preBalance=card.balance ) preBalance = card.balance card.balance = card.balance + _order.coins # 创建充值记录 CardRechargeRecord.add_record( card=card, group=Group.get_group(_order.groupId), order=_order, device=self.device ) self.update_card_balance(card, RMB(balance)) TempValues.remove('%s-%s' % (self.device.devNo, cardNo)) def do(self, **args): if self.event_data["cmdCode"] == "22": return self._do_query_card() if self.event_data["cmdCode"] == "11": return self._do_ic_card() if self.event_data["cmdCode"] == "12": return self._do_ic_card_sync_balance() if self.event_data["cmdCode"] == "16": return self._do_ic_card_sync_response() class MyComNetPayAckEvent(ComNetPayAckEvent): def do_running_order(self, order, result): # type: (ConsumeRecord, dict) -> None """ 处理运行订单 :param order: 用户服务器订单 :param result: device_event 设备侧订单 :return: """ # 订单消息已经被回复过 if order.status in ["running", "finished"]: logger.debug('order<{}> no need to deal. this has done.'.format(repr(order))) return # 启动设备的时候 设备实际启动成功 但是订单串口超时 不知道订单的明确状态 后面启动时间又重新上报 if order.status == "unknown": errorDesc = u"设备信号恢复,订单正常运行" logger.info("order <{}> timeout to running") # 正常运行的订单 else: errorDesc = u"" if 'master' in result: order.association = { 'master': result['master'] } order.servicedInfo.update({'masterOrderNo': result['master']}) order.errorDesc = errorDesc order.isNormal = True order.status = 'running' order.startTime = datetime.datetime.fromtimestamp(result['sts']) order.save() set_start_key_status(start_key=order.startKey, state=START_DEVICE_STATUS.FINISHED, order_id=str(order.id)) def do_finished_order(self, order, result): # type: (ConsumeRecord, dict) -> None """ 处理结束运行订单 :param order: 用户服务器订单 :param result: device_event 设备侧订单 :return: """ portCache = Device.get_port_control_cache(self.device.devNo, str(order.used_port)) self.event_data["portCache"] = portCache # 子单归并主单 if 'sub' in result: order.association = { 'sub': [item['order_id'] for item in self.event_data['sub']] } order.servicedInfo.update( {'subOrderNo': '{}'.format(', '.join([item['order_id'] for item in self.event_data['sub']]))}) # 主单归并自身 elif 'master' in result: order.association = { 'master': result['master'] } order.servicedInfo.update({'masterOrderNo': result['master']}) # 此时理论上服务器订单状态有三种可能(finished在上层已经被排除) # 正常的状态 相当于订单由运行状态 即将切换为finished状态 if order.status == "running": order.isNormal = True order.status = "finished" order.errorDesc = u"" order.finishedTime = datetime.datetime.fromtimestamp(result['fts']) # 非正常状态 相当于订单最开始串口超时 然后直接变为结束 elif order.status == "unknown": order.isNormal = True order.status = "finished" order.errorDesc = u"设备信号恢复,订单正常结束(0001)" order.startTime = datetime.datetime.fromtimestamp(result['sts']) order.finishedTime = datetime.datetime.fromtimestamp(result['fts']) # 正常状态 相当于订单启动失败或者是中间running单没有上来 elif order.status == "created": order.isNormal = True order.status = "finished" order.errorDesc = u"设备信号恢复,订单正常结束(0002)" order.startTime = datetime.datetime.fromtimestamp(result['sts']) order.finishedTime = datetime.datetime.fromtimestamp(result['fts']) else: logger.warning('order<{}> status = <{}> to finished. no deal with'.format(repr(order), order.status)) order.save() set_start_key_status(start_key=order.startKey, state=START_DEVICE_STATUS.FINISHED, order_id=str(order.id)) def do_finished_event(self, order, sub_orders, merge_order_info): # type:(ConsumeRecord, list, dict) -> None """ 订单的状态已经完成 进一步事件 扣费等等 :param order: 处理完毕的订单(主订单) :param sub_orders: 子订单 :param merge_order_info: 合并单的信息 :return: """ order.reload() # 解析event的参数 这个left是转换后的时间 """ 比如1元240分钟,充电后变为120分钟,一分钟后119,然后,过一会进入下一档,80分钟,这时候如果停止,我会把80分钟转换为230多分钟给你 """ left = self.event_data["left"] # 获取端口的缓存 portCache = self.event_data["portCache"] need = portCache["needValue"] coins = portCache["coins"] billingType = portCache["billingType"] # need 和 left 单位始终相同 所以不会造成退费金额出错 backCoins = VirtualCoin(coins) * ((left * 1.0) / need) if not self.device.is_auto_refund: backCoins = VirtualCoin(0) else: backCoins = min(VirtualCoin(coins), backCoins) # 算电量 算时间 if billingType == BillingType.TIME: duration = need - left spendElec = 0 extra = [ {u"本次订购时长": "{}分钟".format(need)}, {u"本次实际使用时长": "{}分钟".format(need-left)}, {u"消费金额": "{}(金币)".format((VirtualCoin(coins) - VirtualCoin(backCoins)).amount)} ] else: # 电量模式下 时间有可能不太准 考虑断电的情况 duration = (self.event_data["fts"] - self.event_data["sts"]) / 60 spendElec = (need - left) / 100.0 extra = [ {u"本次订购电量": "{} 度".format(need/100.0)}, {u"消费金额": "{}(金币)".format((VirtualCoin(coins) - VirtualCoin(backCoins)).amount)} ] clear_frozen_user_balance(self.device, order, duration, spendElec=spendElec, backCoins=backCoins, user=order.user) # 组织消费信息 consumeDict = { "reason": self._get_finish_reason(), "billingType": u"时间计费" if billingType == BillingType.TIME else u"电量计费", "leftTime": left, DEALER_CONSUMPTION_AGG_KIND.DURATION: duration, DEALER_CONSUMPTION_AGG_KIND.SPEND_MONEY: (VirtualCoin(coins) - VirtualCoin(backCoins)).mongo_amount, DEALER_CONSUMPTION_AGG_KIND.REFUNDED_COINS: str(backCoins) } spendElec and consumeDict.update({DEALER_CONSUMPTION_AGG_KIND.ELEC: spendElec}) spendElec and consumeDict.update({DEALER_CONSUMPTION_AGG_KIND.ELECFEE: self.calc_elec_fee(spendElec)}) order.update_service_info(consumeDict) self.notify_user_service_complete( service_name='充电', openid=order.user.managerialOpenId, port=str(order.used_port), address=order.address, reason=consumeDict["reason"], finished_time=order.finishedTime.strftime('%Y-%m-%d %H:%M:%S'), extra=extra ) def merge_order(self, master_order, sub_orders): # type:(ConsumeRecord, list)->dict """ 主板暂时不支持续充 如果续充的话 时间不准 :param master_order: :param sub_orders: :return: """ start_time = Arrow.fromdatetime(master_order.startTime, tzinfo=settings.TIME_ZONE) need = self.event_data["charge_param"] billingType = self.event_data.get("billing_type", BillingType.TIME) coins = master_order.package["coins"] price = master_order.package["price"] portCache = { "openId": master_order.openId, "consumeType": "mobile", "needKind": "needTime" if billingType == BillingType.TIME else "needElec", "estimatedTs": int(start_time.timestamp + 720 * 60 * 60), "needValue": need, "billingType": billingType } for _sub in sub_orders: coins += _sub.package["coins"] price += _sub.package["price"] portCache["coins"] = coins portCache["price"] = price if billingType == BillingType.TIME: master_order.update_service_info({'needTime': need}) else: master_order.update_service_info({'needElec': need}) return portCache def _get_finish_reason(self): if "reason" in self.event_data: return REASON_MAP.get(self.event_data["reason"], u"未知原因") return u"未知原因" class IDPayProcessor(AckEventProcessorIntf): def pre_processing(self, device, event_data): event_data["cardNo"] = str(event_data.pop("card", "")) event_data["fee"] = event_data["cost"] / 10.0 if "sub" in event_data and isinstance(event_data["sub"], list): new_sub = list() for _item in event_data["sub"]: _item["cardNo"] = _item.pop("card") _item["fee"] = _item["cost"] / 10.0 new_sub.append(_item) event_data["sub"] = new_sub return event_data class IDPayAckEvent(IdStartAckEvent): """ 端口 查询卡 """ def __init__(self, smartBox, event_data, pre_processor=None): super(IDPayAckEvent, self).__init__(smartBox, event_data, IDPayProcessor()) def _get_finish_reason(self): if "reason" in self.event_data: return REASON_MAP.get(self.event_data["reason"], u"未知原因") return u"未知原因" def post_after_start(self, order=None): pass def post_after_finish(self, order=None): pass def checkout_order(self, order): fee = VirtualCoin(order.coin) self.card.freeze_balance(transaction_id=str(order.id), fee=fee) def do_finished_event(self, order, merge_order_info): # type:(ConsumeRecord, dict)->None left = self.event_data["left"] billingType = self.event_data["billing_type"] if self.device.is_auto_refund: backCoins = min(VirtualCoin(self.event_data["refund"]), VirtualCoin(merge_order_info["coins"])) else: backCoins = VirtualCoin(0) consumeCoins = VirtualCoin(merge_order_info["coins"]) - backCoins self.card.clear_frozen_balance(str(order.id), backCoins) self.card.reload() extra = [ {u"消费金额": "{}(金币)".format(consumeCoins.amount)}, {u"卡内余额": "{} (金币)".format(self.card.balance.mongo_amount)} ] consumeDict = { 'reason': self._get_finish_reason(), 'chargeIndex': str(order.used_port), 'cardNo': self.event_data['cardNo'], "left": left, "billingType": billingType } if backCoins > VirtualCoin(0): self.record_refund_money_for_card(backCoins, str(self.card.id), orderNo=order.orderNo) consumeDict.update({ DEALER_CONSUMPTION_AGG_KIND.REFUNDED_COINS: backCoins.mongo_amount }) order.update_service_info(consumeDict) self.notify_user_service_complete( service_name='充电', openid=self.card.managerialOpenId, port=str(order.used_port), address=order.address, reason=self.event_data.get('reasonDesc'), finished_time=order.finishedTime.strftime('%Y-%m-%d %H:%M:%S'), extra=extra ) def merge_order(self, master_order, sub_orders): # type:(ConsumeRecord, Iterable[ConsumeRecord])->dict """ 刷卡的退费是由主板决定的 金币以及钱只是为了展示 """ start_time = Arrow.fromdatetime(master_order.startTime, tzinfo=settings.TIME_ZONE) billingType = self.event_data.get("billing_type", BillingType.TIME) coins = master_order.coin price = master_order.money portCache = { "openId": master_order.openId, "consumeType": "card", "estimatedTs": int(start_time.timestamp + 720 * 60 * 60), "billingType": billingType } for _sub in sub_orders: coins += _sub.coin price += _sub.money portCache["coins"] = str(coins) portCache["price"] = str(price) return portCache