# -*- coding: utf-8 -*- # !/usr/bin/env python import logging import time import simplejson as json from django.conf import settings from typing import TYPE_CHECKING, Optional from apilib.utils_sys import memcache_lock from apps import serviceCache, lockCache from apps.web.constant import AppPlatformType from apps.web.core import WechatMixin, BaseAppProxy from apps.web.core.models import WechatAuthorizer, WechatComponentApp from library.wechatpy.client import WeChatClient, WeChatComponentClient from library.wechatpy.constants import WeChatErrorCode from library.wechatbase.exceptions import WeChatException, APILimitedException from library.wechatpy.session.memcachedstorage import MemcachedStorage from library.wechatpy.component import WeChatComponent if TYPE_CHECKING: from apps.web.core.models import WechatManagerApp, WechatUserManagerApp logger = logging.getLogger(__name__) class MyWeChatClient(WeChatClient): TOKEN_LOCK_CACHE_KEY = 'access-token-lock-{appid}' JSAPI_LOCK_CACHE_KEY = 'jsapi-ticket-lock-{appid}' NO_RETRY_ERRCODE = [ '48001', # api unauthorized '40164' # invalid ip not in whitelist ] def __init__(self, appid, secret): super(MyWeChatClient, self).__init__(appid=appid, secret=secret, session=MemcachedStorage(mc=serviceCache, prefix='vivestone'), timeout=5) def fetch_access_token(self, old_token=None): key = MyWeChatClient.TOKEN_LOCK_CACHE_KEY.format(appid=self.appid) retry = 0 while True: current_token = self.session.get(self.access_token_key) if current_token and current_token != old_token: logger.debug( '=== WechatToken === app fetched one other access token. access token = {}'.format( self.appid, current_token)) return current_token with memcache_lock(key=key, value='1', expire=15) as acquired: if acquired: try: new_token = super(MyWeChatClient, self).refresh_access_token() if new_token: logger.debug( '=== WechatToken === app fetch access token success. access token = {}'.format( self.appid, new_token)) return new_token else: raise Exception( '=== WechatToken === app fetch access token is null.'.format(self.appid)) except APILimitedException as e: logger.error(repr(e)) raise e except WeChatException as e: logger.exception(e) if str(e.errCode) in self.NO_RETRY_ERRCODE: raise e except Exception as e: logger.exception(e) else: logger.debug( '=== WechatToken === app not acquire access token memcache key<{}>'.format(self.appid, key)) retry = retry + 1 if retry >= 3: raise WeChatException(errCode=WeChatErrorCode.MY_SYSTEM_ERROR, errMsg='=== WechatToken === app fetch access token timeout.'.format( self.appid), client=self) time.sleep(5) def fetch_jsapi_ticket(self, old_ticket=None): key = MyWeChatClient.JSAPI_LOCK_CACHE_KEY.format(appid=self.appid) retry = 0 while True: current_ticket = self.session.get(self.jsapi_ticket_key) if current_ticket and current_ticket != old_ticket: logger.debug('=== WechatJsapiTicket === app fetched one other jsapi ticket. ticket = {}'.format( self.appid, current_ticket)) return current_ticket with memcache_lock(key=key, value='1', expire=15) as acquired: if acquired: try: new_ticket = super(MyWeChatClient, self).refresh_jsapi_ticket() if new_ticket: logger.debug( '=== WechatJsapiTicket === app fetch jsapi ticket success. ticket = {}'.format( self.appid, new_ticket)) return new_ticket else: raise Exception( '=== WechatJsapiTicket === app fetch jsapi ticket is null.'.format(self.appid)) except APILimitedException as e: logger.error(repr(e)) raise e except WeChatException as e: logger.exception(e) if str(e.errCode) in self.NO_RETRY_ERRCODE: raise e except Exception as e: logger.exception(e) else: logger.debug( '=== WechatJsapiTicket === app not acquire jsapi memcache key<{}>'.format(self.appid, key)) retry = retry + 1 if retry >= 3: raise WeChatException(errCode=WeChatErrorCode.MY_SYSTEM_ERROR, errMsg='=== WechatJsapiTicket === app fetch jsapi ticket timeout.'.format( self.appid), client=self) time.sleep(5) class WechatClientProxy(BaseAppProxy, WechatMixin): def __init__(self, app): # type: (Optional[WechatManagerApp, WechatUserManagerApp])->None super(WechatClientProxy, self).__init__(app) self.__gateway_type__ = AppPlatformType.WECHAT @property def __client__(self): return MyWeChatClient(self.appid, self.secret) @property def templateIdMap(self): return self._app['templateIdMap'] def generate_js_auth_signature(self, url): """ 生成签名信息,主要供经销商获取扫码权限 :return: """ client = MyWeChatClient(self.appid, self.secret) sign_dict = client.jsapi_sign(url=url.split('#')[0]) return { 'signature': sign_dict['sign'], 'appId': sign_dict['appId'], 'jsapi_ticket': sign_dict['jsapi_ticket'], 'url': url, 'timestamp': sign_dict['timestamp'], 'nonceStr': sign_dict['noncestr'] } def is_subscribe_gzh(self, openId): """ 是否关注公众号 :param openId: :return: """ try: user_inf = self.client.user.get(openId) if user_inf['subscribe'] == 1: return True return False except Exception as e: logger.exception(e) return True def notify(self, openId, templateName, url=None, **kwargs): """ 通过公众号推送给终端用户/经销商 :param openId: :type templateName: :return: """ try: logger.debug( 'send template message. appId = %s, secret = %s, openId = %s, template = %s; context = %s; url = %s;' % (self.appid, self.secret, openId, str(self.templateIdMap.get(templateName, {})), str(kwargs), url)) if templateName not in self.templateIdMap: logger.error('template<{}> is not exist. app = {}, agentId = {}'.format( templateName, repr(self.app), self.occupantId)) return if not openId: logger.error('open id is null.') return {'error': 'open id is null.'} if (templateName not in self.templateIdMap) or (self.templateIdMap[templateName] is None): return {'error': 'template name is not exist'} from string import Template templateContext = json.loads(Template(self.templateIdMap[templateName]['context']).substitute(**kwargs)) result = self.client.message.send_template(user_id=openId, template_id=self.templateIdMap[templateName]['templateId'], url=url, data=templateContext) logger.debug( 'result = %s, appId = %s, secret = %s, openId = %s, template = %s' % ( result, self.appid, self.secret, openId, str(self.templateIdMap[templateName]))) if result['errcode'] != 0: return { 'error': 'send error, result = %s, appId = %s, secret = %s, openId = %s, template = %s' % (result, self.appid, self.secret, openId, str(self.templateIdMap[templateName])) } else: return result except Exception, e: logger.exception('error = %s, payload = (appId=%s)' % (e, self.appid)) return {'error': str(e)} def publish(self, openId, templateName, url=None, **kwargs): """ 通过公众号 推送订阅消息给用户 需要用户订阅 https://developers.weixin.qq.com/doc/offiaccount/Subscription_Messages/api.html 字段参数 参数类别 参数说明 参数值限制 说明 thing.DATA 事物 20个以内字符 可汉字、数字、字母或符号组合 number.DATA 数字 32位以内数字 只能数字,可带小数 letter.DATA 字母 32位以内字母 只能字母 symbol.DATA 符号 5位以内符号 只能符号 character_string.DATA 字符串 32位以内数字、字母或符号 可数字、字母或符号组合 time.DATA 时间 24小时制时间格式(支持+年月日),支持填时间段,两个时间点之间用“~”符号连接 例如:15:01,或:2019年10月1日 15:01 date.DATA 日期 年月日格式(支持+24小时制时间),支持填时间段,两个时间点之间用“~”符号连接 例如:2019年10月1日,或:2019年10月1日 15:01 amount.DATA 金额 1个币种符号+10位以内纯数字,可带小数,结尾可带“元” 可带小数 phone_number.DATA 电话 17位以内,数字、符号 电话号码,例:+86-0766-66888866 car_number.DATA 车牌 8位以内,第一位与最后一位可为汉字,其余为字母或数字 车牌号码:粤A8Z888挂 name.DATA 姓名 10个以内纯汉字或20个以内纯字母或符号 中文名10个汉字内;纯英文名20个字母内;中文和字母混合按中文名算,10个字内 phrase.DATA 汉字 5个以内汉字 5个以内纯汉字,例如:配送中 """ try: logger.debug( 'send template message. appId = %s, secret = %s, openId = %s, template = %s; context = %s; url = %s;' % (self.appid, self.secret, openId, str(self.templateIdMap.get(templateName, {})), str(kwargs), url)) if templateName not in self.templateIdMap: logger.error('template<{}> is not exist. app = {}, agentId = {}'.format( templateName, repr(self.app), self.occupantId)) return if not openId: logger.error('open id is null.') return {'error': 'open id is null.'} if (templateName not in self.templateIdMap) or (self.templateIdMap[templateName] is None): return {'error': 'template name is not exist'} from string import Template templateData = json.loads(Template(self.templateIdMap[templateName]['context']).substitute(**kwargs)) temp = { 'touser': openId, 'template_id': self.templateIdMap[templateName]['templateId'], 'page': url, 'miniprogram': '', } temp.update(templateData) try: self.client.message.send_subscribe_bizsend(temp) result = True except Exception as e: logger.error(e) result = False logger.debug( 'result = %s, appId = %s, secret = %s, openId = %s, template = %s' % ( result, self.appid, self.secret, openId, str(self.templateIdMap[templateName]))) return result except Exception, e: logger.exception('error = %s, payload = (appId=%s)' % (e, self.appid)) return {'error': str(e)} def notify_msg(self, openId, template, url=None, **kwargs): """ 通过公众号推送给终端用户/经销商 :param openId: :type templateName: :return: """ try: logger.debug( 'send template message. appId = %s, secret = %s, openId = %s, template = %s; context = %s; url = %s;' % (self.appid, self.secret, openId, template, str(kwargs), url)) if not openId: logger.error('open id is null.') return {'error': 'open id is null.'} from string import Template templateContext = json.loads(Template(template['context']).substitute(**kwargs)) result = self.client.message.send_template(user_id=openId, template_id=template['templateId'], url=url, data=templateContext) logger.debug( 'result = %s, appId = %s, secret = %s, openId = %s, template = %s' % ( result, self.appid, self.secret, openId, str(template))) if result['errcode'] != 0: return { 'error': 'send error, result = %s, appId = %s, secret = %s, openId = %s, template = %s' % (result, self.appid, self.secret, openId, str(template)) } else: return result except Exception, e: logger.exception('error = %s, payload = (appId=%s)' % (e, self.appid)) return {'error': str(e)} def publish_msg(self, openId, template, url=None, **kwargs): """ 通过公众号 推送订阅消息给用户 需要用户订阅 https://developers.weixin.qq.com/doc/offiaccount/Subscription_Messages/api.html 字段参数 参数类别 参数说明 参数值限制 说明 thing.DATA 事物 20个以内字符 可汉字、数字、字母或符号组合 number.DATA 数字 32位以内数字 只能数字,可带小数 letter.DATA 字母 32位以内字母 只能字母 symbol.DATA 符号 5位以内符号 只能符号 character_string.DATA 字符串 32位以内数字、字母或符号 可数字、字母或符号组合 time.DATA 时间 24小时制时间格式(支持+年月日),支持填时间段,两个时间点之间用“~”符号连接 例如:15:01,或:2019年10月1日 15:01 date.DATA 日期 年月日格式(支持+24小时制时间),支持填时间段,两个时间点之间用“~”符号连接 例如:2019年10月1日,或:2019年10月1日 15:01 amount.DATA 金额 1个币种符号+10位以内纯数字,可带小数,结尾可带“元” 可带小数 phone_number.DATA 电话 17位以内,数字、符号 电话号码,例:+86-0766-66888866 car_number.DATA 车牌 8位以内,第一位与最后一位可为汉字,其余为字母或数字 车牌号码:粤A8Z888挂 name.DATA 姓名 10个以内纯汉字或20个以内纯字母或符号 中文名10个汉字内;纯英文名20个字母内;中文和字母混合按中文名算,10个字内 phrase.DATA 汉字 5个以内汉字 5个以内纯汉字,例如:配送中 """ try: logger.debug( 'send template message. appId = %s, secret = %s, openId = %s, template = %s; context = %s; url = %s;' % (self.appid, self.secret, openId, str(template), str(kwargs), url)) if not openId: logger.error('open id is null.') from string import Template templateData = json.loads(Template(template['context']).substitute(**kwargs)) temp = { 'touser': openId, 'template_id': template['templateId'], 'page': url, 'miniprogram': '', } temp.update(templateData) try: self.client.message.send_subscribe_bizsend(temp) result = True except Exception as e: logger.error(e) result = False logger.debug( 'result = %s, appId = %s, secret = %s, openId = %s, template = %s' % ( result, self.appid, self.secret, openId, str(template))) return result except Exception, e: logger.exception('error = %s, payload = (appId=%s)' % (e, self.appid)) return {'error': str(e)} def get_wechat_access_ips(self): return self.client.misc.get_wechat_access_ips() def get_wechat_callback_ips(self): return self.client.misc.get_wechat_callback_ips() def get_qr_str_scene(self, sceneStr, expired=60 * 60): """ 获取带参数的二维码 参数为字符串形式 """ try: response = self.client.qrcode.create({ "expire_seconds": expired, "action_name": "QR_STR_SCENE", "action_info": { "scene": {"scene_str": sceneStr} } }) except WeChatException: return None return response["ticket"] def get_url_in_qr_str_scene(self, sceneStr, expired = 30 * 24 * 60 * 60): """ 获取带参数的二维码 参数为字符串形式 """ try: response = self.client.qrcode.create({ "expire_seconds": expired, "action_name": "QR_STR_SCENE", "action_info": { "scene": {"scene_str": sceneStr} } }) except WeChatException: return None return response["url"] class MyWeChatComponent(WeChatComponent): def __init__(self, app, auto_retry=True): # type:(WechatComponentApp, bool)->None super(MyWeChatComponent, self).__init__( component_appid=app.appid, component_appsecret=app.secret, component_token=app.token, encoding_aes_key=app.aesKey, lock_cache=lockCache, session=MemcachedStorage(mc=serviceCache, prefix='vivestone'), authorizer=WechatAuthorizer, auto_retry=auto_retry) @classmethod def instance(cls): try: appId = settings.WECHAT_3RD_APPID except: appId = None if not appId: return None else: app = WechatComponentApp.objects(appid=appId).first() if app: return cls(app) else: return None class MyWeChatComponentClient(WeChatComponentClient): TOKEN_LOCK_CACHE_KEY = 'access-token-lock-{appid}' JSAPI_LOCK_CACHE_KEY = 'jsapi-ticket-lock-{appid}' NO_RETRY_ERRCODE = [ '48001', # api unauthorized '40164' # invalid ip not in whitelist '61004' # access clientip is not registered request ] def __init__(self, appid, component): super(MyWeChatComponentClient, self).__init__(appid=appid, component=component, session=MemcachedStorage(mc=serviceCache, prefix='vivestone'), timeout=5) def fetch_access_token(self, old_token=None): key = self.TOKEN_LOCK_CACHE_KEY.format(appid=self.appid) retry = 0 while True: current_token = self.session.get(self.access_token_key) if current_token and current_token != old_token: logger.debug( '=== WechatToken === app fetched one other access token. access token = {}'.format( self.appid, current_token)) return current_token with memcache_lock(key=key, value='1', expire=15) as acquired: if acquired: try: new_token = super(MyWeChatComponentClient, self).refresh_access_token() if new_token: logger.debug( '=== WechatToken === app fetch access token success. access token = {}'.format( self.appid, new_token)) return new_token else: raise Exception( '=== WechatToken === app fetch access token is null.'.format(self.appid)) except APILimitedException as e: logger.error(repr(e)) raise e except WeChatException as e: logger.exception(e) if str(e.errCode) in self.NO_RETRY_ERRCODE: raise e except Exception as e: logger.exception(e) else: logger.debug( '=== WechatToken === app not acquire access token memcache key<{}>'.format(self.appid, key)) retry = retry + 1 if retry >= 3: raise WeChatException(errCode=WeChatErrorCode.MY_SYSTEM_ERROR, errMsg='=== WechatToken === app fetch access token timeout.'.format( self.appid), client=self) time.sleep(5) def fetch_jsapi_ticket(self, old_ticket=None): key = self.JSAPI_LOCK_CACHE_KEY.format(appid=self.appid) retry = 0 while True: current_ticket = self.session.get(self.jsapi_ticket_key) if current_ticket and current_ticket != old_ticket: logger.debug('=== WechatJsapiTicket === app fetched one other jsapi ticket. ticket = {}'.format( self.appid, current_ticket)) return current_ticket with memcache_lock(key=key, value='1', expire=15) as acquired: if acquired: try: new_ticket = super(MyWeChatComponentClient, self).refresh_jsapi_ticket() if new_ticket: logger.debug( '=== WechatJsapiTicket === app fetch jsapi ticket success. ticket = {}'.format( self.appid, new_ticket)) return new_ticket else: raise Exception( '=== WechatJsapiTicket === app fetch jsapi ticket is null.'.format(self.appid)) except APILimitedException as e: logger.error(repr(e)) raise e except WeChatException as e: logger.exception(e) if str(e.errCode) in self.NO_RETRY_ERRCODE: raise e except Exception as e: logger.exception(e) else: logger.debug( '=== WechatJsapiTicket === app not acquire jsapi memcache key<{}>'.format(self.appid, key)) retry = retry + 1 if retry >= 3: raise WeChatException(errCode=WeChatErrorCode.MY_SYSTEM_ERROR, errMsg='=== WechatJsapiTicket === app fetch jsapi ticket timeout.'.format( self.appid), client=self) time.sleep(5)