# -*- coding: utf-8 -*- # !/usr/bin/env python import base64 import copy import datetime import hashlib import hmac import json import logging import time from calendar import monthrange from bson.objectid import ObjectId from dateutil.relativedelta import relativedelta from django.conf import settings from django.utils.module_loading import import_string from mongoengine import DoesNotExist from typing import Any, Tuple, TYPE_CHECKING, Optional, Union from apilib.monetary import Percent, RMB, Ratio from apps.web.agent.models import Agent from apps.web.core.exceptions import InsufficientFundsError from apps.web.dealer.constant import TodoTypeEnum, TodoDone from apps.web.dealer.define import DEALER_INCOME_SOURCE, DEALER_INCOME_TYPE from apps.web.dealer.models import DealerRechargeRecord, ItemType, Dealer, VirtualCard, TodoMessage from apps.web.device.models import Group, Device, StockRecord from apps.web.management.models import Manager from apps.web.utils import DealerLoginPageResponseRedirect, DealerMainPageResponseRedirect, \ SubAccountLoginResponseRedirect, concat_front_end_url if TYPE_CHECKING: from library.memcache_backend import CustomizedMemcachedCacheBackend from apps.web.core.payment import WechatPaymentGateway from apps.web.device.models import DeviceRentOrder logger = logging.getLogger(__name__) def gen_login_response(agentId): response = DealerLoginPageResponseRedirect(register = True) if agentId: agent = Agent.objects(id = str(agentId)).get() if agent.featureToggles.has_key('forbiddenDealerRegister') and agent.featureToggles['forbiddenDealerRegister']: response = DealerLoginPageResponseRedirect(register = False) if agent.is_primary: response.set_cookie(key = 'dealer_login_agentid', value = '', max_age = 0, expires = 'Thu, 01-Jan-1970 00:00:00 GMT', domain = settings.COOKIE_DOMAIN) response.set_cookie(key = 'dealer_login_agentid', value = '', max_age = 0, expires = 'Thu, 01-Jan-1970 00:00:00 GMT') response.set_cookie(key = 'dealer_login_managerid', value = agent.managerId, max_age = 24 * 3600 * 30, domain = settings.COOKIE_DOMAIN) else: response.set_cookie(key = 'dealer_login_managerid', value = '', max_age = 0, expires = 'Thu, 01-Jan-1970 00:00:00 GMT', domain = settings.COOKIE_DOMAIN) response.set_cookie(key = 'dealer_login_managerid', value = '', max_age = 0, expires = 'Thu, 01-Jan-1970 00:00:00 GMT') response.set_cookie(key = 'dealer_login_agentid', value = str(agent.id), max_age = 24 * 3600 * 30, domain = settings.COOKIE_DOMAIN) response = agent.put_cookie(response) return response def gen_subaccount_login_response(agentId): response = SubAccountLoginResponseRedirect(agentId) if agentId: response.set_cookie(key = 'dealer_login_agentid', value = str(agentId), max_age = 24 * 3600 * 30, domain = settings.COOKIE_DOMAIN) return response def gen_home_response(agentId): response = DealerMainPageResponseRedirect() response = Agent.record_cookie(agentId, response) return response INCOME_SOURCE_ALIASES = { DEALER_INCOME_SOURCE.RECHARGE: 'apps.web.common.proxy.ClientRechargeModelProxy', DEALER_INCOME_SOURCE.RECHARGE_CARD: 'apps.web.common.proxy.ClientRechargeModelProxy', DEALER_INCOME_SOURCE.RECHARGE_VIRTUAL_CARD: 'apps.web.common.proxy.ClientRechargeModelProxy', DEALER_INCOME_SOURCE.INSURANCE: 'apps.web.common.proxy.ClientRechargeModelProxy', DEALER_INCOME_SOURCE.AD: 'apps.web.ad.models.AdRecord', DEALER_INCOME_SOURCE.REDPACK: 'apps.web.common.proxy.ClientRechargeModelProxy', DEALER_INCOME_SOURCE.AUTO_SIM: 'apps.web.common.proxy.ClientRechargeModelProxy', DEALER_INCOME_SOURCE.REFUND_CASH: 'apps.web.common.proxy.ClientRechargeModelProxy' } _income_source = {} def resolve_income_source(channel = None): try: channel = INCOME_SOURCE_ALIASES[channel] except KeyError: if '.' not in channel and ':' not in channel: from kombu.utils.text import fmatch_best alt = fmatch_best(channel, INCOME_SOURCE_ALIASES) if alt: raise KeyError( 'No such income channel: {0}. Did you mean {1}?'.format( channel, alt)) raise KeyError('No such transport: {0}'.format(channel)) return import_string(channel) def get_income_source_cls(transport = None): # type: (str)->Any if transport not in _income_source: _income_source[transport] = resolve_income_source(transport) return _income_source[transport] # keys DEALER_DAILY_INCOME_CACHE_KEY = 'd-income:{dealerId}:source:{source}:date:{year:d}-{month:d}-{day:d}' DEALER_MONTHLY_INCOME_CACHE_KEY = 'd-income:{dealerId}:source:{source}:date:{year:d}-{month:d}' DEALER_CONSUMPTION_CACHE_KEY = 'd-consumption:{dealerId}:source:{source}:date:{date}' def dealer_daily_income_cache_key(dealerId, source, year, month, day): # type: (ObjectId, str, int, int, int)->str """ :param dealerId: :param source: :param year: :param month: :param day: :return: """ return DEALER_DAILY_INCOME_CACHE_KEY.format(dealerId = str(dealerId), source = str(source), year = year, month = month, day = day) def dealer_monthly_income_cache_key(dealerId, source, year, month): """ :param dealerId: :param source: :param year: :param month: :return: """ return DEALER_MONTHLY_INCOME_CACHE_KEY.format(dealerId = str(dealerId), source = str(source), year = year, month = month) def get_month_range(year, month): # type: (int, int)->Tuple[str, str] """ :param year: :param month: :return: """ DATE_FMT_TEXT = '{year}-{month}-{day}' _, end = monthrange(year = int(year), month = int(month)) return ( # start DATE_FMT_TEXT.format(year = year, month = month, day = 1), # end DATE_FMT_TEXT.format(year = year, month = month, day = end) ) def dealer_consumption_cache_key(dealerId, source, date): # type: (ObjectId, str, str)->str """ :param dealerId: :param source: :param date: :return: """ return DEALER_CONSUMPTION_CACHE_KEY.format(dealerId = str(dealerId), source = str(source), date = str(date)) def get_devices_count_by_owner(cache, ownerId): # type: (CustomizedMemcachedCacheBackend, str)->Tuple[int, int, int] """ :param ownerId: :return: """ def get_devices_by_dealer(dealer): # type:(Dealer)->list groupIds = Group.get_group_ids_of_dealer_and_partner(str(dealer.id)) devices = Device.get_devices_by_group(groupIds, verbose = True) return devices.values() def is_pure_partner(agentId): # type:(str)->bool agent = Agent.objects(id = agentId).get() # type: Agent manager = Manager.objects(id = agent.managerId).get() # type: Manager isPurePartner = manager.supports('partners_are_pure') return isPurePartner def create_partner(username, password, agentId, nickname, annualTrafficCost, agentProfitShare): # type:(str, str, str, str, Percent)->Dealer """ :param username: :param password: :param agentId: :param nickname: :param agentProfitShare: :return: """ return Dealer.create_user(username = username, password = password, agentId = agentId, nickname = nickname, adShow = True, noAdPolicy = 'banner', annualTrafficCost = annualTrafficCost, agentProfitShare = agentProfitShare, isPurePartner = is_pure_partner(agentId)) def update_partner(dealerId, agentId, **kwargs): """ :param dealerId: :param agentId: :param kwargs: :return: """ return Dealer.update_dealer(dealerId, isPurePartner = is_pure_partner(agentId), **kwargs) def consume_stock(devObj, consumeCount, detail = ''): countAll = 0 devNo = devObj.devNo consumptionQuantity = getattr(devObj, 'consumptionQuantity', 0) consumptionQuantity += consumeCount for k, v in devObj.stockDetailDict.items(): # 兑币机实际只有一个品类,硬币 if v >= consumeCount: v = v - consumeCount else: return False devObj.stockDetailDict[k] = v countAll += v try: itemObj = ItemType.objects.get(id = k) except Exception, e: logger.info('get itemtype error=%s' % e) return False more = '%s' % itemObj.title if not detail else '%s(%s)' % (itemObj.title, detail) if len(devObj.stockDetailDict) == 0: # 要考虑兑币机这种没有品类的设备 countAll = getattr(devObj, 'quantity') - consumeCount more = u'出币' try: devObj.save() except Exception, e: logger.info('save obj error,devNo=%s' % devNo) return False result = Device.update_field(dev_no = devNo, update = True, quantity = countAll, consumptionQuantity = consumptionQuantity) if not result: logger.info('update field failed,devNo=%s' % devNo) return False stockTime = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') StockRecord.get_collection().insert( {'logicCode': devObj.logicalCode, 'imei': devNo, 'stockType': 'consume', 'stockTime': stockTime, 'number': consumeCount, 'more': more}) return True # 经销商创建虚拟卡(线下) class VirtualCardBuilder(object): """ #创建虚拟卡空卡可以指定数量 """ @staticmethod def create_virtual_card(dealer, vCard, attachParas = {}, accounting = False): """ 创建虚拟卡号,此卡的 """ from apps.web.user.models import UserVirtualCard num = attachParas.get('number') cardId = attachParas.get('card_id') lis = [] for i in xrange(int(num)): user_card = UserVirtualCard( cardNo = UserVirtualCard.make_no(), cardTypeId = str(cardId), openIds = [], cardName = vCard.cardName, ownerOpenId = vCard.ownerId, # nickname=dealer.nickname, dealerId = vCard.ownerId, groupIds = vCard.groupIds, devTypeList = vCard.devTypeList, price = vCard.price, periodDays = vCard.periodDays, expiredTime = datetime.datetime.now() + datetime.timedelta(seconds = vCard.periodDays * 24 * 3600), startTime = datetime.datetime.now(), dayQuota = vCard.dayQuota, userLimit = 1, quota = vCard.quota, # userDesc=vCard.userDesc, remark = '经销商开卡', status = 'nonactivated' ) user_card.nickname = user_card.cardNo user_card.save() lis.append(user_card.cardNo) return lis @staticmethod def find_dealer_virtual_card(dealerId, status = 'nonactivated'): """ #查询经销商开的空卡 :param dealerId: :param status: :return: """ from apps.web.user.models import UserVirtualCard virtual_cards = UserVirtualCard.objects.filter(ownerOpenId = dealerId, dealerId = dealerId, status = status) return virtual_cards @staticmethod def active_virtual_card(VirtualCardId): """ 经销商给实体卡绑定后激活卡片 :param VirtualCardId: :return: """ result = False from apps.web.user.models import UserVirtualCard try: virtual_card = UserVirtualCard.objects.get(id = VirtualCardId) card_model = VirtualCard.objects.get(id = virtual_card.cardTypeId) except Exception as e: logger.info('VirtualCardBuilder_no this virtual_card cardNo:%s' % VirtualCardId) return result virtual_card.startTime = datetime.datetime.now virtual_card.expiredTime = datetime.datetime.now() + datetime.timedelta( seconds = card_model.periodDays * 24 * 3600) virtual_card.status = 'normal' virtual_card.save() return True @staticmethod def unbind_virtual_card(card): """ 解绑虚拟卡,并改变虚拟卡状态 :param card: :return: """ result = False try: virtual_card = card.bound_virtual_card if datetime.datetime.now() > virtual_card.expiredTime: virtual_card.status = 'expired' else: virtual_card.status = 'used' virtual_card.save() card.boundVirtualCardId = None card.save() result = True except Exception as e: logger.error('card:{},error={}'.format(card.cardNo, e)) pass return result def create_dealer_sim_charge_order(payment_gateway, dealer, devItems, payType = "pay", can_used_balance = None): from apps.web.common.transaction import DealerPaySubType from apps.web.core import ROLE sub_type = DealerPaySubType.SIM_CARD if payType == 'auto': sub_type = DealerPaySubType.AUTO_SIM_CARD elif payType == 'manual': sub_type = DealerPaySubType.MANUAL_SIM_CARD subServiceName = DealerRechargeRecord.get_product(sub_type)['desc'] dealerId = dealer.bossId curr_annualTrafficCost = dealer.annualTrafficCost total_fee = int(0) total_agent_earning_fee = int(0) total_manager_earning_fee = int(0) agent = Agent.objects(id = dealer.agentId).first() # type: Optional[Agent] if not agent: logger.error('agent is not exist.'.format(dealer.agentId)) return None # 代理商的成本价. 代理商给经销商设置流量卡费用必须大于等于这个价格 agentAnnualTrafficCost = agent.annualTrafficCost agentAnnualTrafficCostFen = int((agentAnnualTrafficCost * 100)) primeAgent = agent.primary_agent name = u'%s 设备号' % subServiceName items = [] devNo = None for item in devItems: if isinstance(item, Device): dev = item else: dev = Device.get_dev(item) if not dev: logger.error('{} is not exist.'.format(item)) continue devNo = dev.devNo name = '%s %s' % (name, dev.logicalCode) dev_obj = Device.objects.get(devNo = devNo) trafficCardCost = dev_obj.trafficCardCost if not trafficCardCost: trafficCardCost = getattr(dealer, 'trafficCardCost', None) if not trafficCardCost: trafficCardCost = agent.trafficCardCost platform_cost_fen = RMB.yuan_to_fen(trafficCardCost) if trafficCardCost > curr_annualTrafficCost: annualTrafficCost = trafficCardCost else: annualTrafficCost = curr_annualTrafficCost annualTrafficCostFen = RMB.yuan_to_fen(annualTrafficCost) total_fee += annualTrafficCostFen group = Group.get_group(dev['groupId']) # 可以分给代理商和厂商的钱 total_earn_fee = annualTrafficCostFen - platform_cost_fen # 分给代理商的钱 = 经销商充值费用 - 厂商给代理商配置的成本价格. 如果该值小于0就不分 # 如果该值大于total_earn, 就把total_earn全部分给他 agent_earning_fee = (annualTrafficCostFen - agentAnnualTrafficCostFen) \ if (annualTrafficCostFen - agentAnnualTrafficCostFen) > 0 else 0 if agent_earning_fee > total_earn_fee: agent_earning_fee = total_earn_fee manager_earning_fee = total_earn_fee - agent_earning_fee total_agent_earning_fee += agent_earning_fee total_manager_earning_fee += manager_earning_fee item_payload = { 'name': u'%s %s %s %s' % (dev['devType']['name'], dev['logicalCode'], group['address'], subServiceName), 'devNo': devNo, 'iccid': dev.iccid, 'price': str(annualTrafficCost.amount), 'dealerPrice': str(curr_annualTrafficCost.amount), 'cost': str(trafficCardCost.amount), 'currCost': str(agent.trafficCardCost.amount), 'agentCost': str(agentAnnualTrafficCost.amount), 'number': 1, 'partition': [ { 'role': ROLE.agent, 'id': str(agent.id), 'earned': agent_earning_fee }, { 'role': ROLE.manager, 'id': str(primeAgent.id), 'earned': manager_earning_fee } ]} items.append(item_payload) payload = { 'items': items, 'name': name, 'dealerId': str(dealerId), 'nickname': dealer.nickname, 'totalFee': 0 if payType == 'manual' else total_fee, 'settleInfo': { }, 'wxOrderNo': '', 'attachParas': {} } if str(agent.id) == str(primeAgent.id): payload['settleInfo']['partition'] = [ { 'id': str(agent.id), 'earned': total_agent_earning_fee + total_manager_earning_fee } ] else: payload['settleInfo']['partition'] = [ { 'id': str(agent.id), 'earned': total_agent_earning_fee }, { 'id': str(primeAgent.id), 'earned': total_manager_earning_fee } ] if payType == 'auto': costMoney = RMB(total_fee / 100.0) if costMoney > can_used_balance: raise InsufficientFundsError() record = DealerRechargeRecord.issue( sub_type, payment_gateway, dealer, devNo, **payload) if record: return record return None def create_dealer_sim_charge_verify_order(dealer, partitions, recharge_order): from apps.web.common.transaction import DealerPaySubType total_fee = 0 for partition in partitions: total_fee += partition['earned'] from apps.web.helpers import get_platform_reconcile_pay_gateway payment_gateway = get_platform_reconcile_pay_gateway() sub_type = DealerPaySubType.SIM_ORDER_VERIFY payload = { 'items': None, 'name': DealerRechargeRecord.get_product(sub_type)['desc'], 'dealerId': str(dealer.id), 'nickname': dealer.nickname, 'totalFee': total_fee, 'settleInfo': {'partition': partitions}, 'wxOrderNo': '', 'attachParas': {'rechargeId': str(recharge_order.id)} } record = DealerRechargeRecord.issue(sub_type, payment_gateway, dealer, **payload) if record: return record return None def create_dealer_charge_order_for_api(dealer, **kw): from apps.web.common.transaction import DealerPaySubType from apps.web.core import ROLE from apps.web.helpers import get_inhourse_wechat_env_pay_gateway apiDevicePerCost = dealer.api_app.apiDevicePerCost # type: RMB # 单价 subServiceName = DealerRechargeRecord.get_product(DealerPaySubType.API_COST)['desc'] dealerId = dealer.bossId name = u'%s' % subServiceName dev_count = kw.get('needQuota', 0) total_yuan = apiDevicePerCost * Ratio(dev_count) total_fee = RMB.yuan_to_fen(total_yuan) payment_gateway = get_inhourse_wechat_env_pay_gateway( ROLE.dealer) # type: Union[WechatPaymentGateway] agent = payment_gateway.occupant items = [ kw ] payload = { 'items': items, 'name': name, 'dealerId': str(dealerId), 'nickname': dealer.nickname, 'totalFee': total_fee, 'settleInfo': { 'partition': [ { 'id': str(agent.id), 'earned': total_fee } ] }, 'wxOrderNo': '', 'attachParas': {} } record = DealerRechargeRecord.issue(DealerPaySubType.API_COST, payment_gateway, dealer, **payload) if record: return record return None def create_dealer_charge_order_for_disable_ad(dealer, devList): from apps.web.common.transaction import DealerPaySubType from apps.web.core import ROLE from apps.web.helpers import get_inhourse_wechat_env_pay_gateway disableAdCost = dealer.disable_ad_plan.disableAdCost # type: RMB # 单价 cycle = dealer.disable_ad_plan.cycle # type: RMB # 事件周期 subServiceName = DealerRechargeRecord.get_product(DealerPaySubType.DISABLE_AD)['desc'] dealerId = dealer.bossId payment_gateway = get_inhourse_wechat_env_pay_gateway( ROLE.dealer) # type: Union[WechatPaymentGateway] agent = payment_gateway.occupant dev_count = len(devList) name = u'添加%s(%s台)' % (subServiceName, dev_count) total_yuan = disableAdCost * Ratio(dev_count) total_fee = RMB.yuan_to_fen(total_yuan) payload = { 'items': devList, 'name': name, 'dealerId': str(dealerId), 'nickname': dealer.nickname, 'totalFee': total_fee, 'settleInfo': { 'partition': [ { 'id': str(agent.id), 'earned': total_fee } ] }, 'wxOrderNo': '', 'attachParas': {} } record = DealerRechargeRecord.issue(DealerPaySubType.DISABLE_AD, payment_gateway, dealer, **payload) if record: return record return None class DealerSessionBuilder(object): OPER_ID = 'oper_id' MASTER_ID = '_auth_user_id' def __init__(self, request = None): self.request = request def is_dealer_trustee(self): if self.request.session.get(self.OPER_ID): return True else: return False def check_out_to_dealer_trustee(self, oper_id): self.request.session[self.OPER_ID] = oper_id self.request.session.save() def check_out_to_dealer_master(self): # 校验master_id master_id = self.request.session.get(self.MASTER_ID) dealer = Dealer.objects.filter(id = master_id).first() subAccouny = Dealer.objects.filter(id = master_id).first() if not subAccouny and dealer: self.request.session.clear() return False else: # 切换 self.request.session.pop(self.OPER_ID, None) self.request.session.save() return True class MyToken(object): KEY = 'c4NEwO' @staticmethod def b64encode(j_s): return base64.urlsafe_b64encode(j_s).replace(b'=', b'') @staticmethod def b64decode(b_s): # 补全签发时替换掉的等号,找规律:肯定能被4整除,替换掉的‘=’个数是:4 - 总长 % 4 rem = len(b_s) % 4 if rem > 0: b_s += b'=' * (4 - rem) return base64.urlsafe_b64decode(str(b_s)) @staticmethod def encode(payload, key = KEY, exp = 60 * 15): header = {'typ': 'JWT', 'alg': 'HS256'} header_json = json.dumps(header, sort_keys = True, separators = (',', ':')) header_bs = MyToken.b64encode(header_json.encode()) my_payload = copy.deepcopy(payload) my_payload['exp'] = time.time() + int(exp) payload_json = json.dumps(my_payload, sort_keys = True, separators = (',', ':')) payload_bs = MyToken.b64encode(payload_json.encode()) if isinstance(key, str): key = key.encode() hm = hmac.new(key, header_bs + b'.' + payload_bs, digestmod = hashlib.sha256) hm_bs = MyToken.b64encode(hm.digest()) return header_bs + b'.' + payload_bs + b'.' + hm_bs @staticmethod def decode(jwt_s, key = KEY): header_bs, payload_bs, sign_bs = jwt_s.split(b'.') if isinstance(key, str): key = key.encode() hm = hmac.new(key, header_bs + b'.' + payload_bs, digestmod = hashlib.sha256) new_sign_bs = MyToken.b64encode(hm.digest()) if new_sign_bs != sign_bs: raise payload_json = MyToken.b64decode(payload_bs) payload = json.loads(payload_json) exp = payload['exp'] now_t = time.time() if now_t > exp: raise return payload class RentOrderServer(object): def __init__(self, order, dealer=None): # type:(DeviceRentOrder, Optional[Dealer, None]) -> None self.order = order self.dealer = dealer or order.dealer def execute(self): """ 执行订单的扣款 :return: """ logger.info("device rent order <{}> start execute!".format(self.order.id)) # 几个常量 扣款目前只从设备收益中进行扣除 incomeType = DEALER_INCOME_TYPE.DEVICE_INCOME exTime = datetime.datetime.now() balanceList = self.dealer.__class__.get_income_balance_list(self.dealer, incomeType) for _sourceKey, _balance in balanceList: # 金额不足的情况下继续 if RMB(_balance) < RMB(self.order.billAmount): continue # 对订单金额进行冻结 update = self.dealer.rent_freeze_balance( incomeType, self.order.billAmount, _sourceKey, str(self.order.id) ) if not update: logger.info("device rent order <{}> execute freeze error!".format(self.order.id)) self.order.update_for_fail(exTime, reason=u"扣款失败") return False # 扣款成功的情况下 try: self.order.update_for_success(exTime, _sourceKey) except Exception as e: logger.exception("device rent order <{}> execute update success error = {}!".format(self.order.id, e)) self.dealer.rent_recover_frozen_balance( incomeType, self.order.billAmount, _sourceKey, str(self.order.id) ) return False # 执行成功 self.dealer.rent_clear_frozen_balance(str(self.order.id)) logger.info("device rent order <{}> execute success!".format(self.order.id)) return True else: self.order.update_for_fail(exTime, u"余额不足") # 整个余额都不足够 logger.info("device rent order <{}> dealer <{}> balance not enough".format(self.order.id, self.dealer.id)) return False class TodoProcessor(object): @classmethod def insert_todo(cls, user): pass @classmethod def check_has_done(cls, todo): return False, False