复查与充值轮询按通道查官方单:付呗单查付呗、直连查微信;履约仍幂等防多加,未完成返回业务码便于继续轮询。 Co-authored-by: Cursor <cursoragent@cursor.com>
1198 lines
53 KiB
Python
1198 lines
53 KiB
Python
"""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:
|
||
trade_state = None
|
||
transaction_id = None
|
||
|
||
from jituan.services.pay_confirm import confirm_remote_payment
|
||
remote = confirm_remote_payment(
|
||
dingdanid,
|
||
getattr(dingdan, 'ClubID', None),
|
||
prefer_amount=getattr(dingdan, 'Amount', None),
|
||
)
|
||
if remote.get('ok'):
|
||
trade_state = 'SUCCESS'
|
||
transaction_id = remote.get('transaction_id')
|
||
elif remote.get('pending'):
|
||
return Response({'code': 1, 'msg': '支付中', 'data': {'zhuangtai': 9}})
|
||
else:
|
||
return Response({'code': 2, 'msg': remote.get('msg') or '支付失败'})
|
||
|
||
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.constants import FUBEI_BIZ_ORDER
|
||
from jituan.services.mini_pay_router import maybe_create_fubei_jsapi_params
|
||
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', '')
|
||
|
||
fb_params = maybe_create_fubei_jsapi_params(
|
||
club_id=club_id,
|
||
out_trade_no=dingdanid,
|
||
amount_yuan=jine,
|
||
body=beizhu,
|
||
openid=openid,
|
||
notify_url=NOTIFY_URL,
|
||
biz_type=FUBEI_BIZ_ORDER,
|
||
)
|
||
if fb_params is not None:
|
||
return fb_params
|
||
|
||
# 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
|
||
|
||
|
||
|