新增转盘活动/奖品/机会表与 Czjilu leixing=7 履约;专区 tupian_url;hqsp/详情下发 shi_zhuanpan。普通点单与抢单主链路保持不变。 Co-authored-by: Cursor <cursoragent@cursor.com>
173 lines
8.2 KiB
Python
173 lines
8.2 KiB
Python
"""支付宝 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_LEIXING_ZHUANPAN,
|
||
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)
|
||
if leixing == CZJILU_LEIXING_ZHUANPAN:
|
||
from jituan.services.czjilu_fulfill import fulfill_zhuanpan
|
||
return fulfill_zhuanpan(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)
|