fix: 加固支付宝充值回调幂等履约,避免付成功不到账与误删单
禁止 Czjilu 回调地址回落到订单 notify;回调失败回 fail 重试;取消支付改关单不删;客服补单走同一履约防多加。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -56,7 +56,7 @@ from .chongzhi_payment import (
|
||||
MAX_INTEGRAL,
|
||||
IDENTITY_CONFIG,
|
||||
)
|
||||
from .alipay_czjilu_notify import AlipayCzjiluNotifyView
|
||||
from .alipay_czjilu_notify import AlipayCzjiluNotifyView, CzjiluRepairView
|
||||
from .shangpin_admin import (
|
||||
ShangpinLeixingFabuView,
|
||||
ShangpinZhuanquFabuView,
|
||||
@@ -98,7 +98,7 @@ __all__ = [
|
||||
'CzhqdyView', 'DsqrgmdhView',
|
||||
'INTEGRAL_PRICE', 'INTEGRAL_INCREMENT', 'MAX_INTEGRAL', 'IDENTITY_CONFIG',
|
||||
# alipay_czjilu_notify
|
||||
'AlipayCzjiluNotifyView',
|
||||
'AlipayCzjiluNotifyView', 'CzjiluRepairView',
|
||||
# shangpin_admin
|
||||
'ShangpinLeixingFabuView', 'ShangpinZhuanquFabuView', 'ShangpinFabuView',
|
||||
'GuizeTupianShangchuanView',
|
||||
|
||||
@@ -8,16 +8,44 @@ import traceback
|
||||
|
||||
from django.http import HttpResponse
|
||||
from django.views import View
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework import status
|
||||
|
||||
from products.models import Czjilu
|
||||
from products.czjilu_types import (
|
||||
CZJILU_LEIXING_HUIYUAN, CZJILU_LEIXING_YAJIN, CZJILU_LEIXING_JIFEN,
|
||||
CZJILU_LEIXING_SHANGJIA, CZJILU_LEIXING_KAOHE, CZJILU_LEIXING_FAKUAN,
|
||||
CZJILU_PAID_STATUS, is_czjilu_fulfillable_status,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _fulfill_czjilu_by_leixing(out_trade_no, leixing, paid_amount, source='callback'):
|
||||
"""按 leixing 调幂等履约;入账只在 fulfill_* 内发生一次。"""
|
||||
if leixing == CZJILU_LEIXING_HUIYUAN:
|
||||
from jituan.services.member_recharge import fulfill_member_recharge
|
||||
return fulfill_member_recharge(out_trade_no, paid_amount_yuan=paid_amount, source=source)
|
||||
if leixing == CZJILU_LEIXING_YAJIN:
|
||||
from jituan.services.czjilu_fulfill import fulfill_yajin_recharge
|
||||
return fulfill_yajin_recharge(out_trade_no, paid_amount_yuan=paid_amount)
|
||||
if leixing == CZJILU_LEIXING_JIFEN:
|
||||
from jituan.services.czjilu_fulfill import fulfill_jifen_recharge
|
||||
return fulfill_jifen_recharge(out_trade_no, paid_amount_yuan=paid_amount)
|
||||
if leixing == CZJILU_LEIXING_SHANGJIA:
|
||||
from jituan.services.czjilu_fulfill import fulfill_shangjia
|
||||
return fulfill_shangjia(out_trade_no, paid_amount_yuan=paid_amount)
|
||||
if leixing == CZJILU_LEIXING_KAOHE:
|
||||
from jituan.services.czjilu_fulfill import fulfill_kaohe
|
||||
return fulfill_kaohe(out_trade_no, paid_amount_yuan=paid_amount)
|
||||
if leixing == CZJILU_LEIXING_FAKUAN:
|
||||
from jituan.services.czjilu_fulfill import fulfill_fakuan
|
||||
return fulfill_fakuan(out_trade_no, paid_amount_yuan=paid_amount)
|
||||
raise ValueError(f'未知 leixing={leixing}')
|
||||
|
||||
|
||||
class AlipayCzjiluNotifyView(View):
|
||||
"""支付宝 Czjilu 充值统一回调:验签 + 按 leixing 路由履约。
|
||||
|
||||
@@ -60,58 +88,81 @@ class AlipayCzjiluNotifyView(View):
|
||||
# 查询充值单
|
||||
order = Czjilu.query.filter(dingdan_id=out_trade_no).first()
|
||||
if not order:
|
||||
# 找不到单回 fail,让支付宝重试,避免「钱付了永久无履约」
|
||||
logger.error('[ALIPAY_CZJILU_NOTIFY] 充值单不存在: %s', out_trade_no)
|
||||
return HttpResponse('fail', content_type='text/plain')
|
||||
|
||||
# 已支付:补记收支后直接返回成功(幂等)
|
||||
if order.zhuangtai == 3:
|
||||
# 已支付:幂等成功(绝不再加余额/会员)
|
||||
if order.zhuangtai == CZJILU_PAID_STATUS:
|
||||
logger.info('[ALIPAY_CZJILU_NOTIFY] 订单已支付,幂等返回: %s', out_trade_no)
|
||||
return HttpResponse('success', content_type='text/plain')
|
||||
|
||||
if order.zhuangtai != 9:
|
||||
logger.warning(
|
||||
'[ALIPAY_CZJILU_NOTIFY] 订单状态异常: %s, zhuangtai=%s',
|
||||
out_trade_no, order.zhuangtai,
|
||||
)
|
||||
# 待付(9) 或用户关单(2):官方已付则履约
|
||||
if is_czjilu_fulfillable_status(order.zhuangtai):
|
||||
paid_amount = total_amount or order.jine
|
||||
leixing = order.leixing
|
||||
try:
|
||||
_fulfill_czjilu_by_leixing(out_trade_no, leixing, paid_amount, source='callback')
|
||||
except Exception as fulfill_exc:
|
||||
logger.error(
|
||||
'[ALIPAY_CZJILU_NOTIFY] 履约异常 out_trade_no=%s leixing=%s: %s',
|
||||
out_trade_no, leixing, fulfill_exc, exc_info=True,
|
||||
)
|
||||
# 履约异常返回 fail,让支付宝重试
|
||||
return HttpResponse('fail', content_type='text/plain')
|
||||
|
||||
logger.info('[ALIPAY_CZJILU_NOTIFY] 履约完成: %s, leixing=%s', out_trade_no, leixing)
|
||||
return HttpResponse('success', content_type='text/plain')
|
||||
|
||||
# 按 leixing 路由到对应履约函数
|
||||
paid_amount = total_amount or order.jine
|
||||
leixing = order.leixing
|
||||
try:
|
||||
if leixing == CZJILU_LEIXING_HUIYUAN:
|
||||
from jituan.services.member_recharge import fulfill_member_recharge
|
||||
fulfill_member_recharge(out_trade_no, paid_amount_yuan=paid_amount, source='callback')
|
||||
elif leixing == CZJILU_LEIXING_YAJIN:
|
||||
from jituan.services.czjilu_fulfill import fulfill_yajin_recharge
|
||||
fulfill_yajin_recharge(out_trade_no, paid_amount_yuan=paid_amount)
|
||||
elif leixing == CZJILU_LEIXING_JIFEN:
|
||||
from jituan.services.czjilu_fulfill import fulfill_jifen_recharge
|
||||
fulfill_jifen_recharge(out_trade_no, paid_amount_yuan=paid_amount)
|
||||
elif leixing == CZJILU_LEIXING_SHANGJIA:
|
||||
from jituan.services.czjilu_fulfill import fulfill_shangjia
|
||||
fulfill_shangjia(out_trade_no, paid_amount_yuan=paid_amount)
|
||||
elif leixing == CZJILU_LEIXING_KAOHE:
|
||||
from jituan.services.czjilu_fulfill import fulfill_kaohe
|
||||
fulfill_kaohe(out_trade_no, paid_amount_yuan=paid_amount)
|
||||
elif leixing == CZJILU_LEIXING_FAKUAN:
|
||||
from jituan.services.czjilu_fulfill import fulfill_fakuan
|
||||
fulfill_fakuan(out_trade_no, paid_amount_yuan=paid_amount)
|
||||
else:
|
||||
logger.error('[ALIPAY_CZJILU_NOTIFY] 未知 leixing=%s: %s', leixing, out_trade_no)
|
||||
return HttpResponse('fail', content_type='text/plain')
|
||||
except Exception as fulfill_exc:
|
||||
logger.error(
|
||||
'[ALIPAY_CZJILU_NOTIFY] 履约异常 out_trade_no=%s leixing=%s: %s',
|
||||
out_trade_no, leixing, fulfill_exc, exc_info=True,
|
||||
)
|
||||
# 履约异常返回 fail,让支付宝重试
|
||||
return HttpResponse('fail', content_type='text/plain')
|
||||
|
||||
logger.info('[ALIPAY_CZJILU_NOTIFY] 履约完成: %s, leixing=%s', out_trade_no, leixing)
|
||||
# 终态异常(如会员履约失败 8、考核业务失败 10):停止重试,留人工处理
|
||||
logger.warning(
|
||||
'[ALIPAY_CZJILU_NOTIFY] 订单终态不再履约: %s, zhuangtai=%s',
|
||||
out_trade_no, order.zhuangtai,
|
||||
)
|
||||
return HttpResponse('success', content_type='text/plain')
|
||||
|
||||
except Exception as e:
|
||||
logger.error('[ALIPAY_CZJILU_NOTIFY] 回调处理异常: %s', str(e))
|
||||
logger.error(traceback.format_exc())
|
||||
return HttpResponse('fail', content_type='text/plain')
|
||||
|
||||
|
||||
class CzjiluRepairView(APIView):
|
||||
"""客服补单:查官方支付结果后走同一幂等履约。
|
||||
|
||||
路径:POST /shangpin/czjilu-budan
|
||||
参数:dingdanid
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
from backend.utils import verify_kefu_permission
|
||||
from jituan.services.czjilu_fulfill import repair_czjilu_payment, CzjiluFulfillError
|
||||
from jituan.services.member_recharge import MemberRechargeError
|
||||
|
||||
username = (
|
||||
request.data.get('username')
|
||||
or request.data.get('phone')
|
||||
or getattr(request.user, 'Phone', None)
|
||||
)
|
||||
kefu, permissions_or_resp = verify_kefu_permission(request, username)
|
||||
if not kefu:
|
||||
return permissions_or_resp
|
||||
|
||||
dingdanid = (request.data.get('dingdanid') or request.data.get('dingdan_id') or '').strip()
|
||||
if not dingdanid:
|
||||
return Response({'code': 400, 'message': '订单ID不能为空'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
result = repair_czjilu_payment(dingdanid)
|
||||
return Response({
|
||||
'code': 200,
|
||||
'message': '补单成功' if not result.get('already_done') else '订单已到账(幂等)',
|
||||
'data': result,
|
||||
})
|
||||
except (CzjiluFulfillError, MemberRechargeError) as exc:
|
||||
logger.warning('[CZJILU_REPAIR] 补单未完成 dingdan=%s: %s', dingdanid, exc)
|
||||
return Response({'code': 400, 'message': str(exc)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
except Exception as exc:
|
||||
logger.error('[CZJILU_REPAIR] 补单异常 dingdan=%s: %s', dingdanid, exc, exc_info=True)
|
||||
return Response({'code': 500, 'message': '补单失败,请稍后重试'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
@@ -540,20 +540,20 @@ class ShangjiaShibai(APIView):
|
||||
'message': '订单不存在或不属于当前用户'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 3. 检查订单状态是否为未支付(9)
|
||||
# 3. 检查订单状态是否为未支付(9):关单不删
|
||||
if order.zhuangtai == 9:
|
||||
# 删除订单
|
||||
order.delete()
|
||||
logger.info(f"商家{request.user.ShopProfile.nicheng}删除未支付订单: {dingdanid}")
|
||||
from products.czjilu_types import close_unpaid_czjilu
|
||||
close_unpaid_czjilu(order, reason='用户取消支付')
|
||||
logger.info(f"商家{request.user.ShopProfile.nicheng}关闭未支付订单(不删): {dingdanid}")
|
||||
|
||||
return Response({
|
||||
'code': 200,
|
||||
'message': '订单已删除'
|
||||
'message': '订单已取消'
|
||||
}, status=status.HTTP_200_OK)
|
||||
else:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '订单状态不是未支付,无法删除'
|
||||
'message': '订单状态不是未支付,无法取消'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
except UserShangjia.DoesNotExist:
|
||||
@@ -606,8 +606,9 @@ class ShangjiaChenggong(APIView):
|
||||
'message': '订单不存在或不属于当前用户'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 3. 订单未支付时,尝试补偿履约(回调延迟/失败时通过轮询兜底)
|
||||
if order.zhuangtai == 9:
|
||||
# 3. 未支付/已关单时,尝试补偿履约(回调延迟/失败时通过轮询兜底)
|
||||
from products.czjilu_types import is_czjilu_fulfillable_status
|
||||
if is_czjilu_fulfillable_status(order.zhuangtai):
|
||||
from jituan.services.czjilu_fulfill import confirm_czjilu_paid, CzjiluFulfillError
|
||||
try:
|
||||
confirm_czjilu_paid(dingdanid, request.user.UserUID, expected_leixing=4)
|
||||
|
||||
@@ -551,20 +551,20 @@ class HuiyuanShibai(APIView):
|
||||
'message': '订单类型错误,不是会员订单'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 4. 检查订单状态是否为未支付(9)
|
||||
# 4. 检查订单状态是否为未支付(9):关单不删,保留流水便于官方已付后补履约
|
||||
if order.zhuangtai == 9:
|
||||
# 删除订单
|
||||
order.delete()
|
||||
logger.info(f"用户{request.user.UserUID}删除未支付会员订单: {dingdanid}")
|
||||
from products.czjilu_types import close_unpaid_czjilu
|
||||
close_unpaid_czjilu(order, reason='用户取消支付')
|
||||
logger.info(f"用户{request.user.UserUID}关闭未支付会员订单(不删): {dingdanid}")
|
||||
|
||||
return Response({
|
||||
'code': 200,
|
||||
'message': '会员订单已删除'
|
||||
'message': '会员订单已取消'
|
||||
}, status=status.HTTP_200_OK)
|
||||
else:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '会员订单状态不是未支付,无法删除'
|
||||
'message': '会员订单状态不是未支付,无法取消'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
except Exception as e:
|
||||
@@ -616,8 +616,9 @@ class HuiyuanLunxun(APIView):
|
||||
'message': '订单类型错误,不是会员订单'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 4. 未支付:查微信并幂等履约(与回调互斥,zhuangtai!=9 则跳过)
|
||||
if order.zhuangtai == 9:
|
||||
# 4. 未支付/已关单:查官方并幂等履约
|
||||
from products.czjilu_types import is_czjilu_fulfillable_status
|
||||
if is_czjilu_fulfillable_status(order.zhuangtai):
|
||||
from jituan.services.member_recharge import (
|
||||
confirm_member_recharge_paid,
|
||||
MemberRechargeError,
|
||||
|
||||
@@ -511,20 +511,20 @@ class JifenShibai(APIView):
|
||||
'message': '订单类型错误,不是积分订单'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 4. 检查订单状态是否为未支付(9)
|
||||
# 4. 检查订单状态是否为未支付(9):关单不删
|
||||
if order.zhuangtai == 9:
|
||||
# 删除订单
|
||||
order.delete()
|
||||
logger.info(f"用户{request.user.UserUID}删除未支付积分订单: {dingdanid}")
|
||||
from products.czjilu_types import close_unpaid_czjilu
|
||||
close_unpaid_czjilu(order, reason='用户取消支付')
|
||||
logger.info(f"用户{request.user.UserUID}关闭未支付积分订单(不删): {dingdanid}")
|
||||
|
||||
return Response({
|
||||
'code': 200,
|
||||
'message': '积分订单已删除'
|
||||
'message': '积分订单已取消'
|
||||
}, status=status.HTTP_200_OK)
|
||||
else:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '积分订单状态不是未支付,无法删除'
|
||||
'message': '积分订单状态不是未支付,无法取消'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
except Exception as e:
|
||||
@@ -576,8 +576,9 @@ class JifenLunxun(APIView):
|
||||
'message': '订单类型错误,不是积分订单'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 4. 未支付则查微信并幂等履约
|
||||
if order.zhuangtai == 9:
|
||||
# 4. 未支付/已关单则查官方并幂等履约
|
||||
from products.czjilu_types import is_czjilu_fulfillable_status
|
||||
if is_czjilu_fulfillable_status(order.zhuangtai):
|
||||
from jituan.services.czjilu_fulfill import confirm_czjilu_paid, CzjiluFulfillError
|
||||
try:
|
||||
confirm_czjilu_paid(dingdanid, request.user.UserUID, expected_leixing=3)
|
||||
|
||||
@@ -524,20 +524,20 @@ class YajinShibai(APIView):
|
||||
'message': '订单不存在或不属于当前用户'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 3. 检查订单状态是否为未支付(9)
|
||||
# 3. 检查订单状态是否为未支付(9):关单不删
|
||||
if order.zhuangtai == 9:
|
||||
# 删除订单
|
||||
order.delete()
|
||||
logger.info(f"用户{request.user.UserUID}删除未支付订单: {dingdanid}")
|
||||
from products.czjilu_types import close_unpaid_czjilu
|
||||
close_unpaid_czjilu(order, reason='用户取消支付')
|
||||
logger.info(f"用户{request.user.UserUID}关闭未支付押金订单(不删): {dingdanid}")
|
||||
|
||||
return Response({
|
||||
'code': 200,
|
||||
'message': '订单已删除'
|
||||
'message': '订单已取消'
|
||||
}, status=status.HTTP_200_OK)
|
||||
else:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '订单状态不是未支付,无法删除'
|
||||
'message': '订单状态不是未支付,无法取消'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
except Exception as e:
|
||||
@@ -582,8 +582,9 @@ class YajinLunxun(APIView):
|
||||
'message': '订单不存在或不属于当前用户'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 3. 未支付则查微信并幂等履约
|
||||
if order.zhuangtai == 9:
|
||||
# 3. 未支付/已关单则查官方并幂等履约
|
||||
from products.czjilu_types import is_czjilu_fulfillable_status
|
||||
if is_czjilu_fulfillable_status(order.zhuangtai):
|
||||
from jituan.services.czjilu_fulfill import confirm_czjilu_paid, CzjiluFulfillError
|
||||
try:
|
||||
confirm_czjilu_paid(dingdanid, request.user.UserUID, expected_leixing=2)
|
||||
|
||||
Reference in New Issue
Block a user