299 lines
13 KiB
Python
Executable File
299 lines
13 KiB
Python
Executable File
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) |