# coding=utf-8 import datetime import logging from arrow import Arrow from django.conf import settings from typing import TYPE_CHECKING from apilib.monetary import VirtualCoin, Ratio from apps.web.constant import START_DEVICE_STATUS, DEALER_CONSUMPTION_AGG_KIND from apps.web.core.device_define.cxjz import ChargeMode, REASON_MAP, DEFAULT_REFUND_PROTECTION_TIME, DEFAULT_REFUND_CAL_BASE, DEFAULT_SERVICE_FEE from apps.web.device.models import Device from apps.web.eventer import EventBuilder from apps.web.eventer.base import ComNetPayAckEvent, WorkEvent 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) # 暂时没有卡 event_data = self.deviceAdapter.analyze_event_data(device_event['data']) if event_data is None or 'cmdCode' not in event_data: return None return CXJZWorkEvent(self.deviceAdapter, event_data) class CXJZWorkEvent(WorkEvent): def do(self, **args): pass class MyComNetPayAckEvent(ComNetPayAckEvent): def do_running_order(self, order, result): # type: (ConsumeRecord, dict) -> None """ 处理运行订单 :param order: 用户服务器订单 :param result: device_event 设备侧订单 :return: """ # 订单消息已经被回复过 logger.debug('[{} do_running_order] order = {}, result = {}'.format(self.__class__, repr(order), result)) if order.status in ["running", "finished"]: order.isNormal = True order.save() 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是转换后的时间 need = merge_order_info["needValue"] left = merge_order_info["leftValue"] coins = merge_order_info["coins"] billingType = merge_order_info["billingType"] # 获取端口的缓存 # 算电量 算时间 if billingType == ChargeMode.TIME: backCoins = self._cal_power_refund_money(coins, left, need) duration = need - left spendElec = 0 extra = [ {u"本次订购时长": "{}分钟".format(need)}, {u"本次实际使用时长": "{}分钟".format(need-left)}, {u"消费金额": "{}(金币)".format((VirtualCoin(coins) - VirtualCoin(backCoins)).amount)}, ] else: # 电量模式下 时间有可能不太准 考虑断电的情况 backCoins = self._cal_elec_refund_money(coins, left, need) duration = (self.event_data["fts"] - self.event_data["sts"]) / 60 spendElec = (need - left) / 100.0 extra = [ {u"本次订购电量": u"{} 度".format(need/100.0)}, {u"消费金额": u"{}(金币)".format((VirtualCoin(coins) - VirtualCoin(backCoins)).amount)} ] if backCoins > VirtualCoin(0): extra.append({u"退费金额": u"{}(金币)".format(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 != ChargeMode.ELEC else u"电量计费", DEALER_CONSUMPTION_AGG_KIND.DURATION: duration, DEALER_CONSUMPTION_AGG_KIND.SPEND_MONEY: (VirtualCoin(coins) - VirtualCoin(backCoins)).mongo_amount, } 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) # 首先获取充电的方式 chargeMode = self.event_data["charge_mode"] if chargeMode == ChargeMode.TIME: needKind = "needTime" leftValue = self.event_data["left_time"] # 功率分档模式之下 有二次检测的时间 这个时候 需要及时变更时间 if "second_time" in self.event_data: needValue = self.event_data["second_time"] else: needValue = self.event_data["need_time"] else: needKind = "needElec" needValue = self.event_data["need_elec"] leftValue = self.event_data["left_elec"] coins = master_order.package["coins"] price = master_order.package["price"] portCache = { "openId": master_order.openId, "consumeType": "mobile", "needKind": needKind, "estimatedTs": int(start_time.timestamp + 720 * 60 * 60), "needValue": needValue, "billingType": chargeMode, "leftValue": leftValue } for _sub in sub_orders: coins += _sub.package["coins"] price += _sub.package["price"] portCache["coins"] = coins portCache["price"] = price return portCache def _cal_power_refund_money(self, coins, left, need): # type:(VirtualCoin, float, float) -> VirtualCoin """ 按照闪灵科技 的退费规则进行退费 退费这块主要是两种情况, 1.非正常情况(机器不启动,空载,过载),开始几分钟的时候,全额退费。 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元 最低消费金额可设置,退费计算单位可设置 向上取整 数学公式 UP(A/B) = (A+B-1)/B """ # 首先判断是退费开关 if not self.device.is_auto_refund: return VirtualCoin(0) # 然后判断是否到达退费保护时间 refundProtectionTime = self.device.get("otherConf", dict()).get("refundProtectionTime", DEFAULT_REFUND_PROTECTION_TIME) serviceFee = self.device.get("otherConf", dict()).get("serviceFee", DEFAULT_SERVICE_FEE) refundCalBase = self.device.get("otherConf", dict()).get("refundCalBase", DEFAULT_REFUND_CAL_BASE) left = min(need, left) if (need - left) <= refundProtectionTime: return VirtualCoin(coins) # 按照进制计算时间 upper = lambda x, y: int(x+y-1) // y usedBase = upper(int(need - left), int(refundCalBase)) needBase = upper(int(need), int(refundCalBase)) # 使用进制转换后的时间进行计算退费 leftMoney = max(VirtualCoin(0), VirtualCoin(coins) - VirtualCoin(serviceFee)) # 减去服务费用的剩余的钱 refundMoney = leftMoney * Ratio((needBase - usedBase) * 1.0 / needBase) return refundMoney def _cal_elec_refund_money(self, coins, left, need): # type:(VirtualCoin, float, float) -> VirtualCoin """ 电量计费 不涉及进制转换的问题 """ if not self.device.is_auto_refund: return VirtualCoin(0) # 然后判断是否到达退费保护时间 refundProtectionTime = self.device.get("otherConf", dict()).get("refundProtectionTime", DEFAULT_REFUND_PROTECTION_TIME) serviceFee = self.device.get("otherConf", dict()).get("serviceFee", DEFAULT_SERVICE_FEE) # 由于电量计费 只能使用订单时间来计算 if self.event_data["fts"] - self.event_data["sts"] <= refundProtectionTime * 60: return VirtualCoin(coins) # 使用进制转换后的时间进行计算退费 leftMoney = max(VirtualCoin(0), VirtualCoin(coins) - VirtualCoin(serviceFee)) # 减去服务费用的剩余的钱 refundMoney = leftMoney * Ratio(left * 1.0 / need) return refundMoney def _get_finish_reason(self): if "reason" in self.event_data: return REASON_MAP.get(self.event_data["reason"], u"未知原因") return u"未知原因"