feat: 分俱乐部付呗收款通道,默认仍走微信直连
小程序前端仍传 wechat;后台可切换 wechat/fubei,支持配置多套付呗并退款按原通道路由。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -28,3 +28,37 @@ ADMIN_SCOPE_CLUB = 'CLUB'
|
||||
|
||||
# 集团最高权限账号(写死超级管理员,拥有全部功能权限与集团视图)
|
||||
SUPER_ADMIN_PHONES = frozenset({'1383714110'})
|
||||
|
||||
# —— 支付通道 ——
|
||||
PAY_CHANNEL_WECHAT_JSAPI = 'wechat_jsapi'
|
||||
PAY_CHANNEL_WECHAT_V3 = 'wechat_v3'
|
||||
PAY_CHANNEL_ALIPAY_WAP = 'alipay_wap'
|
||||
PAY_CHANNEL_FUBEI_WX_MINI = 'fubei_wx_mini'
|
||||
|
||||
# 俱乐部「小程序收款」选用哪条通道(前端仍传 pay_method=wechat,后端按此路由)
|
||||
MINI_PAY_CHANNEL_WECHAT = 'wechat'
|
||||
MINI_PAY_CHANNEL_FUBEI = 'fubei'
|
||||
MINI_PAY_CHANNEL_CHOICES = (MINI_PAY_CHANNEL_WECHAT, MINI_PAY_CHANNEL_FUBEI)
|
||||
|
||||
PAY_METHOD_WECHAT = 'wechat'
|
||||
PAY_METHOD_ALIPAY = 'alipay'
|
||||
PAY_METHOD_FUBEI = 'fubei'
|
||||
|
||||
PAY_METHOD_LABELS = {
|
||||
PAY_METHOD_WECHAT: '微信支付',
|
||||
PAY_METHOD_ALIPAY: '支付宝',
|
||||
PAY_METHOD_FUBEI: '微信支付', # 前端不感知,展示仍为微信
|
||||
}
|
||||
|
||||
PAY_METHOD_TO_CHANNEL = {
|
||||
PAY_METHOD_WECHAT: PAY_CHANNEL_WECHAT_JSAPI,
|
||||
PAY_METHOD_ALIPAY: PAY_CHANNEL_ALIPAY_WAP,
|
||||
PAY_METHOD_FUBEI: PAY_CHANNEL_FUBEI_WX_MINI,
|
||||
}
|
||||
|
||||
FUBEI_GATEWAY_PROD = 'https://shq-api.51fubei.com/gateway'
|
||||
FUBEI_GATEWAY_SANDBOX = 'https://shq-api.51fubei.com/gateway' # 付呗文档未单独给沙箱网关时同正式
|
||||
|
||||
# 付呗下单 biz 标记(写入 attach / 流水表,回调与退款用)
|
||||
FUBEI_BIZ_ORDER = 'order'
|
||||
FUBEI_BIZ_CZJILU = 'czjilu'
|
||||
|
||||
41
jituan/migrations/0014_club_mini_pay_channel_and_ledger.py
Normal file
41
jituan/migrations/0014_club_mini_pay_channel_and_ledger.py
Normal file
@@ -0,0 +1,41 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('jituan', '0013_club_pay_withdraw_split'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='club',
|
||||
name='mini_pay_channel',
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
default='wechat',
|
||||
help_text='wechat=微信直连;fubei=付呗;默认 wechat,不影响现网',
|
||||
max_length=16,
|
||||
verbose_name='小程序收款通道',
|
||||
),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PayChannelLedger',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('out_trade_no', models.CharField(db_index=True, max_length=64, unique=True, verbose_name='商户订单号')),
|
||||
('club_id', models.CharField(db_index=True, default='', max_length=16)),
|
||||
('channel', models.CharField(db_index=True, max_length=32, verbose_name='通道')),
|
||||
('channel_config_id', models.IntegerField(blank=True, null=True)),
|
||||
('biz_type', models.CharField(blank=True, default='', max_length=16, verbose_name='order/czjilu')),
|
||||
('provider_order_sn', models.CharField(blank=True, default='', max_length=64, verbose_name='通道侧订单号')),
|
||||
('amount_yuan', models.DecimalField(decimal_places=2, default=0, max_digits=12)),
|
||||
('CreateTime', models.DateTimeField(auto_now_add=True)),
|
||||
('UpdateTime', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '支付通道流水',
|
||||
'db_table': 'pay_channel_ledger',
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -52,6 +52,14 @@ class Club(QModel):
|
||||
goeasy_appkey = models.CharField(max_length=64, blank=True, default='')
|
||||
goeasy_secret = models.CharField(max_length=128, blank=True, default='')
|
||||
config_json = models.JSONField(default=dict, blank=True)
|
||||
# 小程序「微信支付」入口实际走哪条通道:wechat=直连商户号;fubei=付呗(前端仍传 wechat)
|
||||
mini_pay_channel = models.CharField(
|
||||
max_length=16,
|
||||
blank=True,
|
||||
default='wechat',
|
||||
verbose_name='小程序收款通道',
|
||||
help_text='wechat=微信直连;fubei=付呗;默认 wechat,不影响现网',
|
||||
)
|
||||
company_uuid = models.BinaryField(max_length=16, null=True, blank=True)
|
||||
sort_order = models.IntegerField(default=0)
|
||||
CreateTime = models.DateTimeField(auto_now_add=True)
|
||||
@@ -62,6 +70,70 @@ class Club(QModel):
|
||||
verbose_name = '俱乐部'
|
||||
|
||||
|
||||
class ClubPaymentChannel(QModel):
|
||||
"""俱乐部支付通道配置(可多条;同 channel 用 is_default 选主配置)。"""
|
||||
CHANNEL_CHOICES = (
|
||||
('wechat_jsapi', '微信直连-小程序JSAPI'),
|
||||
('wechat_v3', '微信直连-V3转账/退款'),
|
||||
('alipay_wap', '支付宝-手机网站'),
|
||||
('fubei_wx_mini', '付呗-微信小程序'),
|
||||
)
|
||||
|
||||
club_id = models.CharField(max_length=16, db_index=True, verbose_name='俱乐部ID')
|
||||
channel = models.CharField(max_length=32, choices=CHANNEL_CHOICES, db_index=True)
|
||||
name = models.CharField(max_length=64, verbose_name='配置名称', help_text='如:主付呗、备用微信')
|
||||
enabled = models.IntegerField(default=1, verbose_name='1启用0停用')
|
||||
is_default = models.BooleanField(default=True, verbose_name='同通道默认配置')
|
||||
sort_order = models.IntegerField(default=0, verbose_name='前端展示顺序')
|
||||
sandbox = models.BooleanField(default=False, verbose_name='沙箱/联调')
|
||||
app_id = models.CharField(max_length=64, blank=True, default='', verbose_name='AppID/付呗ID')
|
||||
mch_id = models.CharField(max_length=64, blank=True, default='', verbose_name='商户号')
|
||||
api_key_enc = models.TextField(blank=True, default='', verbose_name='API密钥/付呗Secret(加密)')
|
||||
private_key_enc = models.TextField(blank=True, default='', verbose_name='应用私钥(加密)')
|
||||
public_key_enc = models.TextField(blank=True, default='', verbose_name='平台公钥(加密或明文)')
|
||||
cert_serial_no = models.CharField(max_length=64, blank=True, default='')
|
||||
private_key_path = models.CharField(max_length=255, blank=True, default='')
|
||||
platform_cert_dir = models.CharField(max_length=255, blank=True, default='')
|
||||
notify_url = models.CharField(max_length=512, blank=True, default='', verbose_name='异步回调URL')
|
||||
return_url = models.CharField(max_length=512, blank=True, default='', verbose_name='同步返回URL')
|
||||
gateway_url = models.CharField(max_length=255, blank=True, default='', verbose_name='API网关')
|
||||
mini_app_id = models.CharField(
|
||||
max_length=32, blank=True, default='',
|
||||
verbose_name='支付用小程序AppID',
|
||||
help_text='付呗/微信JSAPI;空则回落 club.wx_appid',
|
||||
)
|
||||
extra_json = models.JSONField(blank=True, default=dict, verbose_name='扩展配置')
|
||||
remark = models.CharField(max_length=255, blank=True, default='')
|
||||
CreateTime = models.DateTimeField(auto_now_add=True)
|
||||
UpdateTime = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'club_payment_channel'
|
||||
verbose_name = '俱乐部支付通道'
|
||||
unique_together = [['club_id', 'channel', 'name']]
|
||||
indexes = [
|
||||
models.Index(fields=['club_id', 'channel', 'enabled'], name='idx_cpc_club_ch_en'),
|
||||
models.Index(fields=['club_id', 'is_default'], name='idx_cpc_club_default'),
|
||||
]
|
||||
|
||||
|
||||
class PayChannelLedger(QModel):
|
||||
"""下单时记录实际支付通道,供回调路由与退款选用(前端无感知)。"""
|
||||
out_trade_no = models.CharField(max_length=64, unique=True, db_index=True, verbose_name='商户订单号')
|
||||
club_id = models.CharField(max_length=16, db_index=True, default='')
|
||||
channel = models.CharField(max_length=32, db_index=True, verbose_name='通道')
|
||||
channel_config_id = models.IntegerField(null=True, blank=True)
|
||||
biz_type = models.CharField(max_length=16, blank=True, default='', verbose_name='order/czjilu')
|
||||
provider_order_sn = models.CharField(max_length=64, blank=True, default='', verbose_name='通道侧订单号')
|
||||
amount_yuan = models.DecimalField(max_digits=12, decimal_places=2, default=0)
|
||||
CreateTime = models.DateTimeField(auto_now_add=True)
|
||||
UpdateTime = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'pay_channel_ledger'
|
||||
verbose_name = '支付通道流水'
|
||||
|
||||
|
||||
class UserWxOpenid(QModel):
|
||||
"""微信 openid 与俱乐部用户绑定(club_id + openid 唯一)。"""
|
||||
club_id = models.CharField(max_length=16, db_index=True)
|
||||
|
||||
201
jituan/services/fubei_client.py
Normal file
201
jituan/services/fubei_client.py
Normal file
@@ -0,0 +1,201 @@
|
||||
"""付呗开放平台客户端:签名、小程序下单、退款、回调验签。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import random
|
||||
import string
|
||||
import time
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
from typing import Any, Dict, Optional, Tuple
|
||||
|
||||
import requests
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
METHOD_MINA = 'openapi.payment.order.mina'
|
||||
METHOD_REFUND = 'openapi.payment.order.refund'
|
||||
|
||||
|
||||
def _yuan_str(amount) -> str:
|
||||
d = Decimal(str(amount)).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
|
||||
return format(d, 'f')
|
||||
|
||||
|
||||
def build_sign(params: Dict[str, Any], app_secret: str) -> str:
|
||||
"""付呗签名:参数 ASCII 排序后 key=value&... 再无缝拼接 secret,MD5 大写。"""
|
||||
items = []
|
||||
for k in sorted(params.keys()):
|
||||
if k == 'sign':
|
||||
continue
|
||||
v = params[k]
|
||||
if v is None or v == '':
|
||||
continue
|
||||
items.append(f'{k}={v}')
|
||||
raw = '&'.join(items) + (app_secret or '')
|
||||
return hashlib.md5(raw.encode('utf-8')).hexdigest().upper()
|
||||
|
||||
|
||||
def verify_callback_sign(params: Dict[str, Any], app_secret: str) -> bool:
|
||||
got = (params.get('sign') or '').strip().upper()
|
||||
if not got:
|
||||
return False
|
||||
expect = build_sign(params, app_secret)
|
||||
return got == expect
|
||||
|
||||
|
||||
def _nonce(n: int = 24) -> str:
|
||||
return ''.join(random.choices(string.ascii_letters + string.digits, k=n))
|
||||
|
||||
|
||||
def call_fubei_api(
|
||||
*,
|
||||
gateway_url: str,
|
||||
app_id: str,
|
||||
app_secret: str,
|
||||
method: str,
|
||||
biz: Dict[str, Any],
|
||||
timeout: int = 15,
|
||||
) -> Dict[str, Any]:
|
||||
"""调用付呗 gateway,成功返回 data 字典;失败抛 Exception。"""
|
||||
biz_content = json.dumps(biz, ensure_ascii=False, separators=(',', ':'))
|
||||
payload = {
|
||||
'app_id': app_id,
|
||||
'method': method,
|
||||
'format': 'json',
|
||||
'sign_method': 'md5',
|
||||
'nonce': _nonce(),
|
||||
'version': '1.0',
|
||||
'biz_content': biz_content,
|
||||
}
|
||||
payload['sign'] = build_sign(payload, app_secret)
|
||||
|
||||
url = (gateway_url or '').strip() or 'https://shq-api.51fubei.com/gateway'
|
||||
logger.info('[FUBEI] method=%s app_id=%s biz_keys=%s', method, app_id, list(biz.keys()))
|
||||
resp = requests.post(url, json=payload, timeout=timeout, headers={'Content-Type': 'application/json; charset=utf-8'})
|
||||
try:
|
||||
body = resp.json()
|
||||
except Exception:
|
||||
logger.error('[FUBEI] 非JSON响应 status=%s body=%s', resp.status_code, resp.text[:500])
|
||||
raise Exception(f'付呗接口异常: HTTP {resp.status_code}')
|
||||
|
||||
result_code = body.get('result_code')
|
||||
if str(result_code) not in ('200', '200.0'):
|
||||
msg = body.get('result_message') or body.get('sub_code') or '付呗下单失败'
|
||||
logger.warning('[FUBEI] 业务失败 method=%s code=%s msg=%s body=%s', method, result_code, msg, body)
|
||||
raise Exception(str(msg))
|
||||
|
||||
data = body.get('data')
|
||||
if isinstance(data, str):
|
||||
try:
|
||||
data = json.loads(data)
|
||||
except Exception:
|
||||
pass
|
||||
if not isinstance(data, dict):
|
||||
raise Exception('付呗返回 data 格式异常')
|
||||
return data
|
||||
|
||||
|
||||
def mina_prepay(
|
||||
*,
|
||||
gateway_url: str,
|
||||
app_id: str,
|
||||
app_secret: str,
|
||||
merchant_order_sn: str,
|
||||
sub_openid: str,
|
||||
total_fee,
|
||||
store_id: int,
|
||||
call_back_url: str,
|
||||
body: str = '',
|
||||
attach: str = '',
|
||||
) -> Dict[str, Any]:
|
||||
"""小程序支付下单,返回含 sign_params / order_sn 等。"""
|
||||
biz = {
|
||||
'merchant_order_sn': merchant_order_sn,
|
||||
'sub_openid': sub_openid,
|
||||
'total_fee': float(_yuan_str(total_fee)),
|
||||
'store_id': int(store_id),
|
||||
}
|
||||
if call_back_url:
|
||||
biz['call_back_url'] = call_back_url
|
||||
if body:
|
||||
biz['body'] = (body or '')[:128]
|
||||
if attach:
|
||||
biz['attach'] = (attach or '')[:127]
|
||||
|
||||
return call_fubei_api(
|
||||
gateway_url=gateway_url,
|
||||
app_id=app_id,
|
||||
app_secret=app_secret,
|
||||
method=METHOD_MINA,
|
||||
biz=biz,
|
||||
)
|
||||
|
||||
|
||||
def refund_order(
|
||||
*,
|
||||
gateway_url: str,
|
||||
app_id: str,
|
||||
app_secret: str,
|
||||
merchant_order_sn: str,
|
||||
merchant_refund_sn: str,
|
||||
refund_money=None,
|
||||
order_sn: str = '',
|
||||
) -> Dict[str, Any]:
|
||||
biz: Dict[str, Any] = {
|
||||
'merchant_refund_sn': merchant_refund_sn,
|
||||
}
|
||||
if merchant_order_sn:
|
||||
biz['merchant_order_sn'] = merchant_order_sn
|
||||
if order_sn:
|
||||
biz['order_sn'] = order_sn
|
||||
if refund_money is not None:
|
||||
biz['refund_money'] = float(_yuan_str(refund_money))
|
||||
|
||||
return call_fubei_api(
|
||||
gateway_url=gateway_url,
|
||||
app_id=app_id,
|
||||
app_secret=app_secret,
|
||||
method=METHOD_REFUND,
|
||||
biz=biz,
|
||||
)
|
||||
|
||||
|
||||
def parse_callback_form(post_dict: Dict[str, Any]) -> Tuple[Dict[str, Any], Dict[str, Any]]:
|
||||
"""解析付呗 form 回调,返回 (顶层参数含 sign, data 业务字典)。"""
|
||||
params = {k: (v if not isinstance(v, list) else (v[0] if v else '')) for k, v in post_dict.items()}
|
||||
# 兼容 QueryDict.dict()
|
||||
data_raw = params.get('data') or ''
|
||||
data: Dict[str, Any] = {}
|
||||
if isinstance(data_raw, dict):
|
||||
data = data_raw
|
||||
elif isinstance(data_raw, str) and data_raw.strip():
|
||||
try:
|
||||
data = json.loads(data_raw)
|
||||
except Exception:
|
||||
# 偶发双重编码
|
||||
try:
|
||||
data = json.loads(data_raw.replace("'", '"'))
|
||||
except Exception:
|
||||
logger.error('[FUBEI_NOTIFY] data 解析失败: %s', data_raw[:300])
|
||||
data = {}
|
||||
return params, data
|
||||
|
||||
|
||||
def extract_jsapi_pay_params(sign_params: Dict[str, Any]) -> Dict[str, str]:
|
||||
"""从付呗 sign_params 抽出小程序 wx.requestPayment 所需字段。"""
|
||||
if not sign_params:
|
||||
raise Exception('付呗未返回支付签名包')
|
||||
# 部分字段可能混有 store_id 等,只取调起支付字段
|
||||
out = {
|
||||
'appId': str(sign_params.get('appId') or sign_params.get('appid') or ''),
|
||||
'timeStamp': str(sign_params.get('timeStamp') or sign_params.get('timestamp') or ''),
|
||||
'nonceStr': str(sign_params.get('nonceStr') or sign_params.get('noncestr') or ''),
|
||||
'package': str(sign_params.get('package') or ''),
|
||||
'signType': str(sign_params.get('signType') or sign_params.get('sign_type') or 'MD5'),
|
||||
'paySign': str(sign_params.get('paySign') or sign_params.get('pay_sign') or ''),
|
||||
}
|
||||
if not out['timeStamp'] or not out['package'] or not out['paySign']:
|
||||
raise Exception('付呗签名包字段不完整')
|
||||
return out
|
||||
158
jituan/services/fubei_notify.py
Normal file
158
jituan/services/fubei_notify.py
Normal 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
|
||||
152
jituan/services/mini_pay_router.py
Normal file
152
jituan/services/mini_pay_router.py
Normal file
@@ -0,0 +1,152 @@
|
||||
"""小程序「微信支付」通道路由:按俱乐部选择微信直连或付呗;前端仍传 pay_method=wechat。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from decimal import Decimal
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from jituan.constants import (
|
||||
CLUB_ID_DEFAULT,
|
||||
FUBEI_BIZ_CZJILU,
|
||||
FUBEI_BIZ_ORDER,
|
||||
MINI_PAY_CHANNEL_FUBEI,
|
||||
MINI_PAY_CHANNEL_WECHAT,
|
||||
PAY_CHANNEL_FUBEI_WX_MINI,
|
||||
)
|
||||
from jituan.models import Club, PayChannelLedger
|
||||
from jituan.services.club_payment_channel import get_fubei_config, invalidate_club_payment_cache
|
||||
from jituan.services import fubei_client
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_club_mini_pay_channel(club_id: Optional[str] = None) -> str:
|
||||
"""返回 wechat / fubei;缺省 wechat(现网行为)。"""
|
||||
cid = (club_id or '').strip() or CLUB_ID_DEFAULT
|
||||
club = Club.query.filter(club_id=cid).first()
|
||||
if not club:
|
||||
return MINI_PAY_CHANNEL_WECHAT
|
||||
ch = (getattr(club, 'mini_pay_channel', None) or MINI_PAY_CHANNEL_WECHAT).strip().lower()
|
||||
if ch == MINI_PAY_CHANNEL_FUBEI:
|
||||
return MINI_PAY_CHANNEL_FUBEI
|
||||
return MINI_PAY_CHANNEL_WECHAT
|
||||
|
||||
|
||||
def set_club_mini_pay_channel(club_id: str, channel: str) -> None:
|
||||
cid = (club_id or '').strip() or CLUB_ID_DEFAULT
|
||||
ch = (channel or '').strip().lower()
|
||||
if ch not in (MINI_PAY_CHANNEL_WECHAT, MINI_PAY_CHANNEL_FUBEI):
|
||||
raise ValueError('mini_pay_channel 仅支持 wechat / fubei')
|
||||
club = Club.query.filter(club_id=cid).first()
|
||||
if not club:
|
||||
raise ValueError(f'俱乐部 {cid} 不存在')
|
||||
club.mini_pay_channel = ch
|
||||
club.save(update_fields=['mini_pay_channel'])
|
||||
invalidate_club_payment_cache(cid)
|
||||
|
||||
|
||||
def _default_fubei_notify_url(club_id: Optional[str] = None) -> str:
|
||||
base = 'https://www.abas.asia'
|
||||
club = Club.query.filter(club_id=(club_id or CLUB_ID_DEFAULT)).first()
|
||||
if club and (club.h5_domain or '').strip():
|
||||
base = club.h5_domain.strip().rstrip('/')
|
||||
return f'{base}/hqhd/dingdan/fubei-notify/'
|
||||
|
||||
|
||||
def _remember_ledger(
|
||||
*,
|
||||
out_trade_no: str,
|
||||
club_id: str,
|
||||
channel: str,
|
||||
channel_config_id: Optional[int],
|
||||
biz_type: str,
|
||||
provider_order_sn: str,
|
||||
amount_yuan,
|
||||
) -> None:
|
||||
row = PayChannelLedger.query.filter(out_trade_no=out_trade_no).first()
|
||||
fields = {
|
||||
'club_id': club_id or CLUB_ID_DEFAULT,
|
||||
'channel': channel,
|
||||
'channel_config_id': channel_config_id,
|
||||
'biz_type': biz_type or '',
|
||||
'provider_order_sn': provider_order_sn or '',
|
||||
'amount_yuan': Decimal(str(amount_yuan or 0)),
|
||||
}
|
||||
if row:
|
||||
for k, v in fields.items():
|
||||
setattr(row, k, v)
|
||||
row.save()
|
||||
else:
|
||||
PayChannelLedger.objects.create(out_trade_no=out_trade_no, **fields)
|
||||
|
||||
|
||||
def get_pay_ledger(out_trade_no: str) -> Optional[PayChannelLedger]:
|
||||
if not out_trade_no:
|
||||
return None
|
||||
return PayChannelLedger.query.filter(out_trade_no=out_trade_no).first()
|
||||
|
||||
|
||||
def maybe_create_fubei_jsapi_params(
|
||||
*,
|
||||
club_id: Optional[str],
|
||||
out_trade_no: str,
|
||||
amount_yuan,
|
||||
body: str,
|
||||
openid: str,
|
||||
notify_url: str = '',
|
||||
biz_type: str = FUBEI_BIZ_ORDER,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""若俱乐部选用付呗则下单并返回 wx.requestPayment 参数;否则返回 None(走原微信逻辑)。"""
|
||||
cid = (club_id or '').strip() or CLUB_ID_DEFAULT
|
||||
if get_club_mini_pay_channel(cid) != MINI_PAY_CHANNEL_FUBEI:
|
||||
return None
|
||||
|
||||
cfg = get_fubei_config(cid)
|
||||
if not cfg or not cfg.app_id or not cfg.api_key:
|
||||
raise Exception('俱乐部已切换付呗收款,但付呗开放平台 ID/密钥未配置完整')
|
||||
store_id = cfg.store_id
|
||||
if store_id in (None, '', 0, '0'):
|
||||
raise Exception('俱乐部已切换付呗收款,但门店 store_id 未配置')
|
||||
if not openid:
|
||||
raise Exception('付呗支付缺少用户 openid')
|
||||
|
||||
cb = (notify_url or '').strip() or (cfg.notify_url or '').strip() or _default_fubei_notify_url(cid)
|
||||
# 强制走付呗专用回调(原微信 XML 回调无法解析付呗 form)
|
||||
if 'fubei-notify' not in cb:
|
||||
cb = _default_fubei_notify_url(cid)
|
||||
|
||||
attach = f'biz={biz_type or FUBEI_BIZ_ORDER}'
|
||||
data = fubei_client.mina_prepay(
|
||||
gateway_url=cfg.gateway_url,
|
||||
app_id=cfg.app_id,
|
||||
app_secret=cfg.api_key,
|
||||
merchant_order_sn=out_trade_no,
|
||||
sub_openid=openid,
|
||||
total_fee=amount_yuan,
|
||||
store_id=int(store_id),
|
||||
call_back_url=cb,
|
||||
body=body or '',
|
||||
attach=attach,
|
||||
)
|
||||
sign_params = data.get('sign_params') or {}
|
||||
if isinstance(sign_params, str):
|
||||
import json
|
||||
sign_params = json.loads(sign_params)
|
||||
pay_params = fubei_client.extract_jsapi_pay_params(sign_params)
|
||||
|
||||
_remember_ledger(
|
||||
out_trade_no=out_trade_no,
|
||||
club_id=cid,
|
||||
channel=PAY_CHANNEL_FUBEI_WX_MINI,
|
||||
channel_config_id=cfg.config_id,
|
||||
biz_type=biz_type or FUBEI_BIZ_ORDER,
|
||||
provider_order_sn=str(data.get('order_sn') or ''),
|
||||
amount_yuan=amount_yuan,
|
||||
)
|
||||
logger.info(
|
||||
'[FUBEI_PAY] club=%s out_trade_no=%s biz=%s order_sn=%s',
|
||||
cid, out_trade_no, biz_type, data.get('order_sn'),
|
||||
)
|
||||
return pay_params
|
||||
62
jituan/services/pay_refund_router.py
Normal file
62
jituan/services/pay_refund_router.py
Normal file
@@ -0,0 +1,62 @@
|
||||
"""按支付流水选择退款通道:付呗单走付呗,其余走原微信退款逻辑。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import random
|
||||
import string
|
||||
import time
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from jituan.constants import PAY_CHANNEL_FUBEI_WX_MINI
|
||||
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
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _refund_sn(out_trade_no: str) -> str:
|
||||
ts = str(int(time.time()))
|
||||
rand = ''.join(random.choices('0123456789', k=4))
|
||||
# 付呗 merchant_refund_sn 最长 32
|
||||
base = f'{out_trade_no}R{ts}{rand}'
|
||||
return base[:32]
|
||||
|
||||
|
||||
def try_fubei_refund(
|
||||
*,
|
||||
out_trade_no: str,
|
||||
amount_yuan,
|
||||
club_id: Optional[str] = None,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
若该单曾走付呗,则发起付呗退款并返回 {'code': 0/非0, 'msg': ..., 'refund_id': ...};
|
||||
非付呗单返回 None,由调用方继续走原微信退款。
|
||||
"""
|
||||
ledger = get_pay_ledger(out_trade_no)
|
||||
if not ledger or ledger.channel != PAY_CHANNEL_FUBEI_WX_MINI:
|
||||
return None
|
||||
|
||||
cid = (club_id or ledger.club_id or '').strip()
|
||||
cfg = get_fubei_config(cid)
|
||||
if not cfg or not cfg.app_id or not cfg.api_key:
|
||||
logger.error('[FUBEI_REFUND] 配置缺失 club=%s out_trade_no=%s', cid, out_trade_no)
|
||||
return {'code': 500, 'msg': '付呗退款配置缺失,请检查俱乐部付呗通道'}
|
||||
|
||||
merchant_refund_sn = _refund_sn(out_trade_no)
|
||||
try:
|
||||
data = fubei_client.refund_order(
|
||||
gateway_url=cfg.gateway_url,
|
||||
app_id=cfg.app_id,
|
||||
app_secret=cfg.api_key,
|
||||
merchant_order_sn=out_trade_no,
|
||||
merchant_refund_sn=merchant_refund_sn,
|
||||
refund_money=amount_yuan,
|
||||
order_sn=ledger.provider_order_sn or '',
|
||||
)
|
||||
refund_id = data.get('refund_sn') or merchant_refund_sn
|
||||
logger.info('[FUBEI_REFUND] ok out_trade_no=%s refund=%s', out_trade_no, refund_id)
|
||||
return {'code': 0, 'msg': 'success', 'refund_id': refund_id, 'raw': data}
|
||||
except Exception as exc:
|
||||
logger.error('[FUBEI_REFUND] fail out_trade_no=%s: %s', out_trade_no, exc, exc_info=True)
|
||||
return {'code': 400, 'msg': str(exc)}
|
||||
@@ -86,6 +86,7 @@ from jituan.views_club_catalog import (
|
||||
ClubShangpinLeixingListView,
|
||||
)
|
||||
from jituan.views_shenhe_manage import ClubKhgglAddView
|
||||
from jituan.views_payment_channel import ClubPaymentChannelManageView
|
||||
from jituan.views_identity_tag import (
|
||||
IdentityTagBindListView,
|
||||
IdentityTagBindView,
|
||||
@@ -103,6 +104,7 @@ urlpatterns = [
|
||||
path('auth/perm-debug', ClubPermDebugView.as_view(), name='jituan_perm_debug'),
|
||||
path('auth/dashou-register', ClubDashouRegisterView.as_view(), name='jituan_dashou_register'),
|
||||
path('houtai/club-manage', ClubManageView.as_view(), name='jituan_club_manage'),
|
||||
path('houtai/payment-channel-manage', ClubPaymentChannelManageView.as_view(), name='jituan_payment_channel_manage'),
|
||||
path('houtai/club-cert-upload', ClubCertUploadView.as_view(), name='jituan_club_cert_upload'),
|
||||
path('houtai/admin-assignments', ClubAdminAssignmentListView.as_view(), name='jituan_admin_assignments'),
|
||||
path('houtai/caiwu', ClubCaiwuView.as_view(), name='jituan_caiwu'),
|
||||
|
||||
@@ -453,6 +453,8 @@ class ClubManageView(APIView):
|
||||
'name', 'status', 'wx_appid', 'wx_secret',
|
||||
# 收款商户
|
||||
'mch_id', 'pay_app_id', 'mch_key', 'pay_cert_path', 'pay_key_path',
|
||||
# 小程序收款通道:wechat / fubei
|
||||
'mini_pay_channel',
|
||||
# 提现商户
|
||||
'withdraw_mch_id', 'api_v3_key',
|
||||
'cert_serial_no', 'private_key_path', 'platform_cert_dir',
|
||||
@@ -549,6 +551,7 @@ class ClubManageView(APIView):
|
||||
'mch_id': club.mch_id,
|
||||
'pay_app_id': club.pay_app_id,
|
||||
'withdraw_mch_id': getattr(club, 'withdraw_mch_id', '') or '',
|
||||
'mini_pay_channel': getattr(club, 'mini_pay_channel', None) or 'wechat',
|
||||
'goeasy_appkey': club.goeasy_appkey,
|
||||
'template_id': club.template_id,
|
||||
'sort_order': club.sort_order,
|
||||
@@ -611,6 +614,10 @@ class ClubManageView(APIView):
|
||||
val = request.data[field]
|
||||
if field in ('mch_key', 'api_v3_key', 'wx_secret'):
|
||||
val = (val or '').strip()
|
||||
if field == 'mini_pay_channel':
|
||||
val = (val or 'wechat').strip().lower()
|
||||
if val not in ('wechat', 'fubei'):
|
||||
return Response({'code': 400, 'msg': 'mini_pay_channel 仅支持 wechat/fubei'})
|
||||
setattr(club, field, val)
|
||||
update_fields.append(field)
|
||||
if 'mch_key' in request.data:
|
||||
|
||||
241
jituan/views_payment_channel.py
Normal file
241
jituan/views_payment_channel.py
Normal file
@@ -0,0 +1,241 @@
|
||||
"""俱乐部支付通道后台管理(付呗配置 CRUD + 选用 wechat/fubei)。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from rest_framework.parsers import JSONParser
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from backend.utils import verify_kefu_permission
|
||||
from jituan.constants import (
|
||||
CLUB_ID_DEFAULT,
|
||||
MINI_PAY_CHANNEL_FUBEI,
|
||||
MINI_PAY_CHANNEL_WECHAT,
|
||||
PAY_CHANNEL_FUBEI_WX_MINI,
|
||||
)
|
||||
from jituan.models import Club, ClubPaymentChannel
|
||||
from jituan.services.club_payment_channel import (
|
||||
encrypt_secret_field,
|
||||
invalidate_club_payment_cache,
|
||||
)
|
||||
from jituan.services.mini_pay_router import get_club_mini_pay_channel, set_club_mini_pay_channel
|
||||
from jituan.services.permissions import can_manage_admin_assignments
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _mask_secret(plain: str) -> str:
|
||||
s = (plain or '').strip()
|
||||
if not s:
|
||||
return ''
|
||||
if len(s) <= 8:
|
||||
return '****'
|
||||
return s[:4] + '****' + s[-4:]
|
||||
|
||||
|
||||
def _serialize_channel(row: ClubPaymentChannel, reveal_secret: bool = False) -> dict:
|
||||
from jituan.services.club_payment_channel import decrypt_secret_field
|
||||
secret = decrypt_secret_field(row.api_key_enc) if row.api_key_enc else ''
|
||||
extra = dict(row.extra_json or {})
|
||||
return {
|
||||
'id': row.id,
|
||||
'club_id': row.club_id,
|
||||
'channel': row.channel,
|
||||
'name': row.name,
|
||||
'enabled': row.enabled,
|
||||
'is_default': row.is_default,
|
||||
'sort_order': row.sort_order,
|
||||
'sandbox': row.sandbox,
|
||||
'app_id': row.app_id,
|
||||
'mch_id': row.mch_id,
|
||||
'mini_app_id': row.mini_app_id,
|
||||
'store_id': extra.get('store_id') or extra.get('fubei_store_id') or '',
|
||||
'merchant_id': extra.get('merchant_id') or row.mch_id or '',
|
||||
'api_key_masked': _mask_secret(secret),
|
||||
'api_key': secret if reveal_secret else '',
|
||||
'api_key_configured': bool(secret),
|
||||
'notify_url': row.notify_url,
|
||||
'gateway_url': row.gateway_url,
|
||||
'remark': row.remark,
|
||||
'extra_json': extra,
|
||||
}
|
||||
|
||||
|
||||
class ClubPaymentChannelManageView(APIView):
|
||||
"""POST /jituan/houtai/payment-channel-manage
|
||||
|
||||
actions:
|
||||
- get: 当前俱乐部 mini_pay_channel + 付呗通道列表
|
||||
- set_mini_pay_channel: {mini_pay_channel: wechat|fubei}
|
||||
- save_fubei: 新增或更新付呗配置(按 id 或 name)
|
||||
- set_default / enable / disable
|
||||
- delete
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
parser_classes = [JSONParser]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
username = (request.data.get('phone') or request.data.get('username') or '').strip()
|
||||
if not username:
|
||||
return Response({'code': 401, 'msg': '缺少账号'}, status=401)
|
||||
kefu_obj, permissions = verify_kefu_permission(request, username)
|
||||
if kefu_obj is None:
|
||||
return permissions
|
||||
if not can_manage_admin_assignments(request.user):
|
||||
return Response({'code': 403, 'msg': '仅集团超管可管理支付通道'}, status=403)
|
||||
|
||||
action = (request.data.get('action') or 'get').strip()
|
||||
club_id = (request.data.get('club_id') or '').strip() or CLUB_ID_DEFAULT
|
||||
club = Club.query.filter(club_id=club_id).first()
|
||||
if not club:
|
||||
return Response({'code': 1, 'msg': f'俱乐部 {club_id} 不存在'})
|
||||
|
||||
if action == 'get':
|
||||
rows = ClubPaymentChannel.query.filter(
|
||||
club_id=club_id, channel=PAY_CHANNEL_FUBEI_WX_MINI,
|
||||
).order_by('-is_default', 'sort_order', 'id')
|
||||
return Response({
|
||||
'code': 0,
|
||||
'data': {
|
||||
'club_id': club_id,
|
||||
'mini_pay_channel': get_club_mini_pay_channel(club_id),
|
||||
'fubei_channels': [_serialize_channel(r) for r in rows],
|
||||
},
|
||||
})
|
||||
|
||||
if action == 'set_mini_pay_channel':
|
||||
ch = (request.data.get('mini_pay_channel') or '').strip().lower()
|
||||
try:
|
||||
set_club_mini_pay_channel(club_id, ch)
|
||||
except ValueError as exc:
|
||||
return Response({'code': 400, 'msg': str(exc)})
|
||||
if ch == MINI_PAY_CHANNEL_FUBEI:
|
||||
fb = ClubPaymentChannel.query.filter(
|
||||
club_id=club_id, channel=PAY_CHANNEL_FUBEI_WX_MINI, enabled=1,
|
||||
).first()
|
||||
if not fb:
|
||||
return Response({
|
||||
'code': 0,
|
||||
'msg': '已切换为付呗,但尚未配置付呗通道,请先保存付呗配置',
|
||||
'data': {'mini_pay_channel': ch},
|
||||
'warning': True,
|
||||
})
|
||||
return Response({
|
||||
'code': 0,
|
||||
'msg': '已切换小程序收款通道',
|
||||
'data': {'mini_pay_channel': ch},
|
||||
})
|
||||
|
||||
if action == 'save_fubei':
|
||||
return self._save_fubei(request, club_id)
|
||||
|
||||
if action in ('enable', 'disable', 'set_default', 'delete'):
|
||||
row_id = request.data.get('id')
|
||||
row = ClubPaymentChannel.query.filter(
|
||||
id=row_id, club_id=club_id, channel=PAY_CHANNEL_FUBEI_WX_MINI,
|
||||
).first()
|
||||
if not row:
|
||||
return Response({'code': 1, 'msg': '配置不存在'})
|
||||
if action == 'delete':
|
||||
row.delete()
|
||||
invalidate_club_payment_cache(club_id, PAY_CHANNEL_FUBEI_WX_MINI)
|
||||
return Response({'code': 0, 'msg': '已删除'})
|
||||
if action == 'enable':
|
||||
row.enabled = 1
|
||||
row.save(update_fields=['enabled'])
|
||||
elif action == 'disable':
|
||||
row.enabled = 0
|
||||
row.save(update_fields=['enabled'])
|
||||
elif action == 'set_default':
|
||||
ClubPaymentChannel.objects.filter(
|
||||
club_id=club_id, channel=PAY_CHANNEL_FUBEI_WX_MINI, is_default=True,
|
||||
).exclude(id=row.id).update(is_default=False)
|
||||
row.is_default = True
|
||||
row.enabled = 1
|
||||
row.save(update_fields=['is_default', 'enabled'])
|
||||
invalidate_club_payment_cache(club_id, PAY_CHANNEL_FUBEI_WX_MINI)
|
||||
return Response({'code': 0, 'msg': 'ok', 'data': _serialize_channel(row)})
|
||||
|
||||
return Response({'code': 400, 'msg': f'未知 action: {action}'})
|
||||
except Exception as exc:
|
||||
logger.error('payment-channel-manage 异常: %s', exc, exc_info=True)
|
||||
return Response({'code': 500, 'msg': str(exc)})
|
||||
|
||||
def _save_fubei(self, request, club_id: str):
|
||||
name = (request.data.get('name') or '主付呗').strip() or '主付呗'
|
||||
row_id = request.data.get('id')
|
||||
app_id = (request.data.get('app_id') or '').strip()
|
||||
api_key = request.data.get('api_key')
|
||||
store_id = request.data.get('store_id')
|
||||
merchant_id = (request.data.get('merchant_id') or request.data.get('mch_id') or '').strip()
|
||||
mini_app_id = (request.data.get('mini_app_id') or '').strip()
|
||||
remark = (request.data.get('remark') or '').strip()
|
||||
|
||||
if not app_id:
|
||||
return Response({'code': 400, 'msg': '请填写付呗开放平台 app_id'})
|
||||
if store_id in (None, ''):
|
||||
return Response({'code': 400, 'msg': '请填写门店 store_id'})
|
||||
|
||||
row = None
|
||||
if row_id:
|
||||
row = ClubPaymentChannel.query.filter(
|
||||
id=row_id, club_id=club_id, channel=PAY_CHANNEL_FUBEI_WX_MINI,
|
||||
).first()
|
||||
if not row:
|
||||
row = ClubPaymentChannel.query.filter(
|
||||
club_id=club_id, channel=PAY_CHANNEL_FUBEI_WX_MINI, name=name,
|
||||
).first()
|
||||
creating = row is None
|
||||
if creating:
|
||||
row = ClubPaymentChannel(
|
||||
club_id=club_id,
|
||||
channel=PAY_CHANNEL_FUBEI_WX_MINI,
|
||||
name=name,
|
||||
enabled=1,
|
||||
is_default=True,
|
||||
)
|
||||
|
||||
row.app_id = app_id
|
||||
row.mch_id = merchant_id
|
||||
row.mini_app_id = mini_app_id
|
||||
row.remark = remark
|
||||
if api_key is not None and str(api_key).strip():
|
||||
row.api_key_enc = encrypt_secret_field(str(api_key).strip())
|
||||
elif creating:
|
||||
return Response({'code': 400, 'msg': '新建付呗配置必须填写接口密钥'})
|
||||
|
||||
extra = dict(row.extra_json or {})
|
||||
try:
|
||||
extra['store_id'] = int(store_id)
|
||||
except (TypeError, ValueError):
|
||||
return Response({'code': 400, 'msg': 'store_id 必须是数字'})
|
||||
if merchant_id:
|
||||
extra['merchant_id'] = merchant_id
|
||||
row.extra_json = extra
|
||||
|
||||
# 设为默认时取消同通道其它默认
|
||||
if request.data.get('is_default', True):
|
||||
ClubPaymentChannel.objects.filter(
|
||||
club_id=club_id, channel=PAY_CHANNEL_FUBEI_WX_MINI, is_default=True,
|
||||
).exclude(id=getattr(row, 'id', None) or 0).update(is_default=False)
|
||||
row.is_default = True
|
||||
row.enabled = 1
|
||||
|
||||
row.save()
|
||||
invalidate_club_payment_cache(club_id, PAY_CHANNEL_FUBEI_WX_MINI)
|
||||
|
||||
# 可选:保存后同时切到付呗
|
||||
if request.data.get('activate'):
|
||||
set_club_mini_pay_channel(club_id, MINI_PAY_CHANNEL_FUBEI)
|
||||
|
||||
return Response({
|
||||
'code': 0,
|
||||
'msg': '付呗配置已保存',
|
||||
'data': {
|
||||
'channel': _serialize_channel(row),
|
||||
'mini_pay_channel': get_club_mini_pay_channel(club_id),
|
||||
},
|
||||
})
|
||||
@@ -12,6 +12,7 @@ from .views import CreateOrderView, WechatPayNotifyView, AlipayPayNotifyView, al
|
||||
ShangjiaFakuanApplyView, ShangjiaFakuaiXiugaiView, ShangjiaShujuTongjiView, LianTongDuiHuaZhunBeiView, \
|
||||
LianTongPeiDuiDingDanLieBiaoView, LianTongDingDanZhuangTaiView, LianTongDingDanZhuangTaiPiLiangView, \
|
||||
ZhiDingChaxunView, ZhiDingHuiFuView
|
||||
from jituan.services.fubei_notify import FubeiPayNotifyView
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
@@ -19,6 +20,7 @@ urlpatterns = [
|
||||
#path('pay-notify/', WechatPayNotifyView.as_view(), name='老板下单微信支付回调'),
|
||||
path('pay-notify/', csrf_exempt(WechatPayNotifyView.as_view()), name='老板下单微信支付回调'),
|
||||
path('alipay-notify/', csrf_exempt(AlipayPayNotifyView.as_view()), name='老板下单支付宝支付回调'),
|
||||
path('fubei-notify/', csrf_exempt(FubeiPayNotifyView.as_view()), name='付呗支付回调'),
|
||||
path('alipay-pay/', csrf_exempt(alipay_pay_page), name='支付宝H5中转页'),
|
||||
path('shibai', PaymentFailView.as_view(), name='老板支付失败接口'),
|
||||
path('fucha', PaymentVerifyView.as_view(), name='老板支付成功复查接口'),
|
||||
|
||||
@@ -1305,6 +1305,18 @@ class AdTongYiTuiKuanPingTai(APIView):
|
||||
:return: {'success': True/False, 'msg': '消息', 'refund_id': '退款单号'}
|
||||
"""
|
||||
try:
|
||||
from jituan.services.pay_refund_router import try_fubei_refund
|
||||
from jituan.services.wechat_pay import club_id_from_order
|
||||
fb = try_fubei_refund(
|
||||
out_trade_no=out_trade_no,
|
||||
amount_yuan=jine,
|
||||
club_id=club_id_from_order(out_trade_no),
|
||||
)
|
||||
if fb is not None:
|
||||
if fb.get('code') == 0:
|
||||
return {'success': True, 'msg': fb.get('msg') or 'success', 'refund_id': fb.get('refund_id')}
|
||||
return {'success': False, 'msg': fb.get('msg') or '付呗退款失败'}
|
||||
|
||||
# 检查配置
|
||||
required_settings = ['WEIXIN_APPID', 'WEIXIN_MCHID', 'WEIXIN_SHANGHUMIYAO', 'WEIXIN_CERT_PATH', 'WEIXIN_KEY_PATH']
|
||||
for setting in required_settings:
|
||||
|
||||
@@ -1035,6 +1035,8 @@ class CreateOrderView(APIView):
|
||||
调用微信支付统一下单接口,生成支付参数
|
||||
"""
|
||||
try:
|
||||
from jituan.constants import FUBEI_BIZ_ORDER
|
||||
from jituan.services.mini_pay_router import maybe_create_fubei_jsapi_params
|
||||
from jituan.services.wechat_pay import get_wechat_v2_config
|
||||
pay_cfg = get_wechat_v2_config(club_id)
|
||||
APPID = pay_cfg['appid']
|
||||
@@ -1042,6 +1044,18 @@ class CreateOrderView(APIView):
|
||||
KEY = pay_cfg['key']
|
||||
NOTIFY_URL = getattr(settings, 'WEIXIN_NOTIFY_URL', '')
|
||||
|
||||
fb_params = maybe_create_fubei_jsapi_params(
|
||||
club_id=club_id,
|
||||
out_trade_no=dingdanid,
|
||||
amount_yuan=jine,
|
||||
body=beizhu,
|
||||
openid=openid,
|
||||
notify_url=NOTIFY_URL,
|
||||
biz_type=FUBEI_BIZ_ORDER,
|
||||
)
|
||||
if fb_params is not None:
|
||||
return fb_params
|
||||
|
||||
# NOAUTH 诊断日志:打印生效的 club_id / appid / mchid / openid 指纹
|
||||
_oid_preview = (openid or '')[:6] + '...' + (openid or '')[-4:] if (openid or '') else '(empty)'
|
||||
logger.warning(
|
||||
|
||||
@@ -232,6 +232,21 @@ class ShangjiaChongzhi(APIView):
|
||||
else:
|
||||
raise Exception('未知的支付类型')
|
||||
|
||||
from jituan.constants import FUBEI_BIZ_CZJILU
|
||||
from jituan.services.mini_pay_router import maybe_create_fubei_jsapi_params
|
||||
_club = resolve_club_id_for_write(self.request, self.request.user)
|
||||
fb_params = maybe_create_fubei_jsapi_params(
|
||||
club_id=_club,
|
||||
out_trade_no=dingdanid,
|
||||
amount_yuan=jine,
|
||||
body=PAY_DESCRIPTION,
|
||||
openid=openid,
|
||||
notify_url=NOTIFY_URL,
|
||||
biz_type=FUBEI_BIZ_CZJILU,
|
||||
)
|
||||
if fb_params is not None:
|
||||
return fb_params
|
||||
|
||||
# 生成随机字符串
|
||||
nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
|
||||
|
||||
|
||||
@@ -270,6 +270,20 @@ class HuiyuanGoumai(APIView):
|
||||
else:
|
||||
raise Exception('未知的支付类型')
|
||||
|
||||
from jituan.constants import FUBEI_BIZ_CZJILU
|
||||
from jituan.services.mini_pay_router import maybe_create_fubei_jsapi_params
|
||||
fb_params = maybe_create_fubei_jsapi_params(
|
||||
club_id=pay_club,
|
||||
out_trade_no=dingdanid,
|
||||
amount_yuan=jine,
|
||||
body=PAY_DESCRIPTION,
|
||||
openid=openid,
|
||||
notify_url=NOTIFY_URL,
|
||||
biz_type=FUBEI_BIZ_CZJILU,
|
||||
)
|
||||
if fb_params is not None:
|
||||
return fb_params
|
||||
|
||||
# 生成随机字符串
|
||||
nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
|
||||
|
||||
|
||||
@@ -213,6 +213,21 @@ class JifenBuchong(APIView):
|
||||
else:
|
||||
raise Exception('未知的支付类型')
|
||||
|
||||
from jituan.constants import FUBEI_BIZ_CZJILU
|
||||
from jituan.services.mini_pay_router import maybe_create_fubei_jsapi_params
|
||||
_club = resolve_club_id_for_write(self.request, self.request.user)
|
||||
fb_params = maybe_create_fubei_jsapi_params(
|
||||
club_id=_club,
|
||||
out_trade_no=dingdanid,
|
||||
amount_yuan=jine,
|
||||
body=PAY_DESCRIPTION,
|
||||
openid=openid,
|
||||
notify_url=NOTIFY_URL,
|
||||
biz_type=FUBEI_BIZ_CZJILU,
|
||||
)
|
||||
if fb_params is not None:
|
||||
return fb_params
|
||||
|
||||
# 生成随机字符串
|
||||
nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
|
||||
|
||||
|
||||
@@ -227,6 +227,21 @@ class YajinGoumai(APIView):
|
||||
else:
|
||||
raise Exception('未知的支付类型')
|
||||
|
||||
from jituan.constants import FUBEI_BIZ_CZJILU
|
||||
from jituan.services.mini_pay_router import maybe_create_fubei_jsapi_params
|
||||
_club = resolve_club_id_for_write(self.request, self.request.user)
|
||||
fb_params = maybe_create_fubei_jsapi_params(
|
||||
club_id=_club,
|
||||
out_trade_no=dingdanid,
|
||||
amount_yuan=jine,
|
||||
body=PAY_DESCRIPTION,
|
||||
openid=openid,
|
||||
notify_url=NOTIFY_URL,
|
||||
biz_type=FUBEI_BIZ_CZJILU,
|
||||
)
|
||||
if fb_params is not None:
|
||||
return fb_params
|
||||
|
||||
# 生成随机字符串
|
||||
nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
|
||||
|
||||
|
||||
@@ -200,6 +200,15 @@ class ShopRefundView(APIView):
|
||||
# ========== 微信退款 ==========
|
||||
def _call_wechat_refund(self, order):
|
||||
"""调用微信支付退款接口"""
|
||||
from jituan.services.pay_refund_router import try_fubei_refund
|
||||
fb = try_fubei_refund(
|
||||
out_trade_no=order.OrderID,
|
||||
amount_yuan=order.Amount or 0,
|
||||
club_id=getattr(order, 'ClubID', None),
|
||||
)
|
||||
if fb is not None:
|
||||
return fb
|
||||
|
||||
appid = getattr(settings, 'WEIXIN_APPID', '')
|
||||
mch_id = getattr(settings, 'WEIXIN_MCHID', '')
|
||||
key = getattr(settings, 'WEIXIN_SHANGHUMIYAO', '')
|
||||
|
||||
@@ -858,6 +858,16 @@ class KefuPlatformRefundView(APIView):
|
||||
:param transaction_id: 微信支付订单号(优先使用)
|
||||
:return: {'code': 0, 'msg': '成功', 'refund_id': 'xxx'} 或 {'code': 非0, 'msg': '错误信息'}
|
||||
"""
|
||||
from jituan.services.pay_refund_router import try_fubei_refund
|
||||
from jituan.services.wechat_pay import club_id_from_order
|
||||
fb = try_fubei_refund(
|
||||
out_trade_no=dingdan_id,
|
||||
amount_yuan=jine,
|
||||
club_id=club_id_from_order(dingdan_id),
|
||||
)
|
||||
if fb is not None:
|
||||
return fb
|
||||
|
||||
# 获取配置
|
||||
appid = getattr(settings, 'WEIXIN_APPID', '')
|
||||
mch_id = getattr(settings, 'WEIXIN_MCHID', '')
|
||||
|
||||
@@ -233,6 +233,20 @@ class FaKuanPayView(APIView):
|
||||
else:
|
||||
raise Exception('未知的支付类型')
|
||||
|
||||
from jituan.constants import FUBEI_BIZ_CZJILU
|
||||
from jituan.services.mini_pay_router import maybe_create_fubei_jsapi_params
|
||||
fb_params = maybe_create_fubei_jsapi_params(
|
||||
club_id=pay_club,
|
||||
out_trade_no=dingdanid,
|
||||
amount_yuan=jine,
|
||||
body=PAY_DESCRIPTION,
|
||||
openid=openid,
|
||||
notify_url=NOTIFY_URL,
|
||||
biz_type=FUBEI_BIZ_CZJILU,
|
||||
)
|
||||
if fb_params is not None:
|
||||
return fb_params
|
||||
|
||||
nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
|
||||
total_fee = yuan_to_fen(jine)
|
||||
trade_type = 'JSAPI'
|
||||
@@ -720,6 +734,21 @@ class KaohePayView(APIView):
|
||||
else:
|
||||
raise Exception('未知支付类型')
|
||||
|
||||
from jituan.constants import FUBEI_BIZ_CZJILU
|
||||
from jituan.services.mini_pay_router import maybe_create_fubei_jsapi_params
|
||||
_club = resolve_club_id_for_write(self.request, self.request.user)
|
||||
fb_params = maybe_create_fubei_jsapi_params(
|
||||
club_id=_club,
|
||||
out_trade_no=dingdanid,
|
||||
amount_yuan=jine,
|
||||
body=PAY_DESCRIPTION,
|
||||
openid=openid,
|
||||
notify_url=NOTIFY_URL,
|
||||
biz_type=FUBEI_BIZ_CZJILU,
|
||||
)
|
||||
if fb_params is not None:
|
||||
return fb_params
|
||||
|
||||
nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
|
||||
total_fee = yuan_to_fen(jine)
|
||||
trade_type = 'JSAPI'
|
||||
|
||||
Reference in New Issue
Block a user