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

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

169 lines
8.0 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.
"""支付宝 Czjilu 类充值统一异步回调视图。
所有基于 Czjilu 表的支付宝支付(押金/积分/会员/商家充值/考核/罚款)
共用一个回调入口,按 Czjilu.leixing 路由到对应履约函数。
"""
import logging
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 路由履约。
路径:/shangpin/alipay-czjilu-notify/
不需要 JWT 认证,由支付宝服务器调用。
"""
def post(self, request):
try:
# 注意:必须用 .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_CZJILU_NOTIFY] 收到空数据')
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', '')
total_amount = params.get('total_amount', '')
logger.info(
'[ALIPAY_CZJILU_NOTIFY] out_trade_no=%s, trade_no=%s, trade_status=%s, total_amount=%s',
out_trade_no, trade_no, trade_status, total_amount,
)
# 验签
from jituan.services.alipay_pay import verify_notify_params, club_id_from_czjilu
notify_club_id = club_id_from_czjilu(out_trade_no)
if not verify_notify_params(params, notify_club_id):
logger.warning('[ALIPAY_CZJILU_NOTIFY] 验签失败: out_trade_no=%s', out_trade_no)
return HttpResponse('fail', content_type='text/plain')
# 仅处理交易成功状态
if trade_status not in ('TRADE_SUCCESS', 'TRADE_FINISHED'):
logger.info('[ALIPAY_CZJILU_NOTIFY] 非成功状态 %s: %s', trade_status, out_trade_no)
return HttpResponse('success', content_type='text/plain')
# 查询充值单
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 == CZJILU_PAID_STATUS:
logger.info('[ALIPAY_CZJILU_NOTIFY] 订单已支付,幂等返回: %s', out_trade_no)
return HttpResponse('success', content_type='text/plain')
# 待付(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')
# 终态异常(如会员履约失败 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)