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": "处理回调时出错"}))
|
||
|