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