parent
d6ce64c3b6
commit
9b8412304f
@ -0,0 +1,30 @@ |
|||||||
|
# Generated by Django 3.2.3 on 2021-09-07 19:18 |
||||||
|
|
||||||
|
from django.conf import settings |
||||||
|
from django.db import migrations, models |
||||||
|
import django.db.models.deletion |
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration): |
||||||
|
dependencies = [ |
||||||
|
('api', '0004_auto_20210705_1819'), |
||||||
|
] |
||||||
|
|
||||||
|
operations = [ |
||||||
|
migrations.CreateModel( |
||||||
|
name='ThirdWeChatUserInfo', |
||||||
|
fields=[ |
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
||||||
|
('openid', models.CharField(max_length=64, unique=True, verbose_name='普通用户的标识,对当前公众号唯一')), |
||||||
|
('nickname', models.CharField(blank=True, max_length=64, verbose_name='昵称')), |
||||||
|
('sex', models.SmallIntegerField(default=0, help_text='值为1时是男性,值为2时是女性,值为0时是未知', verbose_name='性别')), |
||||||
|
('subscribe_time', models.BigIntegerField(verbose_name='订阅时间')), |
||||||
|
('head_img_url', models.CharField(blank=True, max_length=256, null=True, verbose_name='用户头像')), |
||||||
|
('address', models.CharField(blank=True, max_length=128, null=True, verbose_name='地址')), |
||||||
|
('subscribe', models.BooleanField(default=0, verbose_name='是否订阅公众号')), |
||||||
|
('created_time', models.DateTimeField(auto_now_add=True, verbose_name='授权时间')), |
||||||
|
('user_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, |
||||||
|
verbose_name='用户ID')), |
||||||
|
], |
||||||
|
), |
||||||
|
] |
@ -0,0 +1,5 @@ |
|||||||
|
#!/usr/bin/env python |
||||||
|
# -*- coding:utf-8 -*- |
||||||
|
# project: 9月 |
||||||
|
# author: NinEveN |
||||||
|
# date: 2021/9/6 |
@ -0,0 +1,5 @@ |
|||||||
|
#!/usr/bin/env python |
||||||
|
# -*- coding:utf-8 -*- |
||||||
|
# project: 9月 |
||||||
|
# author: NinEveN |
||||||
|
# date: 2021/9/6 |
@ -0,0 +1,86 @@ |
|||||||
|
#!/usr/bin/env python |
||||||
|
# -*- coding:utf-8 -*- |
||||||
|
# project: 9月 |
||||||
|
# author: NinEveN |
||||||
|
# date: 2021/9/6 |
||||||
|
|
||||||
|
import xml.etree.ElementTree as ET |
||||||
|
|
||||||
|
|
||||||
|
def parse_xml(web_data): |
||||||
|
if len(web_data) == 0: |
||||||
|
return None |
||||||
|
xml_data = ET.fromstring(web_data) |
||||||
|
msg_type = xml_data.find('MsgType').text |
||||||
|
|
||||||
|
if msg_type == 'event': |
||||||
|
event_type = xml_data.find('Event').text |
||||||
|
if event_type == 'CLICK': |
||||||
|
return Click(xml_data) |
||||||
|
elif event_type in ('subscribe', 'unsubscribe'): |
||||||
|
return Subscribe(xml_data) |
||||||
|
# elif event_type == 'VIEW': |
||||||
|
# return View(xml_data) |
||||||
|
# elif event_type == 'LOCATION': |
||||||
|
# return LocationEvent(xml_data) |
||||||
|
elif event_type == 'SCAN': |
||||||
|
return Scan(xml_data) |
||||||
|
|
||||||
|
elif msg_type == 'text': |
||||||
|
return TextMsg(xml_data) |
||||||
|
elif msg_type == 'image': |
||||||
|
return ImageMsg(xml_data) |
||||||
|
|
||||||
|
|
||||||
|
class Msg(object): |
||||||
|
def __init__(self, xml_data): |
||||||
|
self.ToUserName = xml_data.find('ToUserName').text |
||||||
|
self.FromUserName = xml_data.find('FromUserName').text |
||||||
|
self.CreateTime = xml_data.find('CreateTime').text |
||||||
|
self.MsgType = xml_data.find('MsgType').text |
||||||
|
self.MsgId = xml_data.find('MsgId').text |
||||||
|
|
||||||
|
|
||||||
|
class TextMsg(Msg): |
||||||
|
def __init__(self, xml_data): |
||||||
|
Msg.__init__(self, xml_data) |
||||||
|
self.Content = xml_data.find('Content').text.encode("utf-8") |
||||||
|
|
||||||
|
|
||||||
|
class ImageMsg(Msg): |
||||||
|
def __init__(self, xml_data): |
||||||
|
Msg.__init__(self, xml_data) |
||||||
|
self.PicUrl = xml_data.find('PicUrl').text |
||||||
|
self.MediaId = xml_data.find('MediaId').text |
||||||
|
|
||||||
|
|
||||||
|
class EventMsg(object): |
||||||
|
def __init__(self, xml_data): |
||||||
|
self.ToUserName = xml_data.find('ToUserName').text |
||||||
|
self.FromUserName = xml_data.find('FromUserName').text |
||||||
|
self.CreateTime = xml_data.find('CreateTime').text |
||||||
|
self.MsgType = xml_data.find('MsgType').text |
||||||
|
self.Event = xml_data.find('Event').text |
||||||
|
self.Eventkey = xml_data.find('EventKey').text |
||||||
|
self.Ticket = '' |
||||||
|
|
||||||
|
|
||||||
|
class Click(EventMsg): |
||||||
|
def __init__(self, xml_data): |
||||||
|
EventMsg.__init__(self, xml_data) |
||||||
|
|
||||||
|
|
||||||
|
class Scan(EventMsg): |
||||||
|
def __init__(self, xml_data): |
||||||
|
EventMsg.__init__(self, xml_data) |
||||||
|
self.Ticket = xml_data.find('Ticket').text |
||||||
|
|
||||||
|
|
||||||
|
class Subscribe(EventMsg): |
||||||
|
def __init__(self, xml_data): |
||||||
|
EventMsg.__init__(self, xml_data) |
||||||
|
tick = xml_data.find('Ticket') |
||||||
|
if tick is None: |
||||||
|
self.Ticket = '' |
||||||
|
else: |
||||||
|
self.Ticket = tick.text |
@ -0,0 +1,61 @@ |
|||||||
|
#!/usr/bin/env python |
||||||
|
# -*- coding:utf-8 -*- |
||||||
|
# project: 9月 |
||||||
|
# author: NinEveN |
||||||
|
# date: 2021/9/6 |
||||||
|
|
||||||
|
import time |
||||||
|
|
||||||
|
|
||||||
|
class Msg(object): |
||||||
|
def __init__(self): |
||||||
|
pass |
||||||
|
|
||||||
|
def send(self): |
||||||
|
return "success" |
||||||
|
|
||||||
|
|
||||||
|
class TextMsg(Msg): |
||||||
|
def __init__(self, to_user_name, from_user_name, content): |
||||||
|
super().__init__() |
||||||
|
self.__dict = dict() |
||||||
|
self.__dict['ToUserName'] = to_user_name |
||||||
|
self.__dict['FromUserName'] = from_user_name |
||||||
|
self.__dict['CreateTime'] = int(time.time()) |
||||||
|
self.__dict['Content'] = content |
||||||
|
|
||||||
|
def send(self): |
||||||
|
xml_form = """ |
||||||
|
<xml> |
||||||
|
<ToUserName><![CDATA[{ToUserName}]]></ToUserName> |
||||||
|
<FromUserName><![CDATA[{FromUserName}]]></FromUserName> |
||||||
|
<CreateTime>{CreateTime}</CreateTime> |
||||||
|
<MsgType><![CDATA[text]]></MsgType> |
||||||
|
<Content><![CDATA[{Content}]]></Content> |
||||||
|
</xml> |
||||||
|
""" |
||||||
|
return xml_form.format(**self.__dict) |
||||||
|
|
||||||
|
|
||||||
|
class ImageMsg(Msg): |
||||||
|
def __init__(self, to_user_name, from_user_name, media_id): |
||||||
|
super().__init__() |
||||||
|
self.__dict = dict() |
||||||
|
self.__dict['ToUserName'] = to_user_name |
||||||
|
self.__dict['FromUserName'] = from_user_name |
||||||
|
self.__dict['CreateTime'] = int(time.time()) |
||||||
|
self.__dict['MediaId'] = media_id |
||||||
|
|
||||||
|
def send(self): |
||||||
|
xml_form = """ |
||||||
|
<xml> |
||||||
|
<ToUserName><![CDATA[{ToUserName}]]></ToUserName> |
||||||
|
<FromUserName><![CDATA[{FromUserName}]]></FromUserName> |
||||||
|
<CreateTime>{CreateTime}</CreateTime> |
||||||
|
<MsgType><![CDATA[image]]></MsgType> |
||||||
|
<Image> |
||||||
|
<MediaId><![CDATA[{MediaId}]]></MediaId> |
||||||
|
</Image> |
||||||
|
</xml> |
||||||
|
""" |
||||||
|
return xml_form.format(**self.__dict) |
@ -0,0 +1,20 @@ |
|||||||
|
#!/usr/bin/env python |
||||||
|
# -*- coding: utf-8 -*- |
||||||
|
######################################################################### |
||||||
|
# Author: jonyqin |
||||||
|
# Created Time: Thu 11 Sep 2014 01:53:58 PM CST |
||||||
|
# File Name: ierror.py |
||||||
|
# Description:定义错误码含义 |
||||||
|
######################################################################### |
||||||
|
WXBizMsgCrypt_OK = 0 |
||||||
|
WXBizMsgCrypt_ValidateSignature_Error = -40001 |
||||||
|
WXBizMsgCrypt_ParseXml_Error = -40002 |
||||||
|
WXBizMsgCrypt_ComputeSignature_Error = -40003 |
||||||
|
WXBizMsgCrypt_IllegalAesKey = -40004 |
||||||
|
WXBizMsgCrypt_ValidateAppid_Error = -40005 |
||||||
|
WXBizMsgCrypt_EncryptAES_Error = -40006 |
||||||
|
WXBizMsgCrypt_DecryptAES_Error = -40007 |
||||||
|
WXBizMsgCrypt_IllegalBuffer = -40008 |
||||||
|
WXBizMsgCrypt_EncodeBase64_Error = -40009 |
||||||
|
WXBizMsgCrypt_DecodeBase64_Error = -40010 |
||||||
|
WXBizMsgCrypt_GenReturnXml_Error = -40011 |
@ -0,0 +1,220 @@ |
|||||||
|
#!/usr/bin/env python |
||||||
|
# -*- coding:utf-8 -*- |
||||||
|
# project: 9月 |
||||||
|
# author: NinEveN |
||||||
|
# date: 2021/9/6 |
||||||
|
|
||||||
|
import base64 |
||||||
|
import logging |
||||||
|
import string |
||||||
|
import random |
||||||
|
import struct |
||||||
|
import hashlib |
||||||
|
from Crypto.Cipher import AES |
||||||
|
import xml.etree.cElementTree as ET |
||||||
|
import time |
||||||
|
import socket |
||||||
|
from . import ierror |
||||||
|
|
||||||
|
logger = logging.getLogger(__name__) |
||||||
|
|
||||||
|
|
||||||
|
class XMLParse: |
||||||
|
"""提供提取消息格式中的密文及生成回复消息格式的接口""" |
||||||
|
|
||||||
|
# xml消息模板 |
||||||
|
AES_TEXT_RESPONSE_TEMPLATE = """<xml> |
||||||
|
<Encrypt><![CDATA[%(msg_encrypt)s]]></Encrypt> |
||||||
|
<MsgSignature><![CDATA[%(msg_signature)s]]></MsgSignature> |
||||||
|
<TimeStamp>%(timestamp)s</TimeStamp> |
||||||
|
<Nonce><![CDATA[%(nonce)s]]></Nonce> |
||||||
|
</xml>""" |
||||||
|
|
||||||
|
def extract(self, xml_text): |
||||||
|
"""提取出xml数据包中的加密消息 |
||||||
|
@param xml_text: 待提取的xml字符串 |
||||||
|
@return: 提取出的加密消息字符串 |
||||||
|
""" |
||||||
|
try: |
||||||
|
xml_tree = ET.fromstring(xml_text) |
||||||
|
encrypt = xml_tree.find("Encrypt") |
||||||
|
to_user_name = xml_tree.find("ToUserName") |
||||||
|
return ierror.WXBizMsgCrypt_OK, encrypt.text, to_user_name.text |
||||||
|
except Exception as e: |
||||||
|
logger.error(e) |
||||||
|
return ierror.WXBizMsgCrypt_ParseXml_Error, None, None |
||||||
|
|
||||||
|
def generate(self, encrypt, signature, timestamp, nonce): |
||||||
|
"""生成xml消息 |
||||||
|
@param encrypt: 加密后的消息密文 |
||||||
|
@param signature: 安全签名 |
||||||
|
@param timestamp: 时间戳 |
||||||
|
@param nonce: 随机字符串 |
||||||
|
@return: 生成的xml字符串 |
||||||
|
""" |
||||||
|
resp_dict = { |
||||||
|
'msg_encrypt': encrypt, |
||||||
|
'msg_signature': signature, |
||||||
|
'timestamp': timestamp, |
||||||
|
'nonce': nonce, |
||||||
|
} |
||||||
|
resp_xml = self.AES_TEXT_RESPONSE_TEMPLATE % resp_dict |
||||||
|
return resp_xml |
||||||
|
|
||||||
|
|
||||||
|
class PKCS7Encoder(object): |
||||||
|
"""提供基于PKCS7算法的加解密接口""" |
||||||
|
|
||||||
|
block_size = 32 |
||||||
|
|
||||||
|
def encode(self, text): |
||||||
|
""" 对需要加密的明文进行填充补位 |
||||||
|
@param text: 需要进行填充补位操作的明文 |
||||||
|
@return: 补齐明文字符串 |
||||||
|
""" |
||||||
|
text_length = len(text) |
||||||
|
# 计算需要填充的位数 |
||||||
|
amount_to_pad = self.block_size - (text_length % self.block_size) |
||||||
|
if amount_to_pad == 0: |
||||||
|
amount_to_pad = self.block_size |
||||||
|
# 获得补位所用的字符 |
||||||
|
pad = chr(amount_to_pad) |
||||||
|
return text + pad * amount_to_pad |
||||||
|
|
||||||
|
def decode(self, decrypted): |
||||||
|
"""删除解密后明文的补位字符 |
||||||
|
@param decrypted: 解密后的明文 |
||||||
|
@return: 删除补位字符后的明文 |
||||||
|
""" |
||||||
|
pad = ord(decrypted[-1]) |
||||||
|
if pad < 1 or pad > 32: |
||||||
|
pad = 0 |
||||||
|
return decrypted[:-pad] |
||||||
|
|
||||||
|
|
||||||
|
def get_random_str(): |
||||||
|
""" 随机生成16位字符串 |
||||||
|
@return: 16位字符串 |
||||||
|
""" |
||||||
|
rule = string.ascii_letters + string.digits |
||||||
|
return "".join(random.sample(rule, 16)) |
||||||
|
|
||||||
|
|
||||||
|
class Prpcrypt(object): |
||||||
|
"""提供接收和推送给公众平台消息的加解密接口""" |
||||||
|
|
||||||
|
def __init__(self, key): |
||||||
|
# self.key = base64.b64decode(key+"=") |
||||||
|
self.key = key |
||||||
|
# 设置加解密模式为AES的CBC模式 |
||||||
|
self.mode = AES.MODE_CBC |
||||||
|
|
||||||
|
def encrypt(self, text, app_id): |
||||||
|
"""对明文进行加密 |
||||||
|
:param text: 需要加密的明文 |
||||||
|
:param app_id: 应用id |
||||||
|
@return: 加密得到的字符串 |
||||||
|
""" |
||||||
|
# 16位随机字符串添加到明文开头 |
||||||
|
text = get_random_str() + struct.pack("I", socket.htonl(len(text))).decode('utf-8') + text + app_id |
||||||
|
# 使用自定义的填充方式对明文进行补位填充 |
||||||
|
pkcs7 = PKCS7Encoder() |
||||||
|
text = pkcs7.encode(text) |
||||||
|
# 加密 |
||||||
|
crypto = AES.new(self.key, self.mode, self.key[:16]) |
||||||
|
try: |
||||||
|
ciphertext = crypto.encrypt(text) |
||||||
|
# 使用BASE64对加密后的字符串进行编码 |
||||||
|
return ierror.WXBizMsgCrypt_OK, base64.b64encode(ciphertext) |
||||||
|
except Exception as e: |
||||||
|
logger.error(e) |
||||||
|
return ierror.WXBizMsgCrypt_EncryptAES_Error, None |
||||||
|
|
||||||
|
def decrypt(self, text, app_id): |
||||||
|
""" 对解密后的明文进行补位删除 |
||||||
|
:param text: 密文 |
||||||
|
:param app_id: 应用id |
||||||
|
:return: 删除填充补位后的明文 |
||||||
|
""" |
||||||
|
try: |
||||||
|
cryptor = AES.new(self.key, self.mode, self.key[:16]) |
||||||
|
# 使用BASE64对密文进行解码,然后AES-CBC解密 |
||||||
|
plain_text = cryptor.decrypt(base64.b64decode(text)) |
||||||
|
except Exception as e: |
||||||
|
logger.error(e) |
||||||
|
return ierror.WXBizMsgCrypt_DecryptAES_Error, None |
||||||
|
try: |
||||||
|
pad = plain_text[-1] |
||||||
|
# 去掉补位字符串 |
||||||
|
# pkcs7 = PKCS7Encoder() |
||||||
|
# plain_text = pkcs7.encode(plain_text) |
||||||
|
# 去除16位随机字符串 |
||||||
|
content = plain_text[16:-pad] |
||||||
|
xml_len = socket.ntohl(struct.unpack("I", content[: 4])[0]) |
||||||
|
xml_content = content[4: xml_len + 4] |
||||||
|
from_app_id = content[xml_len + 4:].decode('utf-8') |
||||||
|
except Exception as e: |
||||||
|
logger.error(e) |
||||||
|
return ierror.WXBizMsgCrypt_IllegalBuffer, None |
||||||
|
if from_app_id != app_id: |
||||||
|
return ierror.WXBizMsgCrypt_ValidateAppid_Error, None |
||||||
|
return 0, xml_content |
||||||
|
|
||||||
|
|
||||||
|
class WxMsgCryptBase(object): |
||||||
|
def __init__(self, app_id, app_secret, token, encoding_aes_key): |
||||||
|
try: |
||||||
|
self.key = base64.b64decode(encoding_aes_key + "=") |
||||||
|
assert len(self.key) == 32 |
||||||
|
except Exception as e: |
||||||
|
logger.error(f"{encoding_aes_key} 解密失败 Exception:{e}") |
||||||
|
raise Exception("[error]: EncodingAESKey invalid !") |
||||||
|
self.token = token |
||||||
|
self.app_id = app_id |
||||||
|
|
||||||
|
def encrypt_msg(self, msg, nonce, timestamp=None): |
||||||
|
""" |
||||||
|
:param msg: 企业号待回复用户的消息,xml格式的字符串 |
||||||
|
:param nonce: 随机串,可以自己生成,也可以用URL参数的nonce |
||||||
|
:param timestamp: 时间戳,可以自己生成,也可以用URL参数的timestamp,如为None则自动用当前时间 |
||||||
|
:return: 成功0,sEncryptMsg,失败返回对应的错误码None |
||||||
|
""" |
||||||
|
pc = Prpcrypt(self.key) |
||||||
|
ret, encrypt = pc.encrypt(msg, self.app_id) |
||||||
|
if ret != 0: |
||||||
|
return ret, None |
||||||
|
if timestamp is None: |
||||||
|
timestamp = str(int(time.time())) |
||||||
|
|
||||||
|
try: |
||||||
|
sha = hashlib.sha1(("".join(sorted([self.token, timestamp, nonce, encrypt]))).encode('utf-8')) |
||||||
|
return ret, XMLParse().generate(encrypt, sha.hexdigest(), timestamp, nonce) |
||||||
|
except Exception as e: |
||||||
|
logger.error(e) |
||||||
|
return ierror.WXBizMsgCrypt_ComputeSignature_Error, None |
||||||
|
|
||||||
|
def decrypt_msg(self, msg, msg_signature, timestamp, nonce): |
||||||
|
""" |
||||||
|
:param msg: 密文,对应POST请求的数据 |
||||||
|
:param msg_signature: 签名串,对应URL参数的msg_signature |
||||||
|
:param timestamp: 时间戳,对应URL参数的timestamp |
||||||
|
:param nonce: 随机串,对应URL参数的nonce |
||||||
|
:return: 成功0,失败返回对应的错误码 |
||||||
|
""" |
||||||
|
|
||||||
|
if isinstance(msg, str): |
||||||
|
ret, encrypt, _ = XMLParse().extract(msg) |
||||||
|
if ret != 0: |
||||||
|
return ret, None |
||||||
|
else: |
||||||
|
encrypt = msg.get('Encrypt') |
||||||
|
try: |
||||||
|
sha = hashlib.sha1(("".join(sorted([self.token, timestamp, nonce, encrypt]))).encode('utf-8')) |
||||||
|
if not sha.hexdigest() == msg_signature: |
||||||
|
return ierror.WXBizMsgCrypt_ValidateSignature_Error, None |
||||||
|
pc = Prpcrypt(self.key) |
||||||
|
ret, xml_content = pc.decrypt(encrypt, self.app_id) |
||||||
|
return ret, xml_content |
||||||
|
except Exception as e: |
||||||
|
logger.error(e) |
||||||
|
return ierror.WXBizMsgCrypt_ComputeSignature_Error, None |
@ -0,0 +1,119 @@ |
|||||||
|
#!/usr/bin/env python |
||||||
|
# -*- coding:utf-8 -*- |
||||||
|
# project: 9月 |
||||||
|
# author: NinEveN |
||||||
|
# date: 2021/9/6 |
||||||
|
from hashlib import sha1 |
||||||
|
import requests |
||||||
|
import logging |
||||||
|
import json |
||||||
|
from fir_ser.settings import THIRDLOGINCONF |
||||||
|
from api.utils.mp.utils import WxMsgCryptBase |
||||||
|
from api.utils.storage.caches import get_wx_access_token_cache |
||||||
|
|
||||||
|
logger = logging.getLogger(__name__) |
||||||
|
wx_login_info = THIRDLOGINCONF.wx_official |
||||||
|
|
||||||
|
|
||||||
|
def create_menu(): |
||||||
|
menu_json = { |
||||||
|
"button": [ |
||||||
|
{ |
||||||
|
"type": "click", |
||||||
|
"name": "赞", |
||||||
|
"key": "good" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "分发平台", |
||||||
|
"sub_button": [ |
||||||
|
{ |
||||||
|
"type": "view", |
||||||
|
"name": "官方地址", |
||||||
|
"url": "https://flyapps.cn" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"type": "view", |
||||||
|
"name": "留言反馈", |
||||||
|
"url": "https://flyapps.cn/gbook/" |
||||||
|
}, |
||||||
|
] |
||||||
|
}, |
||||||
|
{ |
||||||
|
"type": "media_id", |
||||||
|
"name": "联系我们", |
||||||
|
"media_id": "qvQxPuAb4GnUgjkxl2xVnbsnldxawf4DXM09biqgP30" |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
|
p_url = f"https://api.weixin.qq.com/cgi-bin/menu/create?access_token={get_wx_access_token_cache()}" |
||||||
|
req = requests.post(url=p_url, data=json.dumps(menu_json, ensure_ascii=False).encode('utf-8')) |
||||||
|
print(req.json()) |
||||||
|
|
||||||
|
|
||||||
|
def show_qrcode_url(ticket): |
||||||
|
return f'https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket={ticket}' |
||||||
|
|
||||||
|
|
||||||
|
def make_wx_login_qrcode(scene_str='web.login', expire_seconds=600): |
||||||
|
""" |
||||||
|
:param scene_str: 场景值ID(字符串形式的ID),字符串类型,长度限制为1到64 |
||||||
|
:param expire_seconds: 该二维码有效时间,以秒为单位。 最大不超过2592000(即30天),此字段如果不填,则默认有效期为30秒。 |
||||||
|
:return: { |
||||||
|
"ticket":"gQH47joAAAAAAAAAASxodHRwOi8vd2VpeGluLnFxLmNvbS9xL2taZ2Z3TVRtNzJXV1Brb3ZhYmJJAAIEZ23sUwMEmm3sUw==", |
||||||
|
"expire_seconds":60, |
||||||
|
"url":"http://weixin.qq.com/q/kZgfwMTm72WWPkovabbI" |
||||||
|
} |
||||||
|
https://developers.weixin.qq.com/doc/offiaccount/Account_Management/Generating_a_Parametric_QR_Code.html |
||||||
|
""" |
||||||
|
t_url = f'https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token={get_wx_access_token_cache()}' |
||||||
|
data = {"expire_seconds": expire_seconds, "action_name": "QR_STR_SCENE", |
||||||
|
"action_info": {"scene": {"scene_str": scene_str}}} |
||||||
|
req = requests.post(t_url, json=data) |
||||||
|
if req.status_code == 200: |
||||||
|
return True, req.json() |
||||||
|
logger.error(f"make wx login qrcode failed {req.status_code} {req.text}") |
||||||
|
return False, req.text |
||||||
|
|
||||||
|
|
||||||
|
def get_userinfo_from_openid(open_id): |
||||||
|
t_url = f'https://api.weixin.qq.com/cgi-bin/user/info?access_token={get_wx_access_token_cache()}&openid={open_id}&lang=zh_CN' |
||||||
|
req = requests.get(t_url) |
||||||
|
if req.status_code == 200: |
||||||
|
return True, req.json() |
||||||
|
logger.error(f"get userinfo from openid failed {req.status_code} {req.text}") |
||||||
|
return False, req.text |
||||||
|
|
||||||
|
|
||||||
|
class WxOfficialBase(object): |
||||||
|
|
||||||
|
def __init__(self, app_id, app_secret, token, encoding_aes_key): |
||||||
|
self.app_id = app_id |
||||||
|
self.app_secret = app_secret |
||||||
|
self.token = token |
||||||
|
self.encoding_aes_key = encoding_aes_key |
||||||
|
|
||||||
|
def get_access_token(self): |
||||||
|
t_url = f'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={self.app_id}&secret={self.app_secret}' |
||||||
|
req = requests.get(t_url) |
||||||
|
if req.status_code == 200: |
||||||
|
return req.json() |
||||||
|
logger.error(f"get access token failed {req.status_code} {req.text}") |
||||||
|
return req.text |
||||||
|
|
||||||
|
|
||||||
|
def make_wx_auth_obj(): |
||||||
|
return WxOfficialBase(**wx_login_info.get('auth')) |
||||||
|
|
||||||
|
|
||||||
|
def check_signature(params): |
||||||
|
tmp_list = sorted([wx_login_info.get('auth', {}).get('token'), params.get("timestamp"), params.get("nonce")]) |
||||||
|
tmp_str = "".join(tmp_list) |
||||||
|
tmp_str = sha1(tmp_str.encode("utf-8")).hexdigest() |
||||||
|
if tmp_str == params.get("signature"): |
||||||
|
return int(params.get("echostr")) |
||||||
|
return '' |
||||||
|
|
||||||
|
|
||||||
|
class WxMsgCrypt(WxMsgCryptBase): |
||||||
|
def __init__(self): |
||||||
|
super().__init__(**wx_login_info.get('auth')) |
@ -0,0 +1,176 @@ |
|||||||
|
#!/usr/bin/env python |
||||||
|
# -*- coding:utf-8 -*- |
||||||
|
# project: 3月 |
||||||
|
# author: NinEveN |
||||||
|
# date: 2021/3/29 |
||||||
|
from rest_framework.pagination import PageNumberPagination |
||||||
|
from rest_framework.response import Response |
||||||
|
import logging |
||||||
|
import random |
||||||
|
from rest_framework.views import APIView |
||||||
|
from rest_framework_xml.parsers import XMLParser |
||||||
|
from api.models import ThirdWeChatUserInfo, UserInfo |
||||||
|
from api.utils.auth import ExpiringTokenAuthentication |
||||||
|
|
||||||
|
from api.utils.mp.chat import reply, receive |
||||||
|
from api.utils.mp.wechat import check_signature, WxMsgCrypt, get_userinfo_from_openid |
||||||
|
from api.utils.response import BaseResponse |
||||||
|
from api.utils.serializer import ThirdWxSerializer |
||||||
|
from api.utils.storage.caches import set_wx_ticket_login_info_cache |
||||||
|
|
||||||
|
logger = logging.getLogger(__name__) |
||||||
|
|
||||||
|
GOOD_XX = [ |
||||||
|
'您是一位有恒心有毅力的人,我很佩服您!', |
||||||
|
'越有内涵的人越虚怀若谷,像您这样有内涵的人我十分敬佩!', |
||||||
|
'你像天上的月亮,也像那闪烁的星星,可惜我不是诗人,否则,当写一万首诗来形容你的美丽!', |
||||||
|
'据考证,你是世界上最大的宝藏,里面藏满了金子、钻石和名画!', |
||||||
|
'虽然你没有一簇樱唇两排贝齿,但你的谈吐高雅脱俗,机智过人,令我折服!', |
||||||
|
'您是一位有恒心有毅力的人,我很佩服您!', |
||||||
|
'我很荣幸,认识你这样有内涵的漂亮朋友!', |
||||||
|
'春花秋月,是诗人们歌颂的情景,可是我对于它,却感到十分平凡。只有你嵌着梨涡的笑容,才是我眼中最美的景象!', |
||||||
|
'你像一片轻柔的云在我眼前飘来飘去,你清丽秀雅的脸上荡漾着春天般美丽的笑容。在你那双又大又亮的眼睛里,我总能捕捉到你的宁静,你的热烈,你的聪颖,你的敏感!', |
||||||
|
'人生旅程上,您丰富我的心灵,开发我得智力,为我点燃了希望的光芒,谢谢您!', |
||||||
|
] |
||||||
|
|
||||||
|
|
||||||
|
class TextXMLParser(XMLParser): |
||||||
|
media_type = 'text/xml' # 微信解析的是 text/xml |
||||||
|
|
||||||
|
|
||||||
|
def reply_login_msg(rec_msg, to_user, from_user, ): |
||||||
|
content = f'还未绑定用户,请通过手机或者邮箱登录账户之后进行绑定' |
||||||
|
u_data_id = -1 |
||||||
|
wx_user_obj = ThirdWeChatUserInfo.objects.filter(openid=to_user).first() |
||||||
|
if wx_user_obj: |
||||||
|
u_data_id = wx_user_obj.user_id.pk |
||||||
|
content = f'用户 {wx_user_obj.user_id.first_name} 登录成功' |
||||||
|
set_wx_ticket_login_info_cache(rec_msg.Ticket, u_data_id) |
||||||
|
reply_msg = reply.TextMsg(to_user, from_user, content) |
||||||
|
return reply_msg.send() |
||||||
|
|
||||||
|
|
||||||
|
def update_or_create_wx_userinfo(to_user, user_obj): |
||||||
|
code, wx_user_info = get_userinfo_from_openid(to_user) |
||||||
|
logger.info(f"get openid:{to_user} info:{to_user} code:{code}") |
||||||
|
if code: |
||||||
|
wx_user_info = { |
||||||
|
'openid': wx_user_info.get('openid'), |
||||||
|
'nickname': wx_user_info.get('nickname'), |
||||||
|
'sex': wx_user_info.get('sex'), |
||||||
|
'subscribe_time': wx_user_info.get('subscribe_time'), |
||||||
|
'head_img_url': wx_user_info.get('headimgurl'), |
||||||
|
'address': f"{wx_user_info.get('country')}-{wx_user_info.get('province')}-{wx_user_info.get('city')}", |
||||||
|
'subscribe': wx_user_info.get('subscribe'), |
||||||
|
} |
||||||
|
ThirdWeChatUserInfo.objects.update_or_create(user_id=user_obj, openid=to_user, defaults=wx_user_info) |
||||||
|
|
||||||
|
|
||||||
|
class ValidWxChatToken(APIView): |
||||||
|
parser_classes = (XMLParser, TextXMLParser) |
||||||
|
|
||||||
|
def get(self, request): |
||||||
|
params = request.query_params |
||||||
|
return Response(check_signature(params)) |
||||||
|
|
||||||
|
def post(self, request): |
||||||
|
params = request.query_params |
||||||
|
data = request.data |
||||||
|
encrypt_obj = WxMsgCrypt() |
||||||
|
ret, encrypt_xml = encrypt_obj.decrypt_msg(data, params.get("msg_signature"), params.get("timestamp"), |
||||||
|
params.get("nonce")) |
||||||
|
logger.info(f"code:{ret}, result {encrypt_xml}") |
||||||
|
result = "success" |
||||||
|
if ret == 0: |
||||||
|
content = '欢迎使用fly应用分发平台,感谢您的关注' |
||||||
|
rec_msg = receive.parse_xml(encrypt_xml) |
||||||
|
if isinstance(rec_msg, receive.Msg): |
||||||
|
to_user = rec_msg.FromUserName |
||||||
|
from_user = rec_msg.ToUserName |
||||||
|
|
||||||
|
if rec_msg.MsgType == 'text': |
||||||
|
content = random.choices([*GOOD_XX, content, rec_msg.Content.decode('utf-8')])[0] |
||||||
|
reply_msg = reply.TextMsg(to_user, from_user, content) |
||||||
|
result = reply_msg.send() |
||||||
|
|
||||||
|
elif rec_msg.MsgType == 'image': |
||||||
|
media_id = rec_msg.MediaId |
||||||
|
reply_msg = reply.ImageMsg(to_user, from_user, media_id) |
||||||
|
result = reply_msg.send() |
||||||
|
else: |
||||||
|
result = reply.Msg().send() |
||||||
|
|
||||||
|
elif isinstance(rec_msg, receive.EventMsg): |
||||||
|
to_user = rec_msg.FromUserName |
||||||
|
from_user = rec_msg.ToUserName |
||||||
|
if rec_msg.Event == 'CLICK': # 公众号点击事件 |
||||||
|
if rec_msg.Eventkey == 'good': |
||||||
|
content = random.choices(GOOD_XX)[0] |
||||||
|
reply_msg = reply.TextMsg(to_user, from_user, content) |
||||||
|
result = reply_msg.send() |
||||||
|
elif rec_msg.Event in ['subscribe', 'unsubscribe']: # 订阅 |
||||||
|
reply_msg = reply.TextMsg(to_user, from_user, content) |
||||||
|
result = reply_msg.send() |
||||||
|
if rec_msg.Eventkey == 'qrscene_web.login': # 首次关注,登录认证操作 |
||||||
|
if rec_msg.Event == 'subscribe': |
||||||
|
result = reply_login_msg(rec_msg, to_user, from_user, ) |
||||||
|
if rec_msg.Event == 'unsubscribe': |
||||||
|
ThirdWeChatUserInfo.objects.filter(openid=to_user).update(subscribe=False) |
||||||
|
|
||||||
|
elif rec_msg.Event == 'SCAN': |
||||||
|
if rec_msg.Eventkey == 'web.login': # 已经关注,然后再次扫码,登录认证操作 |
||||||
|
result = reply_login_msg(rec_msg, to_user, from_user, ) |
||||||
|
elif rec_msg.Eventkey.startswith('web.bind.'): |
||||||
|
uid = rec_msg.Eventkey.split('.')[-1] |
||||||
|
wx_user_obj = ThirdWeChatUserInfo.objects.filter(openid=to_user).first() |
||||||
|
user_obj = UserInfo.objects.filter(uid=uid).first() |
||||||
|
update_or_create_wx_userinfo(to_user, user_obj) |
||||||
|
if wx_user_obj: |
||||||
|
if user_obj and user_obj.uid == wx_user_obj.user_id.uid: |
||||||
|
content = f'账户 {wx_user_obj.user_id.first_name} 已经绑定成功,感谢您的使用' |
||||||
|
else: |
||||||
|
content = f'账户已经被 {wx_user_obj.user_id.first_name} 绑定' |
||||||
|
else: |
||||||
|
content = f'账户绑定 {wx_user_obj.user_id.first_name} 成功' |
||||||
|
set_wx_ticket_login_info_cache(rec_msg.Ticket, user_obj.pk) |
||||||
|
reply_msg = reply.TextMsg(to_user, from_user, content) |
||||||
|
result = reply_msg.send() |
||||||
|
|
||||||
|
else: |
||||||
|
logger.error('密文解密失败') |
||||||
|
|
||||||
|
return Response(result) |
||||||
|
|
||||||
|
|
||||||
|
class PageNumber(PageNumberPagination): |
||||||
|
page_size = 10 # 每页显示多少条 |
||||||
|
page_size_query_param = 'size' # URL中每页显示条数的参数 |
||||||
|
page_query_param = 'page' # URL中页码的参数 |
||||||
|
max_page_size = None # 最大页码数限制 |
||||||
|
|
||||||
|
|
||||||
|
class ThirdWxAccount(APIView): |
||||||
|
authentication_classes = [ExpiringTokenAuthentication, ] |
||||||
|
|
||||||
|
def get(self, request): |
||||||
|
res = BaseResponse() |
||||||
|
# wx_open_id = request.query_params.get("openid", None) |
||||||
|
wx_obj_lists = ThirdWeChatUserInfo.objects.filter(user_id=request.user) |
||||||
|
# if wx_open_id: |
||||||
|
# wx_obj_lists = wx_obj_lists.filter(openid=wx_open_id) |
||||||
|
page_obj = PageNumber() |
||||||
|
info_serializer = page_obj.paginate_queryset(queryset=wx_obj_lists.order_by("-subscribe_time"), |
||||||
|
request=request, |
||||||
|
view=self) |
||||||
|
wx_user_info = ThirdWxSerializer(info_serializer, many=True, ) |
||||||
|
res.data = wx_user_info.data |
||||||
|
res.count = wx_obj_lists.count() |
||||||
|
|
||||||
|
return Response(res.dict) |
||||||
|
|
||||||
|
def delete(self, request): |
||||||
|
openid = request.query_params.get("openid") |
||||||
|
user_id = request.query_params.get("user_id") |
||||||
|
if openid and user_id: |
||||||
|
ThirdWeChatUserInfo.objects.filter(user_id_id=user_id, openid=openid).delete() |
||||||
|
return self.get(request) |
Loading…
Reference in new issue