Files
Ai_Admin/MyApi/app_login_pay.py
2024-10-18 21:17:49 +08:00

299 lines
13 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import base64
import json
import string
from datetime import datetime
from decimal import Decimal
import requests
import time
import random
from xml.etree import ElementTree as ET
from Crypto.Hash import SHA256
from Crypto.PublicKey import RSA
from Crypto.Signature import pkcs1_15
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from django.http import JsonResponse, HttpResponse, HttpResponseRedirect
from django.views.decorators.csrf import csrf_exempt
from django.conf import settings
from .models import TransactionLog, MembershipType, User
from .API_Log import get_logger
log_file = '支付日志demo.log'
logger = get_logger('支付日志demo', log_file, when='midnight', backup_count=7)
def get_sign(sign_str, key_path):
rsa_key = RSA.importKey(open(key_path).read())
signer = pkcs1_15.new(rsa_key)
digest = SHA256.new(sign_str.encode('utf8'))
sign = base64.b64encode(signer.sign(digest)).decode('utf-8')
return sign
def decrypt(nonce, ciphertext, associated_data, api_key):
key_bytes = str.encode(api_key)
nonce_bytes = str.encode(nonce)
ad_bytes = str.encode(associated_data)
data = base64.b64decode(ciphertext)
aesgcm = AESGCM(key_bytes)
return aesgcm.decrypt(nonce_bytes, data, ad_bytes)
def update_user_membership_or_quota(openid, membership_type):
current_time = int(time.time())
try:
user = User.objects.get(openid=openid)
if membership_type.is_quota:
user.coins += membership_type.coins
logger.info(f'用户 {user.id} (openid: {openid}) 充值额度卡,增加金币 {membership_type.coins}')
else:
user.is_member = True
if user.member_start_time is None:
user.member_start_time = current_time
elif user.member_end_time is None or user.member_end_time < current_time:
user.member_start_time = current_time
if user.member_end_time is None or user.member_end_time < current_time:
user.member_end_time = current_time + membership_type.duration_days * 24 * 3600
else:
user.member_end_time += membership_type.duration_days * 24 * 3600
user.coins += membership_type.coins
user.save()
return True
except User.DoesNotExist:
logger.error(f'用户 {openid} 不存在')
return False
except Exception as e:
logger.error(f'更新会员或额度充值时出错: {str(e)}')
return False
@csrf_exempt
def wx_pay(request):
request_data = json.loads(request.body)
logger.info(f"生成订单{request_data}")
type = request_data.get('type', 'default')
wx_price = request_data['total_fee']
openid = request_data['openid']
uuid = request_data.get('uuid')
transaction_type = request_data['transaction_type']
up_time = datetime.now()
membership_types = MembershipType.objects.all()
body_map = {membership_type.type: membership_type.title for membership_type in membership_types}
body_description = body_map.get(type)
transactionNo = str(up_time).replace('.', '').replace('-', '').replace(':', '').replace(' ', '')
membership_type = MembershipType.objects.get(type=type)
price = membership_type.price
newTransaction = TransactionLog.objects.create(
transaction_no=transactionNo,
transaction_status='pending',
user_openid=openid,
transaction_type=transaction_type,
transaction_amount=price,
remark=type,
created_at=up_time
)
logger.info("生成订单:")
logger.info(f"商户订单号:{transactionNo}")
logger.info(f"用户OpenID{openid}")
logger.info(f"用户UUID{uuid}")
logger.info(f"type{type}")
logger.info(f"transaction_type{transaction_type}")
logger.info(f"成功时间:{up_time}")
logger.info(f"金额(前端):{wx_price}")
logger.info(f"金额(后端):{price}")
url = 'https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi'
body = {
"appid": settings.LOGIN_APP_ID,
"mchid": settings.LOGIN_WX_MCH_ID,
"description": body_description,
"out_trade_no": transactionNo,
"notify_url": settings.LOGIN_WX_NOTIFY_URL,
"amount": {"total": int(float(price) * 100), "currency": "CNY"},
"payer": {"openid": openid},
"attach": json.dumps({"type": type, "transaction_type": transaction_type,"uuid":uuid})
}
data = json.dumps(body)
logger.info(f'生成支付请求体: {body}')
random_str = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(32))
time_stamps = str(int(time.time()))
sign_str = f"POST\n/v3/pay/transactions/jsapi\n{time_stamps}\n{random_str}\n{data}\n"
sign = get_sign(sign_str, settings.LOGIN_WX_KEY_PATH)
headers = {
'Content-Type': 'application/json',
'Accept': 'application/json',
'User-Agent': '*/*',
'Authorization': 'WECHATPAY2-SHA256-RSA2048 ' + f'mchid="{settings.LOGIN_WX_MCH_ID}",nonce_str="{random_str}",signature="{sign}",timestamp="{time_stamps}",serial_no="{settings.LOGIN_WX_SERIAL_NO}"',
'Wechatpay-Serial': '66DB35F836EFD4CBEC66F3815D283A2892310324'
}
logger.info(f'生成请求头: {headers}')
response = requests.post(url, data=data, headers=headers)
response_dict = response.json()
logger.info(f'响应头: {response.headers}')
logger.info(f'响应体: {response_dict}')
if response.status_code == 200:
res = {
'appid': settings.LOGIN_APP_ID,
'partnerid': settings.LOGIN_WX_MCH_ID,
'prepayid': response_dict['prepay_id'],
'package': f'prepay_id={response_dict["prepay_id"]}',
'nonceStr': random_str,
'timeStamp': time_stamps,
'paySign': get_sign(
f"{settings.LOGIN_APP_ID}\n{time_stamps}\n{random_str}\n{'prepay_id=' + response_dict['prepay_id']}\n",
settings.LOGIN_WX_KEY_PATH
),
'signType': 'MD5'
}
logger.info(f'签名有效: {res}')
return HttpResponse(json.dumps(res), content_type='application/json')
else:
logger.error(f"支付请求失败:{response_dict}")
return HttpResponse(json.dumps({'msg': "支付失败"}), content_type='application/json', status=400)
@csrf_exempt
def wx_pay_notify(request):
try:
# 确保请求体被正确解码
request_body = request.body.decode('utf-8')
webData = json.loads(request_body)
logger.info(f'回调返回信息:{webData}')
# 提取加密数据
resource = webData.get('resource', {})
ciphertext = resource.get('ciphertext', '')
nonce = resource.get('nonce', '')
associated_data = resource.get('associated_data', '')
if not ciphertext or not nonce or not associated_data:
logger.error('回调数据不完整')
return HttpResponse(json.dumps({"code": "FAIL", "message": "回调数据不完整"}), content_type='application/json')
# 解密回调数据
callback_data = decrypt(nonce, ciphertext, associated_data, settings.LOGIN_V3_KEY)
callback_data_str = callback_data.decode('utf-8') # 将字节对象解码为字符串
callback_data = json.loads(callback_data_str) # 将字符串解析为字典对象
logger.info(f'解密后的回调数据:{callback_data}')
# 提取必要信息
mchid = callback_data.get('mchid', '')
appid = callback_data.get('appid', '')
out_trade_no = callback_data.get('out_trade_no', '')
transaction_id = callback_data.get('transaction_id', '')
trade_state = callback_data.get('trade_state', '')
payer = callback_data.get('payer', {})
openid = payer.get('openid', '')
attach = callback_data.get('attach', '').replace("'", "\"")
attach = json.loads(attach) if attach else {}
type = attach.get('type', '')
transaction_type = attach.get('transaction_type', '')
uuid = attach.get('uuid', '')
success_time_str = callback_data.get('success_time', '')
amount = callback_data.get('amount', {})
amount_total = amount.get('total', 0)
success_time = datetime.strptime(success_time_str, "%Y-%m-%dT%H:%M:%S%z") if success_time_str else None
logger.info("交易结果:")
logger.info(f"商户号:{mchid}")
logger.info(f"AppID{appid}")
logger.info(f"商户订单号:{out_trade_no}")
logger.info(f"微信订单号:{transaction_id}")
logger.info(f"交易状态:{trade_state}")
logger.info(f"用户OpenID{openid}")
logger.info(f"type{type}")
logger.info(f"transaction_type{transaction_type}")
logger.info(f"成功时间:{success_time}")
logger.info(f"金额(分):{amount_total}")
# 获取交易记录、会员类型和用户
try:
transaction_log = TransactionLog.objects.get(transaction_no=out_trade_no)
except TransactionLog.DoesNotExist:
logger.error('交易记录不存在')
return HttpResponse(json.dumps({"code": "FAIL", "message": "交易记录不存在"}), content_type='application/json')
try:
membership_type = MembershipType.objects.get(type=type)
except MembershipType.DoesNotExist:
logger.error('会员类型不存在')
return HttpResponse(json.dumps({"code": "FAIL", "message": "会员类型不存在"}), content_type='application/json')
try:
user = User.objects.get(nickname=uuid)
except User.DoesNotExist:
logger.error('用户不存在')
return HttpResponse(json.dumps({"code": "FAIL", "message": "用户不存在"}), content_type='application/json')
# 验证交易信息
if (transaction_log.user_openid == openid) and (transaction_log.transaction_amount * 100 == amount_total) and (
membership_type.price * 100 == amount_total):
transaction_log.transaction_status = 'completed'
transaction_log.save()
logger.info(f'交易记录更新成功:{transaction_log}')
# 更新用户会员或配额
success = update_user_membership_or_quota(user.openid, membership_type)
# 如果用户有邀请人,则处理返利
if user.inviter_nickname:
inviter = User.objects.filter(nickname=user.inviter_nickname).first()
if inviter:
rebate_amount = round(Decimal(amount_total) * Decimal(0.35) / Decimal(100), 2)
inviter.balance += rebate_amount
inviter.save()
logger.info(f'邀请人{inviter.nickname}返利{rebate_amount}元成功')
return HttpResponse(json.dumps({"code": "SUCCESS", "message": "成功"}))
else:
logger.warning('回调数据与订单记录不一致')
return HttpResponse(json.dumps({"code": "FAIL", "message": "回调数据与订单记录不一致"}))
except Exception as e:
logger.error(f'处理回调时出错:{str(e)}')
return HttpResponse(json.dumps({"code": "FAIL", "message": "处理回调时出错"}))
def wx_resource(nonce, ciphertext, associated_data):
key_bytes = str.encode(settings.LOGIN_V3_KEY)
nonce_bytes = str.encode(nonce)
ad_bytes = str.encode(associated_data)
data = base64.b64decode(ciphertext)
aesgcm = AESGCM(key_bytes)
plaintext = aesgcm.decrypt(nonce_bytes, data, ad_bytes)
plaintext_str = bytes.decode(plaintext)
return eval(plaintext_str)
def wechat_login(request):
redirect_uri = settings.LOGIN_REDIRECT_URI
appid = settings.LOGIN_APP_ID
state = 'random_string' # 可以随机生成字符串,保持安全性
wechat_auth_url = (
f"https://open.weixin.qq.com/connect/oauth2/authorize?"
f"appid={appid}&redirect_uri={redirect_uri}&response_type=code&scope=snsapi_base&state={state}#wechat_redirect"
)
return HttpResponseRedirect(wechat_auth_url)
def wechat_callback(request):
code = request.GET.get('code')
if not code:
return JsonResponse({'error': '缺少code参数'}, status=400)
appid = settings.LOGIN_APP_ID
secret = settings.LOGIN_APP_SECRET
token_url = (
f"https://api.weixin.qq.com/sns/oauth2/access_token?"
f"appid={appid}&secret={secret}&code={code}&grant_type=authorization_code"
)
response = requests.get(token_url)
token_info = response.json()
if 'errcode' in token_info:
logger.error(f"获取access_token失败: {token_info}")
return JsonResponse({'error': '获取access_token失败'}, status=400)
access_token = token_info['access_token']
openid = token_info['openid']
frontend_url = settings.LOGIN_WEB_URI # 替换为您的前端地址
redirect_url = f"{frontend_url}?openid={openid}"
return HttpResponseRedirect(redirect_url)