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)