123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583 |
- # -*- 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<id={}> 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<id={}> fetch access token success. access token = {}'.format(
- self.appid, new_token))
- return new_token
- else:
- raise Exception(
- '=== WechatToken === app<id={}> 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<id={}> 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<id={}> 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<id={}> 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<id={}> fetch jsapi ticket success. ticket = {}'.format(
- self.appid, new_ticket))
- return new_ticket
- else:
- raise Exception(
- '=== WechatJsapiTicket === app<id={}> 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<id={}> 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<id={}> 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<id={}> 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<id={}> fetch access token success. access token = {}'.format(
- self.appid, new_token))
- return new_token
- else:
- raise Exception(
- '=== WechatToken === app<id={}> 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<id={}> 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<id={}> 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<id={}> 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<id={}> fetch jsapi ticket success. ticket = {}'.format(
- self.appid, new_ticket))
- return new_ticket
- else:
- raise Exception(
- '=== WechatJsapiTicket === app<id={}> 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<id={}> 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<id={}> fetch jsapi ticket timeout.'.format(
- self.appid),
- client=self)
- time.sleep(5)
|