feat: 微信扫码改印小程序码,本机相册扫直接调起付款密码
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
a6d2f878ab
commit
ec08885eca
4 files changed
+322
-113
No files matched your search
@@ -1,22 +1,22 @@
|
||||
"""微信 Native 扫码支付(trade_type=NATIVE)。
|
||||
"""微信扫码支付:小程序码(本机相册可扫,直接弹密码)。
|
||||
|
||||
「一瞬间出密码」= 二维码必须是微信返回的 weixin://wxpay/bizpayurl?...
|
||||
他人微信摄像头扫屏幕 → 系统直接弹付款,中间没有网页。
|
||||
为什么不用 weixin:// 付款码:
|
||||
- 那才是「另一台手机摄像头扫 → 瞬间出密码」的官方 Native
|
||||
- 但微信禁止「扫一扫 → 相册」识别付款码(保障支付安全)
|
||||
|
||||
本机相册扫这种付款码会被微信官方拦截(保障支付安全)——不是商户能关的。
|
||||
本机付款请走小程序「本机支付」JSAPI,同样是一瞬间出密码。
|
||||
|
||||
相册可扫的 HTTPS 中转页仍保留(wx-native-pay),但不印进主二维码,
|
||||
因为那条路必然经过网页授权,做不到和 weixin:// 一样的「无感弹窗」。
|
||||
为什么改用小程序码(微信官方文档也写了可用小程序码替代收款码):
|
||||
- 相册可以识别小程序码
|
||||
- 扫完直接进小程序支付页 → wx.requestPayment 出密码
|
||||
- 用户体感就是:相册一扫,马上出支付,没有乱七八糟的 H5 网页
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import logging
|
||||
import random
|
||||
import string
|
||||
import time
|
||||
from urllib.parse import quote
|
||||
|
||||
import requests
|
||||
from django.core.cache import cache
|
||||
@@ -31,28 +31,23 @@ from utils.money import yuan_to_fen
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
CACHE_KEY_PREFIX = 'wx_native_code_'
|
||||
SESSION_KEY_PREFIX = 'wx_native_pay_'
|
||||
SCENE_KEY_PREFIX = 'wx_native_scene_'
|
||||
CACHE_TTL_SECONDS = 60 * 30
|
||||
BRIDGE_URL_BASE = 'https://www.abas.asia/hqhd/dingdan/wx-native-pay/'
|
||||
|
||||
|
||||
def _cache_key(dingdanid: str) -> str:
|
||||
return f'{CACHE_KEY_PREFIX}{dingdanid}'
|
||||
# getwxacodeunlimit scene ≤ 32 字节;前缀 + 随机
|
||||
SCENE_PREFIX = 'np'
|
||||
PAY_PAGE = 'pages/wx-scan-pay/wx-scan-pay'
|
||||
|
||||
|
||||
def _session_key(dingdanid: str) -> str:
|
||||
return f'{SESSION_KEY_PREFIX}{dingdanid}'
|
||||
|
||||
|
||||
def build_native_bridge_url(dingdanid: str, club_id=None) -> str:
|
||||
q = f'out_trade_no={quote(dingdanid or "", safe="")}'
|
||||
if club_id:
|
||||
q += f'&club_id={quote(str(club_id), safe="")}'
|
||||
return f'{BRIDGE_URL_BASE}?{q}'
|
||||
def _scene_key(scene: str) -> str:
|
||||
return f'{SCENE_KEY_PREFIX}{scene}'
|
||||
|
||||
|
||||
def save_native_pay_session(dingdanid, *, jine, body, notify_url, club_id=None):
|
||||
def save_native_pay_session(dingdanid, *, jine, body, notify_url, club_id=None, scene=''):
|
||||
cache.set(
|
||||
_session_key(dingdanid),
|
||||
{
|
||||
@@ -60,6 +55,8 @@ def save_native_pay_session(dingdanid, *, jine, body, notify_url, club_id=None):
|
||||
'body': (body or '微信支付').strip() or '微信支付',
|
||||
'notify_url': (notify_url or '').strip(),
|
||||
'club_id': (club_id or '').strip(),
|
||||
'scene': (scene or '').strip(),
|
||||
'dingdanid': (dingdanid or '').strip(),
|
||||
},
|
||||
CACHE_TTL_SECONDS,
|
||||
)
|
||||
@@ -70,17 +67,70 @@ def get_native_pay_session(dingdanid: str):
|
||||
return data if isinstance(data, dict) else None
|
||||
|
||||
|
||||
def _native_pay_response(code_url: str, *, bridge_url: str = '') -> dict:
|
||||
from jituan.services.alipay_pay import qr_link_to_base64
|
||||
def resolve_scene_to_dingdanid(scene: str) -> str:
|
||||
scene = (scene or '').strip()
|
||||
if not scene:
|
||||
return ''
|
||||
dingdanid = cache.get(_scene_key(scene)) or ''
|
||||
return str(dingdanid).strip()
|
||||
|
||||
out = {
|
||||
'qr_code': code_url,
|
||||
'qr_image': qr_link_to_base64(code_url),
|
||||
'pay_method': PAY_METHOD_WECHAT_NATIVE,
|
||||
|
||||
def _new_scene() -> str:
|
||||
# np + 22 位 = 24,远小于 32
|
||||
rnd = ''.join(random.choices(string.ascii_letters + string.digits, k=22))
|
||||
return f'{SCENE_PREFIX}{rnd}'
|
||||
|
||||
|
||||
def _png_to_data_url(png_bytes: bytes) -> str:
|
||||
b64 = base64.b64encode(png_bytes).decode('ascii')
|
||||
return f'data:image/png;base64,{b64}'
|
||||
|
||||
|
||||
def _fetch_mini_program_code_png(*, club_id: str, scene: str) -> bytes:
|
||||
from utils.weixin_token import get_club_mini_access_token, is_weixin_token_invalid
|
||||
|
||||
access_token = get_club_mini_access_token(club_id)
|
||||
if not access_token:
|
||||
raise Exception(f'俱乐部 {club_id} 获取小程序 access_token 失败,无法生成支付码')
|
||||
|
||||
post_data = {
|
||||
'scene': scene,
|
||||
'page': PAY_PAGE,
|
||||
'width': 430,
|
||||
'auto_color': False,
|
||||
'line_color': {'r': 0, 'g': 0, 'b': 0},
|
||||
'check_path': False,
|
||||
}
|
||||
if bridge_url:
|
||||
out['bridge_url'] = bridge_url
|
||||
return out
|
||||
|
||||
def _req(tok: str):
|
||||
url = f'https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token={tok}'
|
||||
return requests.post(url, json=post_data, timeout=15)
|
||||
|
||||
wx_resp = _req(access_token)
|
||||
if wx_resp.headers.get('content-type', '').startswith('application/json'):
|
||||
try:
|
||||
err = wx_resp.json()
|
||||
if is_weixin_token_invalid(err.get('errcode'), err.get('errmsg', '')):
|
||||
access_token = get_club_mini_access_token(club_id, force_refresh=True)
|
||||
if access_token:
|
||||
wx_resp = _req(access_token)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
content_type = wx_resp.headers.get('content-type', '')
|
||||
if wx_resp.status_code != 200 or 'image' not in content_type:
|
||||
errmsg = '未知错误'
|
||||
try:
|
||||
error_info = wx_resp.json()
|
||||
errmsg = error_info.get('errmsg', errmsg)
|
||||
logger.error('[WX_NATIVE_MINICODE] fail club=%s err=%s', club_id, error_info)
|
||||
except Exception:
|
||||
logger.error(
|
||||
'[WX_NATIVE_MINICODE] fail club=%s status=%s body=%s',
|
||||
club_id, wx_resp.status_code, wx_resp.content[:300],
|
||||
)
|
||||
raise Exception(f'生成小程序支付码失败: {errmsg}')
|
||||
return wx_resp.content
|
||||
|
||||
|
||||
def generate_wechat_native_pay_params(
|
||||
@@ -93,100 +143,64 @@ def generate_wechat_native_pay_params(
|
||||
client_ip='127.0.0.1',
|
||||
scene='native',
|
||||
):
|
||||
"""统一下单 NATIVE,二维码印官方 weixin:// 付款码(他人扫瞬间出密码)。"""
|
||||
"""生成「相册可扫」小程序码二维码。"""
|
||||
dingdanid = (dingdanid or '').strip()
|
||||
notify_url = (notify_url or '').strip()
|
||||
body = (body or '微信支付').strip() or '微信支付'
|
||||
club_id = (club_id or '').strip()
|
||||
if not dingdanid:
|
||||
raise Exception('订单号不能为空')
|
||||
if not notify_url:
|
||||
raise Exception('缺少微信回调地址 notify_url')
|
||||
if not club_id:
|
||||
raise Exception('缺少 club_id,无法生成小程序支付码')
|
||||
|
||||
cached = get_native_pay_session(dingdanid) or {}
|
||||
cached_img = (cached.get('qr_image') or '').strip()
|
||||
cached_scene = (cached.get('scene') or '').strip()
|
||||
if cached_img and cached_scene and resolve_scene_to_dingdanid(cached_scene) == dingdanid:
|
||||
logger.info(
|
||||
'[WX_NATIVE_MINICODE] reuse dingdan=%s club=%s scene=%s',
|
||||
dingdanid, club_id, cached_scene,
|
||||
)
|
||||
return {
|
||||
'qr_code': f'minicode:{cached_scene}',
|
||||
'qr_image': cached_img,
|
||||
'scene': cached_scene,
|
||||
'pay_method': PAY_METHOD_WECHAT_NATIVE,
|
||||
}
|
||||
|
||||
pay_scene = _new_scene()
|
||||
png = _fetch_mini_program_code_png(club_id=club_id, scene=pay_scene)
|
||||
qr_image = _png_to_data_url(png)
|
||||
|
||||
# 给相册/链接兜底页留会话(主二维码仍是 weixin://)
|
||||
save_native_pay_session(
|
||||
dingdanid,
|
||||
jine=jine,
|
||||
body=body,
|
||||
notify_url=notify_url,
|
||||
club_id=club_id,
|
||||
scene=pay_scene,
|
||||
)
|
||||
bridge = build_native_bridge_url(dingdanid, club_id)
|
||||
# 把图片也塞进 session,便于复用
|
||||
sess = get_native_pay_session(dingdanid) or {}
|
||||
sess['qr_image'] = qr_image
|
||||
cache.set(_session_key(dingdanid), sess, CACHE_TTL_SECONDS)
|
||||
cache.set(_scene_key(pay_scene), dingdanid, CACHE_TTL_SECONDS)
|
||||
|
||||
cached = cache.get(_cache_key(dingdanid)) or {}
|
||||
cached_url = (cached.get('code_url') or '').strip()
|
||||
if cached_url:
|
||||
logger.info(
|
||||
'[WX_NATIVE] reuse code_url dingdan=%s club=%s scene=%s',
|
||||
dingdanid, club_id, scene,
|
||||
)
|
||||
return _native_pay_response(cached_url, bridge_url=bridge)
|
||||
|
||||
pay_cfg = get_wechat_v2_config(club_id)
|
||||
log_wechat_v2_pay_diag(
|
||||
pay_cfg, scene=f'native:{scene}', openid='', order_hint=dingdanid,
|
||||
)
|
||||
appid = pay_cfg.get('appid') or ''
|
||||
mch_id = pay_cfg.get('mch_id') or ''
|
||||
key = pay_cfg.get('key') or ''
|
||||
if not all([appid, mch_id, key]):
|
||||
raise Exception(
|
||||
f'微信 Native 配置不完整 club={pay_cfg.get("club_id")} '
|
||||
f'appid={bool(appid)} mch_id={bool(mch_id)} mch_key={bool(key)}'
|
||||
)
|
||||
|
||||
nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
|
||||
total_fee = yuan_to_fen(jine)
|
||||
ip = (client_ip or '127.0.0.1').split(',')[0].strip() or '127.0.0.1'
|
||||
|
||||
sign_data = {
|
||||
'appid': appid,
|
||||
'mch_id': mch_id,
|
||||
'nonce_str': nonce_str,
|
||||
'body': body,
|
||||
'out_trade_no': dingdanid,
|
||||
'total_fee': total_fee,
|
||||
'spbill_create_ip': ip,
|
||||
'notify_url': notify_url,
|
||||
'trade_type': 'NATIVE',
|
||||
}
|
||||
sorted_items = sorted(sign_data.items(), key=lambda x: x[0])
|
||||
sign_string = '&'.join(f'{k}={v}' for k, v in sorted_items if v) + f'&key={key}'
|
||||
sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper()
|
||||
|
||||
xml_data = f"""<xml>
|
||||
<appid>{appid}</appid>
|
||||
<body>{body}</body>
|
||||
<mch_id>{mch_id}</mch_id>
|
||||
<nonce_str>{nonce_str}</nonce_str>
|
||||
<notify_url>{notify_url}</notify_url>
|
||||
<out_trade_no>{dingdanid}</out_trade_no>
|
||||
<spbill_create_ip>{ip}</spbill_create_ip>
|
||||
<total_fee>{total_fee}</total_fee>
|
||||
<trade_type>NATIVE</trade_type>
|
||||
<sign>{sign}</sign>
|
||||
</xml>"""
|
||||
|
||||
response = requests.post(
|
||||
'https://api.mch.weixin.qq.com/pay/unifiedorder',
|
||||
data=xml_data.encode('utf-8'),
|
||||
headers={'Content-Type': 'application/xml'},
|
||||
timeout=10,
|
||||
)
|
||||
parsed = parse_wechat_v2_unifiedorder_response(
|
||||
response.content,
|
||||
log_prefix=f'WX_NATIVE_{scene}',
|
||||
expect='code_url',
|
||||
)
|
||||
code_url = parsed['code_url']
|
||||
cache.set(_cache_key(dingdanid), {'code_url': code_url}, CACHE_TTL_SECONDS)
|
||||
logger.warning(
|
||||
'[WX_NATIVE] unifiedorder ok dingdan=%s club=%s mch=%s scene=%s code=%s',
|
||||
dingdanid, pay_cfg.get('club_id'), mch_id, scene, code_url[:48],
|
||||
'[WX_NATIVE_MINICODE] ok dingdan=%s club=%s biz_scene=%s pay_scene=%s',
|
||||
dingdanid, club_id, scene, pay_scene,
|
||||
)
|
||||
return _native_pay_response(code_url, bridge_url=bridge)
|
||||
return {
|
||||
'qr_code': f'minicode:{pay_scene}',
|
||||
'qr_image': qr_image,
|
||||
'scene': pay_scene,
|
||||
'pay_method': PAY_METHOD_WECHAT_NATIVE,
|
||||
}
|
||||
|
||||
|
||||
def build_official_jsapi_pay_params(
|
||||
def build_mini_jsapi_pay_params(
|
||||
*,
|
||||
dingdanid,
|
||||
jine,
|
||||
@@ -196,21 +210,38 @@ def build_official_jsapi_pay_params(
|
||||
club_id=None,
|
||||
client_ip='127.0.0.1',
|
||||
):
|
||||
"""服务号 AppID + 服务号 openid 做 JSAPI,供相册 HTTPS 中转页调起。"""
|
||||
pay_cfg = get_wechat_v2_config(club_id, app_source='official')
|
||||
"""小程序 AppID + 小程序 openid → JSAPI,供相册扫码落地页调起密码。"""
|
||||
from jituan.constants import FUBEI_BIZ_CZJILU, FUBEI_BIZ_ORDER
|
||||
from jituan.services.mini_pay_router import maybe_create_fubei_jsapi_params
|
||||
|
||||
pay_cfg = get_wechat_v2_config(club_id, app_source='mini')
|
||||
log_wechat_v2_pay_diag(
|
||||
pay_cfg, scene='native_album_jsapi', openid=openid, order_hint=dingdanid,
|
||||
pay_cfg, scene='native_minicode_jsapi', openid=openid, order_hint=dingdanid,
|
||||
)
|
||||
appid = pay_cfg.get('appid') or ''
|
||||
mch_id = pay_cfg.get('mch_id') or ''
|
||||
key = pay_cfg.get('key') or ''
|
||||
if not openid:
|
||||
raise Exception('缺少小程序 openid')
|
||||
|
||||
biz = FUBEI_BIZ_CZJILU if str(dingdanid).startswith('Cz') or str(dingdanid).startswith('FK') or str(dingdanid).startswith('KH') else FUBEI_BIZ_ORDER
|
||||
fb = maybe_create_fubei_jsapi_params(
|
||||
club_id=club_id,
|
||||
out_trade_no=dingdanid,
|
||||
amount_yuan=jine,
|
||||
body=body or '微信支付',
|
||||
openid=openid,
|
||||
notify_url=notify_url,
|
||||
biz_type=biz,
|
||||
)
|
||||
if fb is not None:
|
||||
return fb
|
||||
|
||||
if not all([appid, mch_id, key]):
|
||||
raise Exception(
|
||||
f'服务号支付配置不完整 club={pay_cfg.get("club_id")} '
|
||||
f'请配置 official_appid 与商户密钥'
|
||||
f'小程序支付配置不完整 club={pay_cfg.get("club_id")} '
|
||||
f'appid={bool(appid)} mch_id={bool(mch_id)} mch_key={bool(key)}'
|
||||
)
|
||||
if not openid:
|
||||
raise Exception('缺少服务号 openid')
|
||||
|
||||
nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
|
||||
total_fee = yuan_to_fen(jine)
|
||||
@@ -255,7 +286,7 @@ def build_official_jsapi_pay_params(
|
||||
)
|
||||
parsed = parse_wechat_v2_unifiedorder_response(
|
||||
response.content,
|
||||
log_prefix='WX_NATIVE_ALBUM_JSAPI',
|
||||
log_prefix='WX_NATIVE_MINICODE_JSAPI',
|
||||
expect='prepay_id',
|
||||
)
|
||||
prepay_id = parsed['prepay_id']
|
||||
@@ -283,3 +314,101 @@ def build_official_jsapi_pay_params(
|
||||
'signType': sign_type,
|
||||
'paySign': pay_sign,
|
||||
}
|
||||
|
||||
|
||||
# 兼容旧 H5 中转页导入(若仍有请求)
|
||||
def build_native_bridge_url(dingdanid: str, club_id=None) -> str:
|
||||
from urllib.parse import quote
|
||||
q = f'out_trade_no={quote(dingdanid or "", safe="")}'
|
||||
if club_id:
|
||||
q += f'&club_id={quote(str(club_id), safe="")}'
|
||||
return f'https://www.abas.asia/hqhd/dingdan/wx-native-pay/?{q}'
|
||||
|
||||
|
||||
def build_official_jsapi_pay_params(**kwargs):
|
||||
"""保留给旧 H5 页;新主路径已改小程序码。"""
|
||||
kwargs = dict(kwargs)
|
||||
# 复用 mini 逻辑会错 openid 体系;仍走服务号
|
||||
from jituan.services.wechat_pay import get_wechat_v2_config as _cfg
|
||||
pay_cfg = _cfg(kwargs.get('club_id'), app_source='official')
|
||||
log_wechat_v2_pay_diag(
|
||||
pay_cfg,
|
||||
scene='native_album_jsapi_legacy',
|
||||
openid=kwargs.get('openid'),
|
||||
order_hint=kwargs.get('dingdanid'),
|
||||
)
|
||||
appid = pay_cfg.get('appid') or ''
|
||||
mch_id = pay_cfg.get('mch_id') or ''
|
||||
key = pay_cfg.get('key') or ''
|
||||
openid = kwargs.get('openid') or ''
|
||||
if not all([appid, mch_id, key, openid]):
|
||||
raise Exception('服务号支付配置不完整')
|
||||
dingdanid = kwargs.get('dingdanid')
|
||||
jine = kwargs.get('jine')
|
||||
body = kwargs.get('body') or '微信支付'
|
||||
notify_url = kwargs.get('notify_url')
|
||||
client_ip = kwargs.get('client_ip') or '127.0.0.1'
|
||||
nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
|
||||
total_fee = yuan_to_fen(jine)
|
||||
ip = (client_ip or '127.0.0.1').split(',')[0].strip() or '127.0.0.1'
|
||||
sign_data = {
|
||||
'appid': appid,
|
||||
'mch_id': mch_id,
|
||||
'nonce_str': nonce_str,
|
||||
'body': body,
|
||||
'out_trade_no': dingdanid,
|
||||
'total_fee': total_fee,
|
||||
'spbill_create_ip': ip,
|
||||
'notify_url': notify_url,
|
||||
'trade_type': 'JSAPI',
|
||||
'openid': openid,
|
||||
}
|
||||
sorted_items = sorted(sign_data.items(), key=lambda x: x[0])
|
||||
sign_string = '&'.join(f'{k}={v}' for k, v in sorted_items if v) + f'&key={key}'
|
||||
sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper()
|
||||
xml_data = f"""<xml>
|
||||
<appid>{appid}</appid>
|
||||
<body>{body}</body>
|
||||
<mch_id>{mch_id}</mch_id>
|
||||
<nonce_str>{nonce_str}</nonce_str>
|
||||
<notify_url>{notify_url}</notify_url>
|
||||
<openid>{openid}</openid>
|
||||
<out_trade_no>{dingdanid}</out_trade_no>
|
||||
<spbill_create_ip>{ip}</spbill_create_ip>
|
||||
<total_fee>{total_fee}</total_fee>
|
||||
<trade_type>JSAPI</trade_type>
|
||||
<sign>{sign}</sign>
|
||||
</xml>"""
|
||||
response = requests.post(
|
||||
'https://api.mch.weixin.qq.com/pay/unifiedorder',
|
||||
data=xml_data.encode('utf-8'),
|
||||
headers={'Content-Type': 'application/xml'},
|
||||
timeout=10,
|
||||
)
|
||||
parsed = parse_wechat_v2_unifiedorder_response(
|
||||
response.content, log_prefix='WX_NATIVE_OA_JSAPI', expect='prepay_id',
|
||||
)
|
||||
prepay_id = parsed['prepay_id']
|
||||
timestamp = str(int(time.time()))
|
||||
pay_nonce = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
|
||||
package = f'prepay_id={prepay_id}'
|
||||
sign_type = 'MD5'
|
||||
pay_sign_data = {
|
||||
'appId': appid,
|
||||
'timeStamp': timestamp,
|
||||
'nonceStr': pay_nonce,
|
||||
'package': package,
|
||||
'signType': sign_type,
|
||||
}
|
||||
pay_sign_string = '&'.join(
|
||||
f'{k}={pay_sign_data[k]}' for k in sorted(pay_sign_data.keys())
|
||||
) + f'&key={key}'
|
||||
pay_sign = hashlib.md5(pay_sign_string.encode('utf-8')).hexdigest().upper()
|
||||
return {
|
||||
'appId': appid,
|
||||
'timeStamp': timestamp,
|
||||
'nonceStr': pay_nonce,
|
||||
'package': package,
|
||||
'signType': sign_type,
|
||||
'paySign': pay_sign,
|
||||
}
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
from django.urls import path
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from .views import CreateOrderView, WechatPayNotifyView, AlipayPayNotifyView, alipay_pay_page, wx_native_pay_page, \
|
||||
from .views import CreateOrderView, WechatPayNotifyView, AlipayPayNotifyView, alipay_pay_page, wx_native_pay_page, WxNativeScanPayView, \
|
||||
PaymentFailView, PaymentVerifyView, ContinuePayView, DingdanHuoquView, DingdanXiangqingView, JiedanView, \
|
||||
ShangpinLeixingView, ShangjiaPaifaView, ShangjiaDingdanHuoquView, ShangjiaJiesuanView, \
|
||||
ShangjiaDingdanXiangqingView, ShangjiaChufaShenqingView, ShangjiaTuikuanShenqingView, \
|
||||
@@ -25,6 +25,7 @@ urlpatterns = [
|
||||
path('fubei-notify/', csrf_exempt(FubeiPayNotifyView.as_view()), name='付呗支付回调'),
|
||||
path('alipay-pay/', csrf_exempt(alipay_pay_page), name='支付宝H5中转页'),
|
||||
path('wx-native-pay/', csrf_exempt(wx_native_pay_page), name='微信Native扫码中转页'),
|
||||
path('wx-native-scan-pay', WxNativeScanPayView.as_view(), name='微信小程序码扫码支付'),
|
||||
path('shibai', PaymentFailView.as_view(), name='老板支付失败接口'),
|
||||
path('fucha', PaymentVerifyView.as_view(), name='老板支付成功复查接口'),
|
||||
path('dingdanhuoqu', DingdanHuoquView.as_view(), name='老板订单获取'),
|
||||
|
||||
@@ -11,6 +11,7 @@ from .payment import (
|
||||
ContinuePayView,
|
||||
alipay_pay_page,
|
||||
wx_native_pay_page,
|
||||
WxNativeScanPayView,
|
||||
)
|
||||
from .boss_orders import (
|
||||
DingdanHuoquView,
|
||||
@@ -75,6 +76,7 @@ __all__ = [
|
||||
"ContinuePayView",
|
||||
"alipay_pay_page",
|
||||
"wx_native_pay_page",
|
||||
"WxNativeScanPayView",
|
||||
"DingdanHuoquView", "DingdanXiangqingView", "JiedanView",
|
||||
"DingdanXiangqingView2", "JiedanView2",
|
||||
"ShangpinLeixingView", "ShangjiaPaifaView", "ShangjiaDingdanHuoquView",
|
||||
|
||||
@@ -651,6 +651,83 @@ def wx_native_pay_page(request):
|
||||
return render(request, 'wechat/native_pay.html', {'pay': pay, 'out_trade_no': out_trade_no})
|
||||
|
||||
|
||||
class WxNativeScanPayView(APIView):
|
||||
"""相册扫小程序码落地:用当前用户 openid 对未付款单做 JSAPI,直接出密码。"""
|
||||
authentication_classes = [JWTAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
from jituan.services.wechat_native_pay import (
|
||||
build_mini_jsapi_pay_params,
|
||||
get_native_pay_session,
|
||||
resolve_scene_to_dingdanid,
|
||||
)
|
||||
|
||||
scene = (request.data.get('scene') or '').strip()
|
||||
dingdanid = (request.data.get('dingdanid') or request.data.get('out_trade_no') or '').strip()
|
||||
if scene and not dingdanid:
|
||||
dingdanid = resolve_scene_to_dingdanid(scene)
|
||||
if not dingdanid:
|
||||
return Response({'code': 400, 'message': '无效支付码'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
session = get_native_pay_session(dingdanid)
|
||||
if not session:
|
||||
return Response({'code': 400, 'message': '支付已过期,请返回重新发起'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
club_id = (session.get('club_id') or '').strip()
|
||||
notify_url = (session.get('notify_url') or '').strip()
|
||||
if not notify_url:
|
||||
if dingdanid.startswith('CzHY'):
|
||||
notify_url = getattr(settings, 'HUIYUAN_PAY_NOTIFY_URL', '')
|
||||
elif dingdanid.startswith('CzYJ'):
|
||||
notify_url = getattr(settings, 'YAJIN_PAY_NOTIFY_URL', '')
|
||||
elif dingdanid.startswith('CzJF'):
|
||||
notify_url = getattr(settings, 'JIFEN_PAY_NOTIFY_URL', '')
|
||||
elif dingdanid.startswith('CzSJ'):
|
||||
notify_url = getattr(settings, 'SHANGJIA_CZ_PAY_NOTIFY_URL', '')
|
||||
elif dingdanid.startswith('FK'):
|
||||
notify_url = getattr(settings, 'FAKUAN_PAY_NOTIFY_URL', '')
|
||||
elif dingdanid.startswith('KH'):
|
||||
notify_url = getattr(settings, 'KAOHE_PAY_NOTIFY_URL', '')
|
||||
elif dingdanid.startswith('CzZP'):
|
||||
notify_url = getattr(settings, 'ZHUANPAN_PAY_NOTIFY_URL', '')
|
||||
else:
|
||||
notify_url = getattr(settings, 'WEIXIN_NOTIFY_URL', '')
|
||||
|
||||
pay_openid, _ = get_payment_openid(request, club_id=club_id or None)
|
||||
if not pay_openid:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '当前小程序未绑定微信,请退出后重新登录再支付',
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
xff = request.META.get('HTTP_X_FORWARDED_FOR')
|
||||
client_ip = (xff.split(',')[0].strip() if xff else '') or request.META.get('REMOTE_ADDR') or '127.0.0.1'
|
||||
try:
|
||||
pay_params = build_mini_jsapi_pay_params(
|
||||
dingdanid=dingdanid,
|
||||
jine=session.get('jine'),
|
||||
body=session.get('body') or '微信支付',
|
||||
notify_url=notify_url,
|
||||
openid=pay_openid,
|
||||
club_id=club_id or None,
|
||||
client_ip=client_ip,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.exception('小程序码扫码 JSAPI 下单失败 dingdan=%s', dingdanid)
|
||||
return Response({
|
||||
'code': 500,
|
||||
'message': f'下单失败:{exc}',
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
return Response({
|
||||
'code': 200,
|
||||
'message': 'ok',
|
||||
'dingdanid': dingdanid,
|
||||
'payParams': pay_params,
|
||||
})
|
||||
|
||||
|
||||
class PaymentVerifyView(APIView):
|
||||
"""
|
||||
订单支付成功复查接口 - 前端支付成功后调用,确认订单状态
|
||||
|
||||
Reference in new issue
Block a user