# -*- coding: utf-8 -*- # !/usr/bin/env python """ web.user.models ~~~~~~~~~ """ import copy import datetime import logging import random import string import threading import time import uuid from collections import Counter, defaultdict from arrow import Arrow from bson.objectid import ObjectId from dateutil import relativedelta from django.conf import settings from django.utils.functional import cached_property from mongoengine import Q from mongoengine.document import EmbeddedDocument from mongoengine.errors import NotUniqueError, DoesNotExist from mongoengine.fields import (BooleanField, StringField, IntField, EmbeddedDocumentListField, DateTimeField, PointField, DictField, ObjectIdField, ListField, EmbeddedDocumentField, MapField, GenericLazyReferenceField, LazyReferenceField) from pymongo.errors import DuplicateKeyError from pymongo.results import UpdateResult from typing import Union, Dict, AnyStr, Optional, TYPE_CHECKING, List, Tuple from apilib.monetary import RMB, VirtualCoin, Ratio, Percent, Money from apilib.quantity import Quantity from apilib.systypes import IterConstant from apilib.utils import flatten from apilib.utils_datetime import generate_timestamp_ex, today_format_str, to_datetime, get_tomorrow_zero_time from apilib.utils_mongo import BulkHandlerEx, dict_field_with_money from apilib.utils_string import get_random_str from apps import serviceCache from apps.web.agent.models import Agent, MoniApp from apps.web.common.models import OrderRecordBase, RefundOrderBase from apps.web.common.transaction import OrderNoMaker, OrderMainType, UserPaySubType, UserConsumeSubType, RefundSubType from apps.web.common.validation import CARD_NO_RE from apps.web.constant import (Const, DEALER_CONSUMPTION_AGG_KIND, GLOSSARY_TRANSLATION, USER_RECHARGE_TYPE, RECHARGE_CARD_TYPE, AppPlatformType, APP_PLATFORM_TYPE_TRANSLATION, RechargeRecordVia, RECHARGE_RECORD_VIA_TRANSLATION, ErrorCode, DEVICE_INCOME_STRATEGY, support_policy_device, support_policy_weifule, PARTITION_ROLE) from apps.web.core import PayAppType, ROLE from apps.web.core.bridge import WechatClientProxy from apps.web.core.db import Searchable, MonetaryField, VirtualCoinField, RoleBaseDocument, BooleanIntField from apps.web.core.exceptions import InsufficientFundsError, ServiceException, ImproperlyOperatedBalance from apps.web.core.models import BoundOpenInfo from apps.web.core.models import WechatAuthApp from apps.web.dealer.models import Dealer, VirtualCard, DealerDict from apps.web.device.models import Group, Device, DeviceType from apps.web.exceptions import UserServerException, PostPayOrderError from apps.web.report.ledger import AgentLedgerFirst, PartnerLedgerFirst from apps.web.report.utils import record_consumption_stats from apps.web.user.conf import REFUND_NOTIFY_URL from apps.web.user.constant2 import StartDeviceType, PackageCategory, CONSUME_ORDER_PAY_TIMEOUT, ConsumeOrderServiceItem, UserBalanceChangeCategory from apps.web.utils import concat_front_end_url, concat_user_login_entry_url, concat_user_center_entry_url, set_or_incr_cache, concat_count_down_page_url from library.idgen import IDGenService from apps.web.core.payment import PaymentGateway logger = logging.getLogger(__name__) if TYPE_CHECKING: from apps.web.device.models import FeedBack from apps.web.device.models import DeviceDict, GroupDict from apps.web.common.models import CapitalUser from apps.web.dealer.models import MonthlyPackageTemp from mongoengine.queryset import QuerySet from apps.web.dealer.models import DealerRechargeRecord from apps.web.user.utils2 import StartParamContext, check_consume_order_timeout class MyUserAuthBackend(object): """ 部分接口需要用户鉴权,由于目前用户体系尚未完整建立,在这里首先实现MVP """ # noinspection PyUnusedLocal def authenticate(self, **kwargs): return True def get_user(self, user_id): return self.user_document.objects.with_id(user_id) @property def user_document(self): self._user_doc = MyUser return self._user_doc class EndUserLocation(EmbeddedDocument): logicalCode = StringField(verbose_name = '') # point :: { "type" : "Point" , "coordinates" : [longitude, latitude]} point = PointField(default = None, verbose_name = u'用户的经纬度') type = StringField(verbose_name = u'经纬度类型') createdTime = DateTimeField(default = datetime.datetime.now) @property def coordinates(self): if not self.point: return () else: return tuple(self.point['coordinates']) def __repr__(self): return '' \ % (self.logicalCode, self.point['coordinates'][0], self.point['coordinates'][1], self.type) class UserMoney(EmbeddedDocument): settled = MonetaryField(verbose_name = "已经分账给经销商的金额", default = RMB('0.00')) # type: RMB unsettled = MonetaryField(verbose_name = "未分账给经销商的金额", default = RMB('0.00')) # type: RMB total_recharged = MonetaryField(default = RMB('0.00')) # type: RMB total_consumed = MonetaryField(default = RMB('0.00')) # type: RMB def __repr__(self): return '' \ % (self.settled, self.unsettled, self.total_recharged, self.total_consumed) def to_dict(self): return { 'settled': self.settled, 'unsettled': self.unsettled, 'total_recharged': self.total_recharged, 'total_consumed': self.total_consumed } class UserCoin(EmbeddedDocument): balance = VirtualCoinField(default = VirtualCoin('0.00')) total_recharged = VirtualCoinField(default = VirtualCoin('0.00')) total_consumed = VirtualCoinField(default = VirtualCoin('0.00')) def __repr__(self): return '' \ % (self.balance, self.total_recharged, self.total_consumed) def to_dict(self): return { 'balance': self.balance, 'total_recharged': self.total_recharged, 'total_consumed': self.total_consumed } class UniqueUser(Searchable): openId = StringField(verbose_name='第三方用户ID(微信的OPENID,支付宝的UID)', null=False, unique=True) userId = StringField(verbose_name='用户平台UID') phone = StringField(verbose_name='电话号码', default="") meta = { 'collection': 'unique_user', 'db_alias': 'default', 'indexes': [ {'fields': ['userId'], 'unique': True, 'sparse': True} ] } @classmethod def get_or_create(cls, openId): # type:(str) -> UniqueUser """ 创建或者是获取用户 """ user = cls.objects(openId = openId).first() if user: return user try: return cls(openId = openId).save() except DuplicateKeyError: return cls.objects(openId = openId).first() def update_phone(self, phone): # type:(str) -> bool """ 更新电话号码 """ return bool(self.update(phone=phone)) class MyUser(RoleBaseDocument): """ `EndUser` 终端用户模型 """ sex = IntField(verbose_name=u"性别", default=Const.USER_SEX.UNKNOWN) phoneOS = StringField(verbose_name=u"终端操作系统", default="") city = StringField(verbose_name=u"城市", default="") province = StringField(verbose_name=u"省份", default="") country = StringField(verbose_name=u"国家", default="") avatar = StringField(verbose_name=u"头像地址", default="") nickname = StringField(verbose_name=u"名称", max_length=255, default="") groupId = StringField(verbose_name=u"地址编号", default="") gateway = StringField(verbose_name=u"来自支付宝或微信", default='wechat') chargeBalance = MonetaryField(verbose_name=u"余额", default=RMB('0')) bestowBalance = MonetaryField(verbose_name=u"赠送余额", default=RMB('0')) total_recharged = MonetaryField(verbose_name="累计充值", default=RMB('0')) total_bestow = MonetaryField(verbose_name=u"累计赠送", default=RMB('0')) total_consumed = VirtualCoinField(verbose_name="累计消费", default=RMB('0')) phoneNumber = StringField(verbose_name=u"用户手机号码", default="") authAppId = StringField(verbose_name=u"鉴权appid", default="") openId = StringField(verbose_name=u"openID|支付宝buyerID", default="") unionId = StringField(verbose_name=u"统一用户ID", default="") managerialAppId = StringField(verbose_name=u"管理公众号AppId", default="") managerialOpenId = StringField(verbose_name=u"管理openId", default="") payAppId = StringField(verbose_name=u"最近登录微信授权appid", default="") payOpenId = StringField(verbose_name=u"openId", default="") payOpenIdMap = MapField(EmbeddedDocumentField(BoundOpenInfo)) extra = DictField(verbose_name=u"多余字段", default={}) dateTimeAdded = DateTimeField(default=datetime.datetime.now, verbose_name=u'添加进来的时间') last_login = DateTimeField(default=datetime.datetime.now, verbose_name=u'最近登录时间') lastLoginUserAgent = StringField(verbose_name=u"最后一次登录的userAgent", default="") locations = EmbeddedDocumentListField(document_type=EndUserLocation) agentId = StringField(verbose_name=u'当前用户绑定的直接上级代理商', default='') productAgentId = StringField(verbose_name=u'当前用户绑定的平台代理商', default='') promo = DictField(verbose_name=u'活动相关信息', default={}) favoriteDeviceList = ListField(verbose_name=u'收藏的宝贝', default=[]) smsVendor = StringField(verbose_name=u'sms提供商', default='') ongoingList = ListField(field=DictField(), verbose=u'冻结的金额') blacklistConfig = DictField(verbose_name=u'黑名单相关设置', default={}) meta = { "collection": "MyUser2", "db_alias": "default", "indexes": [{ 'fields': ['$openId', '$nickname'], 'weights': {'openId': 10, 'nickname': 2} }, 'openId', 'nickname', 'dateTimeAdded', # 'groupId' ], # "shard_key": ('openId',) } search_fields = ('openId', 'nickname') _AGENT_GROUP_ID_PREFIX = 'agent_' __mgr_cache = serviceCache def __str__(self): return '{}'.format(self.__class__.__name__, str(self.id), self.openId, self.groupId) @property def balance(self): return self.chargeBalance + self.bestowBalance def pay(self, payment): # type:(PaymentInfo) -> PaymentInfo if not payment: return if not payment.deduct_list: return # 虽然是循环扣除 但是一般情况下只会扣除1次 bulker = BulkHandlerEx(self.__class__.get_collection()) # type: BulkHandlerEx chargeBalanceField = self.__class__.chargeBalance.name bestowBalanceField = self.__class__.bestowBalance.name for deduct in payment.deduct_list: query = { '_id': ObjectId(deduct['id']), } update = { '$inc': { chargeBalanceField: (-RMB(deduct[chargeBalanceField])).mongo_amount, bestowBalanceField: (-RMB(deduct[bestowBalanceField])).mongo_amount } } bulker.update(query, update) result = bulker.execute() if result['success'] == 0 or len(result['info']['writeErrors']) != 0: logger.error("[user pay] pay error, result = {}".format(result)) else: # 添加支付时间并返回 payment.time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") return payment def account_consume(self, order): # type:(ConsumeRecord) -> None payment = order.payment # 获取支付的钱 consumeAmount = payment.actualAmount consumeBestowAmount = payment.totalAmount - consumeAmount # 获取平台余额 charge, bestow, _, __ = self.filter_my_balance() UserBalanceLog.consume( order.user, afterAmount=charge, afterBestowAmount=bestow, consumeAmount=consumeAmount, consumeBestowAmount=consumeBestowAmount, order=order ) def account_refund(self, order): # type:(ConsumeRecord) -> None refund = order.refund # 获取支付的钱 refundAmount = refund.actualAmount refundBestowAmount = refund.totalAmount - refundAmount # 获取平台余额 charge, bestow, _, __ = self.filter_my_balance() UserBalanceLog.refund( order.user, afterAmount=charge, afterBestowAmount=bestow, refundAmount=refundAmount, refundBestowAmount=refundBestowAmount, order=order ) def account_recharge(self, order): # type:(RechargeRecord) -> None # 获取支付的钱 chargeAmount = order.chargeAmount bestowAmount = order.bestowAmount # 获取平台余额 charge, bestow, _, __ = self.filter_my_balance() UserBalanceLog.recharge( order.user, afterAmount=charge, afterBestowAmount=bestow, chargeAmount=chargeAmount, chargeBestowAmount=bestowAmount, order=order ) def is_authenticated(self): if not self.authAppId: return False return True def get_promo_info(self, key): return self.promo.get(key) def set_promo_info(self, key, value): promo = self.promo promo[key] = value return self.update(promo = promo) @property def inhouse_promo_openId(self): return self.get_promo_info("inhouse_openId") def set_inhouse_promo_openId(self, openId): return self.set_promo_info(key = "inhouse_openId", value = openId) @property def feature_keys(self): return ['phoneOS', 'sex', 'gateway'] @property def feature_map(self): return {'phoneOS': self.phoneOS, 'sex': self.sex_in_en, 'gateway': self.gateway} @property def sex_in_en(self): if self.is_female: return 'female' elif self.is_male: return 'male' else: return '' @property def is_female(self): return self.sex == Const.USER_SEX.FEMALE @property def is_male(self): return self.sex == Const.USER_SEX.MALE def get_bound_pay_openid(self, key): # type: (str)->str if self.gateway == AppPlatformType.ALIPAY: return self.openId else: pay_openid_map = self.payOpenIdMap # type: dict bound = pay_openid_map.get(key, BoundOpenInfo(openId = '')) # type: BoundOpenInfo return bound.openId def set_bound_pay_openid(self, key, **payload): # type: (str, Dict)->None self.payOpenIdMap[key] = BoundOpenInfo(**payload) @classmethod def _product_group_id(cls, agent_id): # type:(str)->str return '{}{}'.format(cls._AGENT_GROUP_ID_PREFIX, agent_id) @classmethod def _is_product_group_id(cls, group_id): # type: (str)->bool return group_id.startswith(cls._AGENT_GROUP_ID_PREFIX) def is_product_user(self): return not self.groupId or self.groupId.startswith(self._AGENT_GROUP_ID_PREFIX) @classmethod def get_product_users(cls, agent_id): # type: (str) -> QuerySet productGroupId = cls._product_group_id(agent_id) return cls.objects.filter(groupId=productGroupId) @classmethod def get_or_create(cls, app_platform_type, open_id, group_id=groupId.default, **kwargs): # type: (str, str, str, Dict)->MyUser logger.info('get_or_create cls = {}, app_platform_type = {}, open_id = {}, groupId = {}, kwargs = {}'.format( cls, app_platform_type, open_id, group_id, str(kwargs))) if app_platform_type in [AppPlatformType.ALIPAY]: kwargs.pop('payOpenIdMap', None) if group_id == cls.groupId.default: # 平台个人中心登录 format_group_id = cls._product_group_id(kwargs.get('productAgentId')) else: format_group_id = group_id user = cls.objects(openId = open_id, groupId = format_group_id).first() # type: MyUser if user is not None: need_update = False if 'payOpenIdMap' in kwargs: for key, value in kwargs['payOpenIdMap'].iteritems(): if key in user.payOpenIdMap: continue user.set_bound_pay_openid(key = key, **(value.to_dict())) need_update = True if 'authAppId' in kwargs and kwargs['authAppId'] != user.authAppId: user.authAppId = kwargs['authAppId'] need_update = True if 'agentId' in kwargs and kwargs['agentId'] != user.agentId: user.agentId = kwargs['agentId'] need_update = True if 'productAgentId' in kwargs and kwargs['productAgentId'] != user.productAgentId: user.productAgentId = kwargs['productAgentId'] need_update = True if 'avatar' in kwargs and kwargs['avatar'] != user.avatar: user.avatar = kwargs['avatar'] need_update = True if need_update: user.save() return user else: kwargs.update({'gateway': app_platform_type}) user = cls(openId = open_id, groupId = format_group_id, **kwargs) try: return user.save() except NotUniqueError: return cls.objects(openId = open_id, groupId = format_group_id).first() def group_pay(self, amount): # type: (VirtualCoin)->None """ :param amount: :return: """ #: 检查是否宇宙里未分配的余额和是否小于要付的钱 valid_to_pay = bool(self.get_collection().count_documents( filter = { '_id': ObjectId(self.id), '$expr': {'$lt': [amount.mongo_amount, {'$sum': ["$universe.money.unsettled"]}]} } )) if valid_to_pay: raise NotImplementedError() def refund(self, payCount): try: self.update(inc__balance = payCount.amount, inc__total_consumed = -payCount.amount) except Exception as e: logger.exception( 'refund error = %s, openId = %s, money = %s' % (e, self.openId, payCount)) return 0, u'退金币失败' return 1, '' def update_total_recharge(self, money): # type: (RMB)->int assert isinstance(money, RMB), 'money has to be RMB' updated = self.update(inc__total_recharged = money.amount) return updated def incr_balance(self, coins): # type: (VirtualCoin)->int assert isinstance(coins, VirtualCoin), 'coins had to be VirtualCoin' updated = self.update(inc__balance = coins.amount) return updated def recharge(self, money, bestowMoney): # type: (RMB, RMB)->MyUser """ 给用户充值,同时记录累计充值数额 :param money: 币额 :param bestowMoney: 金额 :return: """ assert isinstance(money, RMB), 'coins had to be VirtualCoin' assert isinstance(bestowMoney, RMB), 'money has to be RMB' updated = self.update( inc__chargeBalance=money, inc__total_recharged=money, inc__bestowBalance=bestowMoney, inc__total_bestow=bestowMoney ) if not updated: raise PostPayOrderError(u'余额和累计充值更新失败') return self.reload() @classmethod def deduct_balance(cls, deduct_list): # type:(dict)->None """ 按照订单的扣款列表直接扣除用户相应balance :param deduct_list: :return: """ bulker = BulkHandlerEx(cls.get_collection()) # type: BulkHandlerEx for deduct in deduct_list: query = { '_id': ObjectId(deduct['id']) } update = { '$inc': { 'balance': (-VirtualCoin(deduct['coins'])).mongo_amount } } bulker.update(query, update) result = bulker.execute() logger.debug(result['info']) if result['success'] == 0: raise ServiceException({'result': 0, 'description': u"扣款失败(1001)"}) else: if len(result['info']['writeErrors']) != 0: raise ServiceException({'result': 0, 'description': u"扣款失败(1002)"}) @classmethod def freeze_balance(cls, transaction_id, payment): # type:(str, PaymentInfo) -> bool """ 按照扣款列表冻结用户相应的金额 :param transaction_id: :param payment: :return: """ bulker = BulkHandlerEx(cls.get_collection()) # type: BulkHandlerEx chargeBalanceField = cls.chargeBalance.name bestowBalanceField = cls.bestowBalance.name for deduct in payment.deduct_list: query = { '_id': ObjectId(deduct['id']), 'ongoingList.transaction_id': { '$ne': transaction_id } } update = { '$inc': { chargeBalanceField: (-RMB(deduct[chargeBalanceField])).mongo_amount, bestowBalanceField: (-RMB(deduct[bestowBalanceField])).mongo_amount }, '$addToSet': { 'ongoingList': { 'transaction_id': transaction_id, chargeBalanceField: deduct[chargeBalanceField], bestowBalanceField: deduct[bestowBalanceField] } } } bulker.update(query, update) result = bulker.execute() logger.debug(result['info']) if result['success'] == 0: raise ServiceException({'result': 0, 'description': u"扣款失败(1001)"}) else: if len(result['info']['writeErrors']) != 0: raise ServiceException({'result': 0, 'description': u"扣款失败(1002)"}) else: return True @classmethod def clear_frozen_balance(cls, transaction_id, payment, refund): # type:(str, PaymentInfo, PaymentInfo)->bool """ 清除冻结金额 实际上就是相当于退费 如果没有退费 也需要调用一次 目的是清理掉transactionId """ try: bulker = BulkHandlerEx(cls.get_collection()) # type: BulkHandlerEx chargeBalanceField = cls.chargeBalance.name bestowBalanceField = cls.bestowBalance.name # 将总消费情况 更新到第一个地址里面去 first = True totalDeduct = payment.totalAmount totalRefund = refund.totalAmount for deduct in refund.deduct_list: query = { '_id': ObjectId(deduct['id']), 'ongoingList': { '$elemMatch': { 'transaction_id': transaction_id } } } if first: update = { '$inc': { chargeBalanceField: deduct[chargeBalanceField], bestowBalanceField: deduct[bestowBalanceField], cls.total_consumed.name: (totalDeduct - totalRefund).mongo_amount }, '$pull': { 'ongoingList': { 'transaction_id': transaction_id } } } else: update = { '$inc': { chargeBalanceField: deduct[chargeBalanceField], bestowBalanceField: deduct[bestowBalanceField], }, '$pull': { 'ongoingList': { 'transaction_id': transaction_id } } } first = False bulker.update(query, update) result = bulker.execute() logger.debug(result['info']) if result['success'] == 0: return False else: if len(result['info']['writeErrors']) != 0: return False else: return True except Exception as e: logger.exception(e) return False @classmethod def recover_frozen_balance(cls, transaction_id, deduct_list): # type:(str, list)->bool """ 回滚冻结的余额 """ bulker = BulkHandlerEx(cls.get_collection()) # type: BulkHandlerEx for deduct in deduct_list: query = { '_id': ObjectId(deduct['id']), 'ongoingList': { '$elemMatch': {'transaction_id': transaction_id}}} update = { '$inc': { 'balance': deduct['coins'] }, '$pull': { 'ongoingList': { 'transaction_id': transaction_id, 'frozenCoins': deduct['coins'] }} } bulker.update(query, update) result = bulker.execute() if result['success'] == 0: logger.error(result['info']) return False else: if len(result['info']['writeErrors']) != 0: logger.error(result['info']) return False else: return True def to_dict(self, default_avatar = settings.DEFAULT_AVATAR_URL): avatar = self.avatar or default_avatar or Agent.get_agent(self.agentId).get("productLogo") return { 'avatar': avatar, 'nickname': self.nickname, 'openId': self.openId, 'groupId': self.groupId, 'sex': self.sex, 'balance': self.balance, 'total_recharged': self.total_recharged, 'total_consumed': self.total_consumed, } @property def product_users(self): return MyUser.objects.filter( __raw__ = { 'openId': self.openId, '$or': [ { 'agentId': self.agentId }, { 'productAgentId': self.productAgentId } ] }) @property def total_balance(self): """ 个人中心获取balance. 需要按照agentId过滤 :return: """ totalCharge, totalBestow, total, dataList = self.filter_my_balance() return totalCharge + totalBestow def filter_my_balance(self, currentGroup=None): """ 个人中心获取balance列表. 需要按照agentId过滤 """ dataList = [] total = 0 totalCharge = RMB(0) totalBestow = RMB(0) chargeBalanceField = self.__class__.chargeBalance.name bestowBalanceField = self.__class__.bestowBalance.name # dealer_partition = defaultdict(lambda: { # chargeBalanceField: RMB(0), # bestowBalanceField: RMB(0) # }) users = self.product_users for user in users: # type: MyUser groupId = user.groupId if user.is_product_user(): logger.debug('groupId <{}> is agent group id.'.format(groupId)) continue group = Group.get_group(groupId) # type: GroupDict if not group: logger.error('no group. id = %s' % groupId) continue dealer = Dealer.get_dealer(group['ownerId']) # type: DealerDict if dealer is None: logger.error('no dealer. id = %s' % group['ownerId']) continue # dealer_partition[dealer['id']][chargeBalanceField] += user.chargeBalance # dealer_partition[dealer['id']][bestowBalanceField] += user.bestowBalance # 过滤掉为0的情况 if user.balance == RMB(0): continue else: total += 1 totalCharge += user.chargeBalance totalBestow += user.bestowBalance address = group.address rv = { 'address': address, 'groupId': groupId, 'groupName': group.groupName, 'dealerId': group.ownerId, 'balance': { chargeBalanceField: user.chargeBalance, bestowBalanceField: user.bestowBalance }, 'currency': False } if currentGroup and dealer.is_currency(currentGroup, group): rv['currency'] = True dataList.append(rv) return totalCharge, totalBestow, total, dataList def get_balance_in_dealer(self, dealer, group): # type: (Dealer, GroupDict)->Tuple[RMB, RMB, RMB] if self.groupId != group.groupId: payload = self.cloneable_user_info payload['agentId'] = dealer.agentId user = MyUser.get_or_create( app_platform_type = self.gateway, open_id = self.openId, group_id = group.groupId, **payload) else: user = self users = user.product_users overall, dealer_balance, currency_balance = RMB(0), RMB(0), RMB(0) dealer_group_ids = Group.get_group_ids_of_dealer(str(dealer.id)) # type: List[GroupDict] groups = Group.get_groups_by_group_ids(dealer_group_ids).values() share_group_ids = dealer.get_currency_group_ids(group, groups) share_group_ids.append(group.groupId) for user in users: overall += user.balance if user.groupId in dealer_group_ids: dealer_balance += user.balance if user.groupId in share_group_ids: currency_balance += user.balance return overall, dealer_balance, currency_balance @classmethod def get_new_user_count(cls, groupIds, start, end): # type: (list, datetime.datetime, datetime.datetime)->int result = cls.get_collection().aggregate( [ { '$match': { "groupId": {"$in": groupIds}, "dateTimeAdded": {"$gte": start, "$lte": end} } }, { '$group': { '_id': '$openId' } }, {'$count': 'count'}, ] ) return next(result, {}).get("count", 0) @classmethod def get_user_count_by_filter(cls, groupIds, filterDict = {}): # type: (list)->int filter = {"groupId": {"$in": groupIds}} filter.update(filterDict) result = cls.get_collection().aggregate( [ { '$match': filter }, { '$group': { '_id': '$openId' } }, {'$count': 'count'}, ] ) return next(result, {}).get("count", 0) def get_extra_info(self, key): return self.extra.get(key) def set_extra_info(self, key, value): extra = self.extra extra[key] = value return self.update(extra = extra) @staticmethod def get_active_info(openId, **kwargs): """ 获取用户的激活状态 安骑换电用户的激活状态在一个经销商下只会有一个激活状态 """ users = MyUser.objects(openId = openId, **kwargs) for user in users: if user.get_extra_info("active"): curUser = user break else: return dict() # 没找到用户的激活信息 return curUser.get_extra_info("active") @staticmethod def del_active_info(openId, **kwargs): users = MyUser.objects(openId = openId, **kwargs) for user in users: if user.get_extra_info("active"): user.set_extra_info("active", {}) user.phoneNumber = "" user.save() break @staticmethod def set_active_info(setInfo, openId, **kwargs): users = MyUser.objects(openId = openId, **kwargs) for user in users: if user.get_extra_info("active"): curUser = user break else: # 用户第一次注册 curUser = user active = curUser.get_extra_info("active") if not active: active = dict() active.update(setInfo) curUser.set_extra_info("active", active) if setInfo.get("isMember"): # 被经销商激活的同时,为用户添加phoneNumber字段 curUser.phoneNumber = active.get("phoneNumber") curUser.save() return @property def cloneable_user_info(self): """ 对于微信, 必须是相同的平台, 用户信息才能一致; 对于京东和支付宝, 用户信息都是一致的 :return: """ return { 'authAppId': self.authAppId, 'avatar': self.avatar, 'nickname': self.nickname, 'managerialAppId': self.managerialAppId, 'managerialOpenId': self.managerialOpenId, 'payOpenIdMap': self.payOpenIdMap, 'gateway': self.gateway, 'productAgentId': self.productAgentId } @property def my_product_user(self): payload = self.cloneable_user_info payload['agentId'] = self.productAgentId return MyUser.get_or_create(self.gateway, self.openId, **payload) @property def collectedDeviceList(self): """ 用户收藏的设备 :return: """ return self.my_product_user.favoriteDeviceList @collectedDeviceList.setter def collectedDeviceList(self, value): cur_user = self.my_product_user cur_user.favoriteDeviceList = value cur_user.save() @property def phone(self): """ 用户的电话号码 之前是存储在productUser上面 现在需要做一次转换适配 """ uniqueUser = UniqueUser.get_or_create(self.openId) # 唯一用户存储了电话的情况下 直接返回唯一用户 if uniqueUser.phone: return uniqueUser.phone # 从以前的存储电话号码的地方找出存储的电话 如有 转移到uniquerUser上面 phoneNumber = self.my_product_user.phoneNumber if phoneNumber: uniqueUser.update_phone(phoneNumber) return phoneNumber else: return phoneNumber @phone.setter def phone(self, value): """ 保存用户的电话号码 将直接保存到uniqueUser上面去 """ uniqueUser = UniqueUser.get_or_create(self.openId) uniqueUser.update_phone(value) @property def user_id(self): """ 用户系统分配ID """ if settings.ID_SERVICE_IP: uniqueUser = UniqueUser.get_or_create(self.openId) if not uniqueUser.userId: try: user_id = IDGenService(server_ip = settings.ID_SERVICE_IP).get_id() uniqueUser.userId = user_id uniqueUser.save() except Exception as e: logger.error('update user id failure.') logger.exception(e) return uniqueUser.userId else: return None def record_and_check_access_day(self, name, limit): dayTime = datetime.datetime.now().strftime(Const.DATE_FMT) key = '%s-%s-%s' % (self.openId, name, dayTime) value = serviceCache.get(key, 0) if value >= limit: return False value += 1 serviceCache.set(key, value, 24 * 60 * 60) return True @property def cards_num(self): """ 统计该用户的该平台下的实体卡数量 :return: """ filters = {"openId": self.openId, "agentId": self.productAgentId} return Card.objects.filter(**filters).count() @property def many_cards(self): """ 是否允许 该用户拥有多张实体卡 :return: """ if "manyCards" in self.extra and self.get_extra_info("manyCards"): return True return False @classmethod def get_dealer_ids(cls, openId, productAgentId): users = cls.objects(openId = openId, productAgentId = productAgentId) group_ids = [] for user in users: if not user.is_product_user(): group_ids.append(user.groupId) groups = Group.get_groups_by_group_ids(group_ids) dealers = set() for group in groups.values(): dealers.add(group.ownerId) return list(dealers) def calc_currency_balance(self, dealer, group, groups = None): # type:(Dealer, GroupDict, List[GroupDict])->RMB share_group_ids = dealer.get_currency_group_ids(group, groups) share_group_ids.append(group.groupId) users = MyUser.objects(openId =self.openId, groupId__in=share_group_ids) return sum((u.balance for u in users), RMB(0)) def calc_currency_total_recharge(self, dealer, group): # type:(Dealer, GroupDict)->RMB share_group_ids = dealer.get_currency_group_ids(group) share_group_ids.append(group.groupId) users = MyUser.objects(openId = self.openId, groupId__in = share_group_ids) if users.count() == 0: return RMB(0) else: return RMB(users.sum('total_recharged')) @property def group(self): if self.is_product_user(): return None if hasattr(self, '__group__'): return getattr(self, '__group__') group = Group.get_group(self.groupId) # type: GroupDict if group: setattr(self, '__group__', group) return group @property def username(self): return self.nickname @property def request_limit_key(self): return self.openId @classmethod def get_day_used_cache(cls, openId): """获取用户的每日使用次数信息""" key = '{}_day_used'.format(openId) result = cls.__mgr_cache.get(key, 0) return result @classmethod def update_day_used_cache(cls, openId): """ 更新设备的每日使用次数信息 :param openId: :return: """ try: key = '{}_day_used'.format(openId) set_or_incr_cache(cls.__mgr_cache, key, 1, 24 * 60 * 60) except Exception: pass @classmethod def clear_day_used_cache(cls, openId): """清除用户的每日使用次数信息""" key = '{}_day_used'.format(openId) cls.__mgr_cache.set(key, 0) @classmethod def get_today_can_use(cls, openId): usedCount = MyUser.get_day_used_cache(openId) if usedCount > serviceCache.get('{}_day_used_limit'.format(openId), 10): return False return True def prepare_refund_cash(self, refund_order, minus_total_consume): # type:(RefundMoneyRecord, VirtualCoin)->bool query = { '_id': self.id, 'ongoingList.transaction_id': { '$ne': 'r_{}'.format(str(refund_order.id)) } } update = { '$inc': { 'chargeBalance': (-refund_order.coins).mongo_amount, 'total_recharged': (-refund_order.money).mongo_amount, }, '$addToSet': { 'ongoingList': { 'transaction_id': 'r_{}'.format(str(refund_order.id)), 'chargeBalance': refund_order.money.mongo_amount, 'minus_total_consume': minus_total_consume.mongo_amount } } } result = self.get_collection().update_one(query, update, upsert = False) # type: UpdateResult return bool(result.modified_count == 1) def commit_refund_cash(self, refund_order): # type:(RefundMoneyRecord)->bool query = { '_id': self.id, 'ongoingList': { '$elemMatch': { 'transaction_id': 'r_{}'.format(str(refund_order.id)) } } } update = { '$pull': { 'ongoingList': {'transaction_id': 'r_{}'.format(str(refund_order.id))} } } result = self.get_collection().update_one(query, update, upsert = False) # type: UpdateResult return bool(result.modified_count == 1) def revoke_refund_cash(self, refund_order): # type:(RefundMoneyRecord)->bool sync_key = 'r_{}'.format(str(refund_order.id)) ongoingItem = None for item in self.ongoingList: if item['transaction_id'] == sync_key: ongoingItem = item break if not ongoingItem: return False query = { '_id': self.id, 'ongoingList': { '$elemMatch': { 'transaction_id': sync_key } } } update = { '$inc': { 'balance': VirtualCoin(ongoingItem['coins']).mongo_amount, 'total_recharged': refund_order.money.mongo_amount, }, '$pull': { 'ongoingList': {'transaction_id': 'r_{}'.format(str(refund_order.id))} } } result = self.get_collection().update_one(query, update, upsert = False) # type: UpdateResult return bool(result.modified_count == 1) @property def isNormal(self): return True class MyUserDetail(Searchable): openId = StringField(verbose_name="Myuser中的id", default="") dealerId = StringField(verbose_name="经销商Id", default="") userName = StringField(verbose_name="用户姓名", default="") userPhone = StringField(verbose_name="用户电话", default="") userUnit = StringField(verbose_name="用户地址单元", default="") userFloor = StringField(verbose_name="用户地址楼层", default="") userRoom = StringField(verbose_name="用户地址房间", default="") meta = { "collection": "MyUserDetail", "db_alias": "logdata"} @classmethod def get_collection(cls): # type: ()->Collection return cls._get_collection() class RechargeRecordDict(dict): def __repr__(self): return '' % (self.get('id'),) @property def v(self): return dict(self) @property def ownerId(self): return self.get('ownerId') @property def groupId(self): return self.get('groupId') @property def money(self): return RMB(self['money']) @property def coins(self): return VirtualCoin(self['coins']) @property def attachParas(self): return self.get('attachParas') @property def extraInfo(self): return self.get('extraInfo') @property def group(self): # type:()->GroupDict _attr_name = '__my_group__' if hasattr(self, _attr_name): return getattr(self, _attr_name) else: group = Group.get_group(self.groupId) setattr(self, _attr_name, group) return group @property def owner(self): _attr_name = '__my_owner__' if hasattr(self, _attr_name): return getattr(self, _attr_name) else: dealer = Dealer.objects(id = self.ownerId).first() setattr(self, _attr_name, dealer) return dealer @property def my_gateway(self): return self.get('gateway') @property def pay_app_type(self): return self.get('payAppType') @property def pay_gateway_key(self): return self.get('payGatewayKey') @property def withdraw_source_key(self): return self.get('withdrawSourceKey') @property def dev_type_name(self): if 'devTypeName' in self: return self.get('devTypeName') elif 'devType' in self: return self.get('devType') else: return u'其他' @property def logicalCode(self): return self.get('logicalCode') @property def address(self): return self.get('address') @property def via(self): return self.get('via') @property def partition_map(self): """ 新订单模型下, 创建订单就会生成partitionMap. 所以直接取 :return: """ return self.extraInfo.get('partitionMap', None) def calc_income_partitions(self, partition_map): totalMoney = self.money leftMoney = totalMoney agent_partitions = partition_map.get(PARTITION_ROLE.AGENT, list()) if agent_partitions: for agentShare in agent_partitions: shareMoney = totalMoney * Percent(agentShare['share']).as_ratio if shareMoney > leftMoney: shareMoney = leftMoney leftMoney = leftMoney - shareMoney agentShare['money'] = shareMoney.mongo_amount partner_partitions = partition_map.get(PARTITION_ROLE.PARTNER, list()) if partner_partitions: for partnerShare in partner_partitions: shareMoney = totalMoney * Percent(partnerShare['share']).as_ratio if shareMoney > leftMoney: shareMoney = leftMoney leftMoney = leftMoney - shareMoney partnerShare['money'] = shareMoney.mongo_amount owner_partitions = partition_map.get(PARTITION_ROLE.OWNER, list()) assert len(owner_partitions) == 1, u'经销商只能有一个' ownerShare = owner_partitions[0] ownerShare['money'] = leftMoney.mongo_amount return partition_map def calc_refund_partitions(self, pay_split_map): totalMoney = self.money leftMoney = totalMoney share_key = lambda _role, _id: '{}_{}'.format(_role, _id) old_shares_map = {} partition_map = { PARTITION_ROLE.AGENT: [], PARTITION_ROLE.PARTNER: [], PARTITION_ROLE.OWNER: [] } agent_partitions = pay_split_map.get(PARTITION_ROLE.AGENT, list()) if agent_partitions: for agentShare in agent_partitions: old_shares_map[share_key(agentShare['role'], agentShare['id'])] = RMB(agentShare['money']) newAgentShare = copy.deepcopy(agentShare) shareMoney = totalMoney * Percent(agentShare['share']).as_ratio if abs(shareMoney) > abs(leftMoney): shareMoney = leftMoney leftMoney = leftMoney - shareMoney newAgentShare['money'] = shareMoney.mongo_amount partition_map[PARTITION_ROLE.AGENT].append(newAgentShare) partner_partitions = pay_split_map.get(PARTITION_ROLE.PARTNER, list()) if partner_partitions: for partnerShare in partner_partitions: old_shares_map[share_key(partnerShare['role'], partnerShare['id'])] = RMB(partnerShare['money']) newPartnerShare = copy.deepcopy(partnerShare) shareMoney = totalMoney * Percent(partnerShare['share']).as_ratio if abs(shareMoney) > abs(leftMoney): shareMoney = leftMoney leftMoney = leftMoney - shareMoney newPartnerShare['money'] = shareMoney.mongo_amount partition_map[PARTITION_ROLE.PARTNER].append(newPartnerShare) owner_partitions = pay_split_map.get(PARTITION_ROLE.OWNER, list()) assert len(owner_partitions) == 1, u'经销商只能有一个' ownerShare = owner_partitions[0] newOwnerShare = copy.deepcopy(ownerShare) old_share_money = RMB(ownerShare['money']) if abs(leftMoney) <= old_share_money: newOwnerShare['money'] = leftMoney.mongo_amount else: diff = abs(leftMoney) - old_share_money shares = list(flatten(partition_map.values())) shares.sort(key = lambda x: Percent(x['share']), reverse = True) for share in shares: if RMB(share['money']) - RMB('0.01') + RMB( old_shares_map[share_key(share['role'], share['id'])]) >= RMB(0): share['money'] = (RMB(share['money']) - RMB('0.01')).mongo_amount diff -= RMB('0.01') leftMoney += RMB('0.01') if diff <= RMB(0): break assert diff == RMB(0), u'分账金额错误' newOwnerShare['money'] = leftMoney.mongo_amount partition_map[PARTITION_ROLE.OWNER] = [newOwnerShare] for role, items in partition_map.iteritems(): for item in items: if RMB(item['money']) == RMB(0): item['money'] = RMB(0).mongo_amount return partition_map class RechargeRecord(OrderRecordBase): """ 三个作用: 1、经销商收益记录(特殊情况包括平台的收益记录,例如1毛保险单情况下) 2、充值订单,模型增加refundInfo信息 3、商家派送(金币,红包等)订单 """ class PayResult(IterConstant): UNPAY = 'unPay' SUCCESS = 'success' FAILED = 'failed' CANCEL = 'cancel' REFUNDING = 'refunding' # 退费处理中 CLOSE = 'close' # 退款订单用, 被退单 orderNo = StringField(verbose_name = u"订单号", unique = True) wxOrderNo = StringField(verbose_name = u"渠道订单号. 聚合商户,是聚合平台商户订单号;直连商户则是微信或支付宝订单号。互联互通订单号也用这个") transactionId = StringField(verbose_name = u"微信或者支付宝订单号", default = None) ownerId = StringField(verbose_name = u"设备的owner", default = "") #: 充值的金钱数额 incr(user.balance) money = MonetaryField(verbose_name = u"充值", default = RMB('0.00')) #: 用户购得的金币,如果是卡,就是充值的钱 coins = VirtualCoinField(verbose_name = u"金币数目", default = VirtualCoin('0.00')) subject = StringField(verbose_name = u"订单产品或者服务描述", default = "") result = StringField(verbose_name = u"充值或者服务结果", default = PayResult.UNPAY) extraInfo = DictField(verbose_name = u"支付订单模型信息", default = {}) description = StringField(verbose_name = u"结果描述,一般为第三方错误码", default = None) # 以下字段为充值订单的模型字段, MODEL中不在体现 #: 当用户余额为0,点击投币,生成充值订单,用户完成后直接自动投相应币数 #: 此举为优化用户体验,大多数情况用户对充值并不优先选择,使其能够缩短支付使用流程,用完即走 isQuickPay = BooleanField(verbose_name = u"是否直接支付使用,默认为否", default = False) selectedQuickPayPackageId = StringField(verbose_name = u"快捷支付所选的投币套餐", default = None) via = StringField(verbose_name = u"充值途径 =: (recharge|sendcoin|refund|chargeCard|swap)", default = 'recharge') finishedTime = DateTimeField(default = None, verbose_name = u"支付完成时间") # 派币需要记录信息 operator = StringField(verbose_name = u"操作员", default = None) attachParas = DictField(verbose_name = u"消费订单信息", default = {}) gateway = StringField(verbose_name = u'支付网关类型', default = '') payAppType = StringField(verbose_name = u'支付应用类型', default = None) payGatewayKey = StringField(verbose_name = u'支付网关key', default = None) withdrawSourceKey = StringField(verbose_name = u'提现商户平台标识,资金池模式下和代理商相同', default = None) isAllocatedCardMoney = BooleanField(verbose_name = u'是否分钱了', default = None) search_fields = ('orderNo', 'wxOrderNo') _shard_key = ('ownerId', 'dateTimeAdded') _origin_meta = { "collection": "RechargeRecord", "db_alias": "default" } meta = _origin_meta def __repr__(self): return '' % (str(self.id), self.orderNo, self.wxOrderNo, self.via) @cached_property def payGateway(self): return PaymentGateway.clone_from_order(self) @cached_property def user(self): return MyUser.objects.filter(openId=self.openId, groupId=self.groupId).first() @cached_property def dealer(self): return Dealer.objects(id=self.ownerId).get() @property def goods(self): # type:()->dict return {} @property def timeout(self): # type:()-> bool """支付超时""" return (datetime.datetime.now() - self.dateTimeAdded).total_seconds() > 60 * 3 @property def chargeAmount(self): return self.money @property def bestowAmount(self): return RMB(self.coins) - RMB(self.money) @property def my_gateway(self): return self.gateway @property def pay_app_type(self): return self.payAppType @property def pay_gateway_key(self): return self.payGatewayKey @property def owner(self): return self.dealer @property def payOrder(self): """ 用户的实际支付的订单 由于业务需要 可能被拆分为多笔 # TODO 需要搞定历史数据库的问题 """ if self.isSubOrder: _id = self.attachParas.get("tradeOrderId", None) or self.extraInfo.get('tradeOrderId', None) return self.__class__.objects.get(id = _id) return self @property def isSubOrder(self): return "tradeOrderId" in self.extraInfo or "tradeOrderId" in self.attachParas @property def card_no(self): return self.attachParas.get('cardNo', None) @property def myuser(self): _attr_name = '__my_user__' if hasattr(self, _attr_name): return getattr(self, _attr_name) else: _myuser = MyUser.get_or_create( open_id = self.openId, group_id = self.groupId, app_platform_type = self.gateway) # type: MyUser setattr(self, _attr_name, _myuser) return _myuser @property def my_subject(self): if self.subject: return self.subject else: return RECHARGE_RECORD_VIA_TRANSLATION.get(self.via) @property def my_description(self): if self.description: return self.description else: return getattr(self, 'desc', '') @property def my_via(self): """ 老的订单模型, 对于recharge, 根据是否快速支付做一个调整 :return: """ return self.via @property def withdraw_source_key(self): return self.withdrawSourceKey @property def partition_map(self): return self.to_dict_obj.partition_map @property def fen_total_fee(self): return int(self.money * 100) @property def amount(self): # type:()->RMB return self.money @property def my_amount(self): if self.via in [RechargeRecordVia.RefundCash, RechargeRecordVia.Cash, RechargeRecordVia.VirtualCard, RechargeRecordVia.MonthlyPackage]: return '{}元'.format(abs(self.money)) else: return self.coins @property def mongo_amount(self): return self.amount.mongo_amount @property def extra_detail_info(self): rv = {} # TODO: 需要根据RechargeRecord建立各VIA下的订单详细信息 if self.via == 'chargeCard': cardId = str(self.attachParas.get('cardId')) card = Card.objects(id = cardId).first() if card: rv.update({'cardNo': card.cardNo, 'cardType': card.cardType}) elif self.via == 'chargeVirtualCard': if 'type' not in self.attachParas: # 老模型在new的情况下, 是没有type字段的 cardNo = self.attachParas.get('cardNo', None) if cardNo: rv.update({'cardNo': cardNo}) cardId = self.attachParas.get('cardId', None) if cardId: cardTmpl = VirtualCard.objects(id = cardId).first() # type: VirtualCard if cardTmpl: rv.update({'cardName': cardTmpl.cardName}) else: if 'cardId' in self.attachParas: virtualCard = UserVirtualCard.objects( id = self.attachParas['cardId']).first() # type: UserVirtualCard rv.update({ 'cardNo': virtualCard.cardNo, 'cardName': virtualCard.cardName }) return rv @property def summary_info(self): rv = { 'id': str(self.id), 'createdTime': self.to_datetime_str(self.dateTimeAdded), 'money': self.money, 'coins': self.coins, 'groupName': self.groupName, 'groupNumber': self.groupNumber, 'address': self.address, 'logicalCode': self.logicalCode, 'devTypeName': self.dev_type_name, 'devTypeCode': self.dev_type_code, 'ownerId': self.ownerId, 'via': self.via } if self.via == 'chargeVirtualCard': rv.pop('coins') return rv @property def created_date(self): return self.dateTimeAdded @property def finished_time(self): if self.result != self.PayResult.SUCCESS: return '' if self.finishedTime: return self.to_datetime_str(self.finishedTime) return self.to_datetime_str(self.dateTimeAdded) @property def ledger_enable(self): return self.result in [self.PayResult.SUCCESS, self.PayResult.REFUNDING] @property def is_ledgered(self): """ 判断该订单是否已经 分账获取了收益 :return: """ if self.result != self.PayResult.SUCCESS: # 失败订单肯定是没有分账的 return False if self.isAllocatedCardMoney: return True # 没有该字段的订单 直接进行一次检查 看看有没有分账 from apps.web.common.proxy import ClientDealerIncomeModelProxy try: ledgerRecord = ClientDealerIncomeModelProxy.get_one(ref_id = self.id) except Exception as e: logger.exception(e) return False return bool(ledgerRecord) @property def notLedgerDesc(self): """ 没有分账的描述值 12小时 72小时分界线 :return: """ # 未超过 12 小时的情况 可以当成是订单尚未结束 if self.dateTimeAdded + datetime.timedelta(hours = 12) > datetime.datetime.now(): desc = u"订单尚未获取收益,可能设备正在运行" # 超过 12 小时但是 没有到 72 小时的情况 告知订单可能可能出现了某种问题 等待系统监测自动结账 elif self.dateTimeAdded + datetime.timedelta(hours = 72) > datetime.datetime.now(): desc = u"订单尚未获取收益,可能由于网络原因造成订单未关闭,系统将于{}小时后强制分账,请勿担心!".format( 72 - (datetime.datetime.now().hour - self.dateTimeAdded.hour)) # 超过72小时还没有分账的 问题单 else: desc = u"订单尚未获取收益,请联系平台运营人员进行处理" return desc @property def used_port(self): if self.attachParas and 'chargeIndex' in self.attachParas and self.attachParas['chargeIndex']: return str(self.attachParas['chargeIndex']) return '' @property def startKey(self): return self.attachParas.get('startKey', None) @property def refundOrders(self): return RefundMoneyRecord.objects.filter(rechargeObjId = self.id) @property def has_refund_order(self): if self.refundOrders: return True else: return False @property def is_temp_package(self): return 'isTempPackage' in self.attachParas and self.attachParas['isTempPackage'] is True @property def to_dict_obj(self): return RechargeRecordDict({ 'via': self.via, 'money': self.money, 'coins': self.coins, 'attachParas': self.attachParas, 'extraInfo': self.extraInfo, 'gateway': self.my_gateway, 'payAppType': self.pay_app_type, 'payGatewayKey': self.pay_gateway_key }) @classmethod def get_record(cls, order_no, dealer_id = None): # type: (str, str)->Optional[RechargeRecord] # TODO 数据库分片修改 # record = RechargeRecord.objects(ownerId = dealer_id, orderNo = order_no).first() record = cls.objects(orderNo = order_no).first() return record @classmethod def calc_count(cls, filter, group = None, limit = None, sort = None, allowDiskUse = False): # type: (dict, dict, int, dict, bool)->int pipeline = [] if filter: pipeline.append({ '$match': filter }) if group: pipeline.append({ '$group': group }) if limit: pipeline.append({ '$limit': limit }) if sort: pipeline.append({ '$sort': sort }) pipeline.append({'$count': 'count'}) result = cls.get_collection().aggregate(pipeline, allowDiskUse = allowDiskUse) return next(result, {}).get('count', 0) @classmethod def get_recharged_records(cls, **kwargs): return cls.objects(via = 'recharge', result = cls.PayResult.SUCCESS, **kwargs).order_by('-dateTimeAdded') @classmethod def get_card_recharged_records(cls, **kwargs): return cls.objects(via = 'chargeCard', result = cls.PayResult.SUCCESS, **kwargs).order_by('-dateTimeAdded') @classmethod def issue_pay_order(cls, context, gateway, **payload): payload.update({ "openId": context.user.openId, "nickname": context.user.nickname, 'gateway': gateway.gateway_type, 'payAppType': gateway.pay_app_type, 'payGatewayKey': gateway.gateway_key, 'withdrawSourceKey': gateway.withdraw_source_key() }) if gateway.pay_app_type in [PayAppType.WECHAT, PayAppType.ALIPAY]: payload['extraInfo'].update({ 'payOpenId': context.user.get_bound_pay_openid(gateway.bound_openid_key) }) if "result" not in payload: payload["result"] = cls.PayResult.UNPAY order = cls(payload) return order.save() @classmethod def issue_from_auto_sim_order(cls, owner, sim_recharge, device, group): # type:(Dealer, DealerRechargeRecord, Device, GroupDict)->RechargeRecord payload = { 'openId': owner.username, 'nickname': owner.nickname, 'orderNo': sim_recharge.orderNo, 'ownerId': sim_recharge.dealerId, 'money': -RMB(sim_recharge.totalFee/100), 'coins': VirtualCoin(0), 'subject': sim_recharge.subject, 'result': cls.PayResult.SUCCESS, 'via': RechargeRecordVia.AutoSim, 'finishedTime': sim_recharge.finishedTime, 'gateway': sim_recharge.my_gateway, 'payAppType': sim_recharge.pay_app_type, 'payGatewayKey': sim_recharge.pay_gateway_key, 'isAllocatedCardMoney': True, 'devNo': device.devNo, 'devType': device.devTypeName, 'devTypeName': device.devTypeName, 'devTypeCode': device.devTypeCode, 'logicalCode': device.logicalCode, 'groupId': device.groupId, 'address': group.address, 'groupNumber': device.groupNumber, 'groupName': group.groupName } record = cls(**payload).save() record.group = group return record @classmethod def viaText(cls, via, quickPay): if via == RechargeRecordVia.Balance: if quickPay: return u'快捷支付' else: return u'优惠充值' else: return RECHARGE_RECORD_VIA_TRANSLATION[via] @classmethod def gatewayText(cls, gateway): return APP_PLATFORM_TYPE_TRANSLATION.get(gateway, u'未知') @classmethod def from_feedback(cls, feedback, refundCoins): # type: (FeedBack, VirtualCoin)->RechargeRecord return cls( orderNo = str(uuid.uuid4()), coins = refundCoins, money = RMB(0), openId = feedback.openId, groupId = feedback.groupId, devNo = feedback.devNo, ownerId = feedback.ownerId, groupName = feedback.groupName, groupNumber = feedback.groupNumber, address = feedback.address, wxOrderNo = u'老板退币', devTypeName = feedback.devTypeName, nickname = feedback.nickname, result = cls.PayResult.SUCCESS, via = 'refund') @classmethod def issue_pay_record(cls, context, **payload): # type: (OrderBuilderContext, Dict)->RechargeRecord user = context.user payment_gateway = context.pay_gateway # type: PaymentGateway payload.update({ 'openId': user.openId, 'nickname': user.nickname, 'gateway': payment_gateway.gateway_type, 'payAppType': payment_gateway.pay_app_type, 'payGatewayKey': payment_gateway.gateway_key, 'withdrawSourceKey': payment_gateway.withdraw_source_key() }) if 'attachParas' not in payload: payload['attachParas'] = {} if 'extraInfo' not in payload: payload['extraInfo'] = {} if payment_gateway.pay_app_type in [PayAppType.WECHAT, PayAppType.ALIPAY]: payload['extraInfo'].update({ 'payOpenId': user.get_bound_pay_openid(payment_gateway.bound_openid_key) }) if "result" not in payload: payload['result'] = cls.PayResult.UNPAY record = cls(**payload) # type: RechargeRecord return record.save() def to_dict(self, json_safe=False): money = str(self.money) if json_safe else self.money return { 'ownerId': self.ownerId, 'nickname': self.nickname, 'money': money, 'gateway': self.my_gateway, 'via': self.via, } def to_json_dict(self): return self.to_dict(json_safe = True) def succeed(self, **kwargs): payload = { 'result': self.PayResult.SUCCESS } if kwargs: payload.update(kwargs) result = self.get_collection().update_one( filter = {'_id': ObjectId(self.id), 'result': {'$nin': [self.PayResult.SUCCESS, self.PayResult.CLOSE]}}, update = {'$set': payload}, upsert = False) return result.matched_count == 1 def close(self, **kwargs): payload = { 'result': self.PayResult.CLOSE } if kwargs: payload.update(kwargs) result = self.get_collection().update_one( filter = {'_id': ObjectId(self.id), 'result': {'$nin': [self.PayResult.SUCCESS, self.PayResult.CLOSE]}}, update = {'$set': payload}, upsert = False) return result.matched_count == 1 def fail(self, **kwargs): self.update(result = self.PayResult.FAILED, **kwargs) return self.reload() def cancel(self): payload = { 'result': self.PayResult.CANCEL, 'finishedTime': datetime.datetime.now() } result = self.get_collection().update_one( filter = {'_id': ObjectId(self.id), 'result': self.PayResult.UNPAY}, update = {'$set': payload}, upsert = False) return result.matched_count == 1 @property def is_success(self): return self.result == self.PayResult.SUCCESS @property def is_refunding(self): return self.result == self.PayResult.REFUNDING @property def is_fail(self): return self.result == self.PayResult.FAILED @property def is_cancel(self): return self.result == self.PayResult.CANCEL @property def is_close(self): return self.result == self.PayResult.CLOSE def to_detail(self): # type: ()->dict """ :return: """ return { 'id': self.id, 'rechargeTradeNo': self.orderNo, 'payResult': self.result, 'ownerId': self.ownerId, 'gateway': self.my_gateway, 'openId': self.openId, 'groupId': self.groupId, 'userNickname': self.nickname, 'orderAmount': self.money, 'coins': self.coins, 'devNo': self.devNo, 'logicalCode': self.logicalCode, 'devTypeName': self.dev_type_name, 'groupName': self.groupName, 'groupNumber': self.groupNumber, 'address': self.address, 'createdTime': self.dateTimeAdded, 'completionTime': self.finished_time, 'via': self.via, 'isQuickPay': self.isQuickPay, 'startKey': self.attachParas.get('startKey', None), # 以下字段和以前做兼容 'outTradeNo': self.orderNo, 'finishedTime': self.finished_time, 'gatewayTradeNo': self.wxOrderNo, 'result': self.result } def to_dict_for_super_admin(self): from apps.web.common.proxy import ClientConsumeModelProxy orderResult = '' isNormalDesc = '' via = '' if self.result == 'unPay': orderResult = u'未付款' elif self.result == 'failed': orderResult = u'充值失败' elif self.result == 'success': orderResult = u'充值成功' if self.via == '': via = u'充值金币' elif self.via == '': via = u'充值卡' if 'consumeRecordId' in self.attachParas: # 部分后支付的设备类型, 在支付的时候会塞入消费单的id consumeRcd = ClientConsumeModelProxy.get_one( shard_filter = {'ownerId': self.ownerId}, id = self.attachParas.get("consumeRecordId")) # type: ConsumeRecord elif 'startKey' in self.attachParas and self.attachParas['startKey']: consumeRcd = ClientConsumeModelProxy.get_one( shard_filter = {'ownerId': self.ownerId}, foreign_id = str(self.id), startKey = self.attachParas['startKey']) # type: ConsumeRecord else: consumeRcd = None if consumeRcd is not None: if consumeRcd.isNormal is True: isNormalDesc = u'消费成功' else: isNormalDesc = u'消费失败' return { 'wxOrderNo': self.wxOrderNo, 'orderNo': self.orderNo, 'orderResult': orderResult, 'rechargeUser': self.nickname + ' ' + self.openId, 'rechargeType': via, 'money': str(self.money), 'coins': str(self.coins), 'orderDetail': self.attachParas, 'orderCreateTime': self.dateTimeAdded.strftime('%Y-%m-%d %H:%M:%S'), 'logicalCode': self.logicalCode, 'devNo': self.devNo, 'devTypeCode': self.dev_type_code, 'consumeOrderNo': consumeRcd.orderNo if consumeRcd else '', 'consumeUser': consumeRcd.nickname + ' ' + consumeRcd.openId if consumeRcd else '', 'consumeCoins': str(consumeRcd.coin) if consumeRcd else '', 'consumeResult': isNormalDesc if consumeRcd else '', 'failedReason': consumeRcd.errorDesc if consumeRcd else '', 'consumeOrderCreateTime': consumeRcd.dateTimeAdded.strftime('%Y-%m-%d %H:%M:%S') if consumeRcd else '', 'consumeDetail': consumeRcd.attachParas if consumeRcd else {}, 'consumeDict': consumeRcd.servicedInfo if consumeRcd else {} } def set_ledgered(self): try: modified = self.update(isAllocatedCardMoney = True) if not modified: logger.error('failed to update isAllocatedCardMoney for record=%r' % (self,)) except Exception as e: logger.exception(e) def new_refund_cash_order(self, refund_order): # type: (RefundMoneyRecord)->RechargeRecord payload = {} for field in self.__class__._fields_ordered: if field in ['id', 'orderNo', 'wxOrderNo', 'transactionId', 'dateTimeAdded', 'ledgerStatus', 'finishedTime', 'notifiedTime', 'operator', 'devTypeName', 'money', 'coins', 'selectedQuickPayPackageId', 'devType', 'attachParas', 'isAllocatedCardMoney', 'subject']: continue payload.update({ field: getattr(self.payOrder, field) }) payload.update({ 'orderNo': refund_order.orderNo, 'money': -refund_order.money, 'coins': -refund_order.coins, 'devTypeName': self.dev_type_name, 'via': RechargeRecordVia.RefundCash, 'isAllocatedCardMoney': False, 'gateway': self.my_gateway, 'payGatewayKey': self.pay_gateway_key, 'payAppType': self.pay_app_type, 'withdrawSourceKey': self.withdraw_source_key, 'result': self.PayResult.REFUNDING, 'extraInfo': { 'refPay': {'objId': str(self.id)} }, 'subject': '{} {}'.format(self.subject, u'退费') }) refund_order_record = self.__class__(**payload).save() # type: RechargeRecord refund_order.refund_order_record = refund_order_record self.update(push__extraInfo__refRefund = { 'objId': str(refund_order_record.id), 'money': refund_order.money.mongo_amount, 'coins': refund_order.coins.mongo_amount }) return refund_order_record def is_refund_available(self, customer): # type:(CapitalUser) -> bool if customer.role == ROLE.dealer: if self.ownerId != str(customer.id): logger.warning('is not my order. {} != {}'.format(self.ownerId, str(customer.id))) return False if self.via not in [USER_RECHARGE_TYPE.RECHARGE, USER_RECHARGE_TYPE.RECHARGE_CASH]: return False return True return False class Dispute(EmbeddedDocument): feedback_id = ObjectIdField() class CardRechargeOrder(Searchable): """ 卡充值订单 """ orderNo = StringField(verbose_name=u"订单号", default="") cardId = StringField(verbose_name=u"实体卡ID", default="") cardNo = StringField(verbose_name=u"实体卡NO", default="") # 卡绑定的发行地址 groupId = StringField(verbose_name=u"设备地址编号", default="") address = StringField(verbose_name=u"设备地址", default="") groupName = StringField(verbose_name=u"交易场地", default="") openId = StringField(verbose_name=u"openId", default="") money = MonetaryField(verbose_name=u"付款金额", default=RMB('0.00')) coins = MonetaryField(verbose_name=u"充值金额", default=RMB('0.00')) remarks = StringField(verbose_name=u"备注", default="") dateTimeAdded = DateTimeField(default=datetime.datetime.now, verbose_name=u'生成时间') sequanceNo = StringField(verbose_name=u"交易流水号", default="") status = StringField(verbose_name=u"状态", default="") rechargeType = StringField(verbose_name=u"充值类型,一般分网络充值、线下设备充值", default="") # netpay/device dealerId = StringField(verbose_name=u'订单ownerId, 集群需要', default=None) rechargeNo = ObjectIdField(verbose_name=u"订单ID") operationLog = ListField(verbose_name=u'历史处理结果', default=[]) processingLog = DictField(verbose_name=u'') meta = { "collection": "cardRechargeOrder", "db_alias": "default", } def __repr__(self): return '' % (str(self.id), self.orderNo, self.cardNo) @cached_property def rechargeOrder(self): return RechargeRecord.objects.filter(id=self.rechargeNo).first() @property def chargeAmount(self): return self.money @property def bestowAmount(self): return RMB(self.coins) - RMB(self.money) # 获取卡的订单,但是没有支付的 @staticmethod def get_last_to_do_one(cardId): obj = CardRechargeOrder.objects( cardId=cardId, status='finishedPay', rechargeType__in=["netpay", "sendCoin"] ).order_by('-dateTimeAdded').first() return obj # 获取卡的订单,但是没有支付的 @staticmethod def get_to_do_list(cardId): objs = CardRechargeOrder.objects.filter( cardId=cardId, status='finishedPay', rechargeType__in=['netpay', "sendCoin"] ) money, coins = RMB(0), RMB(0) orderNos = [] cardOrderNos = [] for obj in objs: money += obj.money coins += obj.coins orderNos.append(obj.rechargeNo) cardOrderNos.append(obj.orderNo) return money, coins, orderNos, cardOrderNos # 更新卡的为到账订单,更新为已经支付 @staticmethod def update_card_order_has_finished(cardId): CardRechargeOrder.get_collection().update( {'cardId': cardId, 'status': 'finishedPay', 'rechargeType': {"$in": ["netpay", "sendCoin"]}}, {'$set': {'status': 'finished'}}, multi = True ) def init_processing_log(self, device, sendMoney, preBalance): # type: (DeviceDict, RMB, RMB)->CardRechargeOrder self.processingLog = { 'devNo': device.devNo, 'logicalCode': device.logicalCode, 'sendMoney': sendMoney.mongo_amount, 'preBalance': preBalance.mongo_amount, 'code': device.get('devType', {}).get('code', ''), 'time': datetime.datetime.now() } # return self.save() def update_after_recharge_ic_card(self, device, sendMoney, preBalance, result = ErrorCode.SUCCESS, description = '', syncBalance = None): # type: (DeviceDict, RMB, RMB, int, basestring, Optional[RMB])->CardRechargeOrder log = { 'devNo': device.devNo, 'logicalCode': device.logicalCode, 'sendMoney': sendMoney.mongo_amount, 'preBalance': preBalance.mongo_amount, 'code': device.get('devType', {}).get('code', ''), 'result': result, 'description': description, 'time': datetime.datetime.now() } if syncBalance: log['syncBalance'] = syncBalance.mongo_amount self.operationLog.append(log) if result == ErrorCode.SUCCESS: self.status = 'finished' return self.save() def update_after_recharge_id_card(self, device, balance, preBalance): # type: (Optional[DeviceDict], RMB, RMB)->CardRechargeOrder if device: log = { 'devNo': device.devNo, 'logicalCode': device.logicalCode, 'balance': balance.mongo_amount, 'preBalance': preBalance.mongo_amount, 'code': device.get('devType', {}).get('code', ''), 'time': datetime.datetime.now() } else: log = { 'balance': balance.mongo_amount, 'preBalance': preBalance.mongo_amount, 'time': datetime.datetime.now() } self.operationLog.append(log) self.status = 'finished' return self.save() @staticmethod def new_one(openId, cardId, cardNo, money, coins, group, rechargeId, rechargeType = 'netpay'): # type:(str, str, str, RMB, RMB, GroupDict, ObjectId, str)->CardRechargeOrder newRcd = CardRechargeOrder( orderNo = OrderNoMaker.make_order_no_32(identifier = cardNo, main_type = OrderMainType.PAY, sub_type = UserPaySubType.CARD_RECHARGE_RECORD), cardId = str(cardId), cardNo = cardNo, openId = openId, money = money, coins = RMB(coins.amount), status = 'finishedPay', rechargeType = rechargeType, dealerId = group.ownerId, groupId = group.groupId, address = group.address, groupName = group.groupName, rechargeNo = rechargeId ) logger.info('make card recharge order = %s' % repr(newRcd)) try: return newRcd.save() except Exception as e: logger.exception('save cardId = %s; cardNo = %s; recharge error = %s' % (cardId, cardNo, e)) return None def to_detail(self): # type: ()->dict """ :return: """ return { 'orderNo': self.orderNo, 'cardNo': self.cardNo, 'openId': self.openId, 'groupId': self.groupId, 'amount': self.money, 'devNo': '', 'logicalCode': '', 'devType': '', 'groupNumber': '', 'groupName': self.groupName, 'address': self.address } @classmethod def get_by_rechargeRecord(cls, record): # type:(RechargeRecord)->CardRechargeOrder return cls.objects(rechargeNo = record.id).first() class CardRechargeRecord(Searchable): orderNo = StringField(verbose_name = "关联订单号", default = "") # 关联订单处理卡退费 cardId = StringField(verbose_name = "实体卡ID", default = "") cardNo = StringField(verbose_name = "实体卡号", default = "") openId = StringField(verbose_name = "openId", default = "") ownerId = ObjectIdField(verbose_name = "dealerId") money = MonetaryField(verbose_name = "付款金额", default = RMB('0.00')) coins = MonetaryField(verbose_name = "充值金额", default = VirtualCoin('0.00')) chargeBalance = MonetaryField(verbose_name="充值余额", default=RMB(0)) bestowBalance = MonetaryField(verbose_name="赠送余额", default=RMB(0)) preChargeBalance = MonetaryField(verbose_name="之前充值余额", default=RMB(0)) preBestowBalance = MonetaryField(verbose_name="之前赠送余额", default=RMB(0)) devNo = StringField(verbose_name = "设备ID", default = "") devTypeCode = StringField(verbose_name = "设备类型编码", default = "") logicalCode = StringField(verbose_name = "设备逻辑编码", default = "") groupId = StringField(verbose_name = "设备地址编号", default="") address = StringField(verbose_name = "设备地址", default = "") groupNumber = StringField(verbose_name = "设备", default = "") groupName = StringField(verbose_name = "交易场地", default = "") #: 在部分场合,设备会失败启动,但这时应该添加该字段为False方便追溯 remarks = StringField(verbose_name="备注", default="") dateTimeAdded = DateTimeField(default=datetime.datetime.now, verbose_name='生成时间') sequanceNo = StringField(verbose_name="交易流水号,有些刷卡的会上报", default="") status = StringField(verbose_name="状态", default="") rechargeType = StringField(verbose_name="充值类型,一般分网络充值、线下设备充值", default="") meta = { "collection": "cardRechargeRecord", "db_alias": "default", # "shard_key":('cardId',), } @property def amount(self): return self.money @property def balance(self): return self.chargeBalance + self.bestowBalance @property def preBalance(self): return self.preChargeBalance + self.preBestowBalance @property def dealer(self): return Dealer.objects(id=self.ownerId).get() @staticmethod def make_no(): timestamp = generate_timestamp_ex() random_int = random.randint(1, 1000) return "%d%03d" % (timestamp, random_int) @classmethod def add_self_card_record(cls, cardId, group, money, device): newRcd = CardRechargeRecord( cardId = cardId, ownerId = ObjectId(group.ownerId), money = money, coins = money, groupId = group.groupId, address = group.address, groupName = group.groupName, status = 'success', rechargeType = 'dealer_oper', devNo = device.devNo, devTypeCode = device.get('devType', {}).get('code', ''), logicalCode = device.logicalCode, groupNumber = device.groupNumber ) try: return newRcd.save() except Exception as e: logger.exception('save card consume rcd error=%s' % e) @staticmethod def add_record(card, group, order, device=None): # type:(Card, GroupDict, CardRechargeOrder, Optional[DeviceDict])->CardRechargeRecord newRcd = CardRechargeRecord( cardId=str(card.id), cardNo=card.cardNo, openId=card.openId, ownerId=ObjectId(group.ownerId), money=order.money, coins=order.coins, chargeBalance=order.chargeAmount + card.chargeBalance, bestowBalance=order.bestowAmount + card.bestowBalance, preChargeBalance=card.chargeBalance, preBestowBalance=card.bestowBalance, groupId=group.groupId, address=group.address, groupName=group.groupName, status='success', rechargeType=order.rechargeType ) if device: newRcd.devNo = device.devNo newRcd.logicalCode = device.logicalCode newRcd.devTypeCode = device.devTypeCode newRcd.groupNumber = device.groupNumber try: return newRcd.save() except Exception as e: logger.exception('save card consume rcd error=%s' % e) class CardConsumeRecord(Searchable): orderNo = StringField(verbose_name = "订单号", default = "") openId = StringField(verbose_name = "微信ID", default = "") cardId = StringField(verbose_name = "实体卡ID", default = "") money = MonetaryField(verbose_name = "消耗的钱(硬币)", default = RMB('0.00')) balance = MonetaryField(verbose_name = "当前余额(硬币)", default = RMB('0.00')) devNo = StringField(verbose_name = "设备ID", default = "") devType = StringField(verbose_name = "设备类型", default = "") logicalCode = StringField(verbose_name = "设备逻辑编码", default = "") groupId = StringField(verbose_name = "设备地址编号", default = "") address = StringField(verbose_name = "设备地址", default = "") groupNumber = StringField(verbose_name = "设备", default = "") groupName = StringField(verbose_name = "交易场地", default = "") result = StringField(verbose_name = "消费结果", default = "") remarks = StringField(verbose_name = "备注", default = "") desc = StringField(verbose_name = "描述", default = "") dateTimeAdded = DateTimeField(verbose_name = '生成时间', default = datetime.datetime.now) finishedTime = DateTimeField(verbose_name = '结束时间', default = datetime.datetime.now) servicedInfo = DictField(verbose_name = '服务内容', default = {}) sid = StringField(verbose_name = "从设备上获取的流水号", default = "") linkedConsumeRcdOrderNo = StringField(verbose_name = u'关联的消费订单号.发布后去掉, 两者一致', default = '') meta = { "collection": "CardConsumeRecord", "db_alias": "default", # "shard_key":('orderNo',), } @staticmethod def make_no(): timestamp = generate_timestamp_ex() random_int = random.randint(1, 1000) return "%d%03d" % (timestamp, random_int) class ServiceProgress(Searchable): open_id = StringField(verbose_name = 'open id', default = '') device_imei = StringField(verbose_name = 'device imei', default = '') port = IntField(verbose_name = 'port', default = 0) isFinished = BooleanField(verbose_name = 'isFinished', default = False) devTypeCode = StringField(verbose_name = 'devTypeCode', default = '') cardId = StringField(verbose_name = 'cardNo', default = '') attachParas = DictField(verbose_name = 'attachParas', default = {}) consumeOrder = DictField(verbose_name = 'consumeOrder', default = {}) finished_time = IntField(verbose_name = 'finished time', default = 0) start_time = IntField(verbose_name = 'start_time', default = 0) consumes = ListField(verbose_name = "对应的所有消费记录单号", default = []) # 应该用一个状态表示服务的执行情况,包括等待服务、服务正在进行、服务结束。只有我们才需要此字段 # 但是只有我们自己的主板支持每个订单的执行情况反馈以及查询,所以没有办法,以前都是裹在一起,现在分开逻辑更清晰。 status = StringField(verbose_name = u'执行状态', default = 'running') # waiting/working/finished/failed weifuleOrderNo = StringField(verbose_name = u'微付乐订单编号,设备也会产生订单', default = '') runningTime = DateTimeField(verbose_name = u'开始运行的时间') datetimeAdded = DateTimeField(verbose_name = "时间", default = datetime.datetime.now) expireAt = DateTimeField(verbose_name = u"TTL到期时间", default = None) meta = { 'collection': 'service_progress', 'db_alias': 'logdata' } @property def used_port(self): """ 兼容性属性. 以前是整形, 后面全部为字符串型 :return: """ return str(self.port) @property def order_no(self): return self.attachParas.get('orderNo', None) # 更新progress,以及关联的消费日志记录。这个函数一般是服务结束的时候调用,以刷新progress,有时候,结束时 # 才扣费的场景,扣费的时候会直接把使用后的消耗,记录到消费日志中,就不需要刷新消费日志 @staticmethod def update_progress_and_consume_rcd(ownerId, queryDict, consumeDict, updateConsume = True, progressDict = None): progressDict = { 'isFinished': True, 'finished_time': int(time.time()) } if progressDict is None else progressDict if 'isFinished' in progressDict and progressDict['isFinished']: progressDict.update({'expireAt': datetime.datetime.now()}) rcds = ServiceProgress.get_collection().find(queryDict).sort('start_time') if rcds.count() == 0: logger.error('can not find the progress info') return orderIdList, orderNoList, cardOrderNoList = [], [], [] for rcd in rcds: # type: Dict consumeOrder = rcd.get('consumeOrder', {}) if 'consumeRecordId' in consumeOrder and consumeOrder['consumeRecordId']: orderIdList.append(consumeOrder['consumeRecordId']) elif 'orderNo' in consumeOrder and consumeOrder['orderNo']: orderNoList.append(consumeOrder['orderNo']) if 'cardOrderNo' in consumeOrder: cardOrderNoList.append(consumeOrder['cardOrderNo']) # 更新进程的数据 try: ServiceProgress.get_collection().update(queryDict, {'$set': progressDict}, multi = True) except Exception, e: logger.exception('update service progress e=%s' % e) if not updateConsume: # 不需要更新就直接返回 return # 更新订单中的时间以及服务信息。如果是卡的信息,因为找不到order,会直接pass # 如果是多条消费单,但是最终只有一条结束事件的场景,统计以及消费信息只记录到第一单上 isFinished = progressDict and progressDict.get('isFinished', False) consume_orders = [] for orderNo in orderNoList: rcd = ConsumeRecord.objects(ownerId = ownerId, orderNo = orderNo).first() # type: ConsumeRecord consume_orders.append(rcd) for orderId in orderIdList: rcd = ConsumeRecord.objects(ownerId = ownerId, id = orderId).first() # type: ConsumeRecord consume_orders.append(rcd) count = 0 uart_data = consumeDict.pop('uartData', None) for rcd in consume_orders: # type: ConsumeRecord try: count += 1 pre_aggInfo = rcd.aggInfo # 如果是结束更新,相应的信息只需要更新到第一条中。非结束的,全部都刷新。 if not isFinished: updated = rcd.update( finishedTime = datetime.datetime.now(), servicedInfo = consumeDict, status = ConsumeRecord.Status.FINISHED) elif count == 1: if uart_data: updated = rcd.update( finishedTime = datetime.datetime.now(), servicedInfo = consumeDict, status = ConsumeRecord.Status.FINISHED, attachParas__uartData = uart_data) else: updated = rcd.update( finishedTime = datetime.datetime.now(), servicedInfo = consumeDict, status = ConsumeRecord.Status.FINISHED) else: continue if not updated: logger.error('%r updated failed' % (rcd,)) if isFinished and count == 1: # 只需要汇总一次使用的数据即可 try: valueDict = {} for kind, value in consumeDict.items(): if kind in DEALER_CONSUMPTION_AGG_KIND.choices(): if kind in pre_aggInfo: #: 会在发起service阶段更新一次报表 #: 此处为结束时更新, 应该更新新数据 - 初始更新的数据 valueDict[kind] = float(str(value)) - float(str(pre_aggInfo[kind])) else: valueDict[kind] = value status = rcd.update_agg_info(valueDict) if status: record_consumption_stats(rcd) else: logger.error( '[update_progress_and_consume_rcd] failed to update_agg_info record=%r' % (rcd,)) except Exception, e: logger.exception('update agg info error=%s' % e) except Exception as e: logger.exception('update consume rcd e=%s' % e) continue for orderNo in cardOrderNoList: if orderNo == '': continue try: CardConsumeRecord.get_collection().update_one({'orderNo': orderNo}, { '$set': {'finished_time': int(time.time()), 'servicedInfo': consumeDict}}) except Exception, e: logger.exception('update consume rcd e=%s' % e) continue @staticmethod def register_card_service(dev, port, card, consumeOrder = None, finishedTime = None): # type:(DeviceDict, int, Card, dict, int)->None consumeOrder = {} if consumeOrder is None else consumeOrder finishedTime = int(time.time()) + 24 * 60 * 60 if finishedTime is None else finishedTime ServiceProgress.get_collection().insert({ 'device_imei': dev.devNo, 'devTypeCode': dev.devTypeCode, 'cardId': str(card.id), 'port': port, 'finished_time': finishedTime, 'start_time': int(time.time()), 'open_id': card.openId, 'consumeOrder': consumeOrder, 'isFinished': False, 'attachParas': {}, 'expireAt': datetime.datetime.now() + datetime.timedelta(days = 91) }) @staticmethod def register_card_service_for_weifule(dev, port, card, consumeOrder): ServiceProgress.get_collection().insert({ 'device_imei': dev['devNo'], 'devTypeCode': dev['devType']['code'], 'cardId': str(card.id), 'port': port, 'finished_time': int(time.time()) + 24 * 60 * 60, 'start_time': int(time.time()), 'open_id': card.openId, 'consumeOrder': consumeOrder, 'isFinished': False, 'attachParas': {}, 'status': 'waiting', 'weifuleOrderNo': consumeOrder['orderNo'], 'expireAt': datetime.datetime.now() + datetime.timedelta(days = 91) }) @classmethod def new_progress_for_order(cls, order, device, cache_info): # type:(ConsumeRecord, DeviceDict, dict)->None try: consumeOrder = { 'consumeRecordId': str(order.id), 'orderNo': order.orderNo, 'coin': VirtualCoin(cache_info['coins']).mongo_amount, 'consumeType': cache_info.get('consumeType') or ( 'mobile_vcard' if u'虚拟卡' in order.remarks else 'mobile') } if 'money' in cache_info: consumeOrder.update({'money': RMB(cache_info['money']).mongo_amount}) if 'unit' in cache_info: consumeOrder.update({'unit': cache_info['unit']}) if 'subOrderNos' in cache_info: consumeOrder.update({'subOrderNos': cache_info['subOrderNos']}) if 'needKind' in cache_info and 'needValue' in cache_info: consumeOrder.update({ cache_info['needKind']: cache_info['needValue'] }) if 'cardId' in cache_info: consumeOrder.update({'cardId': cache_info['cardId']}) update_dict = { 'open_id': cache_info['openId'], 'device_imei': device.devNo, 'devTypeCode': device['devType']['code'], 'port': order.used_port, 'attachParas': order.attachParas, 'start_time': int( Arrow.fromdatetime(order.startTime, tzinfo = settings.TIME_ZONE).timestamp), 'finished_time': int(cache_info['estimatedTs']), 'consumeOrder': consumeOrder, 'isFinished': False, 'expireAt': datetime.datetime.now() + datetime.timedelta(days = 91) } cls.objects(open_id = order.openId, device_imei = device.devNo, port = order.used_port).update_one(upsert = True, **update_dict) except Exception as e: logger.exception(e) @classmethod def finish_progress_for_order(cls, order, device): # type:(ConsumeRecord, DeviceDict)->None cls.get_collection().update_one(filter = { 'open_id': order.openId, 'device_imei': device.devNo, 'port': int(order.used_port), 'consumeOrder.consumeRecordId': str(order.id) }, update = { '$set': { 'isFinished': True, 'finished_time': Arrow.fromdatetime(order.finishedTime, tzinfo = settings.TIME_ZONE).timestamp, 'expireAt': datetime.datetime.now() } }, upsert = False) class Card(Searchable): cardNo = StringField(verbose_name="卡号", default="") cardType = StringField(verbose_name="卡类型", default="") openId = StringField(verbose_name="绑定的微信", default="") nickName = StringField(verbose_name="用户昵称", default="") productAgentId = StringField(verbose_name="当前用户绑定的平台代理商", default = "") cardName = StringField(verbose_name="持卡人姓名", default="") phone = StringField(verbose_name="手机号码", default="") # 绑定的管理APP信息 managerialAppId = StringField(verbose_name="管理公众号AppId", default="") managerialOpenId = StringField(verbose_name="管理openId", default="") dealerId = StringField(verbose_name="经销商ID", default="") groupId = StringField(verbose_name="设备组ID", default="") agentId = StringField(verbose_name="代理商ID", default="") # 以下信息为卡固有信息 remarks = StringField(verbose_name="备注", default="") chargeBalance = MonetaryField(verbose_name="充值余额", default=RMB(0)) bestowBalance = MonetaryField(verbose_name=u"赠送余额", default=RMB(0)) isHaveBalance = BooleanField(verbose_name="是否能够获取到卡的余额数据", default=True) lastMaxBalance = MonetaryField(verbose_name="最后一次充卡金额", default=RMB('0.00')) boundVirtualCardId = ObjectIdField(verbose_name=u"绑定的虚拟卡券ID") showBalance = MonetaryField(verbose_name=u"显示到设备上的卡余额,只有微付乐才会用此字段", default=RMB('0.00')) # 最近一次刷卡的设备 devNo = StringField(verbose_name="设备IMEI", default="") devTypeCode = StringField(verbose_name="设备类型代码", default="") frozen = BooleanField(verbose_name="卡挂失状态", default=False) status = StringField(verbose_name="卡状态", default='active') dateTimeAdded = DateTimeField(verbose_name=u"添加卡的时间", default=datetime.datetime.now) attachParas = DictField(verbose_name='附加参数', default={}) ongoingList = ListField(field=DictField(), verbose=u'冻结的金额') search_fields = ('cardNo', 'cardName', 'phone') meta = { "collection": "recharge_card", "db_alias": "default", } def __repr__(self): return ''.format(str(self.id), self.cardNo) @cached_property def group(self): if not self.groupId: return None return Group.get_group(self.groupId) @cached_property def user(self): if not self.isBinded: return None return MyUser.objects.filter(openId=self.openId, groupId=self.groupId).first() @property def balance(self): return self.chargeBalance + self.bestowBalance @property def nickname(self): return self.nickName @property def is_id_card(self): # type: ()->bool return self.cardType == RECHARGE_CARD_TYPE.ID @property def is_ic_card(self): # type: ()->bool return self.cardType == RECHARGE_CARD_TYPE.IC @property def isBinded(self): """ 用户的绑定状态 """ return True if self.openId and self.openId != Const.DEFAULT_CARD_OPENID else False def recharge(self, money, bestowMoney): assert isinstance(money, RMB), 'coins had to be VirtualCoin' assert isinstance(bestowMoney, RMB), 'money has to be RMB' updated = self.update( inc__chargeBalance=money, inc__bestowBalance=bestowMoney, ) if not updated: raise PostPayOrderError(u'余额和累计充值更新失败') return self.reload() def account_consume(self, order): # type:(ConsumeRecord) -> None self.reload() payment = order.payment # 获取支付的钱 consumeAmount = payment.actualAmount consumeBestowAmount = payment.totalAmount - consumeAmount # 获取当前的余额 CardBalanceLog.consume( self, afterAmount=self.chargeBalance, afterBestowAmount=self.bestowBalance, consumeAmount=consumeAmount, consumeBestowAmount=consumeBestowAmount, order=order ) def account_refund(self, order): # type:(ConsumeRecord) -> None self.reload() refund = order.refund # 获取支付的钱 refundAmount = refund.actualAmount refundBestowAmount = refund.totalAmount - refundAmount CardBalanceLog.refund( self, afterAmount=self.chargeBalance, afterBestowAmount=self.bestowBalance, refundAmount=refundAmount, refundBestowAmount=refundBestowAmount, order=order ) def account_recharge(self, order): # type:(RechargeRecord) -> None # 获取支付的钱 chargeAmount = order.chargeAmount bestowAmount = order.bestowAmount CardBalanceLog.recharge( self, afterAmount=self.chargeBalance, afterBestowAmount=self.bestowBalance, chargeAmount=chargeAmount, chargeBestowAmount=bestowAmount, order=order ) @classmethod def fake_one(cls, groupId, cardId='fake_id', cardNo='fake'): return cls(id=cardId, cardNo=cardNo, openId='', productAgentId='', groupId=groupId) @staticmethod def get_card_status(cardId): status = serviceCache.get(cardId, 'idle') return status @staticmethod def set_card_status(cardId, status): serviceCache.set(cardId, status, 10) @staticmethod def get_dev_cur_card(devNo): return serviceCache.get('%s_cardId' % devNo, None) @staticmethod def set_dev_cur_card(devNo, cardInfo): serviceCache.set('%s_cardId' % devNo, cardInfo, 90) @property def bound_virtual_card(self): # type: ()->Optional[UserVirtualCard] if self.boundVirtualCardId: now = datetime.datetime.now() virtual_card = UserVirtualCard.objects(id = self.boundVirtualCardId, expiredTime__gte = now, startTime__lte = now).first() return virtual_card else: return None def bind_virtual_card(self, card): # type:(UserVirtualCard)->None return self.update(boundVirtualCardId = card.id) def unbind_virtual_card(self, card): # type:(UserVirtualCard)->None """ 以后也许会支持多张卡 :param card: :return: """ return self.update(boundVirtualCardId = None) # 这个那种有余额的卡,才会调用 @staticmethod def update_balance(cardId, balance): card = Card.objects(id=cardId).get() result = card.update(balance = balance) if not result: logger.error('update error, cardId=%s, balance=%s' % (cardId, balance)) return result @staticmethod def check_card_no(cardNo): """ 检查实体卡卡号的合法性 :param cardNo: :return: """ if CARD_NO_RE.match(cardNo): return True return False @classmethod def check_swap_card_no(cls, cardNo, dealerId, agentId): try: card = cls.objects.get(cardNo=cardNo, agentId=agentId) except DoesNotExist: return True, "" if card.dealerId and card.dealerId != dealerId: return False, u"该卡已经被其他经销商绑定,请确认卡号无误" if card.openId: return False, u"该卡号已经被其他用户绑定,请确认卡号无误" if CardConsumeRecord.objects.filter(cardId=str(card.id)): return False, u"该卡号存在用户使用记录, 请确认卡号无误" card.delete() return True, "" @staticmethod def record_dev_card_no(devNo, cardNo): serviceCache.set('%s-cardno' % devNo, cardNo, 2 * 60) @staticmethod def get_dev_card_no(devNo): return serviceCache.get('%s-cardno' % devNo, None) @staticmethod def clear_dev_card_no(devNo): serviceCache.delete('%s-cardno' % devNo) def to_dict(self): data = { "cardId": str(self.id), "cardNo": self.cardNo, "cardName": self.cardName, "phone": self.phone, "cardType": self.cardType, "remarks": self.remarks, "frozen": self.frozen, "balance": self.balance, "lastMaxBalance": self.lastMaxBalance, "bindStatus": self.isBinded, } return data def freeze_transaction_id(self, freezeType): return '{}_{}'.format(freezeType, str(self.id)) @classmethod def freeze_balance(cls, transaction_id, payment): bulker = BulkHandlerEx(cls.get_collection()) # type: BulkHandlerEx chargeBalanceField = cls.chargeBalance.name bestowBalanceField = cls.bestowBalance.name for deduct in payment.deduct_list: query = { '_id': ObjectId(deduct['id']), 'ongoingList.transaction_id': { '$ne': transaction_id } } update = { '$inc': { chargeBalanceField: (-RMB(deduct[chargeBalanceField])).mongo_amount, bestowBalanceField: (-RMB(deduct[bestowBalanceField])).mongo_amount }, '$addToSet': { 'ongoingList': { 'transaction_id': transaction_id, chargeBalanceField: deduct[chargeBalanceField], bestowBalanceField: deduct[bestowBalanceField] } } } bulker.update(query, update) result = bulker.execute() logger.debug(result['info']) if result['success'] == 0: raise ServiceException({'result': 0, 'description': u"扣款失败(1001)"}) else: if len(result['info']['writeErrors']) != 0: raise ServiceException({'result': 0, 'description': u"扣款失败(1002)"}) else: return True @classmethod def clear_frozen_balance(cls, transaction_id, refund): try: bulker = BulkHandlerEx(cls.get_collection()) # type: BulkHandlerEx chargeBalanceField = cls.chargeBalance.name bestowBalanceField = cls.bestowBalance.name for deduct in refund.deduct_list: query = { '_id': ObjectId(deduct['id']), 'ongoingList': { '$elemMatch': { 'transaction_id': transaction_id } } } update = { '$inc': { chargeBalanceField: deduct[chargeBalanceField], bestowBalanceField: deduct[bestowBalanceField], }, '$pull': { 'ongoingList': { 'transaction_id': transaction_id } } } bulker.update(query, update) result = bulker.execute() logger.debug(result['info']) if result['success'] == 0: return False else: if len(result['info']['writeErrors']) != 0: return False else: return True except Exception as e: logger.exception(e) return False @classmethod def recover_frozen_balance(cls, transaction_id, deduct_list): bulker = BulkHandlerEx(cls.get_collection()) # type: BulkHandlerEx for deduct in deduct_list: query = { '_id': ObjectId(deduct['id']), 'ongoingList': { '$elemMatch': {'transaction_id': transaction_id}}} update = { '$inc': { 'balance': deduct['coins'] }, '$pull': { 'ongoingList': { 'transaction_id': transaction_id, 'frozenCoins': deduct['coins'] }} } bulker.update(query, update) result = bulker.execute() if result['success'] == 0: logger.error(result['info']) return False else: if len(result['info']['writeErrors']) != 0: logger.error(result['info']) return False else: return True def clear_card(self): return self.update( cardType = '', openId = '', nickName = '', productAgentId = '', cardName = '', phone = '', managerialAppId = '', managerialOpenId = '', dealerId = '', groupId = '', remarks = '', isHaveBalance = True, lastMaxBalance = RMB('0.0'), devNo = '', devTypeCode = '', frozen = False, status = 'active', boundVirtualCardId = None, attachParas = {} ) class RefundMoneyRecord(RefundOrderBase): """ 用户退款记录 """ refIncomeOrder = ObjectIdField(verbose_name = u'对应收益单', default = None) coins = MonetaryField(verbose_name = u"清理用户金币数", default = None) meta = { "collection": "RefundMoneyRecord", "db_alias": "default", } @classmethod def issue(cls, order, refundCash, **extraInfo): # type:(RechargeRecord, RMB, dict)->RefundMoneyRecord extraInfo.update({'v': 2}) if order.via in [USER_RECHARGE_TYPE.RECHARGE_CARD, USER_RECHARGE_TYPE.RECHARGE_VIRTUAL_CARD]: identifier = order.attachParas['cardNo'] elif order.via in [USER_RECHARGE_TYPE.RECHARGE_MONTHLY_PACKAGE]: identifier = order.attachParas.get('cardId') else: identifier = order.logicalCode refund_order_no = OrderNoMaker.make_order_no_32( identifier = identifier, main_type = OrderMainType.REFUND, sub_type = RefundSubType.REFUND) return cls( rechargeObjId = order.id, # refundSeq=next_seq, orderNo = refund_order_no, money = refundCash, status = cls.Status.PROCESSING, datetimeAdded = datetime.datetime.now(), extraInfo = dict_field_with_money(extraInfo), payAppType = order.pay_app_type ).save() @property def pay_sub_order(self): # type: ()->RechargeRecord if not hasattr(self, '__pay_sub_order__'): from apps.web.common.proxy import ClientRechargeModelProxy pay_order = ClientRechargeModelProxy.get_one(id = str(self.rechargeObjId)) # type: RechargeRecord setattr(self, '__pay_sub_order__', pay_order) return getattr(self, '__pay_sub_order__') @pay_sub_order.setter def pay_sub_order(self, order): setattr(self, '__pay_sub_order__', order) @property def pay_app_type(self): if self.payAppType: return self.payAppType else: return self.pay_sub_order.pay_app_type @property def refund_income_order(self): if not hasattr(self, '__refund_income_order__'): if self.refIncomeOrder: order = RechargeRecord.objects( id = self.refIncomeOrder).first() else: # 对老的方式的兼容 order = RechargeRecord.objects( id = self.pay_sub_order.extraInfo['refRefund'][0]['objId']).first() setattr(self, '__refund_income_order__', order) return getattr(self, '__refund_income_order__') @refund_income_order.setter def refund_income_order(self, obj): setattr(self, '__refund_income_order__', obj) @property def user(self): # type: ()->MyUser return self.pay_sub_order.user @property def notify_url(self): if self.pay_app_type in [PayAppType.WECHAT]: return REFUND_NOTIFY_URL.WECHAT_REFUND_BACK else: return None @property def checkWallet(self): return self.extraInfo.get('checkWallet', False) @property def operatorId(self): return self.extraInfo.get('operatorId', None) @property def is_new_version(self): return self.extraInfo.get('v', 1) > 1 @property def deductCoins(self): """ 兼容以前数据.退现金同时被扣费的钱包余额。后续需要建模为被扣的描述(钱包就是被扣金额, 月卡等有自己的描述) :return: """ if self.is_new_version: return VirtualCoin(self.extraInfo.get('deductCoins', 0)) else: return self.coins @property def frozenCoins(self): """ 不退现金情况下应该退的钱包余额 :return: """ if self.is_new_version: return VirtualCoin(self.extraInfo.get('frozenCoins', 0)) else: return self.coins class VCardConsumeRecord(OrderRecordBase): orderNo = StringField(verbose_name = "订单号", default = "") cardId = StringField(verbose_name = "实体卡ID", default = "") dealerId = StringField(verbose_name = "经销商ID", default = "") # : 在部分场合,设备会失败启动,但这时应该添加该字段为False方便追溯 remarks = StringField(verbose_name = "备注", default = "") desc = StringField(verbose_name = "描述", default = "") finishedTime = DateTimeField(verbose_name = '结束时间', default = datetime.datetime.now) servicedInfo = DictField(verbose_name = '服务内容', default = {}) attachParas = DictField(verbose_name = '服务内容', default = {}) # 本次消费消耗的额度 consumeData = DictField(verbose_name = '消耗的额度', default = {}) # 日限额消耗的额度 consumeDayData = DictField(verbose_name = '消耗的额度', default = {}) meta = { "collection": "VCardConsumeRecord", "db_alias": "default", } @staticmethod def make_no(identifier): # time:14;main_type:1;sub_type:1;identifier:15;extra:5 return '{time}{main_type}{sub_type}{identifier}{reserved}'.format( time = datetime.datetime.now().strftime("%Y%m%d%H%M%S"), main_type = OrderMainType.CONSUME, sub_type = UserConsumeSubType.VIRTUAL_CARD, identifier = '{:0>15}'.format(identifier), reserved = get_random_str(5, string.digits + string.uppercase)) @staticmethod def paginate(cardId, pageIndex, pageSize): records = VCardConsumeRecord.objects.filter(cardId = cardId) \ .order_by('-dateTimeAdded').skip((pageIndex - 1) * pageSize).limit(pageSize) if not records: return 0, [] dataList = [] for record in records: newData = { 'id': str(record.id), 'createdTime': record.dateTimeAdded.strftime(Const.DATETIME_FMT), 'address': record.address, 'devNo': record.devNo, 'openId': record.openId, 'nickname': record.nickname, 'logicalCode': record.logicalCode, 'groupNumber': record.groupNumber, 'groupName': record.groupName, 'unit': record.consumeData.get("unit", ""), 'devTypeCode': record.devTypeCode, 'devTypeName': record.dev_type_name, 'isNormal': True, 'ownerId': record.dealerId } amount = record.consumeData.get("count", "") if amount: amount = Quantity(amount) newData.update({'amount': amount}) if record.servicedInfo: newData.update(record.servicedInfo) dataList.append(newData) return len(dataList), dataList @property def amount(self): _amount = self.consumeData.get("count", None) if _amount is not None: return str(Quantity(_amount)) else: return "" @property def unit(self): return self.consumeData.get("unit", "") @property def created_date(self): return self.dateTimeAdded @property def completion_date(self): return self.finishedTime @property def device_start_time(self): return self.dateTimeAdded @property def device_finished_time(self): return self.finishedTime def to_user_detail(self): """ 消费记录对于用户端显示的详细信息 :return: """ data = { 'id': str(self.id), 'orderNo': self.orderNo, 'createdTime': self.dateTimeAdded.strftime(Const.DATETIME_FMT), 'deviceStatTime': self.device_start_time, 'address': self.address, 'devNo': self.devNo, 'logicalCode': self.logicalCode, 'groupNumber': self.groupNumber, 'groupName': self.groupName, 'devTypeCode': self.devTypeCode, 'devTypeName': self.dev_type_name, 'isNormal': True, 'amount': self.amount, 'unit': self.unit, 'ownerId': self.dealerId, 'consumeType': 'mobile_vcard', 'openId': self.openId, 'userNickname': self.nickname } # 兼容之前的订单 凡是没有订单状态机的订单,一律视为 之前的订单 if hasattr(self, "state") and self.state: data.update({"orderStatus": self.state}) if self.servicedInfo: data.update(self.servicedInfo) return data @property def ownerId(self): return self.dealerId @property def owner(self): from apps.web.dealer.models import Dealer _attr_name = '__my_owner__' if hasattr(self, _attr_name): return getattr(self, _attr_name) else: if not self.dealerId: return None else: dealer = Dealer.objects(id = self.dealerId).first() setattr(self, _attr_name, dealer) return dealer @property def summary(self): return { 'id': str(self.id), 'consumeType': 'mobile_vcard', 'ownerId': self.ownerId, 'logicalCode': self.logicalCode, 'groupName': self.groupName, 'devTypeName': self.dev_type_name, 'amount': self.amount, 'unit': self.unit, 'createdTime': self.dateTimeAdded.strftime(Const.DATETIME_FMT), } VirtualCardQuotaList = List[Dict[str, Union[float, unicode]]] class UserVirtualCard(Searchable): cardNo = StringField(verbose_name = "卡编号", default = "") cardTypeId = StringField(verbose_name = u'卡的类型ID', default = '') # 如果卡停止销售了,这里就可以不允许续充 openIds = ListField(verbose_name = "绑定的用户") # 里面放的是MyUser的Id cardName = StringField(verbose_name = "卡名称", default = "") ownerOpenId = StringField(verbose_name = "卡的所有者", default = "") nickname = StringField(verbose_name = "昵称", default = "") logicalCode = StringField(verbose_name = u'发卡的时候记录的设备编码', default = '') groupId = StringField(verbose_name = u'发卡的时候记录的地址', default = '') dealerId = StringField(verbose_name = "卡的发布老板", default = "") groupIds = ListField(verbose_name = "可用使用卡的地址", default = []) # 空表示没有地址,*表示所有地址下可用 devTypeList = ListField(verbose_name = "设备类型清单", default = []) price = MonetaryField(verbose_name = "卡的售价", default = RMB('0.00')) periodDays = IntField(verbose_name = "卡的可用天数", default = 30) expiredTime = DateTimeField(verbose_name = "过期时间", default = lambda: datetime.datetime.now() + datetime.timedelta(days = 365)) userLimit = IntField(verbose_name = "用户数量限制", default = 10) userDesc = StringField(verbose_name = "描述", default = "") dayQuota = ListField(verbose_name = "日限额度", default = []) # {'unit':u'次','count':2} quota = ListField(verbose_name = "总的额度", default = []) frozenQuota = ListField(verbose_name = u'冻结额度', default = []) startTime = DateTimeField(verbose_name = "买卡的时间", default = datetime.datetime.now) dayUsed = DictField(verbose_name = "日消耗情况", default = {}) # {'2018-12-26':[{'unit':'次','count':3}]} quotaUsed = ListField(verbose_name = "总的消耗情况", default = []) # [{'unit':'次','count':3}] status = StringField(verbose_name = "卡状态", default = 'normal') # expired:过期,耗尽:exhausted, 未激活:nonactivated(经销商开虚拟卡),used(经销商开卡解绑了,并未过期) remark = StringField(verbose_name = "卡片备注", default = "") continueTime = DateTimeField(verbose_name="买卡的时间", default = None) # 接续时间,用于续费后,下一次卡接续开始 ongoingList = ListField(DictField(), verbose = u'冻结的配额') managerialAppId = StringField(verbose_name = "管理公众号AppId", default = None) managerialOpenId = StringField(verbose_name = "管理openId", default = None) # 坤元虚拟卡特有:功率 power = IntField(verbose_name="包月卡功率限制", default=0) meta = { "collection": "UserVirtualCard", "db_alias": "default", } search_fields = ('nickname', 'cardNo', 'cardName') REVERSE_DAY = 3 @property def payVia(self): return "virtualCard" @staticmethod def make_no(): timestamp = generate_timestamp_ex() random_int = random.randint(1, 1000) return "%d%03d" % (timestamp, random_int) # --------------------------------------------------------------- 所有的虚拟卡的新方法 需要兼容以前 一定需要严格测试 @staticmethod def find_match_unit(quotaList, package): """ 从额度列表中找到符合套餐单位的 返回index :param quotaList: [{"unit": "次", "count": 15}, {"unit": "分钟", "count": 600}] :param package: {"unit": "分钟", "time": 30} """ # 只有一种额度的 走之前的流程 不要影响客户的使用 if len(quotaList) == 1: return UserVirtualCard._find_match_unit(quotaList, package) # 多单位的情况下 不再对单位进行转换 如果单位不匹配 就直接跳过 pCount, pUnit = package["time"], package["unit"] for _index, _quota in enumerate(quotaList): if _quota["unit"] != pUnit or float(_quota["count"]) < float(pCount): continue ii = _index break else: ii = -1 return ii @staticmethod def calc_left_quota(quotaList, usedList): """ 计算剩余的额度 :param quotaList: 总的额度列表 :param usedList: 已经使用额度情况 """ # TODO 目前使用双循环解决,其实可以加一个单位排序,一次遍历搞定 时间复杂度将会降低一个量级 leftQuotaList = list() for _quota in quotaList: _qCount, _qUnit = float(_quota["count"]), _quota["unit"] for _used in usedList: # 单位有匹配的 则直接计算额度 if _used["unit"] == _qUnit: _usedQuota = {"count": float(max(_qCount-_used["count"], 0)), "unit": _qUnit} break else: continue # 没有找到这种单位 说明这种额度没有使用过 则剩余额度为满值 else: _usedQuota = {"count": float(_qCount), "unit": _qUnit} leftQuotaList.append(_usedQuota) return leftQuotaList def calc_left_total_quota(self): """ 计算剩余的总额度 将所有的冻结额度都算上使用的 :return: """ usedMap, frozenMap = dict(), dict() for _item in self.quotaUsed: if _item["unit"] in usedMap: usedMap[_item["unit"]] += float(_item["count"]) else: usedMap[_item["unit"]] = float(_item["count"]) for _on in self.ongoingList: if _on["quota"]["unit"] in frozenMap: frozenMap[_on["quota"]["unit"]] += float(_on["quota"]["count"]) else: frozenMap[_on["quota"]["unit"]] = float(_on["quota"]["count"]) quotaList = self.quota usedList = [{"count": _v, "unit": _k} for _k, _v in dict(Counter(usedMap)+Counter(frozenMap)).items()] return self.calc_left_quota(quotaList, usedList) def calc_left_day_quota(self, dayKey): """ 计算当天剩下的总额度 注意key的匹配 将所有的当天的额度都算上使用的 :param dayKey: :return: """ usedMap, frozenMap = dict(), dict() for _item in self.dayUsed.get(dayKey, list()): if _item["unit"] in usedMap: usedMap[_item["unit"]] += _item["count"] else: usedMap[_item["unit"]] = _item["count"] for _on in self.ongoingList: if _on["dayKey"] != dayKey: continue if _on["dayQuota"]["unit"] in frozenMap: frozenMap[_on["dayQuota"]["unit"]] += _on["dayQuota"]["count"] else: frozenMap[_on["dayQuota"]["unit"]] = _on["dayQuota"]["count"] quotaList = self.dayQuota usedList = [{"count": _v, "unit": _k} for _k, _v in dict(Counter(usedMap) + Counter(frozenMap)).items()] return self.calc_left_quota(quotaList, usedList) # --------------------------------------------------------------- 新方法结束 @staticmethod def _find_match_unit(quotaList, package): # type:(VirtualCardQuotaList, dict)->int match = False ii = 0 for t1Dict in quotaList: unit = t1Dict['unit'] count = t1Dict['count'] if unit == u'次' and count >= 1: match = True break elif (unit in [u'分钟', u'小时', u'天']) and (package['unit'] in [u'分钟', u'小时', u'天']): if unit == u'小时': t1Count = count * 60 elif unit == u'天': t1Count = count * 60 * 24 else: t1Count = count if package['unit'] == u'小时': pCount = float(package['time']) * 60 elif package['unit'] == u'天': pCount = float(package['time']) * 60 * 24 else: pCount = float(package['time']) if t1Count >= pCount: match = True break elif (unit in [u'分钟', u'小时', u'天']) and package['unit'] == u'度': # 直接按照12小时计算 if unit == u'小时': t1Count = count * 60 elif unit == u'天': t1Count = count * 60 * 24 else: t1Count = count if t1Count >= 6.0 * 60: # 6小时打底 match = True break elif unit == u'币' and count >= package['coins']: match = True break elif unit == u'度' and package['unit'] == u'度' and count >= package['time']: match = True break elif unit == u'度' and package['unit'] != u'度' and count >= 3.0: match = True break elif package['unit'] == u'次': if unit == u'分钟' and count >= 360.0: match = True break elif unit == u'小时' and count >= 6.0: match = True break elif unit == u'天' and count >= 0.25: match = True break elif unit == u'度' and count >= 3.0: # 默认写死3度电,多退少补 match = True break elif unit == u'币' and count >= package['coins']: match = True break ii += 1 return ii if match else -1 @staticmethod def find_match_unit_and_can_use_count(quotaList, package): """ 找出虚拟卡能否使用多少次 某些设备的特殊需求 例如和动 """ # 兼容之前的 if len(quotaList) == 1: usage_count = 0 for t1Dict in quotaList: unit = t1Dict['unit'] count = t1Dict['count'] if unit == u'次' and count >= 1: usage_count = count break elif (unit in [u'分钟', u'小时', u'天']) and (package['unit'] in [u'分钟', u'小时', u'天']): if unit == u'小时': t1Count = count * 60 elif unit == u'天': t1Count = count * 60 * 24 else: t1Count = count if package['unit'] == u'小时': pCount = float(package['time']) * 60 elif package['unit'] == u'天': pCount = float(package['time']) * 60 * 24 else: pCount = float(package['time']) if t1Count >= pCount: usage_count = t1Count // pCount break elif (unit in [u'分钟', u'小时', u'天']) and package['unit'] == u'度': # 直接按照12小时计算 if unit == u'小时': t1Count = count * 60 elif unit == u'天': t1Count = count * 60 * 24 else: t1Count = count if t1Count >= 6.0 * 60: # 6小时打底 usage_count = t1Count // 6.0 * 60 break elif unit == u'币' and count >= package['coins']: usage_count = count // float(package['coins']) break elif unit == u'度' and package['unit'] == u'度' and count >= package['time']: usage_count = count // float(package['time']) break elif unit == u'度' and package['unit'] != u'度' and count >= 3.0: usage_count = count // 3 break elif package['unit'] == u'次': if unit == u'分钟' and count >= 360.0: usage_count = count // 360.0 break elif unit == u'小时' and count >= 6.0: usage_count = count // 6 break elif unit == u'天' and count >= 0.25: usage_count = count // 0.25 break elif unit == u'度' and count >= 3.0: # 默认写死3度电,多退少补 usage_count = count // 3 break elif unit == u'币' and count >= package['coins']: usage_count = count // float(package['coins']) break return int(usage_count) # 两种单位的 else: pCount, pUnit = package["time"], package["unit"] for _index, _quota in enumerate(quotaList): # 单位不一致 或者数量不够的 直接跳过 if _quota["unit"] != pUnit or float(_quota["count"]) < float(pCount): continue # 然后求出count if pUnit == u"次": return _quota["count"] # 时间的 由于单位一样 直接除法 else: return _quota["count"] // pCount else: return 0 @staticmethod def count_by_unit(unit, package): # type:(unicode, dict)->Union[float, int] """ 根据单位,计算消耗量 根据套餐计算以及单位,计算需要扣除的指标数量。比如单位是:币,那就直接看套餐多少币,就需要扣除多少币 :param unit: :param package: :return: """ if unit == u'次': return 1 elif unit == u'币': return package['coins'] elif unit == u'分钟': if package['unit'] == u'分钟': return float(package['time']) elif package['unit'] == u'小时': return float(package['time']) * 60.0 elif package['unit'] == u'天': return float(package['time']) * 60 * 24.0 else: return 6.0 * 60 elif unit == u'小时': if package['unit'] == u'分钟': return float(package['time']) / 60.0 elif package['unit'] == u'小时': return float(package['time']) elif package['unit'] == u'天': return float(package['time']) * 24.0 else: return 6.0 elif unit == u'天': if package['unit'] == u'分钟': return float(package['time']) / 360.0 elif package['unit'] == u'小时': return float(package['time']) / 24.0 elif package['unit'] == u'天': return float(package['time']) else: return 0.25 elif unit == u'度': if package['unit'] == u'度': return float(package['time']) else: return 3.0 else: raise ValueError(u'does not support unit %s' % (unit,)) def can_use_today(self, package): # type:(dict)->bool """ 检查今天是否可以用卡 :param package: :return: """ if package.get("isTempPackage"): return False # 非正常状态的卡都不让用 if self.status not in ["normal", "warning"]: return False # 先计算出总的剩余额度,以及每日的剩余额度 leftQuota = self.calc_left_total_quota() dayKey = datetime.datetime.now().strftime("%Y-%m-%d") leftDayQuota = self.calc_left_day_quota(dayKey) # 剩余总额度以及剩余日限额度检查没有问题,就直接返回 if UserVirtualCard.find_match_unit(leftQuota, package) >= 0 and UserVirtualCard.find_match_unit(leftDayQuota, package) >= 0: return True return False def freeze_quota(self, package, transaction_id): # type:(dict, str) -> bool """ 消费一次,包括扣掉总额,增加消费记录 :param package: :param transaction_id: 交易的ID :return: """ try: # 找出额度单元,然后扣掉 leftQuota = self.calc_left_total_quota() dayKey = today_format_str() leftDayQuota = self.calc_left_day_quota(dayKey) allIndex = UserVirtualCard.find_match_unit(leftQuota, package) dayIndex = UserVirtualCard.find_match_unit(leftDayQuota, package) if allIndex < 0 or dayIndex < 0: raise InsufficientFundsError(u'虚拟卡配额不足') # 先处理总额度 unit = leftQuota[allIndex]['unit'] consume = {'unit': unit, 'count': UserVirtualCard.count_by_unit(unit, package)} # 处理每日额度 unit = leftDayQuota[dayIndex]['unit'] day_consume = {'unit': unit, 'count': UserVirtualCard.count_by_unit(unit, package)} query = {'_id': self.id, 'ongoingList.transaction_id': {'$ne': transaction_id}} update = { '$addToSet': {'ongoingList': { 'transaction_id': transaction_id, 'quota': consume, 'dayQuota': day_consume, 'dayKey': dayKey }}} result = self.get_collection().update_one(filter=query, update=update, upsert = False) return bool(result.modified_count == 1) finally: try: day3Key = (datetime.datetime.now() - datetime.timedelta(days = self.REVERSE_DAY)).strftime('%Y-%m-%d') for dk in self.dayUsed.keys(): if dk <= day3Key: self.dayUsed.pop(dk) self.save() except Exception as e: logger.exception('save user vcard e={}'.format(e)) def clear_frozen_quota(self, transaction_id, usedTime, spendElec, backCoins): """ 清除冻结的额度,并按照一定的比例退还 """ def calc_left_by_unit(count, unit, ut, ue, uc): if unit in [u'分钟', u'小时', u'天']: if unit == u'分钟': ut = int(ut) elif unit == u'小时': ut = round(ut / 60.0, 2) else: ut = round(ut / (60.0 * 24.0), 2) leftCount = count - ut elif unit in [u'度']: leftCount = count - ue elif unit in [u'次']: # 如果没有用,比如没有插电,一样,不能扣除配额 if int(ut) == 0 and int(ue) == 0: leftCount = count else: leftCount = 0 elif unit in [u'币']: # 注意,这个是已经算好的退币数额 leftCount = uc else: leftCount = 0 return max(leftCount, 0) frozen_item = None for item in self.ongoingList: if item['transaction_id'] == transaction_id: frozen_item = item break if not frozen_item: logger.debug('not find this frozen item. card = {}, consume id = {}'.format(repr(self), transaction_id)) return False, None, None consumeTotal = frozen_item['quota'] consumeTotalLeft = calc_left_by_unit(consumeTotal['count'], consumeTotal['unit'], usedTime, spendElec, backCoins) consumeTotal['count'] -= float(consumeTotalLeft) consumeDay = frozen_item['dayQuota'] consumeDayLeft = calc_left_by_unit(consumeDay['count'], consumeDay['unit'], usedTime, spendElec, backCoins) consumeDay['count'] -= float(consumeDayLeft) day_key = frozen_item['dayKey'] clean_day_key = (datetime.datetime.now() - datetime.timedelta(days = self.REVERSE_DAY)).strftime('%Y-%m-%d') if day_key <= clean_day_key: match_quota_used = None for item in self.quotaUsed: if item['unit'] == consumeTotal['unit']: match_quota_used = item break if not match_quota_used: self.quotaUsed.append({'unit': consumeTotal['unit'], 'count': 0}) self.save() query = { '_id': self.id, 'ongoingList': {'$elemMatch': {'transaction_id': transaction_id}}, 'quotaUsed.unit': consumeTotal['unit'] } update = { '$inc': { 'quotaUsed.$.count': consumeTotal['count'] }, '$pull': {'ongoingList': {'transaction_id': transaction_id}}, } result = self.get_collection().update_one( filter = query, update = update, upsert = False ) modified = bool(result.modified_count == 1) return modified, consumeTotal, consumeDay else: match_quota_used = None for item in self.quotaUsed: if item['unit'] == consumeTotal['unit']: match_quota_used = item break if not match_quota_used: self.quotaUsed.append({'unit': consumeTotal['unit'], 'count': 0}) if day_key not in self.dayUsed: self.dayUsed[day_key] = [] match_day_used = None for item in self.dayUsed.get(day_key, []): if item['unit'] == consumeDay['unit']: match_day_used = item break if not match_day_used: self.dayUsed[day_key].append({'unit': consumeDay['unit'], 'count': 0}) self.save() query = { '_id': self.id, 'ongoingList': {'$elemMatch': {'transaction_id': transaction_id}}, 'quotaUsed.unit': consumeTotal['unit'], 'dayUsed.{}.unit'.format(day_key): consumeDay['unit'] } update = { '$inc': { 'quotaUsed.$.count': consumeTotal['count'], 'dayUsed.{}.$.count'.format(day_key): consumeDay['count'] }, '$pull': {'ongoingList': {'transaction_id': transaction_id}}, } result = self.get_collection().update_one( filter = query, update = update, upsert = False) modified = bool(result.modified_count == 1) self.reload() return modified, consumeTotal, consumeDay def recover_frozen_quota(self, transaction_id): """ 清除虚拟卡的冻结额度 不退换 """ query = {'_id': self.id} update = { '$pull': {'ongoingList': {'transaction_id': transaction_id}}, } result = self.get_collection().update_one( filter = query, update = update, upsert = False ) return bool(result.modified_count == 1) def consume(self, openId, group, dev, package, attachParas, nickname='', orderNo=None): """ 消费一次,包括扣掉总额,增加消费记录 :param openId: :param group: :param dev: :param package: :param attachParas: :param nickname: :param orderNo: 订单编号 :return: """ # 找出额度单元,然后扣掉 leftQuota = self.calc_left_total_quota() dayKey = datetime.datetime.now().strftime("%Y-%m-%d") leftDayQuota = self.calc_left_day_quota(dayKey) allIndex = UserVirtualCard.find_match_unit(leftQuota, package) dayIndex = UserVirtualCard.find_match_unit(leftDayQuota, package) if allIndex < 0 or dayIndex < 0: return None # 先处理总额度 unit = leftQuota[allIndex]['unit'] consumeData = {'unit': unit, 'count': UserVirtualCard.count_by_unit(unit, package)} match = False for tDict in self.quotaUsed: if tDict['unit'] == unit: tDict['count'] = float(tDict['count']) + float(consumeData['count']) match = True break if not match: self.quotaUsed.append(consumeData) # 处理每日额度 unit = leftDayQuota[dayIndex]['unit'] consumeDayData = {'unit': unit, 'count': UserVirtualCard.count_by_unit(unit, package)} match = False for tDict in self.dayUsed.get(dayKey, []): if tDict['unit'] == unit: consumeDayData = {'unit': unit, 'count': UserVirtualCard.count_by_unit(unit, package)} tDict['count'] = float(tDict['count']) + float(consumeDayData['count']) match = True break if not match: self.dayUsed[dayKey] = [consumeDayData] # 清理掉3天以前的每日消费情况 day3Key = (datetime.datetime.now() - datetime.timedelta(days=3)).strftime('%Y-%m-%d') for dk in self.dayUsed.keys(): if dk <= day3Key: self.dayUsed.pop(dk) try: self.save() except Exception as e: logger.exception('[consume] save user vcard e={}'.format(e)) return None # 增加一条消费记录 newRcd = VCardConsumeRecord( orderNo = orderNo if orderNo is not None else VCardConsumeRecord.make_no(dev.logicalCode), openId = openId, nickname = nickname, cardId = str(self.id), dealerId = self.dealerId, devNo = dev['devNo'], devTypeCode = dev.devTypeCode, devTypeName = dev.devTypeName, logicalCode = dev['logicalCode'], groupId = group['groupId'], address = group['address'], groupNumber = dev['groupNumber'], groupName = group['groupName'], attachParas = attachParas, consumeData = consumeData, consumeDayData = consumeDayData ) try: newRcd.save() except Exception as e: logger.error('[consume] save vcard consume e=%s' % e) return None return newRcd def new_consume_record(self, dev, user, consumeTotal, attachParas=None, orderNo=None): newRcd = VCardConsumeRecord( orderNo=orderNo if orderNo is not None else VCardConsumeRecord.make_no(dev.logicalCode), openId=user.openId, nickname=user.nickname, cardId=str(self.id), dealerId=dev.ownerId, devNo=dev.devNo, devTypeCode = dev.devTypeCode, devTypeName = dev.devTypeName, logicalCode=dev.logicalCode, groupId=dev.group.groupId, address=dev.group.address, groupNumber=dev.groupNumber, groupName=dev.group.groupName, attachParas=attachParas or dict(), consumeData=consumeTotal, consumeDayData=consumeTotal ) try: newRcd.save() except Exception as e: logger.error('[consume] save vcard consume e=%s' % e) return None return newRcd def refund_quota(self, consumeRcd, usedTime, spendElec, backCoins): """ 将虚拟卡的额度返回 对应的使用方法是consume :param consumeRcd: :param usedTime: 单位一定是分钟 :param spendElec: 单位一定是度 :param backCoins: 单位一定是金币 :return: """ def calc_left_by_unit(count, unit, ut, ue, uc): if unit in [u'分钟', u'小时', u'天']: if unit == u'分钟': ut = int(ut) elif unit == u'小时': ut = round(ut / 60.0, 2) else: ut = round(ut / (60.0 * 24.0), 2) leftCount = count - ut elif unit in [u'度']: leftCount = count - ue elif unit in [u'次']: # 如果没有用,比如没有插电,一样,不能扣除配额 if int(ut) == 0 and int(ue) == 0: leftCount = count else: leftCount = 0 elif unit in [u'币']: # 注意,这个是已经算好的退币数额 leftCount = float(VirtualCoin(uc)) else: leftCount = 0 return leftCount # 当消费已经定下来的时候 则本次退换的单位实际上已经固定了 consumeLeftTemp = calc_left_by_unit( consumeRcd.consumeData['count'], consumeRcd.consumeData['unit'], usedTime, spendElec, backCoins ) consumeLeft = max(consumeLeftTemp,0) consumeRcd.consumeData['count'] -= float(consumeLeft) consumeDayLeft = calc_left_by_unit( consumeRcd.consumeDayData['count'], consumeRcd.consumeDayData['unit'], usedTime, spendElec, backCoins ) consumeRcd.consumeDayData['count'] -= float(consumeDayLeft) # 刷新消费记录数据 try: consumeRcd.save() except Exception as e: logger.exception('[refund_quota] save consume rcd error = %s' % e) return # 更新额度 for tDict in self.quotaUsed: if tDict['unit'] != consumeRcd.consumeData['unit']: continue tDict['count'] -= consumeLeft dayKey = datetime.datetime.now().strftime("%Y-%m-%d") for tDict in self.dayUsed.get(dayKey, []): if tDict['unit'] != consumeRcd.consumeDayData['unit']: continue tDict['count'] -= float(consumeDayLeft) try: self.save() except Exception as e: logger.exception('[refund_quota] save quota error=%s' % e) # 返回本条记录余下的配套 returnUsedTime,returnSpendElec,returnBackCoins = usedTime,spendElec,backCoins if consumeRcd.consumeData['unit'] in [u'分钟',u'小时',u'秒']: return consumeLeftTemp,returnSpendElec,returnBackCoins if consumeRcd.consumeData['unit'] in [u'度']: return returnUsedTime,consumeLeftTemp,returnBackCoins return returnUsedTime,returnSpendElec,returnBackCoins # 卡续费 def continue_card(self): if not self.continueTime: return vCard = VirtualCard.objects.get(id = self.cardTypeId) # 以最新的属性作为 购买 self.price = vCard.price self.dayQuota = vCard.dayQuota self.userLimit = vCard.userLimit self.cardName = vCard.cardName self.quota = vCard.quota self.userDesc = vCard.userDesc self.dayUsed = {} self.quotaUsed = [] self.status = 'normal' self.expiredTime = self.continueTime + datetime.timedelta(seconds=vCard.periodDays * 24 * 3600) self.continueTime = None self.save() return self @staticmethod def get_user_cards(openId, groupId, ownerId): """ 获取用户所有的虚拟卡 修改: 去掉continueTime的check判断 没意义 """ cardList = [] vCards = UserVirtualCard.objects.filter( dealerId = ownerId, openIds = openId, groupIds__in = ['*', groupId] ) nowTime = datetime.datetime.now() for obj in vCards: if obj.startTime <= nowTime <= obj.expiredTime: cardList.append(obj) return cardList def add_expired_time(self, days): self.expiredTime += datetime.timedelta(days = int(days)) self.save() def support_dev_type(self, devTypeCode): devTypesCount = DeviceType.objects.filter(code = devTypeCode, id__in = self.devTypeList).count() if devTypesCount <= 0: return False return True @property def groupInfo(self): """ 获取虚拟卡的相应的 地址信息 :return: """ if '*' in self.groupIds: groups = [{'groupName': grp['groupName'], 'address': grp['address']} for grp in Group.get_groups_by_group_ids(Group.get_group_ids_of_dealer(self.dealerId)).values()] else: groups = [{'groupName': grp['groupName'], 'address': grp['address']} for grp in Group.get_groups_by_group_ids(self.groupIds).values()] return groups @property def membersInfo(self): """ 虚拟卡成员信息 :return: """ users = MyUser.objects.filter(openId__in = self.openIds).only("openId", "nickname") bindUsersList = list() repeatUsers = set() for user in users: if user.openId in repeatUsers: continue # openId 相同去重 repeatUsers.add(user.openId) tempDict = { 'userName': user.nickname, 'userId': user.openId } user.openId == self.ownerOpenId and tempDict.update({"role": "owner"}) bindUsersList.append(tempDict) return bindUsersList @property def quotaInfo(self): """ 获取和额度相关的信息 :return: """ dayUsedList = self.dayUsed.get(datetime.datetime.now().strftime("%Y-%m-%d"), list()) dayUsed = 0 if len(dayUsedList) > 0: dayUsed = dayUsedList[0]['count'] return { 'quota': self.quota[0]['count'], 'quotaUnit': self.quota[0]['unit'], 'quotaUsed': round(self.quotaUsed[0]['count'], 2) if len(self.quotaUsed) > 0 else 0, 'dayQuota': self.dayQuota[0]['count'], 'dayQuotaUnit': self.dayQuota[0]['unit'], 'dayUsed': round(dayUsed, 2) } @property def devTypes(self): devs = DeviceType.objects.filter(id__in=self.devTypeList).only("majorDeviceType", "name") devTypes = list() for _dev in devs: # type: DeviceType devTypes.append({"devTypeName": _dev.name, "majorDeviceType": _dev.majorDeviceType}) return devTypes def isOwner(self, openId): """是不是卡的所有者""" return openId == self.ownerOpenId def to_dict(self): """ 序列化 :return: """ data = { 'cardId': str(self.id), 'cardNo': self.cardNo, 'cardTypeId': self.cardTypeId, 'cardName': self.cardName, 'periodDays': self.periodDays, 'createTime': self.startTime.strftime('%Y-%m-%d'), 'expiredTime': self.expiredTime.strftime('%Y-%m-%d'), 'userDesc': self.userDesc, 'userLimit': self.userLimit, 'userName': self.nickname, 'logicalCode': self.logicalCode, 'groupId': self.groupId, 'remark': self.remark, 'power': self.power } return data def to_detail(self): data = { 'cardId': str(self.id), 'cardNo': self.cardNo, 'cardTypeId': self.cardTypeId, 'cardName': self.cardName, 'periodDays': self.periodDays, 'createTime': self.startTime.strftime('%Y-%m-%d'), 'expiredTime': self.expiredTime.strftime('%Y-%m-%d'), 'userDesc': self.userDesc, 'userLimit': self.userLimit, 'userName': self.nickname, 'logicalCode': self.logicalCode, 'groupId': self.groupId, 'remark': self.remark, "groups": self.groupInfo, "sharedMembers": self.membersInfo, "devTypes": self.devTypes, # 功率 'power': self.power, # 额度 'quota': { "day": self.dayQuota, "total": self.quota }, # 剩余的额度 'leftQuota': { "day": self.calc_left_day_quota(today_format_str()), "total": self.calc_left_total_quota() } } return data # 以下是霍坡的特殊业务 之前是不支持按次 现在支持了 考虑业务合并 def can_use_hp_gate(self): """霍珀虚拟卡 对于道闸计费 只要卡没过期就可以使用""" # 尚未过期的虚拟卡可用于霍珀道闸 # 要求变更 虚拟卡过期后还能在使用24小时 if self.status not in ["normal", "warning"]: return False return self.expiredTime > datetime.datetime.now() - datetime.timedelta(hours = 24) def consume_hp_gate(self, openId, group, dev, package = None, attachParas={}, nickname=''): # type:(str, GroupDict, DeviceDict, dict, dict, str)->Optional[VCardConsumeRecord] """ 消费一次 :param openId: :param group: :param dev: :param package: :param attachParas: :param nickname: :return: """ # 增加一条消费记录 newRcd = VCardConsumeRecord( orderNo = VCardConsumeRecord.make_no(dev.logicalCode), openId = openId, nickname = nickname, cardId = str(self.id), dealerId = self.dealerId, devNo = dev['devNo'], devTypeCode = dev.devTypeCode, devTypeName = dev.devTypeName, logicalCode = dev['logicalCode'], groupId = group['groupId'], address = group['address'], groupNumber = dev['groupNumber'], groupName = group['groupName'], attachParas = attachParas, remarks = u"道闸刷卡服务" ) try: newRcd.save() self.record_gate_used_times() except Exception as e: logger.error('save vcard consume e=%s' % e) return None return newRcd def record_gate_used_times(self): """ 霍珀要求的 对于道闸次数进行监控,但是不能限制次数 :return: """ dayKey = datetime.datetime.now().strftime("%Y-%m-%d") cacheKey = "{}-{}".format(dayKey, self.cardNo) times = serviceCache.get(cacheKey, 0) times += 1 serviceCache.set(cacheKey, times, timeout = 60 * 60 * 24) if times < 8: status = "normal" else: status = "warning" try: self.update(status = status) except Exception as e: logger.exception(e) return class VirtualCardRechargeRecord(Searchable): orderNo = StringField(verbose_name = u'使用RechargeRecord的orderNo关联两张表', required = True) cardId = StringField(verbose_name = "虚拟卡ID", default = "") cardNo = StringField(verbose_name = "虚拟卡卡号", default = "") openId = StringField(verbose_name = "openId", default = "") gateway = StringField(verbose_name = 'gateway', default = "") groupId = StringField(verbose_name = "设备地址编号", default = "") ownerId = ObjectIdField(verbose_name = "dealerId") dateTimeAdded = DateTimeField(default = datetime.datetime.now, verbose_name = '生成时间') # 以下字段后续废弃, 只做兼容使用 devNo = StringField(verbose_name = "设备ID", default = None) logicalCode = StringField(verbose_name = "设备逻辑编码", default = None) wxOrderNo = StringField(verbose_name = "orderNo", default = None) money = MonetaryField(verbose_name = "付款金额", default = None) coins = MonetaryField(verbose_name = "充值金额", default = None) devTypeCode = StringField(verbose_name = "设备类型编码", default = None) time = StringField(verbose_name = 'time', default = None) devType = StringField(verbose_name = "设备类型", default = None) address = StringField(verbose_name = "设备地址", default = None) groupNumber = StringField(verbose_name = "设备", default = None) groupName = StringField(verbose_name = "交易场地", default = None) remarks = StringField(verbose_name = "备注", default = None) status = StringField(verbose_name = "状态", default = None) meta = { "collection": "virtualCardRechargeRecord", "db_alias": "default", } @classmethod def get_link_orderNo_list(cls, cardId, startTime, endTime, openId = None): records = cls.objects( cardId = cardId, dateTimeAdded__gte = startTime, dateTimeAdded__lt = get_tomorrow_zero_time(endTime)) if openId: records = records.filter(openId = openId) return [record.orderNo for record in records] @property def created_date(self): return self.dateTimeAdded def is_refund_available(self, customer): # type:(CapitalUser) -> bool return False @property def has_refund_order(self): return False @property def isQuickPay(self): return False @classmethod def issue(cls, order): # type:(RechargeRecord)->VirtualCardRechargeRecord return cls( orderNo = order.orderNo, cardId = order.attachParas['cardId'], cardNo = order.attachParas['cardNo'], openId = order.openId, ownerId = order.ownerId, groupId = order.groupId, gateway = order.gateway, dateTimeAdded = order.dateTimeAdded).save() # 监督号的用户,用于和实际系统的用户进行关联 class MoniUser(Searchable): moniAppId = StringField(verbose_name = 'moniAppId', default = '') moniOpenId = StringField(verbose_name = 'moniOpenId', default = '') openId = StringField(verbose_name = u'用户的主APPID', default = '') isSubscribe = BooleanField(verbose_name = u'是否订阅', default = False) checkTime = DateTimeField(verbose_name = u'检查时间') subTime = DateTimeField(verbose_name = u'关注时间') unsubTime = DateTimeField(verbose_name = u'取关时间') subLogicalCode = StringField(verbose_name = u'关注来源logicalCode', default = '') subDealerId = StringField(verbose_name = u'关注来源dealerId', default = '') subAgentId = StringField(verbose_name = u'关注来源agentId', default = '') subPoint = StringField(verbose_name = u'关注来源point', default = '') meta = { "collection": "moni_user", "db_alias": "logdata", } @classmethod def get_or_create(cls, moniOpenId, moniAppId, **kwargs): """ 创建一条用户的记录 """ obj = cls.objects.filter(moniOpenId=moniOpenId, moniAppId=moniAppId).order_by("-id").first() or cls(moniOpenId=moniOpenId, moniAppId=moniAppId) needUpdate = False for _field in cls._fields.keys(): if _field in kwargs and kwargs[_field] != getattr(obj, _field, kwargs[_field]): setattr(obj, _field, kwargs[_field]) needUpdate = True if needUpdate: obj = obj.save() return obj @classmethod def get_or_delete(cls, moniOpenId, moniAppId): cls.objects.filter(moniOpenId=moniOpenId, moniAppId=moniAppId).update(isSubscribe=False) class AskStep(Searchable): operKey = StringField(verbose_name = 'oper key', default = '') answerText = StringField(verbose_name = 'answer text', default = '') answerPics = StringField(verbose_name = 'answer picture', default = '') # '文件名称用,分割' answerFunc = StringField(verbose_name = 'answer func', default = '') @staticmethod def replyUseDevice(moniOpenId, appId, **kwargs): moniUser = MoniUser.objects.filter(moniOpenId = moniOpenId, moniAppId = appId).first() if moniUser: devLink = concat_user_login_entry_url(l=moniUser.subLogicalCode) return [{'msgType': 'text', 'value': u'\ue231戳我使用设备\ue230' % devLink}] return [{'msgType': 'text', 'value': u'您直接点击菜单栏的扫一扫,或者关闭当前页面重新扫码,即可使用'}] @staticmethod def replayQueryBalance(moniOpenId, appId, **kwargs): openId = kwargs.get('openId') agentId = kwargs.get('agentId') users = MyUser.objects.filter(openId = openId) balance = VirtualCoin(0.0) for user in users: balance += user.balance url = concat_user_center_entry_url(agentId=agentId, redirect=concat_front_end_url(uri='/user/index.html#/user/help')) return [{'msgType': 'text', 'value': u'您在系统总的余额是%s,如果界面上显示不对,请您先刷新页面,还是不行的话,联系客服' % (balance, url)}] @staticmethod def get_service_phone_from_rcd(consumeRcd): dealer = Dealer.objects(id=consumeRcd.ownerId).first() # type: Dealer return dealer.service_phone @staticmethod def replyGetToushuUrl(moniOpenId, appId, **kwargs): agentId = kwargs.get('agentId') url = concat_user_center_entry_url(agentId=agentId, redirect=concat_front_end_url(uri='/user/index.html#/complaint/list')) return [{'msgType': 'text', 'value': u'投诉点这里' % url}] # 先检查用户的订单是否扣掉了钱。然后检查设备的连通性。 # 调用此函数的时候,一般是用户付款了,设备没有响应。 @staticmethod def replyQueryConsumeRcd(moniOpenId, appId, **kwargs): try: endTime = datetime.datetime.now() startTime = endTime - datetime.timedelta(days = 7) consumeRcd = ConsumeRecord.objects.filter(openId = kwargs['openId'], dateTimeAdded__gte = startTime, dateTimeAdded__lte = endTime).order_by('-dateTimeAdded').first() agentId = kwargs.get('agentId') url = concat_user_center_entry_url(agentId=agentId, redirect=concat_front_end_url(uri='/user/index.html#/user/feedbackList')) if consumeRcd is None: return [ {'msgType': 'text', 'value': u'系统中没有查到您最近7天的使用记录/::-|,这种情况比较少见,建议您先创建一张报告单,然后拨打客服电话,让客服尽快处理' % url}, {'msgType': 'image', 'value': '1.png'}, {'msgType': 'image', 'value': '2.png'}, {'msgType': 'image', 'value': '3.png'}, {'msgType': 'image', 'value': '4.png'}, ] rcdDesc = u'地址:%s,编号:%s,付款:%s,时间:%s' % (consumeRcd.address, consumeRcd.logicalCode, consumeRcd.coin, consumeRcd.dateTimeAdded.strftime(Const.DATETIME_FMT)) if consumeRcd.isNormal: return [ {'msgType': 'text', 'value': u'查到您最新的订单,信息如下:%s' % rcdDesc}, {'msgType': 'text', 'value': u'钱确实用出去了/:--b,设备如果没有正常响应或者故障,您先拍照留下证据。然后您在服务&投诉中,报告老板,申请上分或者退币。然后在常见问题菜单中,拨打客服电话,让客服尽快处理/::)。如果没有及时响应,您也可以先行自己补款,然后记得向设备老板申请退币/:rose' % url} ] else: return [ {'msgType': 'text', 'value': u'查到您最新的订单,信息如下:%s' % rcdDesc}, {'msgType': 'text', 'value': u'亲,刚查询到您确实有个消费订单,没有成功/:--b。系统已经自动给您退币了,您可以重新扫码尝试启动设备,或者换台别的设备试试吧/::)'} ] except Exception, e: return [{'msgType': 'text', 'value': u'我在系统中没有查到您最近的使用记录/::<,这种情况,建议您在服务&投诉中报告老板,申请上分或者退币。然后在常见问题中,拨打客服电话,让客服尽快处理/::P' % url}] @staticmethod def replyQueryServicedPhone(moniOpenId, appId, **kwargs): agentId = kwargs.get('agentId') endTime = datetime.datetime.now() startTime = endTime - datetime.timedelta(days = 7) consumeRcd = ConsumeRecord.objects.filter(openId = kwargs['openId'], dateTimeAdded__gte = startTime, dateTimeAdded__lte = endTime).order_by('-dateTimeAdded').first() if consumeRcd: phone = AskStep.get_service_phone_from_rcd(consumeRcd) return [{'msgType': 'text', 'value': u'/::P这个是客服电话吧:%s' % phone}] else: url = concat_user_center_entry_url(agentId=agentId, redirect=concat_front_end_url(uri='/user/index.html#/user/help')) return [{'msgType': 'text', 'value': u'这个给您,客服电话' % url}] def reply(self, moniOpenId, appId, **kwargs): if self.answerFunc: return eval('AskStep.%s(moniOpenId,appId,**kwargs)' % self.answerFunc) elif self.answerText: return [{'msgType': 'text', 'value': self.answerText}] elif self.answerPics: return [{'msgType': 'image', 'value': pic} for pic in self.answerPics] else: return [{'msgType': 'text', 'value': ''}] # 聊天模式匹配库。用于批量智能客服(不需要那么复杂的聊天,而且也贵;可以针对性进行数据库查询,更灵活) class AskRobot(Searchable): ask = StringField(verbose_name = 'ask', default = '') # 问题模式,是可以匹配的: 机器/洗衣机/设备/,不启动/没反应/不动/不转/没动静 steps = ListField(verbose_name = 'answer steps', default = []) # 支持 priority = IntField(verbose_name = 'priority', default = 1) askList = [] @staticmethod def load_in_mem_if_not_exist(): # if AskRobot.askList:#调试比较麻烦,直接查询所有吧,以后做了界面,再打开 # return objs = AskRobot.objects.all().order_by('-priority') for obj in objs: AskRobot.askList.append(obj) @staticmethod def match_sentence(ask, sentence, score): wordList = sentence.split('/') for word in wordList: if word in ask: logger.info('it is match,sentence=%s,ask=%s' % (sentence, ask)) return score return 0.0 # 根据图片文件名称找ID,文章标题找ID @staticmethod def find_material_id(proxy, mediaType, value): if mediaType == 'image': results = proxy.client.material.batchget(media_type = 'image', offset = 0, count = 1000) # 简单粗暴,目前看素材不会超过20个 for item in results.get('item', []): if item['name'] == value: return item['media_id'] return None return None @staticmethod def reply(event, moniOpenId, ask, appId, secret, **kwargs): AskRobot.load_in_mem_if_not_exist() # 产生一个线程,让线程后台处理这个客服消息 class replyer(threading.Thread): def __init__(self, event, moniOpenId, ask, appId, secret, **kwargs): super(replyer, self).__init__() self._event = event self._moniOpenId = moniOpenId self._ask = ask self._appId = appId self._secret = secret self._kwargs = kwargs def run(self): try: replys = [] # 如果是关注事件,直接推送文章 if self._event == 'subscribe': materialId = MoniApp.objects.get(appId = self._appId).welcomeArticleMaterialId replys.append({'msgType': 'news', 'value': materialId}) moniUser = MoniUser.objects.filter(moniOpenId = self._moniOpenId, moniAppId = self._appId).first() if moniUser: devLink = concat_user_login_entry_url(l=moniUser.subLogicalCode) replys.append({'msgType': 'text', 'value': u'/:rose亲,欢迎您关注自助设备服务平台!乐乐可以为您解决售后问题以及投诉事宜/:hug,——————— \ue231戳我使用设备\ue230' % devLink}) else: # 找到所有问题的答案 for askConfig in AskRobot.askList: sentenceList = askConfig.ask.split(',') sentenceScore = 100.0 / len(sentenceList) allScore = 0 for sentence in sentenceList: allScore += AskRobot.match_sentence(self._ask, sentence, sentenceScore) if allScore >= 90: logger.info('find match answer key = %s' % ','.join(askConfig.steps)) replys = askConfig.answer(self._moniOpenId, self._appId, **self._kwargs) break logger.info('reply len %s' % len(replys)) # 把答案的文章、图片发给用户 app = WechatAuthApp(appid = self._appId, secret = self._secret) proxy = WechatClientProxy(app) for reply in replys: if not reply: continue try: if reply['msgType'] == 'text' and reply['value']: proxy.client.message.send_text(moniOpenId, reply['value']) elif reply['msgType'] == 'image' and reply['value']: materialId = AskRobot.find_material_id(proxy, 'image', reply['value']) proxy.client.message.send_image(moniOpenId, materialId) elif reply['msgType'] == 'news' and reply['value']: proxy.client.message.send_articles(moniOpenId, reply['value']) time.sleep(1) except Exception, e: logger.exception('send msg to user failed = %s' % (e)) continue except Exception as e: logger.info('replyer error,e=%s' % e) sender = replyer(event, moniOpenId, ask, appId, secret, **kwargs) sender.start() def answer(self, moniOpenId, appId, **kwargs): # 首先把当前回话的历史信息拉出来 historyReplys = serviceCache.get(moniOpenId, '') replyList = [] for stepKey in self.steps: step = AskStep.objects.get(operKey = stepKey) sameReplyCount = historyReplys.count(stepKey) if sameReplyCount == 0: reply = step.reply(moniOpenId, appId, **kwargs) elif sameReplyCount == 1: if 'xiangyin' not in stepKey: reply = [{'msgType': 'text', 'value': u'建议您试试我给的建议哦/::P'}].extend( step.reply(moniOpenId, appId, **kwargs)) else: reply = [{'msgType': 'text', 'value': u'嗯,您的问题是什么呢?/::P'}] elif sameReplyCount == 2: step = AskStep.objects.get(operKey = 'kefudianhua') reply = step.reply(moniOpenId, appId, **kwargs) else: reply = [{'msgType': 'text', 'value': ''}] if reply: historyReplys += '%s,' % stepKey serviceCache.set(moniOpenId, historyReplys, 600) replyList.extend(reply) return replyList class WxAuthTransfer(Searchable): agentId = StringField(verbose_name = u'代理商ID', default = '') oldAppId = StringField(verbose_name = u'老的appid', default = '') oldOpenId = StringField(verbose_name = u'老的openid', default = '') newAppId = StringField(verbose_name = u'新的appid', default = '') newOpenId = StringField(verbose_name = u'新的openid', default = '') # subAgentList = ListField(verbose_name = '子agent列表', default = '') firstPhase = BooleanField(verbose_name = u'第1阶段任务完成', default = False) secondPhase = BooleanField(verbose_name = u'第2阶段任务完成', default = False) meta = { "collection": "wx_auth_transfer", "db_alias": "logdata" } def __repr__(self): return '{}'.format( self.__class__.__name__, str(self.agentId), self.oldAppId, self.oldOpenId, self.newAppId, self.newOpenId) class ICChargeOperating(Searchable): """ deprecated 该功能废弃, 保证脚本能正常进行 """ cardId = StringField(verbose_name = 'cardId', default = '') balance = MonetaryField(verbose_name = u"卡余额", default = RMB('0.00')) needChargeMoney = MonetaryField(verbose_name = u"需要充值的钱", default = RMB('0.00')) dateTimeAdded = DateTimeField(verbose_name = u'操作时间', default = datetime.datetime.now) meta = { "collection": "ic_charge_operating", "db_alias": "logdata" } class BlackListUsers(Searchable): class Status(IterConstant): BLACK = "black" WHITE = 'white' openId = StringField(verbose_name = "被拉黑用户OpenId") dealerId = StringField(verbose_name = "经销商ID") agentId = StringField(verbose_name = "代理商ID") reason = StringField(verbose_name = "被拉黑的理由", default = "") operator = StringField(verbose_name = "操作人") status = StringField(verbose_name = "拉黑状态", choices = Status.choices(), default = Status.BLACK) dateTimeAdded = DateTimeField(verbose_name = "拉黑进去的时间", default = datetime.datetime.now) meta = { "db_alias": "default", 'indexes': [ 'dealerId', ], } @classmethod def add_black_user(cls, openId, dealerId, operator, reason = ""): """ 拉黑用户 :param openId: :param dealerId: :param operator: :param reason: :return: """ try: record = cls.objects.get(dealerId = dealerId, openId = openId) except DoesNotExist: dealer = Dealer.get_dealer(dealerId) or dict() record = cls( openId = openId, dealerId = dealerId, agentId = dealer.get("agentId", ""), operator = operator, reason = reason ).save() record.update(status = cls.Status.BLACK) @classmethod def freed_black_user(cls, openId, dealerId): """ 解除用户的拉黑状态 这个地方不删除记录 防止之后要有需求对拉黑用户进行天数限制 :param openId: :param dealerId: :return: """ try: record = cls.objects.get(dealerId = dealerId, openId = openId) except DoesNotExist: return record.update(status = cls.Status.WHITE) @classmethod def check_black(cls, openId, dealerId): """ 检查该用户是否被拉黑 :param openId: :param dealerId: :return: """ record = cls.objects(openId=openId, dealerId__in=[dealerId, '*']).first() if not record: return False if record.status == cls.Status.WHITE: return False else: return True # 此表用于解决复制卡的问题。每张卡都有一个变化的随机码,每次扣费后会新生成一个随机码。每次扣钱后,会上报上次的随机码 # 如果随机码出现一次重复,说明出现一次复制卡。随机码是刷卡器生成的时间标签,恰好重复的可能性微乎其微。 class WeifuleCardStamp(Searchable): cardNo = StringField(verbose_name = "卡号", default = "") dealerId = StringField(verbose_name = "经销商ID", default = "") dateTimeAdded = DateTimeField(verbose_name = '添加时间', default = datetime.datetime.now()) stamp = StringField(verbose_name = 'stamp', default = "") meta = { "collection": "weifule_card_stamp", "db_alias": "default" } @staticmethod def is_copy_card(cardNo, dealerId, stamp): harfYear = datetime.datetime.now() - datetime.timedelta(days = 6 * 30) copyCount = WeifuleCardStamp.objects.filter(cardNo = cardNo, dealerId = dealerId, dateTimeAdded__gte = harfYear, stamp = stamp).count() if copyCount: return True return False class SwapCardRecord(Searchable): oldCardNo = StringField(verbose_name = u"旧卡卡号") newCardNo = StringField(verbose_name = u"新卡卡号") agentId = StringField(verbose_name = u"补卡的代理商ID") operator = StringField(verbose_name = u"操作人") dateTimeAdded = DateTimeField(verbose_name = u"补卡时间", default = datetime.datetime.now) meta = {'collection': 'SwapCardRecord', 'db_alias': 'logdata'} @classmethod def add_record(cls, oldCardNo, newCardNo, agentId, operator): """ 添加一条补卡记录 :param oldCardNo: 旧卡卡号 :param newCardNo: 新卡卡号 :param agentId: 补卡代理商ID :param operator: 操作者ID :return: """ record = cls( newCardNo = newCardNo, oldCardNo = oldCardNo, agentId = agentId, operator = operator ) record.save() return record class DuibijiOrderMap(Searchable): devOrderId = IntField(verbose_name = 'device order id', unique = True) consumeRcdId = StringField(verbose_name = 'consume rcd id', unique = True) rechargeRcdId = StringField(verbose_name = 'rechargeRcdId id', default = '') status = StringField(verbose_name = 'status', default = '') openId = StringField(verbose_name = 'openId', default = '') dateTimeAdded = DateTimeField(verbose_name = 'dateTimeAdded', default = datetime.datetime.now()) class MonthlyPackageUseInfo(EmbeddedDocument): orderNo = StringField(verbose_name = u"使用订单编号", required = True) orderTime = DateTimeField(verbose_name = u"使用的时间", required = True) def to_dict(self): return {"orderNo": self.orderNo, "orderTime": self.orderTime} class MonthlyPackage(Searchable): # 用户相关信息 name = StringField(verbose_name = u"包月套餐的名称", default = "") groupId = StringField(verbose_name = u"包月绑定的地址组", required = True) openId = StringField(verbose_name = u"用户的唯一ID", required = True) cardNo = StringField(verbose_name = u"刷卡能够使用的卡号", default = "") bothCardAndMobile = BooleanIntField(verbose_name = u"是否是通用的(扫码和刷卡通用)", default = False) # 包月相关信息 startDateTime = DateTimeField(verbose_name = u"有效期起始时间", required = True) expireDateTime = DateTimeField(verbose_name = u"过期的时间", required = True) maxCountOfDay = IntField(verbose_name = u"每日的最大的次数", default = 0, min_value = 0) maxCount = IntField(verbose_name = u"可供使用的最大次数", default = 0, min_value = 0) maxTimeOfCount = IntField(verbose_name = u"每次最大的充电时间 单位分钟", default = 0, min_value = 0) maxElecOfCount = IntField(verbose_name = u"每次最大充电量 单位度", default = 0, min_value = 0) # 基本信息 isDisable = BooleanIntField(verbose_name = u"是否已经失效", default = 0) usedTotal = IntField(verbose_name = u"使用的总次数", default = 0) usedDetail = MapField(verbose_name = u"用户使用情况", field = EmbeddedDocumentListField(document_type = MonthlyPackageUseInfo)) dateTimeAdded = DateTimeField(verbose_name = u"购买的时间 一般与起始时间开始", default = datetime.datetime.now) @property def payVia(self): return "monthlyPackage" @staticmethod def format_date(date): # type: (datetime.date) -> str """ :param date: :return: """ return "T{}".format(date.strftime("%Y_%m_%d")) @classmethod def get_enable_one(cls, openId, groupId, cardNo = ""): """ 找包月的 openId groupId 一定需要对应上 并且 允许通用 或者不允许通用但是卡号不为空 :return: """ group = Group.get_group(groupId) # type: GroupDict dealer = Dealer.objects.get(id = group.ownerId) groupIds = Dealer.get_currency_group_ids(dealer, group) groupIds.append(groupId) objs = cls.objects.filter( openId = openId, groupId__in = groupIds, isDisable = 0 ).filter( Q(cardNo = cardNo) | Q(bothCardAndMobile = 1) ).order_by('dateTimeAdded') result = filter( lambda obj: obj.useable, map(lambda obj: obj.disable(), objs) ) return result[0] if result else None @classmethod def get_user_all(cls, openId): """ 获取用户所有的包月套餐规则 :param openId: :return: """ return cls.objects.filter(openId = openId, isDisable = 0) @classmethod def create_by_template(cls, template, rechargeOrder): # type:(MonthlyPackageTemp, RechargeRecord) -> MonthlyPackage subIndex = rechargeOrder.attachParas.get("subTempIndex") purchasedMonth = template.subTemplate[subIndex].numOfMonth data = { "name": template.subTemplate[subIndex].displayName, "groupId": rechargeOrder.groupId, "openId": rechargeOrder.openId, "cardNo": rechargeOrder.attachParas.get("cardNo", ""), "bothCardAndMobile": template.bothCardAndMobile, "startDateTime": rechargeOrder.dateTimeAdded, "expireDateTime": rechargeOrder.dateTimeAdded + relativedelta.relativedelta(months = purchasedMonth), "maxCountOfDay": template.maxCountOfDay, "maxCount": template.maxCountOfMonth * purchasedMonth, "maxTimeOfCount": template.maxTimeOfCount, "maxElecOfCount": template.maxElecOfCount, } obj = cls(**data) return obj.save() @property def useable(self): """ :return: """ if self.isDisable: return False # 总次数 if len(self.usedDetail) >= self.maxCount: return False # 0 表示不限制 if self.maxCountOfDay == 0: return True else: # 当日的次数超限 dateStr = self.format_date(datetime.date.today()) dateUseInfo = self.usedDetail.get(dateStr, list()) if len(dateUseInfo) >= self.maxCountOfDay: return False return True def deduct(self, order): # type:(ConsumeRecord)->None """ 使用一次, 扣除一次额度, 根据订单进行扣除 :return: """ orderTime = order.dateTimeAdded # type: datetime.datetime orderInfo = MonthlyPackageUseInfo(orderNo = order.orderNo, orderTime = orderTime) updateData = { "inc__usedTotal": 1, "add_to_set__usedDetail__{}".format(self.format_date(orderTime)): orderInfo } return self.update(**updateData) def rollback(self, order): """ 用于抵扣回退 目前存在与5分钟以内 需要全退的情况 """ orderTime = order.dateTimeAdded # type: datetime.datetime orderInfo = MonthlyPackageUseInfo(orderNo=order.orderNo, orderTime=orderTime) updateData = { "dec__usedTotal": 1, "pull__usedDetail__{}".format(self.format_date(orderTime)): orderInfo } return self.update(**updateData) def disable(self): """ 检查将 过期的包月套餐直接清理掉 :return: """ if self.expireDateTime <= datetime.datetime.now(): self.update(isDisable = 1) return self.reload() return self def is_suit_package(self, package): """ 判断当前的包月套餐是否试用用户选择的套餐 时间和电量的 检验是否足够抵扣 单次的直接抵扣一次 其余的暂不支持 :param package: :return: """ if package.get("unit") == u"分钟": unit = "Time" elif package.get("unit") == u"度": unit = "Elec" elif package.get("unit") == u"次": return True else: return False # 这个地方默认的规则时 如果没有适配到合适的套餐 就直接判定为不能够使用 count = getattr(self, "max{}OfCount".format(unit), 0) # 添加一种情况(当count为0时候) 不做限制,只用次数 if count == 0: return True if count >= int(package.get("time", 0xFFFF)): return True return False def to_dict(self): dayUse = len(self.usedDetail.get(self.format_date(date = datetime.date.today()), list())) return { "id": self.id, "name": self.name, "expireDateTime": self.expireDateTime.strftime("%Y-%m-%d %H:%M:%S"), "dayLeft": self.maxCountOfDay - dayUse, "totalLeft": self.maxCount - self.usedTotal, "maxElecOfCount": self.maxElecOfCount, "maxTimeOfCount": self.maxTimeOfCount, } def get_used_detail(self): usedDetail = list() for _k, _v in self.usedDetail.items(): usedDetail.extend([_m.to_dict() for _m in _v]) usedDetail.sort(key = lambda x: x["orderTime"]) return usedDetail @classmethod def get_user_ticket(cls, openId, groupId, cardNo = ""): group = Group.get_group(groupId) # type: GroupDict dealer = Dealer.objects.get(id = group.ownerId) groupIds = Dealer.get_currency_group_ids(dealer, group) groupIds.append(groupId) objs = cls.objects.filter( openId = openId, groupId__in = groupIds, isDisable = 0 ).filter( Q(cardNo = cardNo) | Q(bothCardAndMobile = 1) ).order_by('dateTimeAdded') return map(lambda obj: obj.disable(), objs) @staticmethod def get_can_use_one(all_tickets, package): # type: (List[MonthlyPackage], dict) -> (MonthlyPackage) for ticket in all_tickets: if ticket.useable and ticket.is_suit_package(package): return ticket return None class Redpack(Searchable): # 红包信息 # TODO 建立索引 factoryCode(索引), openId(索引) # 红包活动类型 class RedpackType(): LAXIN = 'laxin' RUHUI = 'ruhui' class Result(IterConstant): FINISHED = 'finished' PROCESSING = 'processing' openId = StringField(verbose_name='openId', default='') title = StringField(verbose_name='红包标题', default='') factoryCode = StringField(verbose_name='TaskId') redpackType = StringField(verbose_name='红包类型', default='') # 红包自身流程 money = MonetaryField(verbose_name='红包金额', default=RMB('0.00')) leastPayMoney = MonetaryField(verbose_name='最小使用金额', default=RMB('0.00')) effectTime = DateTimeField(verbose_name='红包生效时间') expiredTime = DateTimeField(verbose_name='红包过期时间') usedStatus = BooleanField(verbose_name='是否使用', default=False) usedTime = DateTimeField(verbose_name='使用时间') consumeRecordId = StringField(verbose_name='关联消费订单') package = DictField(verbose_name='选中的套餐') # 来源信息 gateway = StringField(verbose_name='平台', default='') logicalCode = StringField(verbose_name='设备号', default='') devNo = StringField(verbose_name='IMEI', default='') dateTimeAdded = DateTimeField(verbose_name='记录添加时间', default=datetime.datetime.now) extra = DictField(verbose_name='其他', default={}) taskStatus = StringField(verbose_name="任务当前状态", default=Result.PROCESSING) showType = StringField(verbose_name="红包展示方式") meta = { "db_alias": "default" } # 红包业务流程 @classmethod def pre_deducted_coins(cls, redpackId, package): # 预计抵扣的金额计算, 此函数目前只用于蓝牙流程 redpack = cls.objects.filter(id=redpackId, usedStatus=False).first() if not redpack: return 0.0 if redpack.money > RMB(package['price']): return round(RMB(package['coins']), 2) else: ratio = Ratio(float(package['coins']) / float(package['price'])) return round(RMB(redpack.money) * ratio, 2) @property def to_deducted_coins(self): if not self.package: return 0.0 if self.money > RMB(self.package['price']): return round(RMB(self.package['coins']), 2) else: ratio = Ratio(float(self.package['coins']) / float(self.package['price'])) return round(RMB(self.money) * ratio, 2) @property def to_deducted_money(self): if not self.package: return self.money else: if self.money > RMB(self.package['price']): return round(RMB(self.package['price']), 2) else: return round(RMB(self.money), 2) @staticmethod def use_redpack(redPacketId, consumeRecordId, package): # type: (str, str, dict) -> int # 1 校验红包(过期, 状态) nowTime = datetime.datetime.now() return Redpack.objects.filter(id=redPacketId, usedStatus=False).update(usedStatus=True, usedTime=nowTime, consumeRecordId=consumeRecordId, package=package) @classmethod def get_redpack_list(cls, filtetr, rmb=None): """ rmb 当前使用金额, 如果没有的话 例: 红包 满2 - 0.2 最小使用金额必须 """ nowTime = datetime.datetime.now() if not rmb: rmb = RMB(0).mongo_amount exp_time = nowTime + datetime.timedelta(minutes=30) # 给30分钟时间给设备启动留用 redPackets = cls.objects.filter( effectTime__lte=nowTime, expiredTime__gte=exp_time, leastPayMoney__lte=rmb, usedStatus=False, taskStatus=cls.Result.FINISHED, **filtetr ).order_by('expiredTime') if not redPackets: return [] return list(map(lambda x: x.to_dict(), redPackets)) def to_dict(self): return { 'openId': self.openId, 'title': self.title, 'factoryCode': self.factoryCode, 'money': RMB(self.money).mongo_amount, 'leastPayMoney': RMB(self.leastPayMoney).mongo_amount, 'effectTime': self.effectTime, 'expiredTime': self.expiredTime, 'usedStatus': self.usedStatus, 'logicalCode': self.logicalCode, 'consumeRecordId': self.consumeRecordId, 'redpackCoins': self.to_deducted_coins, 'redpackMoney': self.to_deducted_money, 'id': str(self.id), 'taskStatus': self.taskStatus, 'showType': self.showType, } def rollback_redpack(self, redPacketId): redPacket = self.objects.filter(id=redPacketId).first() if not Redpack: return False return redPacket.update(usedStatus=False) @classmethod def can_use(cls, dealer, devTypeCode): # type: (Dealer, str)->bool dealer_features = map(lambda x: x['key'], filter(lambda x: x['value'] == True, dealer.feature_list)) if 'disable_redpack' in dealer_features: return False else: if devTypeCode and devTypeCode in Redpack.list_of_devices_that_support_redpack(): return True return False @classmethod def auto_suit_with_money(cls, query, rmb): """ 自动挑选合适的红包抵扣支付金额 # 有两种判断: 1 红包金额大于支付金额, 优先选择与红包金额接近金额支付的红包使用 2 红包金额小于支付金额, 优先选择大的红包金额 """ try: redpacks = cls.get_redpack_list(query) if redpacks: # 第一种情况做筛选 红包金额大于支付金额, 优先选择与红包金额接近金额支付的红包使用 _enough_list = filter(lambda _: RMB(_.get('money')) >= rmb, redpacks) if _enough_list: return min(_enough_list, key=lambda _: RMB(_.get('money', 0))) # 第二种情况 没有足够支付的金额 选取最大的红包进行抵扣 return max(redpacks, key=lambda _: RMB(_.get('money', 0))) except: import traceback logger.error(traceback.format_exc()) return @classmethod def auto_suit_with_coins(cls, query, userBalance, package): """ 自动挑选合适的红包抵扣金币 # 有三种判断: 1 红包足以 抵扣 挑选最小的红包 2 红包 + 金币 抵扣 挑选最小的红包 2 返回 都不足以抵扣 返回空 """ try: package_money = RMB(package.get('price', 0)) if package_money == RMB(0): return package_coins = RMB(package.get('coins', 0)) ratio = Ratio(float(package.get('coins', 0.0)) / float(package.get('price', 0.0))) redpacks = cls.get_redpack_list(query) if redpacks: # 第一种情况 红包足以 抵扣 挑选最小的红包 _enough_list = filter(lambda _: RMB(_.get('money')) >= package_money, redpacks) if _enough_list: return min(_enough_list, key=lambda _: RMB(_.get('money', 0))) # 第二种情况 红包 + 金币 抵扣 挑选最小的红包 _mix_enough_list = filter(lambda _: RMB(_.get('money', 0)) * ratio + RMB(userBalance) >= package_coins, redpacks) if _mix_enough_list: return min(_mix_enough_list, key=lambda _: RMB(_.get('money', 0))) except: import traceback logger.error(traceback.format_exc()) return @classmethod def get_one(self, redpackId): redpack = Redpack.objects.filter(id=redpackId).first() # type: Redpack if not redpack: return {} else: return redpack.to_dict() @classmethod def show_redpack(cls, openId): nowTime = datetime.datetime.now() exp_time = nowTime - datetime.timedelta(days=3) # 筛选出过期时间超过三天或者 使用后时间超过三天的红包 redPackets = cls.objects.filter(Q(expiredTime__gte=exp_time) | Q(usedTime__gte=exp_time), openId=openId, taskStatus=cls.Result.FINISHED).order_by('-expiredTime') if not redPackets: return [] return list(map(lambda x: x.to_dict(), redPackets)) @staticmethod def list_of_devices_that_support_redpack(): from apps.web.core.models import SystemSettings support_redpack_list = SystemSettings.get_support_redpack_list() return support_redpack_list @staticmethod def add_device_to_support_redpack_list(devTypeCode): from apps.web.core.models import SystemSettings support_redpack_list = SystemSettings.get_support_redpack_list() support_redpack_list.append(devTypeCode) support_redpack_list = list(set(support_redpack_list)) SystemSettings.set_support_redpack_list(support_redpack_list) logger.info('Current support redpck list:{}'.format(support_redpack_list)) @staticmethod def remove_device_to_support_redpack_list(devTypeCode): from apps.web.core.models import SystemSettings support_redpack_list = SystemSettings.get_support_redpack_list() if devTypeCode in support_redpack_list: support_redpack_list.remove(devTypeCode) support_redpack_list = list(set(support_redpack_list)) SystemSettings.set_support_redpack_list(support_redpack_list) logger.info('Current support redpck list:{}'.format(support_redpack_list)) @staticmethod def renew_support_repack_list(devTypeCodeList): from apps.web.core.models import SystemSettings SystemSettings.set_support_redpack_list(devTypeCodeList) # 拉新创建红包*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*- @staticmethod def create_redpack_by_laxin(factoryCode, openId, money, leastPayMoney, effectTime, expiredTime, gateway, logicalCode, devNo, extra): if Redpack.objects.filter(factoryCode=factoryCode, openId=openId).first(): return else: red_packet = { 'factoryCode': factoryCode, 'title': '支付宝每日任务红包', 'openId': openId, 'money': money, 'leastPayMoney': leastPayMoney, 'effectTime': effectTime, 'expiredTime': expiredTime, 'gateway': gateway, 'logicalCode': logicalCode, 'devNo': devNo, 'extra': extra, 'taskStatus': Redpack.Result.FINISHED, 'redpackType': Redpack.RedpackType.LAXIN } try: redpack = Redpack.objects.create(**red_packet) except: return None return redpack # 入会创建红包*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*- @staticmethod def create_redpack_by_ruhui(taskId, openId, urlId, money, leastPayMoney, gateway, logicalCode, devNo, extra, showType=''): redpack = Redpack.objects.filter(factoryCode=taskId, openId=openId, redpackType=Redpack.RedpackType.RUHUI, taskStatus__ne=Redpack.Result.FINISHED).first() if redpack: redpack.update(**{ 'logicalCode': logicalCode, 'devNo': devNo, 'extra': extra, 'taskStatus': Redpack.Result.PROCESSING, 'expiredTime': datetime.datetime.now() + datetime.timedelta(days=7), 'urlId': urlId, 'dateTimeAdded': datetime.datetime.now(), 'showType':showType, }) else: red_packet = { 'factoryCode': taskId, 'title': '支付宝每日任务红包.', 'openId': openId, 'money': money, 'leastPayMoney': leastPayMoney, 'gateway': gateway, 'logicalCode': logicalCode, 'devNo': devNo, 'extra': extra, 'taskStatus': Redpack.Result.PROCESSING, 'expiredTime': datetime.datetime.now() + datetime.timedelta(days=7), 'redpackType': Redpack.RedpackType.RUHUI, 'urlId': urlId, 'showType': showType, } try: redpack = Redpack.objects.create(**red_packet) except: return return redpack @staticmethod def take_effect(openId, urlId, **kw): try: obj = Redpack.objects.filter(openId=openId, urlId=urlId, redpackType=Redpack.RedpackType.RUHUI, taskStatus__ne=Redpack.Result.FINISHED).first() if not obj: return extra = obj.extra extra.update(kw) obj.update( extra=extra, taskStatus=Redpack.Result.FINISHED, effectTime=datetime.datetime.now(), expiredTime=datetime.datetime.now() + datetime.timedelta(days=15) ) return obj.reload() except: import traceback logger.error(traceback.format_exc()) class OneCardGateLog(Searchable): devNo = StringField(verbose_name=u"设备编号") logicalCode = StringField(verbose_name=u"逻辑编号") openId = StringField(verbose_name=u"用户信息") control = IntField(verbose_name=u"进出标记") result = BooleanField(verbose_name=u"操纵结果") dateTimeAdded = DateTimeField(verbose_name=u"添加时间") meta = { 'collection': 'OneCardGateLog', 'db_alias': 'logdata' } # ------------------------------------ 以下为新增 ------------------------ class OrderPackage(dict): def belong_category(self, category): # type: (str) -> bool return self.category == category @property def policyType(self): # type: () -> Optional[str, None] return self.get("policyType") or self.get("category") @property def category(self): # type: () -> str """ 套餐的种类 实际上也是相应的启动方式 """ if self.policyType: return self.policyType unit = self.get("unit") if unit in [u"度"]: return PackageCategory.ELEC if unit in [u"元"]: return PackageCategory.COIN if unit in [u"小时", u"分钟"]: return PackageCategory.TIME return "unknown" @property def isPostpaid(self): return self.get("isPostpaid", False) @property def autoRefund(self): return self.get("autoRefund", False) @property def autoStop(self): return self.get("autoStop", False) @property def minFee(self): return RMB(self.get("minFee", 0)) @property def minAfterStartCoins(self): return RMB(self.get("minAfterStartCoins", 0)) @property def isFree(self): return self.get("isFree", False) @property def price(self): """ 套餐的价格 后付费 即先用后付的情况 显示价格为0 """ if self.isFree or self.isPostpaid: return RMB(0) return RMB(self.get("price", 0)) @property def time(self): # type:()->int """ 套餐的时间单位 最终为 分钟 注意区分是否已经初始化过了 """ value = self.get("m_time") if value: return value # 初始化过程 value = self.get('time', 0) if self.belong_category(PackageCategory.TIME) else 0 if self.get("unit") == u"小时": return int(float(value) * 60) if self.get("unit") == u"分钟": return int(value) return 0 @property def elec(self): """ 套餐的电量单位 最终为 度 """ value = self.get("m_elec") if value: return value return float(self.get('time', 0) if self.belong_category(PackageCategory.ELEC) else 0) @property def coin(self): """ 注意 这个coin不是价格的意思 指的是直接是设备运行的硬币数 例如投币 或者某些直接以金额启动设备的套餐 """ value = self.get("m_coin") if value: return value return int(self.get('time', 0) if self.belong_category(PackageCategory.COIN) else 0) @property def name(self): return self.get("name", "") @property def desc(self): return self.get("desc", "") @property def rules(self): """ 即计费规则 """ return self.get("rules") or None @property def refundProtectTime(self): return self.get("refundProtectTime", 0) @property def dumpDict(self): """ 需要被订单固化的数据 """ return { "category": self.category, "name": self.name, "price": self.price.mongo_amount, "m_time": self.time, "m_elec": self.elec, "m_coin": self.coin, "minAfterStartCoins": self.minAfterStartCoins.mongo_amount, "minFee": self.minFee.mongo_amount, "autoRefund": self.autoRefund, "autoStop": self.autoStop, "rules": self.rules, "isFree": self.isFree, "isPostpaid": self.isPostpaid, "desc": self.desc, "refundProtectTime": self.refundProtectTime } @property def showDict(self): return { "category": self.category, "name": self.name, "time": self.time, "elec": self.elec, "coin": self.coin, "minAfterStartCoins": self.minAfterStartCoins.mongo_amount, "minFee": self.minFee.mongo_amount, "autoRefund": self.autoRefund, "autoStop": self.autoStop, "rules": self.rules, "isFree": self.isFree, "isPostpaid": self.isPostpaid, "desc": self.desc, "refundProtectTime": self.refundProtectTime } class PaymentInfo(dict): @property def deduct_list(self): return self.get("deduct_list") or list() @property def time(self): return self.get("time") @time.setter def time(self, value): self.update({"time": value}) @property def isPaid(self): return self.time is not None @property def via(self): return self.get("via") @property def totalAmount(self): s = RMB(0) for _item in self.deduct_list: s += RMB(_item.get("chargeBalance")) s += RMB(_item.get("bestowBalance")) return s @property def actualAmount(self): """ 排除赠送余额之后的支付金额 """ s = RMB(0) for _item in self.deduct_list: s += RMB(_item.get("chargeBalance")) return s @property def bestowAmount(self): s = RMB(0) for _item in self.deduct_list: s += RMB(_item.get("bestowBalance")) return s class ServiceInfo(dict): """ 和设备运行的一切相关信息 """ @property def deviceStartTime(self): return self.get(ConsumeOrderServiceItem.START_TIME) @deviceStartTime.setter def deviceStartTime(self, value): if isinstance(value, datetime.datetime): value = value.strftime("%Y-%m-%d %H:%M:%S") self.update({ConsumeOrderServiceItem.START_TIME: value}) @property def elec(self): return self.get(ConsumeOrderServiceItem.ELEC) @elec.setter def elec(self, value): self.update({ConsumeOrderServiceItem.ELEC: float(value)}) @property def duration(self): return self.get(ConsumeOrderServiceItem.DURATION) @duration.setter def duration(self, value): self.update({ConsumeOrderServiceItem.DURATION: int(value)}) @property def deviceEndTime(self): return self.get(ConsumeOrderServiceItem.END_TIME) @deviceEndTime.setter def deviceEndTime(self, value): if isinstance(value, datetime.datetime): value = value.strftime("%Y-%m-%d %H:%M:%S") self.update({ConsumeOrderServiceItem.END_TIME: value}) @property def maxPower(self): return self.get(ConsumeOrderServiceItem.MAX_POWER) @maxPower.setter def maxPower(self, value): self.update({ConsumeOrderServiceItem.MAX_POWER: value}) @property def reason(self): return self.get(ConsumeOrderServiceItem.REASON) @reason.setter def reason(self, value): self.update({ConsumeOrderServiceItem.REASON: value}) @property def spendMoney(self): return RMB(self.get(ConsumeOrderServiceItem.SPEND, 0)) @spendMoney.setter def spendMoney(self, value): self.update({ConsumeOrderServiceItem.SPEND: RMB(value)}) class ConsumeRecord(Searchable): """ 用户的消费订单记录 """ class Status(IterConstant): CREATED = 'created' # 订单创建初始状态 刚刚下单 WAIT_CONF = 'waitConf' # 用户下完单之后【等待】用户进一步的动作确认 FINISHED = 'finished' # 订单的结束状态 表示相应信息已经被记录 WAITING = 'waiting' # 订单的执行等待状态 设备尚未执行该订单 RUNNING = 'running' # 订单执行运行状态 设备正在执行该订单 END = "end" # 订单运行结束状态 设备已经执行订单完毕 订单完结 TIMEOUT = 'timeout' # 订单启动超时状态 设备启动超时 有可能已经启动了 FAILURE = 'failure' # 订单执行失败状态 设备明确启动失败 订单已经完结 UNKNOWN = 'unknown' # 订单的未知状态 未知的订单状态 WAIT_PAY = 'waitPay' # 订单支付的中间状态 orderNo = StringField(verbose_name=u"订单号", required=True, unique=True) sequenceNo = StringField(verbose_name=u"流水号", unique=True) logicalCode = StringField(verbose_name=u"设备编号", required=True) groupId = StringField(verbose_name=u"设备地址编号", default=None) price = MonetaryField(verbose_name=u"订单价格", default=RMB('0')) startType = IntField(verbose_name=u"启动方式", choices=StartDeviceType.choices()) status = StringField(verbose_name=u"状态", default=Status.CREATED) ownerId = StringField(verbose_name=u"经销商", required=True) remarks = StringField(verbose_name=u"备注(系统、用户)", default='') description = StringField(verbose_name=u"订单描述(错误信息)") association = DictField(verbose_name=u'关联单', default={}) serviceInfo = DictField(verbose_name=u"订单的服务信息", default={}) isFree = BooleanField(verbose_name=u"是否订单免费", default=False) dateTimeAdded = DateTimeField(verbose_name=u"创建时间", default=datetime.datetime.now) finishedTime = DateTimeField(verbose_name=u"结束时间") cardId = StringField(verbose_name=u"卡ID") openId = StringField(verbose_name=u"用户", required=True) nickname = StringField(verbose_name=u'用户昵称', default='') # 以下信息属于订单快照 防止引用被修改 devNo = StringField(verbose_name=u"设备ID", required=True) port = IntField(verbose_name=u"启动端口") devTypeName = StringField(verbose_name=u"设备类型名称", default=None) devTypeCode = StringField(verbose_name=u"设备类型编码", default=None) address = StringField(verbose_name=u"设备地址", default=None) groupNumber = StringField(verbose_name=u"设备", default=None) groupName = StringField(verbose_name=u"交易场地", default=None) # 退款状态应该是用户下单的时候即被锁定 和设备、支付、套餐有关系 startPackage = DictField(verbose_name=u'启动套餐', required=True) paymentInfo = DictField(verbose_name=u'支付的相关信息', default={}) refundInfo = DictField(verbose_name=u"退还的相关信息", default={}) dailyStats = LazyReferenceField(verbose_name="统计关联单", document_type='DealerGroupStats') feedbackId = ObjectIdField(verbose_name=u'用户反馈ID') search_fields = ('openId', 'devNo', 'orderNo', 'remarks', 'logicalCode') _shard_key = ('ownerId', 'dateTimeAdded') _origin_meta = { "collection": "ConsumeRecord", "db_alias": "default" } meta = _origin_meta def __str__(self): return '{}'.format(self.__class__.__name__, str(self.id), self.orderNo) @cached_property def owner(self): # 防止id报错 if not self.ownerId: return None from apps.web.dealer.models import Dealer dealer = Dealer.objects(id=self.ownerId).first() return dealer @cached_property def user(self): # type:() -> MyUser return MyUser.objects.filter(openId=self.openId, groupId=self.groupId).first() @cached_property def card(self): return Card.objects.filter(id=self.cardId).first() @property def coin(self): return self.price @property def package(self): # type: () -> OrderPackage return OrderPackage(self.startPackage) @property def device(self): # type:()-> DeviceDict return Device.get_dev(self.devNo) @property def group(self): # type:() -> GroupDict return Group.get_group(self.groupId) @property def startLockKey(self): return "{}-{}-start-device-lock".format(self.openId, self.orderNo) @property def isPayTimeOut(self): """ 订单到支付下单的一个过程 """ now = datetime.datetime.now() return (now - self.dateTimeAdded).total_seconds() > CONSUME_ORDER_PAY_TIMEOUT @property def isPostPaid(self): """ True 后付费订单 使用完给钱 False 预付费订单 先给钱再使用然后再决定是否退款 """ return self.package.isPostpaid @property def isStartNetPay(self): return self.startType == StartDeviceType.ON_LIEN @property def isStartCardPay(self): return self.startType == StartDeviceType.CARD @property def payer(self): if self.startType == StartDeviceType.ON_LIEN: return self.user else: return self.card @property def payment(self): # type: () -> PaymentInfo return PaymentInfo(self.paymentInfo) @property def refund(self): # type: () -> PaymentInfo return PaymentInfo(self.refundInfo) @property def service(self): # type:() -> ServiceInfo return ServiceInfo(self.serviceInfo) @property def device_start_time(self): return self.service.deviceStartTime @property def device_end_time(self): return self.service.deviceEndTime @property def isPaid(self): if self.isFree: return True return self.payment.isPaid @property def actualAmount(self): """ 用户订单实际消费的金额 """ return self.payment.actualAmount - self.refund.actualAmount @property def detail_link(self): if self.device.deviceAdapter.support_count_down(self.openId, self.port): return concat_count_down_page_url(devNo=self.devNo, port=self.port) else: return concat_front_end_url(uri='/user/index.html#/user/consumeDetail?id={}'.format(str(self.id))) @property def receiptDesc(self): rv = { 'orderNo': self.orderNo, 'createdTime': self.dateTimeAdded, 'payment': u'{} 元'.format(self.price) } if self.isFree: rv.update({"payment": u"本次免费"}) if self.isPostPaid: rv.update({"payment": u"先用后付"}) return rv @property def startKey(self): return self.orderNo @property def actualAmount(self): return self.payment.actualAmount - self.refund.actualAmount @property def bestowAmount(self): return self.payment.bestowAmount - self.refund.bestowAmount @property def isNormal(self): """ 是否为正常订单 """ if self.status in [self.Status.FAILURE, self.Status.UNKNOWN, self.Status.TIMEOUT]: return False if self.status in [self.Status.WAIT_CONF, self.Status.CREATED]: return False return True @property def device_identity_info(self): return { 'logicalCode': self.logicalCode, 'devTypeCode': self.devTypeCode, 'devTypeName': self.devTypeName, 'groupName': self.groupName, 'groupNumber': self.groupNumber, 'address': self.address, 'groupId': self.groupId } @classmethod def make_no(cls, *args, **kwargs): """ 有可能会重复 需要保证唯一性 """ timestamp = generate_timestamp_ex() random_int = random.randint(1, 1000) return "%d%03d" % (timestamp, random_int) @classmethod def new_one(cls, orderNo, user, device, context): # type:(str, MyUser, DeviceDict, StartParamContext) -> ConsumeRecord order = cls( orderNo=orderNo, sequenceNo=context.sequence, logicalCode=device.logicalCode, groupId=device.groupId, price=context.package.price, startType=context.startType, ownerId=device.ownerId, openId=user.openId, cardId=context.cardId, nickname=user.nickname, devNo=device.devNo, port=context.port, devTypeName=device.devTypeName, devTypeCode=device.devTypeCode, address=device.group.address, groupNumber=device.groupNumber, groupName=device.group.groupName, startPackage=context.package.dumpDict, isFree=context.package.isFree, autoRefund=context.package.autoRefund ) return order.save() def update_payment(self, payment): # type:(dict) -> bool """支付信息添加""" if self.isPaid: return False self.paymentInfo = payment return self.save() def frozen_payer_balance(self): """ 执行订单的支付过程 实际上是将用户的金额冻结 """ if not self.payment: return if self.isPaid: return payer, payment = self.payer, self.payment result = payer.__class__.freeze_balance(str(self.id), self.payment) if not result: return # 更新一次付款信息 payment.time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") self.update_payment(payment) payer.account_consume(self) def clear_payer_frozen(self, refundMoney=RMB(0)): from apps.web.user.utils2 import generate_refund refundInfo = PaymentInfo(generate_refund(self, refundMoney)) result = self.payer.__class__.clear_frozen_balance( str(self.id), self.payment, refundInfo ) if not result: return if refundMoney == RMB(0): return refundInfo.time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") self.refundInfo = refundInfo self.save() self.payer.account_refund(self) def link_state(self, state): self.dailyStats = state self.finishedTime = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") self.save() def to_user_detail(self): data = { "id": str(self.id), "orderNo": self.orderNo, "ownerId": self.ownerId, "money": self.price, "createdTime": self.dateTimeAdded, "completionTime": self.finishedTime, "startResult": True if self.device_start_time else False, "errorDesc": self.description, "deviceStatTime": self.device_start_time, "deviceFinishedTime": self.device_end_time, "isRefund": True if self.refund.time else False, "orderStatus": self.status } if self.refund.time: data.update({ "refundedMoney": self.refund.totalAmount }) data.update(self.device_identity_info) return data def to_detail(self): data = { "id": str(self.id), 'orderNo': self.orderNo, 'createdTime': self.dateTimeAdded, 'completionTime': self.finishedTime, 'deviceStatTime': self.device_start_time, 'deviceFinishedTime': self.device_end_time, 'amount': self.price, 'openId': self.openId, 'groupId': self.groupId, 'userNickname': u'用户' if not self.nickname else self.nickname, 'ownerId': self.ownerId, 'devNo': self.devNo, 'logicalCode': self.logicalCode, 'groupName': self.groupName, 'address': self.address, 'groupNumber': self.groupNumber, 'devType': self.devTypeName, # 兼容 'devTypeName': self.devTypeName, 'remarks': self.remarks, 'startResult': 'success' if self.isNormal else 'failed', 'errorDesc': self.description, 'servicedInfo': [ u'%s: %s'.encode('utf-8') % (GLOSSARY_TRANSLATION.get(k, k), v) for k, v in self.service.iteritems() ], # 以下字段和以前做兼容 'finishedTime': self.device_end_time, 'port': self.port } return data class BalanceLog(Searchable): meta = { 'abstract': True, } bAmount = MonetaryField(verbose_name=u"变动前充值余额", required=True) bBestowAmount = MonetaryField(verbose_name=u"变动前赠送余额", required=True) aAmount = MonetaryField(verbose_name=u"变动后充值余额", required=True) aBestowAmount = MonetaryField(verbose_name=u"变动后赠送金额", required=True) # 充值订单 1 退费订单 2 消费订单 3 category = IntField(verbose_name=u"变动类型", choices=UserBalanceChangeCategory.choices()) sourceObj = GenericLazyReferenceField(verbose_name=u'资金变动来源', required=True) dateTimeAdded = DateTimeField(verbose_name=u"变动时间", default=datetime.datetime.now) @cached_property def source(self): return self.sourceObj.fetch() @property def beforeBalance(self): return self.bAmount + self.bBestowAmount @property def afterBalance(self): return self.aBestowAmount + self.aAmount def to_dict(self): data = { "id": str(self.id), "category": self.category, "beforeBalance": self.beforeBalance, "afterBalance": self.afterBalance, "dateTimeAdded": self.dateTimeAdded.strftime("%Y-%m-%d %H:%M:%S"), "sourceId": str(self.sourceObj.id) } return data class UserBalanceLog(BalanceLog): """ 用户账户余额变化记录 """ openId = StringField(verbose_name=u"资金主体标识", required=True) productAgentId = StringField(verbose_name=u"平台标识", required=True) @classmethod def consume(cls, user, afterAmount, afterBestowAmount, consumeAmount, consumeBestowAmount, order): # type: (MyUser, RMB, RMB, RMB, RMB, ConsumeRecord) -> UserBalanceLog return cls( openId=user.openId, productAgentId=user.productAgentId, bAmount=afterAmount+consumeAmount, bBestowAmount=afterBestowAmount+consumeBestowAmount, aAmount=afterAmount, aBestowAmount=afterBestowAmount, sourceObj=order, category=UserBalanceChangeCategory.CONSUME ).save() @classmethod def recharge(cls, user, afterAmount, afterBestowAmount, chargeAmount, chargeBestowAmount, order): return cls( openId=user.openId, productAgentId=user.productAgentId, bAmount=afterAmount-chargeAmount, bBestowAmount=afterBestowAmount-chargeBestowAmount, aAmount=afterAmount, aBestowAmount=afterBestowAmount, sourceObj=order, category=UserBalanceChangeCategory.RECHARGE ).save() @classmethod def refund(cls, user, afterAmount, afterBestowAmount, refundAmount, refundBestowAmount, order): return cls( openId=user.openId, productAgentId=user.productAgentId, bAmount=afterAmount-refundAmount, bBestowAmount=afterBestowAmount-refundBestowAmount, aAmount=afterAmount, aBestowAmount=afterBestowAmount, sourceObj=order, category=UserBalanceChangeCategory.REFUND ).save() @classmethod def get_logs(cls, user, pageIndex, pageSize): # type:(MyUser, int, int) -> QuerySet return cls.objects.filter( openId=user.openId, productAgentId=user.productAgentId ).skip((pageIndex-1)*pageSize).limit(pageSize) class CardBalanceLog(BalanceLog): cardId = StringField(verbose_name=u"资金主体标识", required=True) openId = StringField(verbose_name=u"卡的持有人", required=True) @classmethod def consume(cls, card, afterAmount, afterBestowAmount, consumeAmount, consumeBestowAmount, order): # type: (Card, RMB, RMB, RMB, RMB, ConsumeRecord) -> UserBalanceLog return cls( cardId=str(card.id), openId=card.openId, bAmount=afterAmount + consumeAmount, bBestowAmount=afterBestowAmount + consumeBestowAmount, aAmount=afterAmount, aBestowAmount=afterBestowAmount, sourceObj=order, category=UserBalanceChangeCategory.CONSUME ).save() @classmethod def recharge(cls, card, afterAmount, afterBestowAmount, chargeAmount, chargeBestowAmount, order): return cls( cardId=str(card.id), openId=card.openId, bAmount=afterAmount - chargeAmount, bBestowAmount=afterBestowAmount - chargeBestowAmount, aAmount=afterAmount, aBestowAmount=afterBestowAmount, sourceObj=order, category=UserBalanceChangeCategory.RECHARGE ).save() @classmethod def refund(cls, card, afterAmount, afterBestowAmount, refundAmount, refundBestowAmount, order): return cls( cardId=str(card.id), openId=card.openId, bAmount=afterAmount - refundAmount, bBestowAmount=afterBestowAmount - refundBestowAmount, aAmount=afterAmount, aBestowAmount=afterBestowAmount, sourceObj=order, category=UserBalanceChangeCategory.REFUND ).save() @classmethod def get_logs(cls, card, pageIndex, pageSize): return cls.objects.filter( cardId=str(card.id) ).skip((pageIndex - 1) * pageSize).limit(pageSize)