311 lines
14 KiB
Python
311 lines
14 KiB
Python
![]() |
import base64
|
|||
|
import json
|
|||
|
import string
|
|||
|
from datetime import datetime, timedelta
|
|||
|
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
|
|||
|
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 = '支付日志.log'
|
|||
|
logger = get_logger('支付日志', log_file, when='midnight', backup_count=7)
|
|||
|
|
|||
|
|
|||
|
def wx_pay(request):
|
|||
|
request_data = json.loads(request.body)
|
|||
|
type = request_data.get('type', 'default')
|
|||
|
wx_price = request_data['total_fee'] # 从请求获取订单金额
|
|||
|
openid = request_data['openid'] # 从请求获取用户openid
|
|||
|
transaction_type = request_data['transaction_type'] # 续费或者开通
|
|||
|
up_time = datetime.now()
|
|||
|
|
|||
|
# 从数据库中动态获取 body_map
|
|||
|
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"type:{type}")
|
|||
|
logger.info(f"transaction_type:{transaction_type}")
|
|||
|
logger.info(f"成功时间:{up_time}")
|
|||
|
logger.info(f"金额(前端):{wx_price}")
|
|||
|
logger.info(f"金额(后端):{price}")
|
|||
|
|
|||
|
# 生成统一下单的报文body
|
|||
|
url = 'https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi'
|
|||
|
body = {
|
|||
|
"appid": settings.WX_APP_ID,
|
|||
|
"mchid": settings.WX_MCH_ID,
|
|||
|
"description": body_description,
|
|||
|
"out_trade_no": transactionNo,
|
|||
|
"notify_url": settings.WX_NOTIFY_URL, # 后端接收回调通知的接口
|
|||
|
"amount": {"total": int(float(price) * 100), "currency": "CNY"}, # 微信金额单位为分
|
|||
|
"payer": {"openid": openid},
|
|||
|
"attach": json.dumps({"type": type, "transaction_type": transaction_type})
|
|||
|
}
|
|||
|
data = json.dumps(body)
|
|||
|
|
|||
|
# 定义生成签名的函数
|
|||
|
def get_sign(sign_str):
|
|||
|
rsa_key = RSA.importKey(open(settings.WX_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
|
|||
|
|
|||
|
# 生成请求随机串
|
|||
|
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)
|
|||
|
|
|||
|
# 生成HTTP请求头
|
|||
|
headers = {
|
|||
|
'Content-Type': 'application/json',
|
|||
|
'Accept': 'application/json',
|
|||
|
'User-Agent': '*/*',
|
|||
|
'Authorization': 'WECHATPAY2-SHA256-RSA2048 ' + f'mchid="{settings.WX_MCH_ID}",nonce_str="{random_str}",signature="{sign}",timestamp="{time_stamps}",serial_no="{settings.WX_SERIAL_NO}"',
|
|||
|
'Wechatpay-Serial': '66DB35F836EFD4CBEC66F3815D283A2892310324'
|
|||
|
}
|
|||
|
|
|||
|
# 发送请求获得prepay_id
|
|||
|
response = requests.post(url, data=data, headers=headers) # 获取预支付交易会话标识(prepay_id)
|
|||
|
|
|||
|
# 应答签名验证
|
|||
|
wechatpaySerial = response.headers['Wechatpay-Serial'] # 获取HTTP头部中包括回调报文的证书序列号
|
|||
|
wechatpaySignature = response.headers['Wechatpay-Signature'] # 获取HTTP头部中包括回调报文的签名
|
|||
|
wechatpayTimestamp = response.headers['Wechatpay-Timestamp'] # 获取HTTP头部中包括回调报文的时间戳
|
|||
|
wechatpayNonce = response.headers['Wechatpay-Nonce'] # 获取HTTP头部中包括回调报文的随机串
|
|||
|
|
|||
|
# 获取微信平台证书
|
|||
|
url2 = "https://api.mch.weixin.qq.com/v3/certificates"
|
|||
|
|
|||
|
# 生成证书请求随机串
|
|||
|
random_str2 = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(32))
|
|||
|
|
|||
|
# 生成证书请求时间戳
|
|||
|
time_stamps2 = str(int(time.time()))
|
|||
|
|
|||
|
# 生成请求证书的签名串
|
|||
|
data2 = ""
|
|||
|
sign_str2 = f"GET\n/v3/certificates\n{time_stamps2}\n{random_str2}\n{data2}\n"
|
|||
|
|
|||
|
# 生成签名
|
|||
|
sign2 = get_sign(sign_str2)
|
|||
|
|
|||
|
# 生成HTTP请求头
|
|||
|
headers2 = {
|
|||
|
'Content-Type': 'application/json',
|
|||
|
'Accept': 'application/json',
|
|||
|
'Authorization': 'WECHATPAY2-SHA256-RSA2048 ' + f'mchid="{settings.WX_MCH_ID}",nonce_str="{random_str2}",signature="{sign2}",timestamp="{time_stamps2}",serial_no="{settings.WX_SERIAL_NO}"'
|
|||
|
}
|
|||
|
|
|||
|
# 发送请求获得证书
|
|||
|
response2 = requests.get(url2, headers=headers2) # 只需要请求头
|
|||
|
cert = response2.json()
|
|||
|
|
|||
|
# 证书解密
|
|||
|
def decrypt(nonce, ciphertext, associated_data):
|
|||
|
key = settings.WX_API_KEY
|
|||
|
key_bytes = str.encode(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)
|
|||
|
|
|||
|
nonce = cert["data"][0]['encrypt_certificate']['nonce']
|
|||
|
ciphertext = cert["data"][0]['encrypt_certificate']['ciphertext']
|
|||
|
associated_data = cert["data"][0]['encrypt_certificate']['associated_data']
|
|||
|
serial_no = cert["data"][0]['serial_no']
|
|||
|
certificate = decrypt(nonce, ciphertext, associated_data)
|
|||
|
|
|||
|
# 签名验证
|
|||
|
if wechatpaySerial == serial_no: # 应答签名中的序列号同证书序列号应相同
|
|||
|
print('serial_no match')
|
|||
|
|
|||
|
def verify(data, signature): # 验签函数
|
|||
|
key = RSA.importKey(certificate) # 直接使用解密后的证书
|
|||
|
verifier = pkcs1_15.new(key)
|
|||
|
hash_obj = SHA256.new(data.encode('utf8'))
|
|||
|
return verifier.verify(hash_obj, base64.b64decode(signature))
|
|||
|
|
|||
|
data3 = f"{wechatpayTimestamp}\n{wechatpayNonce}\n{response.text}\n"
|
|||
|
try:
|
|||
|
verify(data3, wechatpaySignature)
|
|||
|
|
|||
|
# 生成调起支付API需要的参数并返回前端
|
|||
|
res = {
|
|||
|
'timeStamp': time_stamps,
|
|||
|
'nonceStr': random_str,
|
|||
|
'package': 'prepay_id=' + response.json()['prepay_id'],
|
|||
|
'paySign': get_sign(
|
|||
|
f"{settings.WX_APP_ID}\n{time_stamps}\n{random_str}\n{'prepay_id=' + response.json()['prepay_id']}\n"),
|
|||
|
}
|
|||
|
print(f'签名有效: {res}')
|
|||
|
logger.info(f'签名有效: {res}')
|
|||
|
return HttpResponse(json.dumps(res), content_type='application/json')
|
|||
|
except (ValueError, TypeError):
|
|||
|
logger.info(f'签名无效')
|
|||
|
print('签名无效')
|
|||
|
return HttpResponse(json.dumps({'msg': "支付失败"}), content_type='application/json')
|
|||
|
|
|||
|
def wx_resource(nonce, ciphertext, associated_data):
|
|||
|
key_bytes = str.encode(settings.WX_API_KEY) # APIv3_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 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
|
|||
|
logger.info(f'第一次开会员:{current_time} -- {current_time + membership_type.duration_days * 24 * 3600}')
|
|||
|
elif user.member_end_time is None or user.member_end_time < current_time: # 如果会员已经过期
|
|||
|
logger.info(f'过期:{current_time} -- {current_time + membership_type.duration_days * 24 * 3600}')
|
|||
|
user.member_start_time = current_time
|
|||
|
|
|||
|
# 计算会员到期时间
|
|||
|
if user.member_end_time is None or user.member_end_time < current_time:
|
|||
|
logger.info(f'第一次开会员2:{user.member_end_time} -- {current_time + membership_type.duration_days * 24 * 3600}')
|
|||
|
user.member_end_time = current_time + membership_type.duration_days * 24 * 3600 # 将天数转换为秒
|
|||
|
else: # 如果会员尚未过期,则在现有会员结束时间上添加续费的天数
|
|||
|
logger.info(f'续费:{user.member_end_time} -- {user.member_end_time + membership_type.duration_days * 24 * 3600}')
|
|||
|
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_notify(request):
|
|||
|
webData = json.loads(request.body)
|
|||
|
logger.info(f'回调返回信息:{webData}')
|
|||
|
ciphertext = webData['resource']['ciphertext']
|
|||
|
nonce = webData['resource']['nonce']
|
|||
|
associated_data = webData['resource']['associated_data']
|
|||
|
try:
|
|||
|
callback_data = wx_resource(nonce, ciphertext, associated_data)
|
|||
|
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')
|
|||
|
openid = callback_data.get('payer').get('openid')
|
|||
|
attach = callback_data.get('attach')
|
|||
|
attach = attach.replace("'", "\"")
|
|||
|
attach = json.loads(attach)
|
|||
|
type = attach.get('type')
|
|||
|
transaction_type = attach.get('transaction_type')
|
|||
|
success_time_str = callback_data.get('success_time')
|
|||
|
amount_total = callback_data.get('amount').get('total')
|
|||
|
success_time = datetime.strptime(success_time_str, "%Y-%m-%dT%H:%M:%S%z")
|
|||
|
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}")
|
|||
|
transaction_log = TransactionLog.objects.get(transaction_no=out_trade_no)
|
|||
|
membership_type = MembershipType.objects.get(type=type) # 获取会员类型对象
|
|||
|
|
|||
|
# 验证订单信息一致性
|
|||
|
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(openid, membership_type)
|
|||
|
|
|||
|
# 给邀请人返利
|
|||
|
user = User.objects.get(openid=openid)
|
|||
|
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 TransactionLog.DoesNotExist:
|
|||
|
logger.error(f'交易记录不存在')
|
|||
|
return HttpResponse(json.dumps({"code": "FAIL", "message": "交易记录不存在"}))
|
|||
|
except Exception as e:
|
|||
|
logger.error(f'处理回调时出错:{str(e)}')
|
|||
|
return HttpResponse(json.dumps({"code": "FAIL", "message": "处理回调时出错"}))
|
|||
|
|