feat: 分俱乐部付呗收款通道,默认仍走微信直连

小程序前端仍传 wechat;后台可切换 wechat/fubei,支持配置多套付呗并退款按原通道路由。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-07-21 20:02:24 +08:00
parent f36db77f6b
commit 5762f00d40
20 changed files with 1105 additions and 0 deletions

View File

@@ -0,0 +1,158 @@
"""付呗支付异步回调:验签后按订单/充值单履约(前端无感知)。"""
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