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