"""会员购买支付流程视图。 包含会员购买参数生成、支付回调、失败处理、轮询。 """ # ==================== 标准库 ==================== import os import json import time import random import string import uuid import hashlib import requests import logging import decimal from datetime import timedelta from decimal import Decimal, InvalidOperation, ROUND_CEILING import defusedxml.ElementTree as ET # ==================== Django 内置 ==================== from django.conf import settings from django.db import transaction, connection from django.db.models import Q from gvsdsdk.fluent import db, func, FQ from django.shortcuts import render, get_object_or_404 from django.http import HttpResponse from django.utils import timezone # ==================== DRF 框架 ==================== from rest_framework.views import APIView, View from rest_framework.response import Response from rest_framework import permissions, status from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.parsers import MultiPartParser, FormParser, JSONParser from rest_framework.throttling import AnonRateThrottle # ==================== 项目工具函数 ==================== from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss from utils.money import format_yuan, quantize_yuan, yuan_to_fen from backend.utils import ( update_guanshi_daily_by_action, update_guanshi_xufei_daily, update_zuzhang_daily_by_action ) from users.fadan_fenhong_utils import process_fadan_fenhong # ==================== 各App模型 ==================== from ..models import ( ShangpinLeixing, ShangpinZhuanqu, Shangpin, Huiyuan, Czjilu, Gsfenhong, Huiyuangoumai, DuociFenhong ) from users.models import ( AdminProfile, UserDashou, UserGuanshi, UserZuzhang, UserShangjia ) from users.business_models import User from orders.models import CommissionRate from config.models import Gonggao, Lunbo from orders.models import Penalty # 用于罚款类型 from jituan.services.club_config import ( get_commission_rate, get_commission_rate_object, get_huiyuan_price, get_huiyuan_fenchong, ) from jituan.services.club_context import resolve_effective_club_id from jituan.services.club_write import resolve_club_id_for_write from jituan.services.club_user import get_payment_openid from jituan.services.club_penalty import resolve_gsfenhong_club_id import traceback # ==================== 日志实例 ==================== logger = logging.getLogger(__name__) class HuiyuanGoumai(APIView): """ 会员充值支付参数接口 路径:/shangpin/huiyuangm 请求方式:POST 权限:JWT认证 参数:huiyuanid(会员ID) 返回:支付参数(payParams)和订单ID(dingdanid) """ permission_classes = [IsAuthenticated] def post(self, request): try: # 1. 获取会员ID参数 huiyuanid = request.data.get('huiyuanid') is_trial = bool(request.data.get('is_trial') or request.data.get('isTrial')) if not huiyuanid: return Response({ 'code': 400, 'message': '会员ID不能为空' }, status=status.HTTP_400_BAD_REQUEST) # 2. 验证用户身份和状态 try: dashou = request.user.DashouProfile except UserDashou.DoesNotExist: return Response({ 'code': 400, 'message': '用户不是打手,无法购买会员' }, status=status.HTTP_400_BAD_REQUEST) # 3. 检查打手账号状态 if dashou.zhanghaozhuangtai != 1: return Response({ 'code': 400, 'message': '账号状态异常,无法进行购买' }, status=status.HTTP_400_BAD_REQUEST) # 4. 查询会员信息 try: huiyuan = Huiyuan.query.get(huiyuan_id=huiyuanid) except Huiyuan.DoesNotExist: return Response({ 'code': 400, 'message': '会员信息不存在' }, status=status.HTTP_400_BAD_REQUEST) club_id = resolve_effective_club_id(request, request.user) from jituan.services.member_recharge import validate_member_purchase ok, err_msg, club_price, purchase_days = validate_member_purchase( request.user.UserUID, huiyuanid, club_id, is_trial, ) if not ok: return Response({ 'code': 400, 'message': err_msg or '不可购买', }, status=status.HTTP_400_BAD_REQUEST) # 5. 生成订单ID(前缀CzHY表示会员充值) timestamp_ms = int(time.time() * 1000) timestamp_ns = int(time.time_ns() // 1000) % 1000000 random_num = random.randint(0, 99) dingdanid = f"CzHY{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}" # 6. 创建充值记录订单(金额仅用本俱乐部配置) jine = quantize_yuan(club_price) # 组合订单说明:会员介绍 + 配置描述 shuoming = f"{huiyuan.jieshao} - {settings.HUIYUAN_PAY_DESCRIPTION}" chongzhi_jilu = Czjilu.query.create( dingdan_id=dingdanid, zhuangtai=9, # 未支付状态 yonghuid=request.user.UserUID, jine=jine, leixing=1, # 充值类型:1=会员 shuoming=shuoming, huiyuan_id=huiyuanid, # 存储会员ID club_id=club_id, is_trial=is_trial, purchase_days=int(purchase_days), ) logger.info( f"创建会员充值订单: 用户{request.user.UserUID}, 订单{dingdanid}, " f"会员{huiyuanid}, 俱乐部{club_id}, 体验={is_trial}, " f"金额{jine}元, 天数{purchase_days}") # 7. 生成微信支付参数 try: pay_openid, _ = get_payment_openid(request) if not pay_openid: from jituan.services.member_recharge import MEMBER_CANCELLED_STATUS chongzhi_jilu.zhuangtai = MEMBER_CANCELLED_STATUS chongzhi_jilu.shuoming = f"{shuoming} | 下单失败:openid不存在"[:100] chongzhi_jilu.save(update_fields=['zhuangtai', 'shuoming']) return Response({ 'code': 400, 'message': '用户openid不存在', 'dingdanid': dingdanid, }, status=status.HTTP_400_BAD_REQUEST) pay_params = self.generate_wechat_pay_params( dingdanid=dingdanid, jine=jine, openid=pay_openid, pay_type='huiyuan' ) except Exception as e: from jituan.services.member_recharge import MEMBER_CANCELLED_STATUS chongzhi_jilu.zhuangtai = MEMBER_CANCELLED_STATUS chongzhi_jilu.shuoming = f"{shuoming} | 下单失败:{str(e)[:40]}"[:100] chongzhi_jilu.save(update_fields=['zhuangtai', 'shuoming']) logger.error(f"生成会员支付参数失败: {str(e)}") return Response({ 'code': 500, 'message': f'支付参数生成失败: {str(e)}' }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # 8. 返回支付参数和订单ID return Response({ 'code': 200, 'message': '支付参数生成成功', 'payParams': pay_params, 'dingdanid': dingdanid, 'club_id': club_id, 'jine': format_yuan(jine), 'is_trial': is_trial, 'purchase_days': purchase_days, }, status=status.HTTP_200_OK) except Exception as e: logger.error(f"会员充值接口异常: {str(e)}", exc_info=True) return Response({ 'code': 500, 'message': '服务器内部错误,请稍后重试' }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) def generate_wechat_pay_params(self, dingdanid, jine, openid, pay_type): """生成微信支付参数(完整版,无省略)""" # 微信支付配置 - 从settings中获取 from jituan.services.club_write import resolve_club_id_for_write from jituan.services.wechat_pay import get_wechat_v2_config pay_cfg = get_wechat_v2_config(resolve_club_id_for_write(self.request, self.request.user)) APPID = pay_cfg['appid'] MCHID = pay_cfg['mch_id'] KEY = pay_cfg['key'] # 根据支付类型获取对应的配置 if pay_type == 'yajin': PAY_DESCRIPTION = settings.YAJIN_PAY_DESCRIPTION NOTIFY_URL = settings.YAJIN_PAY_NOTIFY_URL elif pay_type == 'jifen': PAY_DESCRIPTION = settings.JIFEN_PAY_DESCRIPTION NOTIFY_URL = settings.JIFEN_PAY_NOTIFY_URL elif pay_type == 'huiyuan': PAY_DESCRIPTION = settings.HUIYUAN_PAY_DESCRIPTION NOTIFY_URL = settings.HUIYUAN_PAY_NOTIFY_URL else: raise Exception('未知的支付类型') # 生成随机字符串 nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32)) # 金额转换(元转分) total_fee = yuan_to_fen(jine) # 交易类型(小程序支付使用JSAPI) trade_type = 'JSAPI' # 获取客户端IP user_ip = self.get_client_ip() # 1. 第一次签名(统一下单接口) sign_data = { 'appid': APPID, 'mch_id': MCHID, 'nonce_str': nonce_str, 'body': PAY_DESCRIPTION, 'out_trade_no': dingdanid, 'total_fee': total_fee, 'spbill_create_ip': user_ip, 'notify_url': NOTIFY_URL, 'trade_type': trade_type, 'openid': openid # 小程序支付必须传openid } # 按照参数名ASCII码从小到大排序 sorted_sign_data = sorted(sign_data.items(), key=lambda x: x[0]) # 拼接字符串 sign_string = '&'.join([f"{key}={value}" for key, value in sorted_sign_data if value]) sign_string += f'&key={KEY}' # MD5签名 sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper() # 构造XML数据 xml_data = f""" {APPID} {PAY_DESCRIPTION} {MCHID} {nonce_str} {NOTIFY_URL} {openid} {dingdanid} {user_ip} {total_fee} {trade_type} {sign} """ # 调用微信统一下单接口(官方地址) response = requests.post( 'https://api.mch.weixin.qq.com/pay/unifiedorder', data=xml_data.encode('utf-8'), headers={'Content-Type': 'application/xml'}, timeout=10 # 设置超时 ) # 解析返回的XML root = ET.fromstring(response.content) return_code = root.find('return_code').text result_code = root.find('result_code').text if return_code == 'SUCCESS' and result_code == 'SUCCESS': # 获取prepay_id prepay_id = root.find('prepay_id').text # 2. 第二次签名(小程序支付参数) timestamp = str(int(time.time())) nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32)) package = f'prepay_id={prepay_id}' sign_type = 'MD5' # 生成支付签名(注意:这里key的大小写和字段名与第一次不同) pay_sign_data = { 'appId': APPID, # 注意:这里是appId,不是appid 'timeStamp': timestamp, # 注意:这里是timeStamp,不是timestamp 'nonceStr': nonce_str, 'package': package, 'signType': sign_type } sorted_pay_sign_data = sorted(pay_sign_data.items(), key=lambda x: x[0]) pay_sign_string = '&'.join([f"{key}={value}" for key, value in sorted_pay_sign_data]) pay_sign_string += f'&key={KEY}' pay_sign = hashlib.md5(pay_sign_string.encode('utf-8')).hexdigest().upper() # 返回前端需要的支付参数 return { 'appId': APPID, # 对应前端:appId 'timeStamp': timestamp, # 对应前端:timeStamp 'nonceStr': nonce_str, # 对应前端:nonceStr 'package': package, # 对应前端:package 'signType': sign_type, # 对应前端:signType 'paySign': pay_sign # 对应前端:paySign } else: # 获取错误信息 err_code = root.find('err_code') err_code_des = root.find('err_code_des') return_msg = root.find('return_msg') error_details = f"return_code: {return_code}" if err_code is not None: error_details += f", err_code: {err_code.text}" if err_code_des is not None: error_details += f", err_code_des: {err_code_des.text}" if return_msg is not None: error_details += f", return_msg: {return_msg.text}" raise Exception(f"微信支付下单失败: {error_details}") def get_client_ip(self): """获取客户端IP(完整版)""" request = self.request x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') if x_forwarded_for: ip = x_forwarded_for.split(',')[0] else: ip = request.META.get('REMOTE_ADDR') return ip if ip else '127.0.0.1' class HuiyuanHuitiao(View): """ 会员支付回调接口 路径:/shangpin/huiyuanhuitiao """ def post(self, request): try: # 1. 解析微信回调XML数据 request_body = request.body logger.info(f"收到会员支付回调,原始数据: {request_body.decode('utf-8')}") root = ET.fromstring(request_body) return_code = root.find('return_code').text result_code = root.find('result_code').text out_trade_no = root.find('out_trade_no').text transaction_id = root.find('transaction_id').text total_fee = int(root.find('total_fee').text) logger.info(f"会员回调数据: 订单{out_trade_no}, 状态{return_code}/{result_code}, 金额{total_fee}分") if return_code != 'SUCCESS' or result_code != 'SUCCESS': logger.warning(f"会员支付回调状态异常: {return_code}/{result_code}") return self.wechat_response('FAIL', '支付失败') from jituan.services.wechat_pay import club_id_from_czjilu, verify_wechat_v2_signature notify_club = club_id_from_czjilu(out_trade_no) if not verify_wechat_v2_signature(request_body, notify_club): logger.error(f"会员签名验证失败: {out_trade_no}") return self.wechat_response('FAIL', '签名验证失败') # 查询订单 try: order = Czjilu.query.get(dingdan_id=out_trade_no) except Czjilu.DoesNotExist: logger.error(f"会员订单不存在: {out_trade_no}") return self.wechat_response('FAIL', '订单不存在') if order.leixing != 1: logger.error(f"订单类型错误: {order.leixing}, 期望1(会员)") return self.wechat_response('FAIL', '订单类型错误') if order.zhuangtai != 9: logger.warning(f"会员订单状态不是未支付: {order.dingdan_id}, 当前状态: {order.zhuangtai}") if order.zhuangtai == 3: from jituan.services.member_recharge import repair_member_entitlement_if_paid repair_member_entitlement_if_paid(out_trade_no) try: from decimal import Decimal from jituan.services.szjilu_accounting import repair_wechat_income_if_missing repair_wechat_income_if_missing( Decimal(str(order.jine)), getattr(order, 'club_id', None), biz_ref=f'cz:{out_trade_no}', ) except Exception as exc: logger.error('会员回调补记收支失败 %s: %s', out_trade_no, exc, exc_info=True) return self.wechat_response('SUCCESS', 'OK') wechat_amount_yuan = total_fee / 100 order_amount_yuan = float(order.jine) if abs(wechat_amount_yuan - order_amount_yuan) > 0.01: logger.error(f"会员金额不一致: 订单金额{order_amount_yuan}元, 支付金额{wechat_amount_yuan}元") return self.wechat_response('FAIL', '金额不一致') from jituan.services.member_recharge import fulfill_member_recharge, MemberRechargeError try: fulfill_member_recharge( out_trade_no, paid_amount_yuan=wechat_amount_yuan, source='callback', ) except MemberRechargeError as exc: logger.error('会员回调履约失败 %s: %s', out_trade_no, exc) return self.wechat_response('FAIL', str(exc)) logger.info(f"会员支付回调处理完成: {out_trade_no}") return self.wechat_response('SUCCESS', 'OK') except ET.ParseError as e: logger.error(f"会员XML解析失败: {str(e)}") return self.wechat_response('FAIL', '数据格式错误') except Exception as e: logger.error(f"会员支付回调处理异常: {str(e)}", exc_info=True) return self.wechat_response('FAIL', '处理异常') # 履约逻辑见 jituan.services.member_recharge(回调与 /huiyuanlx 确认共用) def verify_signature(self, xml_data): """验证微信支付签名(与原代码一致)""" try: root = ET.fromstring(xml_data) sign = root.find('sign').text if not sign: logger.error("会员签名字段不存在") return False params = {} for child in root: if child.tag != 'sign' and child.text is not None: params[child.tag] = child.text sorted_params = sorted(params.items(), key=lambda x: x[0]) sign_string = '&'.join([f"{key}={value}" for key, value in sorted_params if value]) sign_string += f'&key={settings.WEIXIN_SHANGHUMIYAO}' calculated_sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper() return sign == calculated_sign except Exception as e: logger.error(f"会员验证签名异常: {str(e)}") return False def wechat_response(self, return_code, return_msg): """返回微信回调响应""" xml_response = f""" """ logger.info(f"会员返回微信响应: {return_code} - {return_msg}") return HttpResponse(xml_response, content_type='application/xml') # views.py class HuiyuanShibai(APIView): """ 会员支付失败处理接口 路径:/shangpin/huiyuanshibai 请求方式:POST 权限:JWT认证 参数:dingdanid(订单ID) 返回:删除结果 """ permission_classes = [IsAuthenticated] def post(self, request): try: # 1. 获取订单ID dingdanid = request.data.get('dingdanid') if not dingdanid: return Response({ 'code': 400, 'message': '订单ID不能为空' }, status=status.HTTP_400_BAD_REQUEST) # 2. 查询订单(确保是当前用户的订单) try: order = Czjilu.query.get( dingdan_id=dingdanid, yonghuid=request.user.UserUID ) except Czjilu.DoesNotExist: return Response({ 'code': 400, 'message': '订单不存在或不属于当前用户' }, status=status.HTTP_400_BAD_REQUEST) # 3. 检查订单类型是否为会员(1) if order.leixing != 1: return Response({ 'code': 400, 'message': '订单类型错误,不是会员订单' }, status=status.HTTP_400_BAD_REQUEST) # 4. 检查订单状态是否为未支付(9) if order.zhuangtai == 9: # 删除订单 order.delete() logger.info(f"用户{request.user.UserUID}删除未支付会员订单: {dingdanid}") return Response({ 'code': 200, 'message': '会员订单已删除' }, status=status.HTTP_200_OK) else: return Response({ 'code': 400, 'message': '会员订单状态不是未支付,无法删除' }, status=status.HTTP_400_BAD_REQUEST) except Exception as e: logger.error(f"会员支付失败处理异常: {str(e)}", exc_info=True) return Response({ 'code': 500, 'message': '服务器内部错误' }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # views.py class HuiyuanLunxun(APIView): """ 会员支付成功轮询接口 路径:/shangpin/huiyuanlx 请求方式:POST 权限:JWT认证 参数:dingdanid(订单ID) 返回:订单状态和会员信息 """ permission_classes = [IsAuthenticated] def post(self, request): try: # 1. 获取订单ID dingdanid = request.data.get('dingdanid') if not dingdanid: return Response({ 'code': 400, 'message': '订单ID不能为空' }, status=status.HTTP_400_BAD_REQUEST) # 2. 查询订单(确保是当前用户的订单) try: order = Czjilu.query.get( dingdan_id=dingdanid, yonghuid=request.user.UserUID ) except Czjilu.DoesNotExist: return Response({ 'code': 400, 'message': '订单不存在或不属于当前用户' }, status=status.HTTP_400_BAD_REQUEST) # 3. 检查订单类型是否为会员(1) if order.leixing != 1: return Response({ 'code': 400, 'message': '订单类型错误,不是会员订单' }, status=status.HTTP_400_BAD_REQUEST) # 4. 未支付:查微信并幂等履约(与回调互斥,zhuangtai!=9 则跳过) if order.zhuangtai == 9: from jituan.services.member_recharge import ( confirm_member_recharge_paid, MemberRechargeError, ) try: confirm_member_recharge_paid(dingdanid, request.user.UserUID) order = Czjilu.query.get(dingdan_id=dingdanid, yonghuid=request.user.UserUID) except MemberRechargeError as exc: logger.info(f"会员确认未就绪 {dingdanid}: {exc}") return Response({ 'code': 400, 'message': str(exc), }, status=status.HTTP_400_BAD_REQUEST) if order.zhuangtai != 3: return Response({ 'code': 400, 'message': '会员订单尚未支付完成' }, status=status.HTTP_400_BAD_REQUEST) from jituan.services.member_recharge import repair_member_entitlement_if_paid repair_member_entitlement_if_paid(dingdanid) # 5. 查询会员信息 try: huiyuan = Huiyuan.query.get(huiyuan_id=order.huiyuan_id) # 6. 查询会员购买记录 goumai_record = Huiyuangoumai.query.filter( yonghu_id=request.user.UserUID, huiyuan_id=order.huiyuan_id ).first() if not goumai_record: return Response({ 'code': 400, 'message': '会员购买记录不存在' }, status=status.HTTP_400_BAD_REQUEST) # 7. 格式化到期时间 #from django.utils.timezone import localtime #daoqi_time = localtime(goumai_record.daoqi_time).strftime('%Y-%m-%d %H:%M:%S') # 直接格式化,不使用时区转换 daoqi_time = goumai_record.daoqi_time.strftime('%Y-%m-%d %H:%M:%S') current_user = request.user dashou = current_user.DashouProfile logger.info(f"会员轮询成功: 订单{dingdanid}已支付,用户{request.user.UserUID}, 会员{order.huiyuan_id}") # 8. 返回会员信息(用于更新前端全局变量) return Response({ 'code': 200, 'message': '支付流程全部完成', 'jifen': dashou.jifen, 'huiyuan': { 'huiyuanid': order.huiyuan_id, 'huiyuanming': huiyuan.jieshao, 'daoqi': daoqi_time } }, status=status.HTTP_200_OK) except Huiyuan.DoesNotExist: logger.error(f"会员信息不存在: {order.huiyuan_id}") return Response({ 'code': 400, 'message': '会员信息不存在' }, status=status.HTTP_400_BAD_REQUEST) except Exception as e: logger.error(f"会员轮询异常: {str(e)}", exc_info=True) return Response({ 'code': 500, 'message': '服务器内部错误' }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # views.py in shangpin app