更新了项目文件
This commit is contained in:
		
							
								
								
									
										299
									
								
								MyApi/app_login_pay.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										299
									
								
								MyApi/app_login_pay.py
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,299 @@ | ||||
| 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) | ||||
		Reference in New Issue
	
	Block a user