class WxHandler(tornado.web.RequestHandler): """ 微信handler处理类 """ '''微信配置文件''' wx_config = WxConfig() '''微信网页授权server''' wx_author_server = WxAuthorServer() '''redis服务''' wx_token_cache = TokenCache() def post(self, flag): if flag == 'wxauthor': '''微信网页授权''' code = self.get_argument('code') state = self.get_argument('state') # 获取重定向的url redirect_url = self.wx_config.wx_menu_state_map[state] logger.debug('【微信网页授权】将要重定向的地址为:redirct_url[' + redirect_url + ']') logger.debug('【微信网页授权】用户同意授权,获取code>>>>code[' + code + ']state[' + state + ']') if code: # 通过code换取网页授权access_token data = self.wx_author_server.get_auth_access_token(code) openid = data['openid'] logger.debug('【微信网页授权】openid>>>>openid[' + openid + ']') if openid: # 跳到自己的业务界面 self.redirect(redirect_url) else: # 获取不到openid logger.debug('获取不到openid')
class WxKF(object): _token_cache = TokenCache() _wx_author_server = WxAuthorServer() def add_kf(self): access_token = self._token_cache.get_cache( self._token_cache.KEY_ACCESS_TOKEN) if access_token: url = WxConfig.add_kf + access_token data = self.add_kf_data() r = requests.post(url, data.encode('utf-8')) logger.debug('【微信多客服系统】Response' + str(r.status_code)) if r.status_code == 200: res = r.text logger.debug('[微信多客服系统]' + res) json_res = json.loads(res) if 'errcode' in json.loads(res): errcode = json_res['errcode'] return errcode else: logger.error('微信多客服系统获取不到access_token') def add_kf_data(self): '''创建客服数据''' kf_data = { 'kf_account': 'bzw_lolface@gh_cacb86407b98', 'nickname': '小包' } KF_DATA = json.dumps(kf_data, ensure_ascii=False) logger.debug('【微信客服创建客服数据KF_DATA[' + str(KF_DATA) + ']') return KF_DATA def invite_kf(self): access_token = self._token_cache.get_cache( self._token_cache.KEY_ACCESS_TOKEN) if access_token: url = WxConfig.invite_kf + access_token invite_data = { 'kf_account': 'bzw_lolface@gh_cacb86407b98', 'invite_wx': 'bzw_lolface' } r = requests.post( url, json.dumps(invite_data, ensure_ascii=False).encode('utf-8')) logger.debug('[微信多客服系统]Response' + str(r.status_code)) if r.status_code == 200: res = r.text logger.debug('[微信多客服系统]' + res) json_res = json.loads(res) if 'errcode' in json.loads(res): errcode = json_res('errcode') return errcode else: logger.error('[微信多客服系统获取不到access_token]')
class WdAPI(self): baseURL = 'https://api.vdian.com/api' _token_cache = TokenCache() def fetch def getAccessToken(self): return _token_cache.get_cache(self._token_cache.KEY_WD_ACCESS_TOKEN)
class WxShedule(object): """ 定时任务调度器 excute 执行定时器任务 get_access_token 获取微信全局唯一票据access_token get_jsapi_ticket 获取JS_SDK权限签名的jsapi_ticket """ _token_cache = TokenCache() # 微信token缓存实例 _expire_time_access_token = 7000 * 1000 # token过期时间 _check_menu_period = 24 * 60 * 60 def excute(self): """执行定时器任务""" logger.info('【获取微信全局唯一票据access_token】>>>执行定时器任务') tornado.ioloop.IOLoop.instance().call_later(0, self.get_access_token) tornado.ioloop.PeriodicCallback( self.get_access_token, self._expire_time_access_token).start() def get_access_token(self): """获取微信全局唯一票据access_token""" url = WxConfig.config_get_access_token_url r = requests.get(url) logger.info('【获取微信全局唯一票据access_token】Response[' + str(r.status_code) + ']') if r.status_code == 200: res = r.text logger.info('【获取微信全局唯一票据access_token】>>>' + res) d = json.loads(res) if 'access_token' in d.keys(): access_token = d['access_token'] # 添加至redis中 self._token_cache.set_access_cache( self._token_cache.KEY_ACCESS_TOKEN, access_token) # 获取JS_SDK权限签名的jsapi_ticket self.get_jsapi_ticket() return access_token elif 'errcode' in d.keys(): errcode = d['errcode'] logger.info( '【获取微信全局唯一票据access_token-SDK】errcode[' + errcode + '] , will retry get_access_token() method after 10s') tornado.ioloop.IOLoop.instance().call_later( 10, self.get_access_token) else: logger.error('【获取微信全局唯一票据access_token】request access_token error' + ',will retry get_access_token() method after 10s') tornado.ioloop.IOLoop.instance().call_later( 10, self.get_access_token) def get_jsapi_ticket(self): """获取JS_SDK权限签名的jsapi_ticket""" access_token = self._token_cache.get_cache( self._token_cache.KEY_ACCESS_TOKEN) if access_token: url = 'https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=%s&type=jsapi' % access_token r = requests.get(url) logger.info('【微信JS-SDK】获取JS_SDK权限签名的jsapi_ticket的Response[' + str(r.status_code) + ']') if r.status_code == 200: res = r.text logger.info('【微信JS-SDK】获取JS_SDK权限签名的jsapi_ticket>>>>' + res) d = json.loads(res) errcode = d['errcode'] if errcode == 0: jsapi_ticket = d['ticket'] # 添加至redis中 self._token_cache.set_access_cache( self._token_cache.KEY_JSAPI_TICKET, jsapi_ticket) else: logger.info( '【微信JS-SDK】获取JS_SDK权限签名的jsapi_ticket>>>>errcode[' + errcode + ']') logger.info( '【微信JS-SDK】request jsapi_ticket error, will retry get_jsapi_ticket() method after 10s' ) tornado.ioloop.IOLoop.instance().call_later( 10, self.get_jsapi_ticket) else: logger.info( '【微信JS-SDK】request jsapi_ticket error, will retry get_jsapi_ticket() method after 10s' ) tornado.ioloop.IOLoop.instance().call_later( 10, self.get_jsapi_ticket) else: logger.error( '【微信JS-SDK】获取JS_SDK权限签名的jsapi_ticket时,access_token获取失败, will retry get_access_token() method after 10s' ) tornado.ioloop.IOLoop.instance().call_later( 10, self.get_access_token) def get_menu(self): """获取menu,如果没有menu则进行创建""" menu = WxMenuServer() try: data = menu.get_menu() logger.debug('微信获取menu' + str(data)) if data == 0: pass else: menu.create_menu() except Exception as e: logger.error('Exception:' + e)
class WxSignatureHandler(tornado.web.RequestHandler): """ 微信服务器签名验证, 消息回复 check_signature: 校验signature是否正确 """ pattern = re.compile(r'\d{15}') _token_cache = TokenCache() _media_cache = WxMediaCache() workpath = os.getcwd() def data_received(self, chunk): pass def get(self): try: signature = self.get_argument('signature') timestamp = self.get_argument('timestamp') nonce = self.get_argument('nonce') echostr = self.get_argument('echostr') result = self.check_signature(signature, timestamp, nonce) logger.debug('微信sign校验,signature=' + signature + ',×tamp=' + timestamp + '&nonce=' + nonce + '&echostr=' + echostr) if result: logger.debug('微信sign校验,返回echostr=' + echostr) self.write(echostr) else: logger.error('微信sign校验,---校验失败') except Exception as e: logger.error('微信sign校验,---Exception' + str(e)) @tornado.web.asynchronous def post(self): body = self.request.body logger.debug('微信消息回复中心】收到用户消息' + str(body.decode('utf-8'))) data = ET.fromstring(body) ToUserName = data.find('ToUserName').text FromUserName = data.find('FromUserName').text self._from_name = FromUserName self._to_name = ToUserName MsgType = data.find('MsgType').text if MsgType == 'text' or MsgType == 'voice': '''文本消息 or 语音消息''' try: MsgId = data.find("MsgId").text if MsgType == 'text': Content = data.find('Content').text # 文本消息内容 results = self.pattern.search(Content) if results: # 找到了符合订单ID的内容 order_id = results.group() self._order_id = order_id http_client = tornado.httpclient.AsyncHTTPClient() token = self._token_cache.get_cache( self._token_cache.KEY_WD_ACCESS_TOKEN) params = {"order_id": order_id} public = { "access_token": token, "version": "1.0", "format": "json", "method": "vdian.order.get" } url = r'https://api.vdian.com/api?param={"order_id":"%s"}&public={"method":"vdian.order.get","access_token":"%s","version":"1.0","format":"json"}' % ( order_id, token) http_client.fetch(url, callback=self.on_response) else: reply_content = WxConfig.COMMON_COPYWRITE CreateTime = int(time.time()) out = self.reply_text(FromUserName, ToUserName, CreateTime, reply_content) self.write(out) self.finish() except Exception as e: logger.error(str(e)) self.finish() elif MsgType == 'event': '''接收事件推送''' try: CreateTime = int(time.time()) Event = data.find('Event').text if Event == 'subscribe': # 关注事件 out = self.reply_text(FromUserName, ToUserName, CreateTime, WxConfig.ATTENTION_INIT_COPYWRITE_1) self.write(out) self.send_service_message_text( WxConfig.ATTENTION_INIT_COPYWRITE_2) elif Event == 'CLICK': key = data.find('EventKey').text if key == 'reprint': # 转载 out = self.reply_text(FromUserName, ToUserName, CreateTime, WxConfig.REPRINT_COPYWRITE) self.write(out) except Exception as e: logger.error(str(e)) finally: self.finish() elif MsgType == 'image': try: CreateTime = int(time.time()) out = self.reply_text(FromUserName, ToUserName, CreateTime, WxConfig.PART_IN_GUESSGANME_WAITTING) self.write(out) except Exception as e: logger.error(str(e)) finally: self.finish() def reply_text(self, FromUserName, ToUserName, CreateTime, Content): textTpl = """<xml> <ToUserName><![CDATA[%s]]></ToUserName> <FromUserName><![CDATA[%s]]></FromUserName> <CreateTime>%s</CreateTime> <MsgType><![CDATA[%s]]></MsgType> <Content><![CDATA[%s]]></Content></xml>""" out = textTpl % (FromUserName, ToUserName, CreateTime, "text", Content) return out def reply_image(self, FromUserName, ToUserName, CreateTime, Media_ID): textTpl = """<xml><ToUserName><![CDATA[%s]]></ToUserName><FromUserName><![CDATA[%s]]></FromUserName><CreateTime>%s</CreateTime><MsgType><![CDATA[image]]></MsgType><Image><MediaId><![CDATA[%s]]></MediaId></Image></xml>""" out = textTpl % (FromUserName, ToUserName, CreateTime, Media_ID) return out def check_signature(self, signature, timestamp, nonce): """校验token是否正µ确""" token = WxConfig.AppCustomToken L = [timestamp, nonce, token] L.sort() s = L[0] + L[1] + L[2] sha1 = hashlib.sha1(s.encode('utf-8')).hexdigest() logger.debug('sha1=' + sha1 + '&signature=' + signature) return sha1 == signature def get_font_path(self): """获取字体的路径""" return self.workpath + "/core/product/SourceHanSerifCN-Medium.otf" def get_random_path(self): """获取随机源图片本地路径""" randomNumber = random.randint(1, 100) if randomNumber <= 35: peakNumber = 0 elif randomNumber <= 70: peakNumber = 1 elif randomNumber <= 85: peakNumber = 2 else: peakNumber = 3 return self.workpath + "/core/static/Puzzle_%d.jpeg" % peakNumber def send_service_message_text(self, message): """发送文字类型客服消息""" token = self._token_cache.get_cache(self._token_cache.KEY_ACCESS_TOKEN) url = WxConfig.server_message_send_url + token headers = {'content-type': 'charset=utf8'} dic = { "touser": self._from_name, "msgtype": "text", "text": { "content": message } } return requests.post(url=url, data=json.dumps( dic, ensure_ascii=False).encode('utf8'), headers=headers) def on_response(self, response): try: CreateTime = int(time.time()) if response.error: out = self.reply_text(self._fddrom_name, self._to_name, CreateTime, WxConfig.HTTP_RESPONSE_ERROR_COPYWRITE) self.write(out) else: CreateTime = int(time.time()) res_json = json.loads(response.body) if res_json["status"]["status_code"] != 0 or res_json[ "result"]["status_ori"] == 10: out = self.reply_text(self._from_name, self._to_name, CreateTime, WxConfig.PART_IN_FAILURE_COPYWRITE) self.write(out) logger.info("==========非法订单以及请求==========") logger.info(res_json) return self.send_service_message_text( WxConfig.PART_IN_SUCCESS_COPYWRITE) name = res_json["result"]["buyer_info"]["name"] exit_media_id = self._media_cache.get_cache(self._order_id) if exit_media_id is not None: out = self.reply_image(self._from_name, self._to_name, CreateTime, exit_media_id) self.write(out) else: token = self._token_cache.get_cache( self._token_cache.KEY_ACCESS_TOKEN) rawImagePath = self.get_random_path() playload_image = {'access_token': token, 'type': 'image'} logger.info("【新创建图片】" + rawImagePath) namefont = ImageFont.truetype(self.get_font_path(), 20) idFont = ImageFont.truetype(self.get_font_path(), 12) im = Image.open(rawImagePath) draw = ImageDraw.Draw(im) draw.text((340, 363), name[0:9], fill=(0, 0, 0), font=namefont) draw.text((490, 980), self._order_id, fill=(165, 165, 165), font=idFont) newPath = self.workpath + "/core/product/" + self._order_id + '.jpeg' im.save(newPath) data = {'media': open(newPath, 'rb')} r = requests.post( url= 'http://file.api.weixin.qq.com/cgi-bin/media/upload', params=playload_image, files=data) image_json = json.loads(r.text) media_id = image_json["media_id"] self._media_cache.set_cache(self._order_id, media_id) out = self.reply_image(self._from_name, self._to_name, CreateTime, media_id) self.write(out) except Exception as e: logger.error(str(e)) finally: self.finish()
class WxMenuServer(object): """ 微信自定义菜单 create_menu 自定义菜单创建接口 get_menu 自定义菜单查询接口 delete_menu 自定义菜单删除接口 create_menu_data 创建菜单数据 """ _token_cache = TokenCache() # 微信token缓存 _wx_author_server = WxAuthorServer() # 微信网页授权server def create_menu(self): """自定义菜单创建接口""" access_token = self._token_cache.get_cache( self._token_cache.KEY_ACCESS_TOKEN) if access_token: url = WxConfig.menu_create_url + access_token data = self.create_menu_data() r = requests.post(url, data.encode('utf-8')) logger.debug('【微信自定义菜单】自定义菜单创建接口Response[' + str(r.status_code) + ']') if r.status_code == 200: res = r.text logger.debug('【微信自定义菜单】自定义菜单创建接口' + res) json_res = json.loads(res) if 'errcode' in json_res.keys(): errcode = json_res['errcode'] return errcode else: logger.error('【微信自定义菜单】自定义菜单创建接口获取不到access_token') def get_menu(self): """自定义菜单查询接口""" access_token = self._token_cache.get_cache( self._token_cache.KEY_ACCESS_TOKEN) if access_token: url = WxConfig.menu_get_url + access_token r = requests.get(url) logger.debug('【微信自定义菜单】自定义菜单查询接口Response[' + str(r.status_code) + ']') if r.status_code == 200: res = r.text logger.debug('【微信自定义菜单】自定义菜单查询接口' + res) json_res = json.loads(res) if 'errcode' in json_res.keys(): errcode = json_res['errcode'] if errcode == 0: return json_res return errcode else: logger.error('【微信自定义菜单】自定义菜单查询接口获取不到access_token') def delete_menu(self): """自定义菜单删除接口""" access_token = self._token_cache.get_cache( self._token_cache.KEY_ACCESS_TOKEN) if access_token: url = WxConfig.menu_delete_url + access_token r = requests.get(url) logger.debug('【微信自定义菜单】自定义菜单删除接口Response[' + str(r.status_code) + ']') if r.status_code == 200: res = r.text logger.debug('【微信自定义菜单】自定义菜单删除接口' + res) json_res = json.loads(res) if 'errcode' in json_res.keys(): errcode = json_res['errcode'] return errcode else: logger.error('【微信自定义菜单】自定义菜单删除接口获取不到access_token') def create_menu_data(self): """创建菜单数据""" menu_data = {'button': []} # 大菜单 menu_l1_1 = {'name': '最新优惠', 'sub_button': []} menu_l1_2 = {'name': '会员中心', 'sub_button': []} menu_l2_1 = { 'type': 'view', 'name': '会员享9折优惠', 'url': self._wx_author_server.get_code_url('menuIndex0') } menu_l2_2 = { 'type': 'view', 'name': '申请会员', 'url': self._wx_author_server.get_code_url('apply_vip') } menu_l1_1['sub_button'].append(menu_l2_1) menu_l1_2['sub_button'].append(menu_l2_2) menu_data['button'].append(menu_l1_1) menu_data['button'].append(menu_l1_2) MENU_DATA = json.dumps(menu_data, ensure_ascii=False) logger.debug('【微信自定义菜单】创建菜单数据MENU_DATA[' + str(MENU_DATA) + ']') return MENU_DATA
class WxMenuServer(object): """ 微信自定义菜单 create_menu 自定义菜单创建接口 get_menu 自定义菜单查询接口 delete_menu 自定义菜单删除接口 create_menu_data 创建菜单数据 """ _token_cache = TokenCache() # 微信token缓存 def create_menu(self): """自定义菜单创建接口""" access_token = self._token_cache.get_cache( self._token_cache.KEY_ACCESS_TOKEN) if access_token: url = WxConfig.menu_create_url + access_token data = self.create_menu_data() r = requests.post(url, data.encode('utf-8')) logger.debug('【微信自定义菜单】自定义菜单创建接口Response[' + str(r.status_code) + ']') if r.status_code == 200: res = r.text logger.debug('【微信自定义菜单】自定义菜单创建接口' + res) json_res = json.loads(res) if 'errcode' in json_res.keys(): errcode = json_res['errcode'] return errcode else: logger.error('【微信自定义菜单】自定义菜单创建接口获取不到access_token') def get_menu(self): """自定义菜单查询接口""" access_token = self._token_cache.get_cache( self._token_cache.KEY_ACCESS_TOKEN) if access_token: url = WxConfig.menu_get_url + access_token r = requests.get(url) logger.debug('【微信自定义菜单】自定义菜单查询接口Response[' + str(r.status_code) + ']') if r.status_code == 200: res = r.text logger.debug('【微信自定义菜单】自定义菜单查询接口' + res) json_res = json.loads(res) if 'errcode' in json_res.keys(): errcode = json_res['errcode'] return errcode else: logger.error('【微信自定义菜单】自定义菜单查询接口获取不到access_token') def delete_menu(self): """自定义菜单删除接口""" access_token = self._token_cache.get_cache( self._token_cache.KEY_ACCESS_TOKEN) if access_token: url = WxConfig.menu_delete_url + access_token r = requests.get(url) logger.debug('【微信自定义菜单】自定义菜单删除接口Response[' + str(r.status_code) + ']') if r.status_code == 200: res = r.text logger.debug('【微信自定义菜单】自定义菜单删除接口' + res) json_res = json.loads(res) if 'errcode' in json_res.keys(): errcode = json_res['errcode'] return errcode else: logger.error('【微信自定义菜单】自定义菜单删除接口获取不到access_token') def create_menu_data(self): """创建菜单数据""" menu_data = { "button": [{ "name": "精选内容", "sub_button": [{ "type": "view", "name": "精选文章", "url": "http://mp.weixin.qq.com/mp/homepage?__biz=MjM5Nzc5MDkwMQ==&hid=3&sn=70ce35abda6a8236f90a3a36e964dc2d" }, { "type": "view", "name": "影评剧评", "url": "http://mp.weixin.qq.com/mp/homepage?__biz=MjM5Nzc5MDkwMQ==&hid=4&sn=aeaee3a48324880d0c5e6ea335d5b9cc" }, { "type": "view", "name": "明星专访", "url": "http://mp.weixin.qq.com/mp/homepage?__biz=MjM5Nzc5MDkwMQ==&hid=5&sn=24c252b77d42b6d93bd021bbc4eaf67f" }] }, { "type": "view", "name": "李易峰专刊", "url": "https://weidian.com/s/209793342?ifr=shopdetail&wfr=c" }, { "name": "联系我们", "sub_button": [{ "type": "media_id", "name": "加入我们", "media_id": "b-Ij3Ifb-J_iT6cv09d5mOV7UvvCeEZ9fQqxIEqAXW0" }, { "type": "click", "name": "欢迎转载", "key": "reprint" }, { "type": "media_id", "name": "商务合作", "media_id": "b-Ij3Ifb-J_iT6cv09d5mKHcKOLuXzDAqMAT9ns8XWU" }] }] } MENU_DATA = json.dumps(menu_data, ensure_ascii=False) logger.debug('【微信自定义菜单】创建菜单数据MENU_DATA[' + str(MENU_DATA) + ']') return MENU_DATA
'url': url } def __create_nonce_str(self): return ''.join( random.choice(string.ascii_letters + string.digits) for _ in range(15)) def __create_timestamp(self): return int(time.time()) def sign(self): string = '&'.join([ '%s=%s' % (key.lower(), self.ret[key]) for key in sorted(self.ret) ]) self.ret['signature'] = hashlib.sha1( string.encode('utf-8')).hexdigest() logger.debug('【微信JS-SDK】获取JS-SDK权限签名>>>>dict[' + str(self.ret) + ']') return self.ret if __name__ == '__main__': token_cache = TokenCache() jsapi_ticket = token_cache.get_cache(token_cache.KEY_JSAPI_TICKET) # 注意 URL 一定要动态获取,不能 hardcode url = '%s/page/index' % WxConfig.AppHost sign = WxSign(jsapi_ticket, url) print(sign.sign())