小程序前端仍传 wechat;后台可切换 wechat/fubei,支持配置多套付呗并退款按原通道路由。 Co-authored-by: Cursor <cursoragent@cursor.com>
159 lines
6.3 KiB
Python
159 lines
6.3 KiB
Python
"""付呗支付异步回调:验签后按订单/充值单履约(前端无感知)。"""
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import logging
|
||
import traceback
|
||
|
||
from django.http import HttpResponse
|
||
from django.views import View
|
||
|
||
from jituan.constants import FUBEI_BIZ_CZJILU, FUBEI_BIZ_ORDER
|
||
from jituan.services.club_payment_channel import get_fubei_config
|
||
from jituan.services import fubei_client
|
||
from jituan.services.mini_pay_router import get_pay_ledger
|
||
from jituan.services.wechat_pay import club_id_from_czjilu, club_id_from_order
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class FubeiPayNotifyView(View):
|
||
"""
|
||
付呗异步通知。
|
||
GET:回调 URL 有效性验证,必须返回小写 success。
|
||
POST:支付成功通知。
|
||
路径:/dingdan/fubei-notify/
|
||
"""
|
||
|
||
def get(self, request):
|
||
return HttpResponse('success', content_type='text/plain')
|
||
|
||
def post(self, request):
|
||
try:
|
||
post = request.POST.dict() if request.POST else {}
|
||
if not post and request.body:
|
||
# 偶发 JSON
|
||
try:
|
||
post = json.loads(request.body.decode('utf-8'))
|
||
except Exception:
|
||
post = {}
|
||
|
||
params, data = fubei_client.parse_callback_form(post)
|
||
merchant_order_sn = str(data.get('merchant_order_sn') or '').strip()
|
||
result_code = str(params.get('result_code') or '')
|
||
|
||
logger.info(
|
||
'[FUBEI_NOTIFY] result_code=%s merchant_order_sn=%s trade_no=%s',
|
||
result_code, merchant_order_sn, data.get('trade_no') or data.get('platform_order_no'),
|
||
)
|
||
|
||
if not merchant_order_sn:
|
||
logger.warning('[FUBEI_NOTIFY] 缺少 merchant_order_sn')
|
||
return HttpResponse('fail', content_type='text/plain')
|
||
|
||
club_id = self._resolve_club_id(merchant_order_sn, data)
|
||
cfg = get_fubei_config(club_id)
|
||
if not cfg or not cfg.api_key:
|
||
logger.error('[FUBEI_NOTIFY] 无付呗密钥 club=%s order=%s', club_id, merchant_order_sn)
|
||
return HttpResponse('fail', content_type='text/plain')
|
||
|
||
if not fubei_client.verify_callback_sign(params, cfg.api_key):
|
||
logger.error('[FUBEI_NOTIFY] 验签失败 order=%s', merchant_order_sn)
|
||
return HttpResponse('fail', content_type='text/plain')
|
||
|
||
if result_code not in ('200', '200.0'):
|
||
logger.info('[FUBEI_NOTIFY] 非成功 result_code=%s order=%s', result_code, merchant_order_sn)
|
||
return HttpResponse('success', content_type='text/plain')
|
||
|
||
transaction_id = str(
|
||
data.get('trade_no')
|
||
or data.get('platform_order_no')
|
||
or data.get('order_sn')
|
||
or ''
|
||
)
|
||
total_fee = data.get('total_fee') or data.get('cash_fee') or data.get('order_price')
|
||
attach = str(data.get('attach') or '')
|
||
biz = self._biz_type(merchant_order_sn, attach)
|
||
|
||
if biz == FUBEI_BIZ_ORDER:
|
||
ok = self._fulfill_order(merchant_order_sn, transaction_id)
|
||
else:
|
||
ok = self._fulfill_czjilu(merchant_order_sn, total_fee, transaction_id)
|
||
|
||
return HttpResponse('success' if ok else 'fail', content_type='text/plain')
|
||
except Exception as exc:
|
||
logger.error('[FUBEI_NOTIFY] 异常: %s', exc)
|
||
logger.error(traceback.format_exc())
|
||
return HttpResponse('fail', content_type='text/plain')
|
||
|
||
def _resolve_club_id(self, merchant_order_sn: str, data: dict) -> str:
|
||
ledger = get_pay_ledger(merchant_order_sn)
|
||
if ledger and ledger.club_id:
|
||
return ledger.club_id
|
||
# 先试订单,再试充值单
|
||
try:
|
||
cid = club_id_from_order(merchant_order_sn)
|
||
if cid:
|
||
return cid
|
||
except Exception:
|
||
pass
|
||
try:
|
||
cid = club_id_from_czjilu(merchant_order_sn)
|
||
if cid:
|
||
return cid
|
||
except Exception:
|
||
pass
|
||
return 'xq'
|
||
|
||
def _biz_type(self, merchant_order_sn: str, attach: str) -> str:
|
||
ledger = get_pay_ledger(merchant_order_sn)
|
||
if ledger and ledger.biz_type:
|
||
return ledger.biz_type
|
||
if 'biz=order' in attach:
|
||
return FUBEI_BIZ_ORDER
|
||
if 'biz=czjilu' in attach:
|
||
return FUBEI_BIZ_CZJILU
|
||
# 启发式:点单 Order 表优先
|
||
try:
|
||
from orders.models import Order
|
||
if Order.query.filter(OrderID=merchant_order_sn).exists():
|
||
return FUBEI_BIZ_ORDER
|
||
except Exception:
|
||
pass
|
||
return FUBEI_BIZ_CZJILU
|
||
|
||
def _fulfill_order(self, out_trade_no: str, transaction_id: str) -> bool:
|
||
try:
|
||
from orders.views.payment import WechatPayNotifyView
|
||
view = WechatPayNotifyView()
|
||
view._handle_success(out_trade_no, transaction_id, None)
|
||
return True
|
||
except Exception as exc:
|
||
logger.error('[FUBEI_NOTIFY] 订单履约失败 %s: %s', out_trade_no, exc, exc_info=True)
|
||
return False
|
||
|
||
def _fulfill_czjilu(self, out_trade_no: str, total_fee, transaction_id: str) -> bool:
|
||
from products.models import Czjilu
|
||
from products.czjilu_types import (
|
||
CZJILU_PAID_STATUS,
|
||
is_czjilu_fulfillable_status,
|
||
)
|
||
from products.views.alipay_czjilu_notify import _fulfill_czjilu_by_leixing
|
||
|
||
order = Czjilu.query.filter(dingdan_id=out_trade_no).first()
|
||
if not order:
|
||
logger.error('[FUBEI_NOTIFY] 充值单不存在: %s', out_trade_no)
|
||
return False
|
||
if order.zhuangtai == CZJILU_PAID_STATUS:
|
||
return True
|
||
if not is_czjilu_fulfillable_status(order.zhuangtai):
|
||
logger.warning('[FUBEI_NOTIFY] 充值单终态不再履约 %s zhuangtai=%s', out_trade_no, order.zhuangtai)
|
||
return True
|
||
paid_amount = total_fee if total_fee not in (None, '') else order.jine
|
||
try:
|
||
_fulfill_czjilu_by_leixing(out_trade_no, order.leixing, paid_amount, source='fubei_callback')
|
||
return True
|
||
except Exception as exc:
|
||
logger.error('[FUBEI_NOTIFY] 充值履约失败 %s: %s', out_trade_no, exc, exc_info=True)
|
||
return False
|