From 51edcf9fcec2cb0dd0c8777eb06f40e9648928fc Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Thu, 9 Jul 2026 19:50:40 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BD=BF=E7=94=A8=E4=BA=8C=E7=BB=B4=E7=A0=81?= =?UTF-8?q?=E8=BF=9B=E8=A1=8C=E6=94=AF=E4=BB=98=E5=AE=9D=E4=BA=A4=E6=98=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jituan/services/alipay_pay.py | 84 +++++++++++++++++++++++++++++++++++ orders/views/payment.py | 25 +++++++---- 2 files changed, 101 insertions(+), 8 deletions(-) diff --git a/jituan/services/alipay_pay.py b/jituan/services/alipay_pay.py index 13e82e3..a26be75 100644 --- a/jituan/services/alipay_pay.py +++ b/jituan/services/alipay_pay.py @@ -234,6 +234,90 @@ def build_wap_pay_url(out_trade_no, amount, subject, body='', club_id=None, expi return f"{cfg['GATEWAY']}?{query_string}" +# ==================== 当面付二维码(precreate)==================== + +def build_precreate_qr(out_trade_no, amount, subject, body='', club_id=None, expire_minutes=30): + """ + 调用 alipay.trade.precreate 生成当面付二维码链接。 + + 与 wap.pay 不同:precreate 是服务器主动 POST 调用,返回 qr_code 链接, + 由商户自行生成二维码展示;用户用支付宝 APP 扫码完成支付。 + 适用于微信小程序等无法跳转 alipay:// scheme 的环境。 + + 返回: + dict: {'qr_code': 'https://qr.alipay.com/...', 'out_trade_no': ...} + 失败抛异常。 + """ + import requests + + cfg = get_alipay_config(club_id) + + biz_content = { + 'out_trade_no': out_trade_no, + 'total_amount': str(amount), + 'subject': subject or '订单支付', + 'body': body or '', + 'timeout_express': f'{expire_minutes}m', + } + + params = { + 'app_id': cfg['APP_ID'], + 'method': 'alipay.trade.precreate', + 'charset': 'utf-8', + 'sign_type': 'RSA2', + 'timestamp': time.strftime('%Y-%m-%d %H:%M:%S'), + 'version': '1.0', + 'biz_content': json.dumps(biz_content, separators=(',', ':'), ensure_ascii=False), + 'notify_url': cfg['NOTIFY_URL'], + } + + # 签名(sign_type 参与签名,与 wap.pay 一致) + sign_content = _build_sign_content(params) + params['sign'] = sign_rsa2(sign_content, cfg['APP_PRIVATE_KEY']) + + # 服务器主动调用网关 + resp = requests.post(cfg['GATEWAY'], data=params, timeout=10) + try: + result = resp.json() + except Exception: + raise Exception(f"支付宝 precreate 返回非 JSON: {resp.text[:500]}") + + biz_resp = result.get('alipay_trade_precreate_response', {}) + code = biz_resp.get('code', '') + + if code == '10000': + return { + 'qr_code': biz_resp.get('qr_code', ''), + 'out_trade_no': biz_resp.get('out_trade_no', out_trade_no), + } + + # 失败:拼接完整错误信息 + sub_code = biz_resp.get('sub_code', '') + sub_msg = biz_resp.get('sub_msg', '') + raise Exception( + f"支付宝预下单失败: code={code}, msg={biz_resp.get('msg', '')}, " + f"sub_code={sub_code}, sub_msg={sub_msg}" + ) + + +def qr_link_to_base64(qr_link): + """ + 把二维码链接内容转成 PNG base64 data URI(供前端 image 直接展示)。 + 依赖 qrcode + Pillow;缺失时抛 ImportError 提示安装。 + """ + import io + import base64 + try: + import qrcode + except ImportError: + raise ImportError("缺少 qrcode 库,请执行: pip install qrcode[pil]") + + img = qrcode.make(qr_link) + buf = io.BytesIO() + img.save(buf, format='PNG') + return 'data:image/png;base64,' + base64.b64encode(buf.getvalue()).decode('ascii') + + # ==================== 辅助 ==================== def club_id_from_order(order_id): diff --git a/orders/views/payment.py b/orders/views/payment.py index e3b7e1b..f8f2160 100644 --- a/orders/views/payment.py +++ b/orders/views/payment.py @@ -969,6 +969,13 @@ class CreateOrderView(APIView): KEY = pay_cfg['key'] NOTIFY_URL = getattr(settings, 'WEIXIN_NOTIFY_URL', '') + # NOAUTH 诊断日志:打印生效的 club_id / appid / mchid / openid 指纹 + _oid_preview = (openid or '')[:6] + '...' + (openid or '')[-4:] if (openid or '') else '(empty)' + logger.warning( + "[WX_PAY_NOAUTH_DIAG] dingdanid=%s club_id=%s appid=%s mch_id=%s openid_preview=%s openid_len=%d", + dingdanid, club_id, APPID, MCHID, _oid_preview, len(openid or ''), + ) + if not all([APPID, MCHID, KEY, NOTIFY_URL]): raise Exception('微信支付配置不完整') @@ -1029,6 +1036,7 @@ class CreateOrderView(APIView): ) # 解析返回的XML + logger.warning("[WX_PAY_NOAUTH_DIAG] wechat_raw_response=%s", response.content.decode('utf-8', errors='replace')) root = ET.fromstring(response.content) return_code = root.find('return_code').text @@ -1087,22 +1095,23 @@ class CreateOrderView(APIView): def generate_alipay_pay_params(self, dingdanid, jine, subject, club_id=None): """ - 生成支付宝 wap.pay 跳转 URL,存入 cache 供中转页取用。 - 同时返回 pay_url 供前端直接加载(绕过中转页跳转限制)。 + 生成支付宝当面付二维码支付参数。 + 返回 qr_image(base64 PNG)供前端 image 直接展示,用户用支付宝扫码支付。 + 适用于微信小程序 web-view 无法跳转 alipay:// scheme 的场景。 """ try: - from jituan.services.alipay_pay import build_wap_pay_url - pay_url = build_wap_pay_url( + from jituan.services.alipay_pay import build_precreate_qr, qr_link_to_base64 + result = build_precreate_qr( out_trade_no=dingdanid, amount=jine, subject=subject, club_id=club_id, ) - # 存入 cache,30 分钟过期(供中转页/查询接口判断支付方式) - cache_key = f'alipay_pay_url_{dingdanid}' - cache.set(cache_key, pay_url, 60 * 30) + qr_code = result['qr_code'] + qr_image = qr_link_to_base64(qr_code) return { - 'pay_url': pay_url, + 'qr_code': qr_code, + 'qr_image': qr_image, 'pay_method': 'alipay', } except Exception as e: