232 lines
12 KiB
Python
232 lines
12 KiB
Python
"""
|
||
Author: xiao 792282@qq.com
|
||
Date: 2025-08-10 11:36:05
|
||
LastEditors: xiao 792282@qq.com
|
||
LastEditTime: 2025-08-17 13:38:40
|
||
FilePath: django_公共认证接口/小程序认证接口.py
|
||
Description: 这是默认设置,可以在设置》工具》File Description中进行配置
|
||
"""
|
||
import os
|
||
|
||
import django
|
||
|
||
from sixGuoDjango.settings import BASE_DIR
|
||
from wechat_token.token工具包 import sixToken
|
||
from wechat_项目工具包.EMS中国邮政.ems邮政 import ems邮政物流工具
|
||
|
||
os.environ.setdefault(key="DJANGO_SETTINGS_MODULE", value=f"{BASE_DIR.name}.settings")
|
||
django.setup()
|
||
|
||
import jmespath
|
||
|
||
from rest_framework.viewsets import ViewSet
|
||
|
||
import base64
|
||
import hashlib
|
||
import os
|
||
from pathlib import Path
|
||
|
||
import orjson
|
||
import pydash
|
||
from Crypto.Cipher import AES
|
||
from Crypto.Util.Padding import pad
|
||
from django.conf import settings
|
||
from django.core.handlers.wsgi import WSGIRequest
|
||
from django.http import HttpResponse
|
||
from requests import post
|
||
from rest_framework import status
|
||
from rest_framework.request import Request
|
||
from rest_framework.response import Response
|
||
|
||
|
||
# async def async_function(FromUserName: str, result_data_list, channelID):
|
||
# """ 异步调用:协程"""
|
||
# pass
|
||
# print("在这里打印值", result_data_list)
|
||
# result_data = pydash.get(obj=result_data_list, path="0")
|
||
#
|
||
# data = {'type': 'chat_messages', 'message': {"xiao": "zizi", "result_data": result_data}, }
|
||
# await channel_layer.send(channel=channelID, message=data, )
|
||
#
|
||
|
||
class sixmini(ViewSet):
|
||
"""小程序消息的认和回调接口"""
|
||
token = "xiaozizi"
|
||
EncodingAESKey = "sdnUVuOoDCzMqFJZ9uECMXSYEbVAYnQTqQIWSkI1KfP" # EncodingAESKey(消息加密密钥)(不程序的)
|
||
"""
|
||
消息推送启用并设置消息推送配置后,用户发给小程序的消息以及开发者需要的事件推送,都将被微信转发至该服务器地址中。
|
||
开发者也可使用云函数或微信云托管免鉴权接收消息推送,立即开通 认证
|
||
https://mp.weixin.qq.com/wxamp/devprofile/get_profile?token=2052988734&lang=zh_CN
|
||
"""
|
||
|
||
def sendCustomMessage(self, FromUserName: str, post_data_type):
|
||
""""""
|
||
# 回复消息的请求
|
||
uri = f"https://api.weixin.qq.com/cgi-bin/message/custom/send"
|
||
post_data_text = {
|
||
"touser": FromUserName, # string 是 用户的 OpenID
|
||
"msgtype": "text", # string 是消息类型。text表示文本消息;image表示图片消息;link表示图文链接;miniprogrampage表示小程序卡片。
|
||
"text": {"content": "你好"},
|
||
}
|
||
post_data_image = {
|
||
"touser": FromUserName, # string 是 用户的 OpenID
|
||
"msgtype": "image", # object 否 图片消息,msgtype="image" 时必填。
|
||
"image": {"media_id": "g_2HeEUOFmcJoWEic-ex4rj9BPVQYvkXAdTFWOMifCkxreYQhiJxxbaN8bDNfHuR"},
|
||
# 发送的图片的媒体ID,通过 uploadTempMedia上传图片文件获得。
|
||
}
|
||
post_data_link = {
|
||
"touser": FromUserName, # string 是 用户的 OpenID
|
||
"msgtype": "link", # object 否 图文链接,msgtype="link" 时必填
|
||
"link": {
|
||
"title": "Happy Day", # string 否 消息标题
|
||
"description": "Is Really A Happy Day", # string 否 图文链接消息
|
||
"url": "URL", # string 否 图文链接消息被点击后跳转的链接
|
||
"thumb_url": "THUMB_URL", # 图文链接消息的图片链接,支持 JPG、PNG 格式,较好的效果为大图 640 X 320,小图 80 X 80
|
||
},
|
||
}
|
||
post_data_miniprogrampage = {
|
||
"touser": FromUserName, # string 是 用户的 OpenID
|
||
"msgtype": "miniprogrampage",
|
||
# string 是消息类型。text表示文本消息;image表示图片消息;link表示图文链接;miniprogrampage表示小程序卡片。
|
||
"miniprogrampage": {
|
||
"title": "title", # string 否 消息标题
|
||
"pagepath": "pagepath",
|
||
# 小程序的页面路径,跟app.json对齐,支持参数,比如pages/index/index?foo=bar
|
||
"thumb_media_id": "thumb_media_id",
|
||
# 小程序消息卡片的封面, image 类型的 media_id,通过 uploadTempMedia接口上传图片文件获得,建议大小为 520*416
|
||
},
|
||
}
|
||
wecom_token = sixToken.get_wecom_token()
|
||
mini_token = sixToken.get_mini_token()
|
||
result_data = post(
|
||
url=uri, params={"access_token": mini_token},
|
||
data=orjson.dumps(post_data_text).decode("utf-8"),
|
||
)
|
||
|
||
def uploadTempMedia(self, request: WSGIRequest | Request, *args, **kwargs):
|
||
|
||
# 上传图片的接口
|
||
uri = f"https://api.weixin.qq.com/cgi-bin/media/upload"
|
||
wecom_token = sixToken.get_wecom_token()
|
||
file_path = Path(settings.BASE_DIR).joinpath("django_环境/常用类/xiao小程序项目图片/ico运力管理系统.png")
|
||
file = {"media": open(file=file_path, mode="rb", )}
|
||
result_data = post(url=uri, params={"access_token": wecom_token, "type": "image", }, files=file)
|
||
print(result_data.json())
|
||
|
||
def 消息加密方法(self, msg: str, appid: str, encoding_aes_key: str, token: str, timestamp: str,
|
||
nonce: str, ) -> dict:
|
||
""""""
|
||
# 1. 生成 AESKey(Base64 解码 EncodingAESKey + "=")
|
||
aes_key = base64.b64decode(encoding_aes_key + "=")
|
||
# 2. 构造 FullStr = random(16B) + msg_len(4B) + msg + appid
|
||
random_16b = os.urandom(16) # 16 字节随机字符串
|
||
# msg_len = struct.pack( ">I", len( msg_bytes ) ) # 4 字节网络字节序 学习理解
|
||
msg_bytes_1 = msg.encode("utf-8")
|
||
msg_len_1 = len(msg_bytes_1).to_bytes(length=4, byteorder="big") # 大端序
|
||
# 结果是一样的
|
||
# print( msg_len_1, msg_len_1, "这二个一是一样的吗" )
|
||
appid_bytes = appid.encode("utf-8")
|
||
full_str = random_16b + msg_len_1 + msg_bytes_1 + appid_bytes
|
||
# 3. AES-256-CBC 加密(PKCS#7 填充)
|
||
iv = aes_key[:16] # IV 取 AESKey 前 16 字节( 自定义的随机密钥字符串的base64 前16位)
|
||
cipher = AES.new(key=aes_key, mode=AES.MODE_CBC, iv=iv)
|
||
encrypted = cipher.encrypt(pad(data_to_pad=full_str, block_size=AES.block_size))
|
||
# 4. Base64 编码得到 Encrypt
|
||
encrypt = base64.b64encode(encrypted).decode("utf-8")
|
||
# 5. 生成 MsgSignature(sha1(token + timestamp + nonce + encrypt))
|
||
params = sorted([str(timestamp), nonce, token, encrypt])
|
||
signature_str = "".join(params)
|
||
msg_signature = hashlib.sha1(signature_str.encode("utf-8")).hexdigest()
|
||
return {"Encrypt": encrypt, "MsgSignature": msg_signature, "TimeStamp": timestamp, "Nonce": nonce, }
|
||
|
||
def 消息解密(self, encrypt: str):
|
||
"""
|
||
知识点: 微信请求发送过来的 encrypt 是 b64encode编码后的数据, 是由 iv + 编码数据 组成的
|
||
"""
|
||
aes_key = base64.b64decode(self.EncodingAESKey + "=") # 补 全 Base64 编码缺失的 '='
|
||
# Step 2: Base64 解码加密数据
|
||
encrypted_data = base64.b64decode(encrypt)
|
||
# Step 3: 提取 IV 和密文
|
||
try:
|
||
# 获取初始向量(IV取AES Key的前16字节)
|
||
iv = aes_key[:16]
|
||
# 创建AES-CBC解密器
|
||
cipher = AES.new(key=aes_key, mode=AES.MODE_CBC, iv=iv)
|
||
# 执行AES解密
|
||
decrypted = cipher.decrypt(encrypted_data)
|
||
# 解析解密后的数据:# 1. 去除前 16 字节的随机字符串
|
||
content_start = decrypted[16:]
|
||
# 使用int.from_bytes提取消息长度(大端序)这里的 content_start 是去除掉了解密后面的前16位字符串的长度 # 2. 读取4字节的消息长度(大端序)
|
||
xml_len = int.from_bytes(content_start[:4], byteorder='big')
|
||
# 提取XML消息内容
|
||
xml_content = content_start[4:4 + xml_len].decode('utf-8')
|
||
return orjson.loads(xml_content)
|
||
except Exception as e:
|
||
raise ValueError("Decryption failed: " + str(e))
|
||
|
||
def 更新物流信息(self, data):
|
||
WayBillId = jmespath.search(data=data, expression="WayBillId")
|
||
OrderId = jmespath.search(data=data, expression="OrderId")
|
||
ems邮政物流工具().更新物流快递数据(order物流公司订单编号=WayBillId, )
|
||
|
||
def 确认收货(self, data):
|
||
print("确认订单请求接口进来了", data)
|
||
确认收货的方法 = pydash.get(obj=data, path="confirm_receive_method", default=None)
|
||
order支付微信侧订单号 = pydash.get(obj=data, path="transaction_id")
|
||
if 确认收货的方法:
|
||
print("确认收货的回请求进来了")
|
||
ems邮政物流工具().确认收货(order支付微信侧订单号=order支付微信侧订单号)
|
||
|
||
def web_view_auth(self, request: WSGIRequest | Request, ):
|
||
""""""
|
||
pass
|
||
# get请求的方法读取文件并且返回文件
|
||
print("认证web_view的请求进来了没有")
|
||
return HttpResponse(content="8cfc2017094adff8d7c66b94e209456e")
|
||
|
||
def api_get(self, request: Request, *args, **kwargs):
|
||
"""微信小程序接口的回调"""
|
||
signature = request.query_params.get("signature", "")
|
||
timestamp = request.query_params.get("timestamp", "")
|
||
nonce = request.query_params.get("nonce", "")
|
||
echostr = request.query_params.get("echostr", False)
|
||
# TODO:sorted([token, timestamp, nonce])这是就是对三个参数进行字典排序
|
||
# hashstr = "".join(sorted([self.token, timestamp, nonce])) # 这是就是对三个参数进行字典排序
|
||
hashstr = pydash.join(array=sorted([self.token, timestamp, nonce]), separator="")
|
||
sig = hashlib.new(name="sha1", data=hashstr.encode("utf-8")).hexdigest()
|
||
if sig == signature:
|
||
# 当上面的值判断正确后 官方要求原样返回 echostr ;
|
||
return HttpResponse(content=echostr, content_type="text/plan")
|
||
else:
|
||
return Response(data="不对", )
|
||
|
||
def api_post(self, request: Request, *args, **kwargs):
|
||
"""
|
||
调用本接口: 就可以向已经订阅消息的用户发送消息,(同时需要注意的是:消息模板需要在小程序的管理后台订阅好)
|
||
:param request:
|
||
:param args:
|
||
:param kwargs:
|
||
:return:
|
||
"""
|
||
Encrypt = request.data.get("Encrypt", False) #
|
||
xml_content = self.消息解密(encrypt=Encrypt)
|
||
Event = pydash.get(obj=xml_content, path="Event")
|
||
MsgType = pydash.get(obj=xml_content, path="MsgType")
|
||
print("小程序官方推送的消息", xml_content)
|
||
if Event == "trade_manage_order_settlement":
|
||
# 这里的通知是微信官方发过来的, 同时,是用户没有确认收货时 达到时间后自动触发的
|
||
self.确认收货(data=xml_content)
|
||
if Event == "subscribe_msg_popup_event":
|
||
print("----此接口是记录的用户点击订阅消息的次数!", xml_content)
|
||
# subMsgObj = weiuser.objects.get(mini_openid=FromUserName)
|
||
if Event == "add_express_path":
|
||
# 这里发生回调,是说明物流的快递信息有推送
|
||
self.更新物流信息(data=xml_content)
|
||
if MsgType == "text":
|
||
pass
|
||
# 客服收到的文字消息(此时只是测试,直接返回了文本本身)
|
||
# self.sendCustomMessage(FromUserName=FromUserName, post_data_type="text")
|
||
if MsgType == "image":
|
||
pass # 客服收到了图片消息(只是测试,收到消息后,并没有做任何的处理)
|
||
return Response(data={"message": "xiao方法被调用了"}, status=status.HTTP_200_OK)
|