Files
Django/products/views/yajin_payment.py

614 lines
24 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""押金支付流程视图。
包含押金购买的参数生成、支付回调、失败处理、轮询。
"""
# ==================== 标准库 ====================
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 YajinGoumai(APIView):
"""
押金充值支付参数接口
路径:/shangpin/yajingoumai
请求方式POST
权限JWT认证
参数jine金额1-10000元
返回:支付参数(payParams)和订单ID(dingdanid)
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
# 1. 验证用户身份和状态
try:
dashou = request.user.DashouProfile
except UserDashou.DoesNotExist:
return Response({
'code': 400,
'message': '用户不是打手,无法充值押金'
}, status=status.HTTP_400_BAD_REQUEST)
if dashou.zhanghaozhuangtai != 1:
return Response({
'code': 400,
'message': '账号状态异常,无法进行充值'
}, status=status.HTTP_400_BAD_REQUEST)
# 2. 获取并验证金额参数
jine = request.data.get('jine')
if not jine:
return Response({
'code': 400,
'message': '请填写充值金额'
}, status=status.HTTP_400_BAD_REQUEST)
try:
jine = int(jine)
except (ValueError, TypeError):
return Response({
'code': 400,
'message': '金额格式错误请填写1-10000的整数'
}, status=status.HTTP_400_BAD_REQUEST)
# 验证金额范围
if jine < 1 or jine > 10000:
return Response({
'code': 400,
'message': '充值金额必须在1-10000元之间'
}, status=status.HTTP_400_BAD_REQUEST)
# 3. 生成订单ID使用你提供的算法前缀改为CzYJ
timestamp_ms = int(time.time() * 1000)
timestamp_ns = int(time.time_ns() // 1000) % 1000000
random_num = random.randint(0, 99)
dingdanid = f"CzYJ{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}"
# 4. 创建充值记录订单
chongzhi_jilu = Czjilu.query.create(
dingdan_id=dingdanid,
zhuangtai=9, # 未支付状态
yonghuid=request.user.UserUID,
jine=jine,
leixing=2, # 充值类型2=押金
shuoming=settings.YAJIN_PAY_DESCRIPTION,
club_id=resolve_club_id_for_write(request, request.user),
)
logger.info(f"创建押金充值订单: 用户{request.user.UserUID}, 订单{dingdanid}, 金额{jine}")
# 5. 生成微信支付参数
try:
pay_openid, _ = get_payment_openid(request)
if not pay_openid:
chongzhi_jilu.delete()
return Response({'code': 400, 'message': '用户openid不存在'}, status=status.HTTP_400_BAD_REQUEST)
pay_params = self.generate_wechat_pay_params(
dingdanid=dingdanid,
jine=jine,
openid=pay_openid,
pay_type='yajin'
)
except Exception as e:
# 支付参数生成失败,删除订单记录
chongzhi_jilu.delete()
logger.error(f"生成微信支付参数失败: {str(e)}")
return Response({
'code': 500,
'message': f'支付参数生成失败: {str(e)}'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# 6. 返回支付参数和订单ID
return Response({
'code': 200,
'message': '支付参数生成成功',
'payParams': pay_params, # 支付参数前端接收为payParams
'dingdanid': dingdanid # 订单ID前端接收为dingdanid
}, 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):
"""生成微信小程序支付参数"""
# 获取配置
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"""<xml>
<appid>{APPID}</appid>
<body>{PAY_DESCRIPTION}</body>
<mch_id>{MCHID}</mch_id>
<nonce_str>{nonce_str}</nonce_str>
<notify_url>{NOTIFY_URL}</notify_url>
<openid>{openid}</openid>
<out_trade_no>{dingdanid}</out_trade_no>
<spbill_create_ip>{user_ip}</spbill_create_ip>
<total_fee>{total_fee}</total_fee>
<trade_type>{trade_type}</trade_type>
<sign>{sign}</sign>
</xml>"""
# 调用微信统一下单接口(官方地址)
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从DRF的request对象中获取"""
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 YajinHuitiao(View):
"""
押金支付回调接口
路径:/shangpin/yajinhuitiao
注意这个接口不需要JWT认证由微信服务器调用
"""
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 # 订单ID
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}")
from jituan.services.wechat_pay import club_id_from_czjilu, verify_wechat_v2_signature
notify_club = club_id_from_czjilu(out_trade_no)
# 2. 验证基本返回状态
if return_code != 'SUCCESS' or result_code != 'SUCCESS':
logger.warning(f"微信支付回调状态异常: {return_code}/{result_code}")
return self.wechat_response('FAIL', '支付失败')
# 3. 验证签名(确保是微信发来的)
if not verify_wechat_v2_signature(request_body, notify_club):
logger.error(f"签名验证失败: {out_trade_no}")
return self.wechat_response('FAIL', '签名验证失败')
# 4. 查询订单
try:
order = Czjilu.query.get(dingdan_id=out_trade_no)
logger.info(f"找到订单: {order.dingdan_id}, 状态: {order.zhuangtai}, 金额: {order.jine}")
except Czjilu.DoesNotExist:
logger.error(f"订单不存在: {out_trade_no}")
return self.wechat_response('FAIL', '订单不存在')
# 5. 检查订单状态是否为未支付9
if order.zhuangtai != 9:
logger.warning(f"订单状态不是未支付: {order.dingdan_id}, 当前状态: {order.zhuangtai}")
try:
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')
# 6. 验证金额(微信返回的是分,需要转换)
wechat_amount_yuan = total_fee / 100 # 转换为元
order_amount_yuan = float(order.jine) # 订单金额(元)
# 允许1分钱的误差浮点数比较
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.czjilu_fulfill import fulfill_yajin_recharge, CzjiluFulfillError
try:
fulfill_yajin_recharge(out_trade_no, paid_amount_yuan=wechat_amount_yuan)
except CzjiluFulfillError 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)
# 返回FAIL让微信重试
return self.wechat_response('FAIL', '处理异常')
def verify_signature(self, xml_data):
"""验证微信支付签名"""
try:
root = ET.fromstring(xml_data)
sign = root.find('sign').text
if not sign:
logger.error("签名字段不存在")
return False
# 获取所有参数并排除sign
params = {}
for child in root:
if child.tag != 'sign' and child.text is not None:
params[child.tag] = child.text
# 按照参数名ASCII码从小到大排序
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()
is_valid = sign == calculated_sign
if not is_valid:
logger.error(f"签名验证失败: 计算签名{calculated_sign}, 收到签名{sign}")
return is_valid
except Exception as e:
logger.error(f"验证签名异常: {str(e)}")
return False
def wechat_response(self, return_code, return_msg):
"""返回微信支付回调响应"""
xml_response = f"""<xml>
<return_code><![CDATA[{return_code}]]></return_code>
<return_msg><![CDATA[{return_msg}]]></return_msg>
</xml>"""
logger.info(f"返回微信响应: {return_code} - {return_msg}")
return HttpResponse(xml_response, content_type='application/xml')
# views.py
class YajinShibai(APIView):
"""
押金支付失败处理接口
路径:/shangpin/yajinshibai
请求方式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. 检查订单状态是否为未支付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 YajinLunxun(APIView):
"""
押金支付成功轮询接口
路径:/shangpin/yajinlunxun
请求方式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. 未支付则查微信并幂等履约
if order.zhuangtai == 9:
from jituan.services.czjilu_fulfill import confirm_czjilu_paid, CzjiluFulfillError
try:
confirm_czjilu_paid(dingdanid, request.user.UserUID, expected_leixing=2)
order = Czjilu.query.get(dingdan_id=dingdanid, yonghuid=request.user.UserUID)
except CzjiluFulfillError as exc:
logger.info(f"押金确认未就绪 {dingdanid}: {exc}")
return Response({
'code': 400,
'message': str(exc),
}, status=status.HTTP_400_BAD_REQUEST)
if order.zhuangtai != 3:
logger.info(f"订单{dingdanid}尚未支付完成,当前状态: {order.zhuangtai}")
return Response({
'code': 400,
'message': '订单尚未支付完成'
}, status=status.HTTP_400_BAD_REQUEST)
# 4. 获取打手押金(通过反向查询,最快捷的方式)
try:
dashou = request.user.DashouProfile
yajin = float(dashou.yajin) # 转为浮点数方便前端处理
logger.info(f"轮询成功: 订单{dingdanid}已支付,用户{request.user.UserUID}押金为{yajin}")
return Response({
'code': 200,
'message': '支付流程全部完成',
'yajin': yajin # 返回当前押金(前端需要更新全局变量)
}, status=status.HTTP_200_OK)
except UserDashou.DoesNotExist:
logger.error(f"打手信息不存在: 用户{request.user.UserUID}")
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