Files
Django/orders/views/payment.py
XingQue 1ab59efddb fix: 加固支付宝充值回调幂等履约,避免付成功不到账与误删单
禁止 Czjilu 回调地址回落到订单 notify;回调失败回 fail 重试;取消支付改关单不删;客服补单走同一履约防多加。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-20 22:59:59 +08:00

1197 lines
54 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.
"""orders.views.payment - auto-generated by split script."""
# ==================== 标准库 ====================
import hmac
import os
import json
import time
import random
import string
import traceback
import threading
import requests
import hashlib
import xml.etree.ElementTree as ET
import xmltodict
import logging
from decimal import Decimal
from django.conf import settings
from django.db import models, transaction
from gvsdsdk.fluent import db, func, FQ
from django.db.models import F, Q, OuterRef, Subquery
from django.core.cache import cache
from django.core.exceptions import ObjectDoesNotExist
from django.http import HttpResponse
from django.shortcuts import render
from django.utils import timezone
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import permissions, status
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.parsers import JSONParser
from rest_framework.throttling import AnonRateThrottle
from rest_framework_simplejwt.authentication import JWTAuthentication
from tencentcloud.common import credential
from tencentcloud.common.profile.client_profile import ClientProfile
from tencentcloud.common.profile.http_profile import HttpProfile
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
from tencentcloud.sts.v20180813 import sts_client, models
from utils.weixin_broadcast import WeixinBroadcastSender
from utils.money import yuan_to_fen
from utils.chat_utils import (
_send_group_message, _subscribe_users_to_group, establish_order_chat,
prepare_order_group_chat, resolve_pair_group_id,
parse_pair_from_group_id, resolve_pair_from_order, check_pair_chat_permission,
query_pair_orders, order_to_chat_dict,
)
from utils.fadan_utils import check_fadan_qiangdan_eligible
from orders.utils import (
calc_shangjia_order_fencheng,
update_daily_payout,
settle_shangjia_order_guanshi_fenhong
)
from shop.utils import calculate_pingtai_and_dianpu_shouyi, validate_shangpin_and_dianpu, update_dianpu_daily_stat
from products.utils import update_shangpin_daily_stat
from backend.utils import update_shangjia_daily, update_dashou_daily_by_action, pick_leixing_id, datetime_aliases, fmt_datetime
from rank.services import record_dashou_biaoxian
from rank.utils import check_dashou_biaoqian_required
from orders.notice_tasks import dingdan_guangbo
from ..models import (
Order, MerchantOrderExt, PlatformOrderExt, PlayerDeliveryImage,
PenaltyRecord, PenaltyEvidenceImage, Penalty, PenaltyAppealImage, PenaltyBonus, PenaltyBonusRate,
CommissionRate, PlayerRating, RefundRecord,
)
from jituan.services.club_config import get_commission_rate, get_commission_rate_object
from jituan.services.club_context import resolve_club_id_from_request
from jituan.services.club_user import get_payment_openid
from jituan.services.club_penalty import resolve_penalty_club_id, resolve_penalty_record_club_id
from users.fadan_fenhong_utils import lock_penalty_bonus
from users.fadan_fenhong_utils import lock_penalty_bonus
from products.models import Shangpin, ShangpinLeixing, Huiyuangoumai
from users.models import UserDashou, UserShangjia, UserBoss
from users.business_models import User
from rank.models import DashouBiaoxian, Chenghao, DingdanBiaoqian, YonghuChenghao
from config.models import (
ShangjiaLianjie
)
logger = logging.getLogger(__name__)
class PaymentFailView(APIView):
"""
前端支付失败接口 - 处理用户主动取消或支付失败
只允许删除状态为9未支付的订单
"""
permission_classes = [permissions.IsAuthenticated]
def post(self, request):
try:
dingdanid = request.data.get('dingdanid')
if not dingdanid:
return Response({'code': 1, 'msg': '订单ID不能为空', 'data': None})
with transaction.atomic():
# 锁定订单防止并发
dingdan = Order.query.select_for_update().filter(OrderID=dingdanid).first()
if not dingdan:
return Response({'code': 2, 'msg': '订单不存在', 'data': None})
if dingdan.User1ID != f"Boss{request.user.UserUID}":
return Response({'code': 3, 'msg': '无权操作此订单', 'data': None})
# 只允许删除未支付订单状态9
if dingdan.Status != 9:
return Response({'code': 3, 'msg': '只能删除未支付的订单', 'data': None})
# 删除扩展表和主表
PlatformOrderExt.query.filter(Order=dingdan).delete()
dingdan.delete()
return Response({'code': 0, 'msg': '订单已删除', 'data': None})
except Exception as e:
return Response({'code': 99, 'msg': f'删除订单失败: {str(e)}', 'data': None})
class WechatPayNotifyView(APIView):
"""微信支付回调接口 - 接收微信官方支付结果通知(含详细调试日志)"""
throttle_classes = [AnonRateThrottle]
permission_classes = [AllowAny]
def post(self, request):
# 记录原始请求数据(调试用)
xml_data = request.body.decode('utf-8')
# 最外层异常捕获,确保任何错误都能被记录并返回适当响应
try:
# 1. 解析 XML
try:
root = ET.fromstring(xml_data)
except ET.ParseError as e:
logger.error(f"XML解析失败: {e}")
return self._gen_response_xml('FAIL', 'XML解析失败')
# 2. 获取关键参数
return_code = root.find('return_code').text if root.find('return_code') is not None else ''
result_code = root.find('result_code').text if root.find('result_code') is not None else ''
out_trade_no = root.find('out_trade_no').text if root.find('out_trade_no') is not None else ''
transaction_id = root.find('transaction_id').text if root.find('transaction_id') is not None else ''
from jituan.services.wechat_pay import club_id_from_order, verify_wechat_v2_signature
notify_club_id = club_id_from_order(out_trade_no)
# 3. 签名验证
if not verify_wechat_v2_signature(xml_data, notify_club_id):
logger.warning("签名验证失败")
return self._gen_response_xml('FAIL', '签名验证失败')
# 4. 根据支付结果处理
if return_code == 'SUCCESS' and result_code == 'SUCCESS':
self._handle_success(out_trade_no, transaction_id, root)
else:
logger.info(f"支付失败或异常return_code={return_code}, result_code={result_code}")
self._handle_failure(out_trade_no)
# 返回微信要求的成功响应必须返回SUCCESS即使内部处理失败也要返回否则微信会重复通知
return self._gen_response_xml('SUCCESS', 'OK')
except Exception as e:
# 捕获任何未处理的异常,记录完整堆栈
logger.error(f"回调处理发生未捕获异常: {str(e)}")
logger.error(traceback.format_exc())
return self._gen_response_xml('FAIL', f'服务器内部错误: {str(e)}')
def _verify_signature(self, xml_data):
"""微信支付签名验证MD5"""
try:
root = ET.fromstring(xml_data)
sign = root.find('sign').text
# 构建待签名字符串排除sign字段
data = {child.tag: child.text for child in root if child.tag != 'sign'}
# 按key排序
sorted_keys = sorted(data.keys())
stringA = '&'.join([f"{k}={data[k]}" for k in sorted_keys])
stringSignTemp = f"{stringA}&key={settings.WEIXIN_SHANGHUMIYAO}"
calc_sign = hashlib.md5(stringSignTemp.encode('utf-8')).hexdigest().upper()
logger.debug(f"计算签名: {calc_sign}, 微信签名: {sign}")
return calc_sign == sign
except Exception as e:
logger.error(f"签名验证过程异常: {e}")
logger.error(traceback.format_exc())
return False
def _gen_response_xml(self, return_code, return_msg):
"""生成返回给微信的XML响应"""
xml = f"""<xml>
<return_code><![CDATA[{return_code}]]></return_code>
<return_msg><![CDATA[{return_msg}]]></return_msg>
</xml>"""
logger.info(f"返回给微信的响应: {xml}")
return HttpResponse(xml, content_type='application/xml')
@transaction.atomic
def _handle_success(self, out_trade_no, transaction_id, root):
"""处理支付成功"""
logger.info(f"进入 _handle_success, out_trade_no={out_trade_no}, transaction_id={transaction_id}")
# 锁定订单防止并发
try:
dingdan = Order.query.select_for_update().select_related(
'pingtai_kuozhan', 'shangjia_kuozhan'
).filter(OrderID=out_trade_no).first()
except Exception as e:
logger.error(f"查询订单异常: {e}")
raise
if not dingdan:
logger.error(f"订单不存在: {out_trade_no}")
return
logger.info(f"订单当前状态: {dingdan.Status}")
# 幂等:已支付则补试收支记账(防止首次入账失败后再也无法统计)
if dingdan.Status != 9:
logger.info(f"订单 {out_trade_no} 已处理过,状态={dingdan.Status},补试收支记账")
try:
from jituan.services.szjilu_accounting import repair_wechat_income_if_missing
repair_wechat_income_if_missing(
dingdan.Amount, getattr(dingdan, 'ClubID', None), biz_ref=f'order:{dingdan.OrderID}',
)
except Exception as e:
logger.error(f'订单补记收支失败 {out_trade_no}: {e}', exc_info=True)
return
# 保存微信交易号
if transaction_id:
dingdan.WechatTransactionID = transaction_id
logger.info(f"保存微信交易号: {transaction_id}")
else:
logger.warning("微信未返回transaction_id")
# 更新订单状态
if dingdan.AssignedID and dingdan.AssignedID.strip():
dingdan.Status = 7
logger.info("指定单状态设为7")
else:
dingdan.Status = 1
logger.info("普通单状态设为1")
dingdan.save()
logger.info(f"订单状态已更新为 {dingdan.Status}")
# 删除同一用户同一商品的其他未支付订单改为逻辑删除状态25
try:
abandoned_count = Order.query.filter(
User1ID=dingdan.User1ID,
ProductID=dingdan.ProductID,
Status=9
).exclude(OrderID=out_trade_no).update(Status=25)
logger.info(f"{abandoned_count} 个未支付订单标记为已废弃(25)")
except Exception as e:
logger.error(f"标记其他未支付订单异常: {e}")
# 更新老板扩展表
try:
laoban_id = None
if hasattr(dingdan, 'pingtai_kuozhan'):
laoban_id = dingdan.pingtai_kuozhan.BossID
if laoban_id:
user_main = User.query.filter(UserUID=laoban_id).select_related('BossProfile').first()
if user_main and hasattr(user_main, 'BossProfile'):
boss = user_main.BossProfile
boss.zonge = (boss.zonge or 0) + (dingdan.Amount or 0)
boss.alldingdan = (boss.alldingdan or 0) + 1
boss.save()
logger.info(f"更新老板 {laoban_id} 数据成功")
except Exception as e:
logger.error(f"更新老板数据异常: {e}")
# 更新商品库存
try:
if dingdan.ProductID:
Shangpin.query.filter(id=dingdan.ProductID).update(
kucun=F('kucun') - 1
)
logger.info(f"商品 {dingdan.ProductID} 库存减1")
except Exception as e:
logger.error(f"更新商品库存异常: {e}")
# ========== 新增:商品和店铺每日统计 ==========
try:
# 商品每日统计只要有商品ID就执行
if dingdan.ProductID:
# 从扩展表获取已计算好的收益值(下单时已存储)
pingtai_shouyi = Decimal('0.00')
dianpu_shouyi = Decimal('0.00')
if hasattr(dingdan, 'pingtai_kuozhan'):
pingtai_shouyi = dingdan.pingtai_kuozhan.PlatformIncome or Decimal('0.00')
dianpu_shouyi = dingdan.pingtai_kuozhan.ShopIncome or Decimal('0.00')
update_shangpin_daily_stat(
ProductID=dingdan.ProductID,
caozuo_leixing=1, # 1 = 下单
dingdan_jiage=dingdan.Amount,
PlatformIncome=pingtai_shouyi,
dianpu_zongshouyi=dianpu_shouyi
)
logger.info(f"商品统计更新完成商品ID: {dingdan.ProductID}")
# 店铺每日统计仅当存在店铺ID时执行
dianpu_id = None
if hasattr(dingdan, 'pingtai_kuozhan'):
dianpu_id = dingdan.pingtai_kuozhan.ShopID
if dianpu_id:
dianpu_shouyi = dingdan.pingtai_kuozhan.ShopIncome or Decimal('0.00')
update_dianpu_daily_stat(
ShopID=dianpu_id,
caozuo_leixing=1, # 1 = 下单
dingdan_jiage=dingdan.Amount,
ShopIncome=dianpu_shouyi
)
logger.info(f"店铺统计更新完成店铺ID: {dianpu_id}")
except Exception as e:
logger.error(f"商品/店铺统计更新失败: {e}")
logger.error(traceback.format_exc())
# 统计失败不影响主流程
# =============================================
# 更新收支记录(总累计);统计失败不得回滚订单支付
try:
from jituan.services.szjilu_accounting import repair_wechat_income_if_missing
repair_wechat_income_if_missing(
dingdan.Amount, getattr(dingdan, 'ClubID', None), biz_ref=f'order:{dingdan.OrderID}',
)
except Exception as e:
logger.error(f'订单收支统计失败 {out_trade_no}: {e}', exc_info=True)
# 构建通知数据(你的 _get_game_type_name 方法保留在视图里直接用)
order_info = {
'OrderID': dingdan.OrderID,
'game_type': self._get_game_type_name(dingdan.ProductTypeID),
'amount': str(dingdan.Amount),
'order_desc': (dingdan.Description or '')[:50],
}
dingdan_guangbo.delay(order_info) # 瞬间返回,不阻塞请求
# 异步广播通知
#self._broadcast_async(dingdan)
logger.info(f"订单 {out_trade_no} 支付成功处理完成")
@transaction.atomic
def _handle_failure(self, out_trade_no):
"""处理支付失败"""
logger.info(f"进入 _handle_failure, out_trade_no={out_trade_no}")
try:
dingdan = Order.query.select_for_update().filter(OrderID=out_trade_no).first()
if not dingdan:
logger.warning(f"订单不存在: {out_trade_no}")
return
logger.info(f"订单当前状态: {dingdan.Status}")
# 仅当订单状态为9未支付时才标记失败不直接删除
if dingdan.Status == 9:
dingdan.Status = 14 # 自定义支付失败状态
dingdan.save()
logger.info(f"订单 {out_trade_no} 支付失败标记为14")
else:
logger.info(f"订单 {out_trade_no} 状态非9不处理")
except Exception as e:
logger.error(f"处理支付失败异常: {e}")
logger.error(traceback.format_exc())
def _update_szjilu(self, jine, club_id=None):
"""szjilu 已废弃,财务只写 daily_income_stat此处不再操作。"""
pass
def _broadcast_async(self, dingdan):
"""异步广播通知"""
try:
def task():
try:
order_info = {
'OrderID': dingdan.OrderID,
'game_type': self._get_game_type_name(dingdan.ProductTypeID),
'amount': str(dingdan.Amount),
'order_desc': (dingdan.Description or '')[:50],
}
logger.info(f"准备广播订单: {order_info}")
sender = WeixinBroadcastSender()
result = sender.broadcast_order(order_info)
if result.get('success'):
logger.info(f"订单广播完成: {result.get('msg')}")
else:
logger.warning(f"订单广播异常: {result.get('msg')}")
except Exception as e:
logger.error(f"广播任务异常: {e}")
logger.error(traceback.format_exc())
thread = threading.Thread(target=task)
thread.daemon = True
thread.start()
logger.info("广播线程已启动")
except Exception as e:
logger.error(f"启动广播线程异常: {e}")
def _get_game_type_name(self, leixing_id):
try:
gt = ShangpinLeixing.query.filter(id=leixing_id).first()
return gt.jieshao if gt else '游戏订单'
except Exception as e:
logger.error(f"获取游戏类型名称异常: {e}")
return '游戏订单'
class AlipayPayNotifyView(WechatPayNotifyView):
"""支付宝支付异步回调接口 - 接收支付宝官方支付结果通知。
继承 WechatPayNotifyView 以复用 _handle_success/_handle_failure 履约逻辑。
"""
def post(self, request):
try:
# 详细记录原始请求(调试回调是否到达)
logger.warning(f"[ALIPAY_NOTIFY_DEBUG] 收到回调, method={request.method}, content_type={request.content_type}")
logger.warning(f"[ALIPAY_NOTIFY_DEBUG] POST keys={list(request.POST.keys())}")
# 支付宝回调为 form-urlencoded
# 注意:必须用 .dict() 而不是 dict()
# dict(request.POST) 会返回 {key: [value_list]}(值是列表)
# request.POST.dict() 返回 {key: value}(值是字符串),符合验签需求
params = request.POST.dict() if request.POST else {}
if not params:
logger.warning("[ALIPAY_NOTIFY_DEBUG] 支付宝回调: 收到空数据")
return HttpResponse('fail', content_type='text/plain')
out_trade_no = params.get('out_trade_no', '')
trade_no = params.get('trade_no', '')
trade_status = params.get('trade_status', '')
logger.warning(f"[ALIPAY_NOTIFY_DEBUG] out_trade_no={out_trade_no}, trade_no={trade_no}, trade_status={trade_status}")
logger.warning(f"[ALIPAY_NOTIFY_DEBUG] params keys={list(params.keys())}")
# 验签
from jituan.services.alipay_pay import verify_notify_params, club_id_from_order
notify_club_id = club_id_from_order(out_trade_no)
logger.warning(f"[ALIPAY_NOTIFY_DEBUG] notify_club_id={notify_club_id}")
verify_result = verify_notify_params(params, notify_club_id)
logger.warning(f"[ALIPAY_NOTIFY_DEBUG] verify_result={verify_result}")
if not verify_result:
logger.warning(f"[ALIPAY_NOTIFY_DEBUG] 支付宝验签失败: out_trade_no={out_trade_no}")
return HttpResponse('fail', content_type='text/plain')
# 处理支付结果
if trade_status in ('TRADE_SUCCESS', 'TRADE_FINISHED'):
logger.warning(f"[ALIPAY_NOTIFY_DEBUG] 调用 _handle_success")
# 充值单误打进订单回调:转交 Czjilu 履约,禁止空 success 吞掉通知
from products.czjilu_types import looks_like_czjilu_out_trade_no
from products.models import Czjilu
if looks_like_czjilu_out_trade_no(out_trade_no) or Czjilu.query.filter(dingdan_id=out_trade_no).exists():
logger.error(
'[ALIPAY_NOTIFY_DEBUG] 充值单误入订单回调,改走 Czjilu 履约: %s',
out_trade_no,
)
from products.views.alipay_czjilu_notify import _fulfill_czjilu_by_leixing
cz = Czjilu.query.filter(dingdan_id=out_trade_no).first()
if not cz:
logger.error('[ALIPAY_NOTIFY_DEBUG] Czjilu 不存在仍回 fail: %s', out_trade_no)
return HttpResponse('fail', content_type='text/plain')
if cz.zhuangtai == 3:
return HttpResponse('success', content_type='text/plain')
from products.czjilu_types import is_czjilu_fulfillable_status
if not is_czjilu_fulfillable_status(cz.zhuangtai):
logger.warning(
'[ALIPAY_NOTIFY_DEBUG] Czjilu 终态不再履约 %s zhuangtai=%s',
out_trade_no, cz.zhuangtai,
)
return HttpResponse('success', content_type='text/plain')
try:
paid_amount = params.get('total_amount') or cz.jine
_fulfill_czjilu_by_leixing(out_trade_no, cz.leixing, paid_amount, source='order_notify_redirect')
except Exception as redirect_exc:
logger.error(
'[ALIPAY_NOTIFY_DEBUG] 充值单转履约失败 %s: %s',
out_trade_no, redirect_exc, exc_info=True,
)
return HttpResponse('fail', content_type='text/plain')
return HttpResponse('success', content_type='text/plain')
# 老板订单:不存在则 fail让支付宝重试禁止假 success
from orders.models import Order
if not Order.query.filter(OrderID=out_trade_no).exists():
logger.error(f"[ALIPAY_NOTIFY_DEBUG] 订单不存在,回 fail 以便重试: {out_trade_no}")
return HttpResponse('fail', content_type='text/plain')
self._handle_success(out_trade_no, trade_no, None)
logger.warning(f"[ALIPAY_NOTIFY_DEBUG] _handle_success 完成")
else:
logger.info(f"支付宝支付状态非成功: {trade_status}, out_trade_no={out_trade_no}")
# 支付宝要求返回 "success"(纯文本),否则会重复通知
return HttpResponse('success', content_type='text/plain')
except Exception as e:
logger.error(f"[ALIPAY_NOTIFY_DEBUG] 支付宝回调处理异常: {str(e)}")
logger.error(traceback.format_exc())
return HttpResponse('fail', content_type='text/plain')
def alipay_pay_page(request):
"""支付宝H5中转页 - 中转跳转到支付宝支付,以及支付完成后的返回页。"""
pay_url = request.GET.get('pay_url', '')
out_trade_no = request.GET.get('out_trade_no', '')
# 优先从 cache 取 pay_url前端传订单号避免 URL 过长)
if not pay_url and out_trade_no:
cache_key = f'alipay_pay_url_{out_trade_no}'
pay_url = cache.get(cache_key) or ''
if pay_url:
# 中转模式:自动跳转到支付宝
return render(request, 'alipay/pay.html', {'pay_url': pay_url})
elif out_trade_no:
# 返回模式:支付完成返回
return render(request, 'alipay/pay.html', {'out_trade_no': out_trade_no})
else:
return render(request, 'alipay/pay.html', {})
class PaymentVerifyView(APIView):
"""
订单支付成功复查接口 - 前端支付成功后调用,确认订单状态
当订单状态为9时主动查询微信支付状态若已支付则更新本地状态
需要token认证
"""
permission_classes = [permissions.IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
try:
# ---------- 1. 获取参数 ----------
dingdanid = request.data.get('dingdanid')
if not dingdanid:
return Response({'code': 1, 'msg': '订单ID不能为空'})
# ---------- 2. 查询订单 ----------
dingdan = Order.query.filter(OrderID=dingdanid).first()
if not dingdan:
return Response({'code': 2, 'msg': '订单不存在'})
# ---------- 3. 验证订单归属(安全) ----------
current_user = request.user
user_identifier = f"Boss{current_user.UserUID}"
if dingdan.User1ID != user_identifier:
logger.warning(f"用户 {current_user.UserUID} 尝试复查他人订单 {dingdanid}")
return Response({'code': 403, 'msg': '无权访问'})
# ---------- 4. 如果订单状态已是1/7直接返回成功 ----------
if dingdan.Status in [1, 7]:
return Response({
'code': 0,
'msg': '订单状态正常',
'data': {'dingdanid': dingdanid, 'zhuangtai': dingdan.Status}
})
# ---------- 5. 如果状态为9则主动查询支付状态 ----------
if dingdan.Status == 9:
# 使用分布式锁防止并发处理同一订单
lock_key = f"payment_verify_lock_{dingdanid}"
if not cache.add(lock_key, "locked", timeout=10):
# 已有其他进程在处理,让前端稍后重试
return Response({'code': 1, 'msg': '订单正在处理中,请稍后'})
try:
# 判断支付方式并主动查单
is_alipay = cache.get(f'alipay_pay_url_{dingdanid}') is not None
trade_state = None
transaction_id = None
if is_alipay:
# 支付宝订单主动查单alipay.trade.query
from jituan.services.alipay_pay import query_alipay_trade
alipay_status = query_alipay_trade(dingdanid, getattr(dingdan, 'ClubID', None))
if alipay_status in ('TRADE_SUCCESS', 'TRADE_FINISHED'):
trade_state = 'SUCCESS'
else:
return Response({'code': 1, 'msg': '支付中', 'data': {'zhuangtai': 9}})
else:
# 微信订单:主动查单(按订单 club 读后台商户配置)
trade_state, transaction_id = self._query_wechat_payment(
dingdanid, club_id=getattr(dingdan, 'ClubID', None),
)
if trade_state != 'SUCCESS':
# 兜底cache 标记可能已过期30分钟尝试查支付宝确认
from jituan.services.alipay_pay import query_alipay_trade
alipay_status = query_alipay_trade(dingdanid, getattr(dingdan, 'ClubID', None))
if alipay_status in ('TRADE_SUCCESS', 'TRADE_FINISHED'):
trade_state = 'SUCCESS'
logger.info('cache 过期但支付宝查单成功 dingdan=%s', dingdanid)
elif trade_state in ['NOTPAY', 'USERPAYING']:
return Response({'code': 1, 'msg': '支付中', 'data': {'zhuangtai': 9}})
else:
return Response({'code': 2, 'msg': '支付失败'})
if trade_state == 'SUCCESS':
# 支付成功,执行本地更新
self._process_payment_success(dingdan, transaction_id, current_user.UserUID)
# 重新获取订单以获取最新状态
dingdan.refresh_from_db()
return Response({
'code': 0,
'msg': '支付成功',
'data': {'dingdanid': dingdanid, 'zhuangtai': dingdan.Status}
})
except Exception as e:
logger.error(f"查询支付异常: {e}", exc_info=True)
return Response({'code': 1, 'msg': '查询失败,请稍后重试'})
finally:
cache.delete(lock_key)
else:
# ---------- 6. 其他异常状态 ----------
status_msg = {
2: '订单已取消',
3: '订单已完成',
4: '退款审核中',
5: '已退款',
6: '退款失败',
8: '结算中',
0: '订单已作废',
}.get(dingdan.Status, '订单状态异常')
return Response({'code': 3, 'msg': status_msg, 'data': {'zhuangtai': dingdan.Status}})
except Exception as e:
logger.error(f"订单复查接口异常: {e}", exc_info=True)
return Response({'code': 99, 'msg': f'系统错误: {str(e)}'})
def _process_payment_success(self, dingdan, transaction_id, operator_id):
"""
支付成功后的原子操作(与微信回调逻辑完全一致)
在事务内完成所有状态更新和数据一致性操作
"""
with transaction.atomic():
# 重新锁定订单,防止并发更新
dingdan = Order.query.select_for_update().get(pk=dingdan.pk)
if dingdan.Status != 9:
try:
from jituan.services.szjilu_accounting import repair_wechat_income_if_missing
repair_wechat_income_if_missing(
dingdan.Amount,
getattr(dingdan, 'ClubID', None),
biz_ref=f'order:{dingdan.OrderID}',
)
except Exception as e:
logger.error(f'订单复查补记收支失败 {dingdan.OrderID}: {e}', exc_info=True)
return
# 更新订单状态
dingdan.Status = 7 if dingdan.AssignedID and dingdan.AssignedID.strip() else 1
dingdan.WechatTransactionID = transaction_id
dingdan.AssignedCS = operator_id # 可选记录操作人ID
dingdan.save()
# 删除同一用户同一商品的其他未支付订单逻辑删除状态25
Order.query.filter(
User1ID=dingdan.User1ID,
ProductID=dingdan.ProductID,
Status=9
).exclude(OrderID=dingdan.OrderID).update(Status=25)
# 更新老板扩展表
laoban_id = dingdan.pingtai_kuozhan.BossID if hasattr(dingdan, 'pingtai_kuozhan') else None
if laoban_id:
user_main = User.query.filter(UserUID=laoban_id).select_related('BossProfile').first()
if user_main and hasattr(user_main, 'BossProfile'):
boss = user_main.BossProfile
boss.zonge = (boss.zonge or 0) + (dingdan.Amount or 0)
boss.alldingdan = (boss.alldingdan or 0) + 1
boss.save()
# 更新商品库存
if dingdan.ProductID:
Shangpin.query.filter(id=dingdan.ProductID).update(
kucun=F('kucun') - 1
)
# ========== 新增:商品和店铺每日统计 ==========
try:
# 商品每日统计只要有商品ID就执行
if dingdan.ProductID:
# 从扩展表获取已计算好的收益值(下单时已存储)
pingtai_shouyi = Decimal('0.00')
dianpu_shouyi = Decimal('0.00')
if hasattr(dingdan, 'pingtai_kuozhan'):
pingtai_shouyi = dingdan.pingtai_kuozhan.PlatformIncome or Decimal('0.00')
dianpu_shouyi = dingdan.pingtai_kuozhan.ShopIncome or Decimal('0.00')
update_shangpin_daily_stat(
ProductID=dingdan.ProductID,
caozuo_leixing=1, # 1 = 下单
dingdan_jiage=dingdan.Amount,
PlatformIncome=pingtai_shouyi,
dianpu_zongshouyi=dianpu_shouyi
)
logger.info(f"商品统计更新完成商品ID: {dingdan.ProductID}")
# 店铺每日统计仅当存在店铺ID时执行
dianpu_id = None
if hasattr(dingdan, 'pingtai_kuozhan'):
dianpu_id = dingdan.pingtai_kuozhan.ShopID
if dianpu_id:
dianpu_shouyi = dingdan.pingtai_kuozhan.ShopIncome or Decimal('0.00')
update_dianpu_daily_stat(
ShopID=dianpu_id,
caozuo_leixing=1, # 1 = 下单
dingdan_jiage=dingdan.Amount,
ShopIncome=dianpu_shouyi
)
logger.info(f"店铺统计更新完成店铺ID: {dianpu_id}")
except Exception as e:
logger.error(f"商品/店铺统计更新失败: {e}")
logger.error(traceback.format_exc())
# 统计失败不影响主流程
# =============================================
try:
from jituan.services.szjilu_accounting import repair_wechat_income_if_missing
repair_wechat_income_if_missing(
dingdan.Amount,
getattr(dingdan, 'ClubID', None),
biz_ref=f'order:{dingdan.OrderID}',
)
except Exception as e:
logger.error(f'订单收支统计失败 {dingdan.OrderID}: {e}', exc_info=True)
def _query_wechat_payment(self, out_trade_no, club_id=None):
"""
调用微信支付订单查询接口,返回 (trade_state, transaction_id)
若查询失败,抛出异常
"""
from jituan.services.wechat_pay import club_id_from_order, get_wechat_v2_config
pay_cfg = get_wechat_v2_config(club_id or club_id_from_order(out_trade_no))
appid = pay_cfg['appid']
mch_id = pay_cfg['mch_id']
key = pay_cfg['key']
nonce_str = self._generate_nonce_str()
params = {
'appid': appid,
'mch_id': mch_id,
'out_trade_no': out_trade_no,
'nonce_str': nonce_str,
}
# 生成签名
sorted_keys = sorted(params.keys())
stringA = '&'.join([f"{k}={params[k]}" for k in sorted_keys])
stringSignTemp = f"{stringA}&key={key}"
sign = hashlib.md5(stringSignTemp.encode('utf-8')).hexdigest().upper()
params['sign'] = sign
xml_data = self._dict_to_xml(params)
url = 'https://api.mch.weixin.qq.com/pay/orderquery'
response = requests.post(url, data=xml_data, timeout=10)
result = xmltodict.parse(response.content)['xml']
if result.get('return_code') == 'SUCCESS' and result.get('result_code') == 'SUCCESS':
return result.get('trade_state'), result.get('transaction_id')
else:
err_msg = result.get('return_msg', '未知错误')
raise Exception(f"微信订单查询失败: {err_msg}")
def _update_szjilu(self, jine, club_id=None):
"""szjilu 已废弃,财务只写 daily_income_stat此处不再操作。"""
pass
# 不抛出异常,因为收支记录不是核心流程,但记录错误供排查
def _generate_nonce_str(self):
"""生成随机字符串长度32"""
return ''.join(random.choices(string.ascii_letters + string.digits, k=32))
def _dict_to_xml(self, data):
"""将字典转换为XML字符串"""
xml = ['<xml>']
for k, v in data.items():
xml.append(f'<{k}>{v}</{k}>')
xml.append('</xml>')
return ''.join(xml)
class CreateOrderView(APIView):
"""
创建订单并调用微信支付接口
"""
permission_classes = [permissions.IsAuthenticated]
def post(self, request):
try:
# 1. 获取前端传递的字段
shangpin_id = request.data.get('shangpin_id')
nicheng = request.data.get('nicheng', '').strip()
beizhu = request.data.get('Remark', '').strip()
zhiding = request.data.get('zhiding', '').strip()
jine = request.data.get('jine', 0)
pay_method = request.data.get('pay_method', 'wechat').strip().lower()
# 2. 验证必填字段
if not shangpin_id:
return Response({'code': 1, 'msg': '商品ID不能为空', 'data': None})
if not nicheng:
return Response({'code': 2, 'msg': '游戏昵称不能为空', 'data': None})
if len(nicheng) > 50:
return Response({'code': 3, 'msg': '游戏昵称过长', 'data': None})
# 3. 查询商品信息
shangpin = Shangpin.query.filter(id=shangpin_id).first()
if not shangpin:
return Response({'code': 4, 'msg': '商品不存在', 'data': None})
is_valid, err_msg, _ = validate_shangpin_and_dianpu(shangpin_id)
if not is_valid:
return Response({'code': 5, 'msg': err_msg, 'data': None})
# 检查库存
if hasattr(shangpin, 'kucun') and shangpin.kucun <= 0:
return Response({'code': 5, 'msg': '商品库存不足', 'data': None})
# 获取商品价格
shangpin_jiage = getattr(shangpin, 'jiage', 0)
if not shangpin_jiage:
return Response({'code': 6, 'msg': '商品价格异常', 'data': None})
#获取商品图片
shangpin_tupian = getattr(shangpin, 'tupian_url', None)
# 4. 验证指定打手(如果存在)
zhiding_id_value = None
if zhiding:
# 查询主表中是否存在该打手
zhiding_user = User.query.filter(
UserUID=zhiding,
#user_type='PlayerID'
).select_related('DashouProfile').first()
if not zhiding_user:
return Response({'code': 7, 'msg': '指定打手不存在', 'data': None})
# 查询打手扩展表
try:
dashou_profile = zhiding_user.DashouProfile
except UserDashou.DoesNotExist:
dashou_profile = None
if not dashou_profile:
return Response({'code': 8, 'msg': '指定用户不存在或已封禁', 'data': None})
# 检查在线状态
if getattr(dashou_profile, 'zhanghaozhuangtai', 0) == 0:
return Response({'code': 8, 'msg': '指定用户不存在或已封禁', 'data': None})
zhiding_id_value = zhiding
# 5. 生成订单ID
timestamp = int(time.time())
timestamp_ns = int(time.time_ns() // 1000) % 1000000
random_num = random.randint(0, 99)
dingdanid = f"PT{timestamp:011d}{timestamp_ns:06d}{random_num:02d}"
# 6. 查询利率并计算分成
club_id = resolve_club_id_from_request(request)
dashou_lilu = get_commission_rate_object(club_id, '1')
# 🔥 新增:判断是否使用商品配置的额外分成
if shangpin.kaioi_ewai_dashou_fencheng and shangpin.ewai_dashou_fencheng is not None:
# 使用商品指定的额外分成金额
dashou_fencheng = shangpin.ewai_dashou_fencheng
else:
# 按原有逻辑计算打手分成
dashou_fencheng = Decimal(str(shangpin_jiage)) * Decimal(str(dashou_lilu.Rate))
# 查询管事利率(字符'2'
#guanshi_lilu = CommissionRate.query.filter(Platform='2').first()
if not (shangpin.kaioi_ewai_dashou_fencheng and shangpin.ewai_dashou_fencheng is not None):
if get_commission_rate(club_id, '1') <= 0 and not CommissionRate.query.filter(Platform='1').exists():
return Response({'code': 9, 'msg': '系统利率配置不完整', 'data': None})
# 开始数据库事务
with transaction.atomic():
# 获取当前用户信息
current_user = request.user
laoban_yonghuid = getattr(current_user, 'UserUID', '') or getattr(current_user, 'yonghuid', '')
user_openid, club_id = get_payment_openid(request, current_user, club_id)
# 支付宝支付不需要 openid
if pay_method == 'wechat' and not user_openid:
return Response({'code': 10, 'msg': '用户openid不存在', 'data': None})
# 创建订单主表数据
order_data = {
'OrderID': dingdanid,
'Status': 9, # 未付款状态
'Amount': shangpin_jiage,
'Platform':1,
'PlayerCommission': dashou_fencheng,
'AssignedID': zhiding_id_value,
'ProductID': shangpin_id,
'GrabRequirement': getattr(shangpin, 'yaoqiuleixing', None),
'MembershipID': getattr(shangpin, 'huiyuan_id', ''),
'CommissionReq': getattr(shangpin, 'yongjin', 0),
'Description': getattr(shangpin, 'jieshao', ''),
'Remark': beizhu,
'Nickname': nicheng,
'ImageURL':shangpin_tupian,
'User1ID':f"Boss{request.user.UserUID}",
'ProductTypeID':shangpin.leixing_id,
'ClubID': club_id,
}
# 创建订单记录
dingdan = Order.query.create(**order_data)
# CreateOrderView.post 事务内部,替换原有的扩展表创建代码
dianpu_id, pingtai_shouyi, dianpu_shouyi, shangji_dianpu_id, shangji_fenhong = calculate_pingtai_and_dianpu_shouyi(
shangpin_id=shangpin_id,
dingdan_jiage=shangpin_jiage,
dashou_fencheng=dashou_fencheng
)
pingtaikuozhan_data = {
'Order': dingdan,
'BossID': laoban_yonghuid,
'PlatformIncome': pingtai_shouyi,
# ---------- 新增上级分红快照 ----------
'ParentShopID': shangji_dianpu_id,
'ParentBonus': shangji_fenhong if shangji_fenhong else Decimal('0.00'),
}
if dianpu_id:
pingtaikuozhan_data['ShopID'] = dianpu_id
pingtaikuozhan_data['ShopIncome'] = dianpu_shouyi
else:
pingtaikuozhan_data['ShopIncome'] = Decimal('0.00')
PlatformOrderExt.query.create(**pingtaikuozhan_data)
# 计算收益
'''dianpu_id, pingtai_shouyi, dianpu_shouyi = calculate_pingtai_and_dianpu_shouyi(
ProductID=shangpin_id,
dingdan_jiage=shangpin_jiage,
PlayerCommission=dashou_fencheng
)
# 构建扩展表数据平台收益必定保存店铺收益和店铺ID仅当dianpu_id存在时才赋值
pingtaikuozhan_data = {
'dingdan': dingdan,
'laoban_id': laoban_yonghuid,
'pingtai_shouyi': pingtai_shouyi,
}
if dianpu_id:
pingtaikuozhan_data['dianpu_id'] = dianpu_id
pingtaikuozhan_data['dianpu_shouyi'] = dianpu_shouyi
else:
# 无店铺时店铺收益字段保持默认0.00也可显式设为0
pingtaikuozhan_data['dianpu_shouyi'] = Decimal('0.00')
PlatformOrderExt.query.create(**pingtaikuozhan_data)'''
# 7. 根据支付方式生成支付参数
if pay_method == 'alipay':
pay_params = self.generate_alipay_pay_params(
dingdanid=dingdanid,
jine=shangpin_jiage,
subject=getattr(settings, 'WEIXIN_PAY_DESCRIPTION', '星阙订单'),
club_id=club_id,
)
else:
pay_params = self.generate_wechat_pay_params(
dingdanid=dingdanid,
jine=shangpin_jiage,
beizhu=getattr(settings, 'WEIXIN_PAY_DESCRIPTION', '星阙订单'),
user_ip=self.get_client_ip(request),
openid=user_openid,
club_id=club_id,
)
if not pay_params:
# 手动抛出异常触发事务回滚
raise Exception('支付参数生成失败')
# 8. 返回成功响应
response_data = {
'code': 0,
'msg': '订单创建成功',
'data': {
'dingdanid': dingdanid,
'payMethod': pay_method,
'payParams': pay_params
}
}
return Response(response_data)
except Exception as e:
# 这里会自动回滚事务
return Response({'code': 99, 'msg': f'系统错误: {str(e)}', 'data': None})
def get_client_ip(self, request):
"""获取客户端真实IP地址"""
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
def generate_wechat_pay_params(self, dingdanid, jine, beizhu, user_ip, openid, club_id=None):
"""
调用微信支付统一下单接口,生成支付参数
"""
try:
from jituan.services.wechat_pay import get_wechat_v2_config
pay_cfg = get_wechat_v2_config(club_id)
APPID = pay_cfg['appid']
MCHID = pay_cfg['mch_id']
KEY = pay_cfg['key']
NOTIFY_URL = getattr(settings, 'WEIXIN_NOTIFY_URL', '')
# NOAUTH 诊断日志:打印生效的 club_id / appid / mchid / openid 指纹
_oid_preview = (openid or '')[:6] + '...' + (openid or '')[-4:] if (openid or '') else '(empty)'
logger.warning(
"[WX_PAY_NOAUTH_DIAG] dingdanid=%s club_id=%s appid=%s mch_id=%s openid_preview=%s openid_len=%d",
dingdanid, club_id, APPID, MCHID, _oid_preview, len(openid or ''),
)
if not all([APPID, MCHID, KEY, NOTIFY_URL]):
raise Exception('微信支付配置不完整')
# 生成随机字符串
nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
# 金额(单位:分)
total_fee = yuan_to_fen(jine)
# 交易类型
trade_type = 'JSAPI'
# 生成签名
sign_data = {
'appid': APPID,
'mch_id': MCHID,
'nonce_str': nonce_str,
'body': beizhu,
'out_trade_no': dingdanid,
'total_fee': total_fee,
'spbill_create_ip': user_ip,
'notify_url': NOTIFY_URL,
'trade_type': trade_type,
'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>{beizhu}</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
logger.warning("[WX_PAY_NOAUTH_DIAG] wechat_raw_response=%s", response.content.decode('utf-8', errors='replace'))
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 = root.find('prepay_id').text
# 生成前端支付参数
timestamp = str(int(time.time()))
nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
package = f'prepay_id={prepay_id}'
# 生成支付签名
pay_sign_data = {
'appId': APPID,
'timeStamp': timestamp,
'nonceStr': nonce_str,
'package': package,
'signType': 'MD5'
}
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()
# 返回支付参数(格式与前端完全匹配)
result = {
'timeStamp': timestamp,
'nonceStr': nonce_str,
'package': package,
'signType': 'MD5',
'paySign': pay_sign
}
return result
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}")
except Exception as e:
raise e
def generate_alipay_pay_params(self, dingdanid, jine, subject, club_id=None):
"""
生成支付宝 wap.pay URL 并转成二维码图片。
用户用支付宝 APP 扫一扫二维码,在支付宝内置浏览器完成支付
(支付宝环境支持 alipay:// scheme不受微信 web-view 限制)。
无需签约"当面付"产品,仅需已开通的"手机网站支付"
"""
try:
from jituan.services.alipay_pay import build_wap_pay_url, qr_link_to_base64
pay_url = build_wap_pay_url(
out_trade_no=dingdanid,
amount=jine,
subject=subject,
club_id=club_id,
)
qr_image = qr_link_to_base64(pay_url)
# 标记支付宝订单fucha 接口据此跳过微信查询,直接等异步回调更新状态)
cache.set(f'alipay_pay_url_{dingdanid}', pay_url, 60 * 30)
return {
'qr_code': pay_url,
'qr_image': qr_image,
'pay_method': 'alipay',
}
except Exception as e:
raise e