# coding=utf-8 # noinspection PyUnresolvedReferences import datetime import logging from collections import defaultdict from typing import TYPE_CHECKING, Optional from UserDict import UserDict from contextlib2 import AbstractContextManager from apilib.monetary import RMB from apps.web.core.exceptions import ServiceException from apps.web.dealer.proxy import DealerGroupStats from apps.web.device.models import Device from apps.web.exceptions import UnifiedConsumeOrderError, UserServerException from apps.web.user.constant2 import StartDeviceType, ConsumeOrderServiceItem from apps.web.user.models import MyUser, ConsumeRecord, OrderPackage, Card if TYPE_CHECKING: from apps.web.device.models import DeviceDict, GroupDict logger = logging.getLogger(__name__) class OrderContext(object, UserDict): def __init__(self): super(OrderContext, self).__init__() @property def client(self): # noinspection PyUnresolvedReferences return self._client @property def terminal(self): # type:() -> Optional[DeviceDict, GroupDict] # noinspection PyUnresolvedReferences return self._device or self._group @property def user(self): # noinspection PyUnresolvedReferences return self._user @property def goods(self): # type:() -> Goods # noinspection PyUnresolvedReferences return self._goods class OrderContextManager(AbstractContextManager): def __init__(self, **kwargs): self._source = kwargs def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): super(OrderContextManager, self).__exit__(exc_type, exc_val, exc_tb) def get_starter(self): """检查启动实体""" userInfo = self._source.get("userInfo") if not userInfo: raise UnifiedConsumeOrderError(u"用户异常【下单参数异常】,请重新扫码下单(00000)") user = userInfo.pop("user", MyUser.objects.filter(openId=userInfo["openId"], groupId=userInfo["groupId"]).first()) # type: MyUser return user def get_device(self): """检查启动设备""" devInfo = self._source.get("devInfo") if not devInfo: raise UnifiedConsumeOrderError(u"设备异常【下单参数异常】,请重新扫码下单(10000)") device = Device.get_dev_by_l(devInfo.get("logicalCode")) # type: DeviceDict if not device: raise UnifiedConsumeOrderError(u"设备异常,请重新扫码下单(10001)") if not device.online: raise UnifiedConsumeOrderError(u"设备异常【离线】,请重新扫码下单(10002)") if not device.is_registered: raise UnifiedConsumeOrderError(u"设备异常【未注册】,请重新扫码下单(10003)") if device.is_fault: raise UnifiedConsumeOrderError(u"设备异常【故障】,请重新扫码下单(10004)") return device def get_start_context(self, user, device): """检查启动其余环境""" startInfo = self._source.get("startInfo") context = StartParamContext() packageId, startType, port \ = startInfo["packageId"], startInfo["startType"], startInfo["port"] if startType not in [StartDeviceType.ON_LIEN, StartDeviceType.CARD]: raise UnifiedConsumeOrderError(u"启动参数异常【方式异常】,请重新扫码下单(20001)") context.startType = startType try: package = device.deviceAdapter.prepare_package(packageId, startInfo, startType) except ServiceException as se: raise UnifiedConsumeOrderError(se.result["description"]) if not package: raise UnifiedConsumeOrderError(u"启动参数异常【套餐异常】,请重新扫码下单(20000)") context.package = package context.port = port return context def buildOrder(self): user = self.get_starter() device = self.get_device() context = self.get_start_context(user, device) order = ConsumeRecord.new_one( orderNo=ConsumeRecord.make_no(), user=user, device=device, context=context ) logger.debug("[{} build order] order = {} has been create".format(self.__class__.__name__, order)) return order class UnifiedConsumeOrderManager(OrderContextManager): def __exit__(self, exc_type, exc_val, exc_tb): if exc_type == UnifiedConsumeOrderError or exc_type is None: return False else: logger.exception(exc_tb) raise UnifiedConsumeOrderError(u"下单异常(00000)") def get_starter(self): userInfo = self._source.get("userInfo") if not userInfo: raise UnifiedConsumeOrderError(u"用户异常【下单参数异常】,请重新扫码下单(00000)") user = userInfo.pop("user", MyUser.objects.filter(openId=userInfo["openId"], groupId=userInfo["groupId"]).first()) # type: MyUser if not user: raise UnifiedConsumeOrderError(u"用户异常【未找到用户】,请重新扫码下单(00001)") if not user.isNormal: raise UnifiedConsumeOrderError(u"用户异常,请重新扫码下单(00002)") # 检验用户是否有未完成订单 主要是没有付费的后付费的 notCompleteOrder = get_user_not_complete_order(user) if notCompleteOrder: raise UnifiedConsumeOrderError(u"当前存在尚未完成订单【{}】,请前往查看".format(notCompleteOrder.orderNo)) return user def get_start_context(self, user, device): # type:(MyUser, DeviceDict) -> StartParamContext context = super(UnifiedConsumeOrderManager, self).get_start_context(user, device) canUse, msg = device.is_port_can_use(context.port) if not canUse: raise UnifiedConsumeOrderError(u"启动设备失败【{}】".format(msg)) return context class UnifiedCardConsumeOrderManager(OrderContextManager): def get_starter(self): userInfo = self._source.get("userInfo") card = Card.objects.filter(id=userInfo['cardId']).first() # type: Card if not card: raise UnifiedConsumeOrderError(u"卡异常【未找到卡】,请重新下单(00001)") if card.frozen: raise UnifiedConsumeOrderError(u"用户异常【冻结】,请重新下单(00002)") # 检验用户是否有未完成订单 主要是没有付费的后付费的 notCompleteOrder = get_user_not_complete_order(card) if notCompleteOrder: raise UnifiedConsumeOrderError(u"当前存在尚未完成订单【{}】,请前往查看".format(notCompleteOrder.orderNo)) return card def get_start_context(self, user, device): # type:(MyUser, DeviceDict) -> StartParamContext context = super(UnifiedCardConsumeOrderManager, self).get_start_context(user, device) context.cardId = str(user.id) context.sequence = self._source["startInfo"]["sequenceNo"] return context class StartParamContext(object): def __init__(self): self._port = None self._startType = None self._package = None self._cardId = None self._sequence = None @property def port(self): return self._port or 0 @port.setter def port(self, value): self._port = int(value) @property def startType(self): return self._startType @startType.setter def startType(self, value): self._startType = value @property def package(self): # type:() -> OrderPackage return self._package @package.setter def package(self, value): self._package = OrderPackage(value) @property def cardId(self): return self._cardId @cardId.setter def cardId(self, value): self._cardId = value @property def sequence(self): return self._sequence @sequence.setter def sequence(self, value): self._sequence = value class ConsumeOrderStateEngine(object): """ 订单状态引擎 驱使订单状态的改变 """ def __init__(self, order): # type: (ConsumeRecord) -> None self._order = order def to_wait_confirm(self): """ 订单 待确认状态 """ assert self._order.status == self._order.Status.CREATED self._order.update(status=self._order.Status.WAIT_CONF) # 开启订单的超时检查 check_consume_order_timeout(self._order) def to_running(self, result): # type:(dict) -> None """ 订单 已经启动 此时的状态一定切换 """ assert self._order.status in [ self._order.Status.CREATED, self._order.Status.WAIT_CONF, self._order.Status.UNKNOWN, self._order.Status.TIMEOUT ] # 更新订单的启动时间 serviceInfo = self._order.service if "deviceStartTime" in result: serviceInfo.deviceStartTime = result["deviceStartTime"] else: serviceInfo.deviceStartTime = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") self._order.update( status=self._order.Status.RUNNING, serviceInfo=serviceInfo ) try: PublishOrderInfoToUser.order_executing(self._order.user, self._order.device, self._order.reload()) except Exception as e: logger.exception(e) def to_end(self, result): # type:(dict) -> None """ 订单运行结束 由事件驱动 """ assert self._order.status == self._order.Status.RUNNING # 更新订单的结束事件 serviceInfo = self._order.service.copy() for kind in result: if kind not in ConsumeOrderServiceItem.choices(): continue serviceInfo.update({kind: self._trans_value(result[kind])}) self._order.update( status=self._order.Status.END, serviceInfo=serviceInfo ) try: PublishOrderInfoToUser.order_finished(self._order.user, self._order.device, self._order.reload()) except Exception as e: logger.exception(e) def to_wait_pay(self): """ 后付费订单的临时状态 """ assert self._order.status == self._order.Status.END self._order.update(status=self._order.Status.WAIT_PAY) def to_finished(self): """ 订单完全结束 设备运行结束才能够结单 同时结单前需要检查支付信息是否已经完成 """ assert self._order.status in [self._order.Status.END, self._order.Status.WAIT_PAY] # 没有支付过的订单 无法切换到订单的结束状态 if not self._order.isPaid: logger.warning("[ConsumeOrderStateEngine] to finished error, order is not paid!") return self._order.update( status=self._order.Status.FINISHED ) state = DealerGroupStats.update_group_stats( group=self._order.group, order=self._order ) if state: self._order.link_state(state) def to_failure(self, desc=""): """ 订单失败 等同于订单结束 """ self._order.update( status=self._order.Status.FAILURE, description=desc ) def to_timeout(self, desc=""): """ 订单执行超时 """ self._order.update( status=self._order.Status.TIMEOUT, description=desc ) def to_unknown(self, desc=""): """ 订单状态未知 不进行任何的操作 """ self._order.update( status=self._order.Status.UNKNOWN, description=desc ) @staticmethod def _trans_value(value): if isinstance(value, RMB): return value.mongo_amount return value class PublishOrderInfoToUser(object): @staticmethod def order_executing(user, device, order): device.deviceAdapter.notify_service_start_to_user(order, user) @staticmethod def order_finished(user, device, order): device.deviceAdapter.notify_service_end_to_user(order) def get_user_not_complete_order(user, owner=None): """ 获取用户未完成的订单 (没有付钱的) """ # 首先查找状态还在进行中的订单 query = ConsumeRecord.objects.filter( openId=user.openId, status__in=[ConsumeRecord.Status.RUNNING, ConsumeRecord.Status.END, ConsumeRecord.Status.WAIT_PAY] ) if owner: query = query.filter(ownerId=str(owner.id)) if isinstance(user, Card): query.filter(cardId=str(user.id)) # 判断订单是否已经被支付 curOrder = None for order in query: # type: ConsumeRecord if order.isPaid: continue curOrder = order break return curOrder def generate_net_payment(order): # type:(ConsumeRecord) -> dict """ 生成订单的用户支付扣款相关信息 """ needPayMoney = order.price user = order.user share_group_ids = order.owner.get_currency_group_ids(order.group) chargeBalanceField = user.__class__.chargeBalance.name bestowBalanceField = user.__class__.bestowBalance.name # ONLY虽然会加快查询的速度,但是好像也会忽略相应的默认值 需要有一个补漏的措施 currency_users = list(user.__class__.objects.filter( openId=user.openId, groupId__in=share_group_ids ).only( chargeBalanceField, bestowBalanceField, )) # 始终保证从当前地址开始扣除 currency_users.insert(0, user) # 添加默认参数 这个地方也可以将deduct单项作为一个object实例化 这样就避免键值不统一 deduct = defaultdict(lambda: { user.__class__.id.name: "", chargeBalanceField: RMB(0).mongo_amount, bestowBalanceField: RMB(0).mongo_amount }) for _user in currency_users: # type: MyUser minPay = min(_user.chargeBalance or RMB(0), needPayMoney) deduct[_user.id].update({ user.__class__.id.name: str(_user.id), chargeBalanceField: minPay.mongo_amount }) needPayMoney -= minPay if needPayMoney == RMB(0): break # 充值余额走完 还是没有支付完 就走赠送余额 else: for __user in currency_users: minPay = min(__user.bestowBalance or RMB(0), needPayMoney) deduct[__user.id].update({ bestowBalanceField: minPay.mongo_amount }) needPayMoney -= minPay if needPayMoney == RMB(0): break else: raise UserServerException(u"用户支付异常") return { "via": "user", "itemId": str(user.id), "deduct_list": deduct.values() } def generate_card_payment(order): # type:(ConsumeRecord) -> dict needPayMoney = order.price card = order.card # type: Card chargeBalanceField = card.__class__.chargeBalance.name bestowBalanceField = card.__class__.bestowBalance.name deduct = defaultdict(lambda: { card.__class__.id.name: str(card.id), chargeBalanceField: RMB(0).mongo_amount, bestowBalanceField: RMB(0).mongo_amount }) # 首先使用充值余额 minPay = min(card.chargeBalance or RMB(0), needPayMoney) deduct[str(card.id)].update({ chargeBalanceField: minPay.mongo_amount }) needPayMoney -= minPay # 充值余额不足的 剩余的由赠送余额支付 if needPayMoney > RMB(0): minPay = min(card.bestowBalance or RMB(0), needPayMoney) deduct[str(card.id)].update({ bestowBalanceField: minPay.mongo_amount }) needPayMoney -= minPay if needPayMoney > RMB(0): raise UserServerException(u"卡支付异常") return { "via": "card", "itemId": str(card.id), "deduct_list": deduct.values() } def generate_refund(order, refundMoney): # type: (ConsumeRecord, RMB) -> dict payment = order.payment refund = [ { "id": _["id"], "chargeBalance": RMB(0).mongo_amount, "bestowBalance": RMB(0).mongo_amount } for _ in payment.deduct_list ] # 首先退还的是赠送余额 for _index, _deduct in enumerate(payment.deduct_list): minRefund = min(RMB(_deduct["bestowBalance"]), refundMoney) refund[_index].update({ "bestowBalance": minRefund.mongo_amount, }) refundMoney -= minRefund if refundMoney == RMB(0): break else: for __index, __deduct in enumerate(payment.deduct_list): minRefund = min(RMB(__deduct["chargeBalance"]), refundMoney) refund[__index].update({ "chargeBalance": minRefund.mongo_amount }) refundMoney -= minRefund if refundMoney == RMB(0): break else: raise UserServerException(u"退款异常") return { "vid": order.payment.via, "itemId": order.payment["itemId"], "deduct_list": refund } def check_consume_order_timeout(order): """ 检测消费订单是否超时 """ # TODO 建立延时任务 如果超过一定时间 直接将订单置为失败 def check_consume_order_pay_timeout(order): """ 检查消费订单的支付是否超时 """ # TODO 建立延时任务 如果订单支付超时 直接切换成为正常结束状态 def notify_user(managerialOpenId, dealerId, templateName, **kwargs): try: if not managerialOpenId or not dealerId: logger.error('managerialOpenId is null') return from taskmanager.mediator import task_caller task_caller('report_to_user_via_wechat', openId = managerialOpenId, dealerId = dealerId, templateName = templateName, **kwargs) except Exception as e: logger.exception(e) def get_paginate(data, pageSize, pageIndex): # type:(list, int, int) -> int if len(data) == pageSize*pageIndex: return pageSize * pageIndex + 1 return (pageIndex-1) * pageSize + len(data)