From 5762f00d40caea98de8ca4303ba67fbef60883c4 Mon Sep 17 00:00:00 2001 From: XingQue Date: Tue, 21 Jul 2026 20:02:24 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=86=E4=BF=B1=E4=B9=90=E9=83=A8?= =?UTF-8?q?=E4=BB=98=E5=91=97=E6=94=B6=E6=AC=BE=E9=80=9A=E9=81=93=EF=BC=8C?= =?UTF-8?q?=E9=BB=98=E8=AE=A4=E4=BB=8D=E8=B5=B0=E5=BE=AE=E4=BF=A1=E7=9B=B4?= =?UTF-8?q?=E8=BF=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 小程序前端仍传 wechat;后台可切换 wechat/fubei,支持配置多套付呗并退款按原通道路由。 Co-authored-by: Cursor --- jituan/constants.py | 34 +++ .../0014_club_mini_pay_channel_and_ledger.py | 41 +++ jituan/models.py | 72 ++++++ jituan/services/fubei_client.py | 201 +++++++++++++++ jituan/services/fubei_notify.py | 158 ++++++++++++ jituan/services/mini_pay_router.py | 152 +++++++++++ jituan/services/pay_refund_router.py | 62 +++++ jituan/urls.py | 2 + jituan/views.py | 7 + jituan/views_payment_channel.py | 241 ++++++++++++++++++ orders/urls.py | 2 + orders/views/admin.py | 12 + orders/views/payment.py | 14 + products/views/chongzhi_payment.py | 15 ++ products/views/huiyuan_payment.py | 14 + products/views/jifen_payment.py | 15 ++ products/views/yajin_payment.py | 15 ++ shop/views/shop_refund_views.py | 9 + users/views/kefu_orders.py | 10 + users/views/pay.py | 29 +++ 20 files changed, 1105 insertions(+) create mode 100644 jituan/migrations/0014_club_mini_pay_channel_and_ledger.py create mode 100644 jituan/services/fubei_client.py create mode 100644 jituan/services/fubei_notify.py create mode 100644 jituan/services/mini_pay_router.py create mode 100644 jituan/services/pay_refund_router.py create mode 100644 jituan/views_payment_channel.py diff --git a/jituan/constants.py b/jituan/constants.py index 38a7731..8169224 100644 --- a/jituan/constants.py +++ b/jituan/constants.py @@ -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' diff --git a/jituan/migrations/0014_club_mini_pay_channel_and_ledger.py b/jituan/migrations/0014_club_mini_pay_channel_and_ledger.py new file mode 100644 index 0000000..2c00828 --- /dev/null +++ b/jituan/migrations/0014_club_mini_pay_channel_and_ledger.py @@ -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', + }, + ), + ] diff --git a/jituan/models.py b/jituan/models.py index 50c2601..5a346af 100644 --- a/jituan/models.py +++ b/jituan/models.py @@ -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) diff --git a/jituan/services/fubei_client.py b/jituan/services/fubei_client.py new file mode 100644 index 0000000..6d88fbb --- /dev/null +++ b/jituan/services/fubei_client.py @@ -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 diff --git a/jituan/services/fubei_notify.py b/jituan/services/fubei_notify.py new file mode 100644 index 0000000..676981d --- /dev/null +++ b/jituan/services/fubei_notify.py @@ -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 diff --git a/jituan/services/mini_pay_router.py b/jituan/services/mini_pay_router.py new file mode 100644 index 0000000..5f79ea8 --- /dev/null +++ b/jituan/services/mini_pay_router.py @@ -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 diff --git a/jituan/services/pay_refund_router.py b/jituan/services/pay_refund_router.py new file mode 100644 index 0000000..c606f36 --- /dev/null +++ b/jituan/services/pay_refund_router.py @@ -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)} diff --git a/jituan/urls.py b/jituan/urls.py index 6dfb14f..2fe8c44 100644 --- a/jituan/urls.py +++ b/jituan/urls.py @@ -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'), diff --git a/jituan/views.py b/jituan/views.py index 24754ce..c55ad8c 100644 --- a/jituan/views.py +++ b/jituan/views.py @@ -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: diff --git a/jituan/views_payment_channel.py b/jituan/views_payment_channel.py new file mode 100644 index 0000000..d8f6dd0 --- /dev/null +++ b/jituan/views_payment_channel.py @@ -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), + }, + }) diff --git a/orders/urls.py b/orders/urls.py index 7a83996..10beaea 100644 --- a/orders/urls.py +++ b/orders/urls.py @@ -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='老板支付成功复查接口'), diff --git a/orders/views/admin.py b/orders/views/admin.py index 0a46c85..cbe68db 100644 --- a/orders/views/admin.py +++ b/orders/views/admin.py @@ -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: diff --git a/orders/views/payment.py b/orders/views/payment.py index 945852c..dc15b43 100644 --- a/orders/views/payment.py +++ b/orders/views/payment.py @@ -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( diff --git a/products/views/chongzhi_payment.py b/products/views/chongzhi_payment.py index 2f318b6..be030b3 100644 --- a/products/views/chongzhi_payment.py +++ b/products/views/chongzhi_payment.py @@ -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)) diff --git a/products/views/huiyuan_payment.py b/products/views/huiyuan_payment.py index 2c8f542..3c1e949 100644 --- a/products/views/huiyuan_payment.py +++ b/products/views/huiyuan_payment.py @@ -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)) diff --git a/products/views/jifen_payment.py b/products/views/jifen_payment.py index 04516cd..28a07be 100644 --- a/products/views/jifen_payment.py +++ b/products/views/jifen_payment.py @@ -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)) diff --git a/products/views/yajin_payment.py b/products/views/yajin_payment.py index 5361d1d..5c829a9 100644 --- a/products/views/yajin_payment.py +++ b/products/views/yajin_payment.py @@ -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)) diff --git a/shop/views/shop_refund_views.py b/shop/views/shop_refund_views.py index 2ceb5ce..92199aa 100644 --- a/shop/views/shop_refund_views.py +++ b/shop/views/shop_refund_views.py @@ -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', '') diff --git a/users/views/kefu_orders.py b/users/views/kefu_orders.py index 30f0fc2..3b9e986 100644 --- a/users/views/kefu_orders.py +++ b/users/views/kefu_orders.py @@ -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', '') diff --git a/users/views/pay.py b/users/views/pay.py index 36bccf5..0c9e247 100644 --- a/users/views/pay.py +++ b/users/views/pay.py @@ -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'