269 lines
13 KiB
Python
269 lines
13 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):
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
:param request:
|
|||
|
|
:type request:
|
|||
|
|
:param args:
|
|||
|
|
:type args:
|
|||
|
|
:param kwargs:
|
|||
|
|
:type 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 + 编码数据 组成的
|
|||
|
|
:param encrypt:
|
|||
|
|
:type encrypt:
|
|||
|
|
:return:
|
|||
|
|
:rtype:
|
|||
|
|
"""
|
|||
|
|
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, order物流订单号=OrderId)
|
|||
|
|
|
|||
|
|
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支付微信侧订单号)
|
|||
|
|
确认收货的时候数据 = {
|
|||
|
|
'ToUserName': 'gh_f754c53eb962',
|
|||
|
|
'FromUserName': 'ohQkD7HmLYEYrFjFau6juOhduRU8',
|
|||
|
|
'CreateTime': 1784839115,
|
|||
|
|
'MsgType': 'event', # 类型,微信官方默认的
|
|||
|
|
'Event': 'trade_manage_order_settlement', # 接收到消息的类型, 可以判断是不是当前用户 确认收货
|
|||
|
|
'transaction_id': '4200003120202607191690098150', # 微信官方生成的支付订单号
|
|||
|
|
'merchant_id': '1700383419', # 微信官方的商户号
|
|||
|
|
'sub_merchant_id': '', # 子商户有的商户ID
|
|||
|
|
'merchant_trade_no': '548663656512',
|
|||
|
|
'pay_time': 1784410374, # 接收到消息的时间戳
|
|||
|
|
'shipped_time': 1784666314, # 商家发货的时间
|
|||
|
|
'confirm_receive_method': 2, # 当前用户 确认收货的方式 2:表示是自动确认收货 1:表示用户主动点击收货
|
|||
|
|
'confirm_receive_time': 1784839114, # 当前用户确认收货的时间
|
|||
|
|
'settlement_time': 1784839115, # 微信官方 确认的 结算时间
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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):
|
|||
|
|
"""
|
|||
|
|
:param request:
|
|||
|
|
:param args:
|
|||
|
|
:param kwargs:
|
|||
|
|
:return:
|
|||
|
|
"""
|
|||
|
|
print("认证请求进来了")
|
|||
|
|
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])) # 这是就是对三个参数进行字典排序
|
|||
|
|
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")
|
|||
|
|
FromUserName = pydash.get(obj=xml_content, path="FromUserName")
|
|||
|
|
MsgType = pydash.get(obj=xml_content, path="MsgType")
|
|||
|
|
print("微信官方到底推送了啥信息啊...........为什么", )
|
|||
|
|
print(xml_content)
|
|||
|
|
print("微信官方到底推送了啥信息啊...........为什么", )
|
|||
|
|
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":
|
|||
|
|
# 这里发生回调,是说明物流的快递信息有推送
|
|||
|
|
pass
|
|||
|
|
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)
|