更新ios
This commit is contained in:
431
app.py
431
app.py
@@ -42,10 +42,12 @@ else:
|
|||||||
|
|
||||||
import base64
|
import base64
|
||||||
import csv
|
import csv
|
||||||
|
import hashlib
|
||||||
import io
|
import io
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import urllib
|
import urllib
|
||||||
|
from urllib.parse import quote
|
||||||
import uuid
|
import uuid
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
import qrcode
|
import qrcode
|
||||||
@@ -3646,6 +3648,435 @@ def _parse_xml_callback(xml_data):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# 微信 H5 JSAPI 支付相关 API
|
||||||
|
# ========================================
|
||||||
|
|
||||||
|
@app.route('/api/payment/wechat/h5/auth-url', methods=['GET'])
|
||||||
|
def get_wechat_h5_auth_url():
|
||||||
|
"""
|
||||||
|
获取微信 H5 网页授权 URL
|
||||||
|
|
||||||
|
用于在微信内置浏览器中获取用户 openid,以便发起 JSAPI 支付
|
||||||
|
|
||||||
|
Query Parameters:
|
||||||
|
plan_name: 套餐名称
|
||||||
|
billing_cycle: 计费周期
|
||||||
|
promo_code: 优惠码(可选)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{ "success": true, "auth_url": "https://open.weixin.qq.com/..." }
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if 'user_id' not in session:
|
||||||
|
return jsonify({'success': False, 'error': '未登录'}), 401
|
||||||
|
|
||||||
|
plan_name = request.args.get('plan_name')
|
||||||
|
billing_cycle = request.args.get('billing_cycle')
|
||||||
|
promo_code = request.args.get('promo_code', '')
|
||||||
|
|
||||||
|
if not plan_name or not billing_cycle:
|
||||||
|
return jsonify({'success': False, 'error': '参数不完整'}), 400
|
||||||
|
|
||||||
|
# 构建 state 参数(包含订单信息,用于回调后恢复)
|
||||||
|
import base64
|
||||||
|
state_data = {
|
||||||
|
'user_id': session['user_id'],
|
||||||
|
'plan_name': plan_name,
|
||||||
|
'billing_cycle': billing_cycle,
|
||||||
|
'promo_code': promo_code,
|
||||||
|
'timestamp': int(time.time())
|
||||||
|
}
|
||||||
|
state = base64.urlsafe_b64encode(json.dumps(state_data).encode()).decode()
|
||||||
|
|
||||||
|
# 回调地址
|
||||||
|
redirect_uri = quote('https://api.valuefrontier.cn/api/payment/wechat/h5/callback')
|
||||||
|
|
||||||
|
# 构建微信授权 URL(使用 snsapi_base 静默授权,只获取 openid)
|
||||||
|
auth_url = (
|
||||||
|
f"https://open.weixin.qq.com/connect/oauth2/authorize?"
|
||||||
|
f"appid={WECHAT_MP_APPID}"
|
||||||
|
f"&redirect_uri={redirect_uri}"
|
||||||
|
f"&response_type=code"
|
||||||
|
f"&scope=snsapi_base"
|
||||||
|
f"&state={state}"
|
||||||
|
f"#wechat_redirect"
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"[微信H5支付] 生成授权URL: user_id={session['user_id']}, plan={plan_name}")
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'auth_url': auth_url
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[微信H5支付] 生成授权URL失败: {e}")
|
||||||
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/payment/wechat/h5/callback', methods=['GET'])
|
||||||
|
def wechat_h5_payment_callback():
|
||||||
|
"""
|
||||||
|
微信 H5 网页授权回调
|
||||||
|
|
||||||
|
微信授权后会携带 code 和 state 回调到此接口
|
||||||
|
获取 openid 后重定向到前端支付页面
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
code = request.args.get('code')
|
||||||
|
state = request.args.get('state')
|
||||||
|
|
||||||
|
if not code:
|
||||||
|
print("[微信H5支付] 回调缺少 code 参数")
|
||||||
|
return redirect('https://valuefrontier.cn/home/pages/account/subscription?wechat_pay_error=missing_code')
|
||||||
|
|
||||||
|
# 解析 state
|
||||||
|
try:
|
||||||
|
import base64
|
||||||
|
state_data = json.loads(base64.urlsafe_b64decode(state).decode())
|
||||||
|
user_id = state_data.get('user_id')
|
||||||
|
plan_name = state_data.get('plan_name')
|
||||||
|
billing_cycle = state_data.get('billing_cycle')
|
||||||
|
promo_code = state_data.get('promo_code', '')
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[微信H5支付] 解析 state 失败: {e}")
|
||||||
|
return redirect('https://valuefrontier.cn/home/pages/account/subscription?wechat_pay_error=invalid_state')
|
||||||
|
|
||||||
|
# 使用 code 换取 openid
|
||||||
|
token_url = "https://api.weixin.qq.com/sns/oauth2/access_token"
|
||||||
|
params = {
|
||||||
|
'appid': WECHAT_MP_APPID,
|
||||||
|
'secret': WECHAT_MP_APPSECRET,
|
||||||
|
'code': code,
|
||||||
|
'grant_type': 'authorization_code'
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.get(token_url, params=params, timeout=10)
|
||||||
|
result = response.json()
|
||||||
|
|
||||||
|
if 'openid' not in result:
|
||||||
|
print(f"[微信H5支付] 获取 openid 失败: {result}")
|
||||||
|
error_msg = result.get('errmsg', 'unknown_error')
|
||||||
|
return redirect(f'https://valuefrontier.cn/home/pages/account/subscription?wechat_pay_error={error_msg}')
|
||||||
|
|
||||||
|
openid = result['openid']
|
||||||
|
print(f"[微信H5支付] 获取 openid 成功: {openid[:10]}...")
|
||||||
|
|
||||||
|
# 将 openid 加密存储,避免在 URL 中暴露
|
||||||
|
# 使用简单的 base64 + 时间戳校验
|
||||||
|
openid_token_data = {
|
||||||
|
'openid': openid,
|
||||||
|
'user_id': user_id,
|
||||||
|
'timestamp': int(time.time()),
|
||||||
|
'plan_name': plan_name,
|
||||||
|
'billing_cycle': billing_cycle,
|
||||||
|
'promo_code': promo_code
|
||||||
|
}
|
||||||
|
openid_token = base64.urlsafe_b64encode(json.dumps(openid_token_data).encode()).decode()
|
||||||
|
|
||||||
|
# 重定向到前端支付页面
|
||||||
|
redirect_url = (
|
||||||
|
f"https://valuefrontier.cn/home/pages/account/subscription"
|
||||||
|
f"?wechat_h5_pay=ready"
|
||||||
|
f"&token={openid_token}"
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"[微信H5支付] 重定向到前端: {redirect_url[:80]}...")
|
||||||
|
return redirect(redirect_url)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[微信H5支付] 回调处理失败: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return redirect(f'https://valuefrontier.cn/home/pages/account/subscription?wechat_pay_error=callback_failed')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/payment/wechat/jsapi/create-order', methods=['POST'])
|
||||||
|
def create_wechat_jsapi_order():
|
||||||
|
"""
|
||||||
|
创建微信 JSAPI 支付订单
|
||||||
|
|
||||||
|
用于微信内置浏览器中的 H5 支付
|
||||||
|
|
||||||
|
Request Body:
|
||||||
|
{
|
||||||
|
"token": "xxx", // 从 OAuth 回调获取的 token(包含 openid 等信息)
|
||||||
|
}
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"order_id": "xxx",
|
||||||
|
"order_no": "xxx",
|
||||||
|
"amount": 99.00,
|
||||||
|
"payment_params": {
|
||||||
|
"appId": "xxx",
|
||||||
|
"timeStamp": "xxx",
|
||||||
|
"nonceStr": "xxx",
|
||||||
|
"package": "prepay_id=xxx",
|
||||||
|
"signType": "MD5",
|
||||||
|
"paySign": "xxx"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
token = data.get('token')
|
||||||
|
|
||||||
|
if not token:
|
||||||
|
return jsonify({'success': False, 'error': '缺少 token 参数'}), 400
|
||||||
|
|
||||||
|
# 解析 token
|
||||||
|
try:
|
||||||
|
import base64
|
||||||
|
token_data = json.loads(base64.urlsafe_b64decode(token).decode())
|
||||||
|
openid = token_data.get('openid')
|
||||||
|
user_id = token_data.get('user_id')
|
||||||
|
plan_name = token_data.get('plan_name')
|
||||||
|
billing_cycle = token_data.get('billing_cycle')
|
||||||
|
promo_code = token_data.get('promo_code', '') or None
|
||||||
|
timestamp = token_data.get('timestamp', 0)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[JSAPI支付] 解析 token 失败: {e}")
|
||||||
|
return jsonify({'success': False, 'error': 'token 无效'}), 400
|
||||||
|
|
||||||
|
# 验证 token 时效(30分钟内有效)
|
||||||
|
if time.time() - timestamp > 1800:
|
||||||
|
return jsonify({'success': False, 'error': 'token 已过期,请重新授权'}), 400
|
||||||
|
|
||||||
|
# 验证用户登录状态
|
||||||
|
if 'user_id' not in session or session['user_id'] != user_id:
|
||||||
|
return jsonify({'success': False, 'error': '用户状态异常,请重新登录'}), 401
|
||||||
|
|
||||||
|
print(f"[JSAPI支付] 创建订单: user_id={user_id}, plan={plan_name}, openid={openid[:10]}...")
|
||||||
|
|
||||||
|
# 计算价格
|
||||||
|
price_result = calculate_subscription_price_simple(user_id, plan_name, billing_cycle, promo_code)
|
||||||
|
|
||||||
|
if 'error' in price_result:
|
||||||
|
return jsonify({'success': False, 'error': price_result['error']}), 400
|
||||||
|
|
||||||
|
amount = price_result['final_amount']
|
||||||
|
|
||||||
|
# 检查是否为免费升级
|
||||||
|
if amount <= 0 and price_result.get('is_upgrade'):
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'error': '当前剩余价值可直接免费升级',
|
||||||
|
'should_free_upgrade': True
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
# 生成订单号
|
||||||
|
order_no = f"WXH5{int(time.time())}{user_id}{uuid.uuid4().hex[:6].upper()}"
|
||||||
|
|
||||||
|
# 创建订单记录
|
||||||
|
order = PaymentOrder(
|
||||||
|
user_id=user_id,
|
||||||
|
order_no=order_no,
|
||||||
|
plan_name=plan_name,
|
||||||
|
billing_cycle=billing_cycle,
|
||||||
|
amount=amount,
|
||||||
|
original_amount=price_result.get('original_price', amount),
|
||||||
|
discount_amount=price_result.get('discount_amount', 0),
|
||||||
|
status='pending',
|
||||||
|
payment_method='wechat_jsapi',
|
||||||
|
payment_source='h5'
|
||||||
|
)
|
||||||
|
|
||||||
|
# 关联优惠码
|
||||||
|
if promo_code and price_result.get('promo_applied'):
|
||||||
|
promo = PromoCode.query.filter_by(code=promo_code.upper()).first()
|
||||||
|
if promo:
|
||||||
|
order.promo_code_id = promo.id
|
||||||
|
|
||||||
|
db.session.add(order)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# 调用微信统一下单接口(JSAPI 模式)
|
||||||
|
jsapi_result = _create_wechat_jsapi_prepay(order_no, amount, plan_name, openid)
|
||||||
|
|
||||||
|
if not jsapi_result['success']:
|
||||||
|
# 标记订单失败
|
||||||
|
order.status = 'failed'
|
||||||
|
order.remark = jsapi_result.get('error', '创建预支付订单失败')
|
||||||
|
db.session.commit()
|
||||||
|
return jsonify({'success': False, 'error': jsapi_result.get('error', '创建支付订单失败')}), 500
|
||||||
|
|
||||||
|
# 更新订单的 prepay_id
|
||||||
|
order.prepay_id = jsapi_result.get('prepay_id')
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
print(f"[JSAPI支付] 订单创建成功: order_no={order_no}, amount={amount}")
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'data': {
|
||||||
|
'order_id': order.id,
|
||||||
|
'order_no': order_no,
|
||||||
|
'amount': amount,
|
||||||
|
'plan_name': plan_name,
|
||||||
|
'payment_params': jsapi_result['payment_params']
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
print(f"[JSAPI支付] 创建订单失败: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return jsonify({'success': False, 'error': f'创建订单失败: {str(e)}'}), 500
|
||||||
|
|
||||||
|
|
||||||
|
def _create_wechat_jsapi_prepay(order_no, amount, body, openid):
|
||||||
|
"""
|
||||||
|
调用微信统一下单接口创建 JSAPI 预支付订单
|
||||||
|
|
||||||
|
Args:
|
||||||
|
order_no: 商户订单号
|
||||||
|
amount: 金额(元)
|
||||||
|
body: 商品描述
|
||||||
|
openid: 用户的 openid
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{
|
||||||
|
'success': True/False,
|
||||||
|
'prepay_id': 'xxx',
|
||||||
|
'payment_params': {...} # 前端调用支付需要的参数
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from wechat_pay_config import WECHAT_PAY_CONFIG
|
||||||
|
|
||||||
|
# 金额转换为分
|
||||||
|
total_fee = int(amount * 100)
|
||||||
|
|
||||||
|
# 构建请求参数
|
||||||
|
params = {
|
||||||
|
'appid': WECHAT_MP_APPID, # 使用服务号 AppID
|
||||||
|
'mch_id': WECHAT_PAY_CONFIG['mch_id'],
|
||||||
|
'nonce_str': uuid.uuid4().hex,
|
||||||
|
'body': f'价值前研-{body}订阅',
|
||||||
|
'out_trade_no': order_no,
|
||||||
|
'total_fee': str(total_fee),
|
||||||
|
'spbill_create_ip': request.remote_addr or '127.0.0.1',
|
||||||
|
'notify_url': WECHAT_PAY_CONFIG['notify_url'],
|
||||||
|
'trade_type': 'JSAPI',
|
||||||
|
'openid': openid
|
||||||
|
}
|
||||||
|
|
||||||
|
# 生成签名
|
||||||
|
api_key = WECHAT_PAY_CONFIG['api_key']
|
||||||
|
sign = _generate_wechat_sign(params, api_key)
|
||||||
|
params['sign'] = sign
|
||||||
|
|
||||||
|
# 转换为 XML
|
||||||
|
xml_data = _dict_to_xml(params)
|
||||||
|
|
||||||
|
print(f"[JSAPI支付] 统一下单请求: order_no={order_no}, amount={amount}元={total_fee}分")
|
||||||
|
|
||||||
|
# 发送请求
|
||||||
|
response = requests.post(
|
||||||
|
'https://api.mch.weixin.qq.com/pay/unifiedorder',
|
||||||
|
data=xml_data.encode('utf-8'),
|
||||||
|
headers={'Content-Type': 'text/xml'},
|
||||||
|
timeout=30
|
||||||
|
)
|
||||||
|
|
||||||
|
# 解析响应
|
||||||
|
result = _xml_to_dict(response.text)
|
||||||
|
print(f"[JSAPI支付] 统一下单响应: {result}")
|
||||||
|
|
||||||
|
if result.get('return_code') != 'SUCCESS':
|
||||||
|
error_msg = result.get('return_msg', '通信失败')
|
||||||
|
print(f"[JSAPI支付] 统一下单失败: {error_msg}")
|
||||||
|
return {'success': False, 'error': error_msg}
|
||||||
|
|
||||||
|
if result.get('result_code') != 'SUCCESS':
|
||||||
|
error_msg = result.get('err_code_des') or result.get('err_code', '业务失败')
|
||||||
|
print(f"[JSAPI支付] 统一下单业务失败: {error_msg}")
|
||||||
|
return {'success': False, 'error': error_msg}
|
||||||
|
|
||||||
|
prepay_id = result.get('prepay_id')
|
||||||
|
if not prepay_id:
|
||||||
|
return {'success': False, 'error': '未获取到 prepay_id'}
|
||||||
|
|
||||||
|
# 生成前端调用支付需要的参数
|
||||||
|
timestamp = str(int(time.time()))
|
||||||
|
nonce_str = uuid.uuid4().hex
|
||||||
|
package = f"prepay_id={prepay_id}"
|
||||||
|
|
||||||
|
# 签名参数(注意:这里的 appId 是大写 I)
|
||||||
|
sign_params = {
|
||||||
|
'appId': WECHAT_MP_APPID,
|
||||||
|
'timeStamp': timestamp,
|
||||||
|
'nonceStr': nonce_str,
|
||||||
|
'package': package,
|
||||||
|
'signType': 'MD5'
|
||||||
|
}
|
||||||
|
pay_sign = _generate_wechat_sign(sign_params, api_key)
|
||||||
|
|
||||||
|
payment_params = {
|
||||||
|
'appId': WECHAT_MP_APPID,
|
||||||
|
'timeStamp': timestamp,
|
||||||
|
'nonceStr': nonce_str,
|
||||||
|
'package': package,
|
||||||
|
'signType': 'MD5',
|
||||||
|
'paySign': pay_sign
|
||||||
|
}
|
||||||
|
|
||||||
|
print(f"[JSAPI支付] 支付参数生成成功: prepay_id={prepay_id[:20]}...")
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'prepay_id': prepay_id,
|
||||||
|
'payment_params': payment_params
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[JSAPI支付] 统一下单异常: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return {'success': False, 'error': str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
def _generate_wechat_sign(params, api_key):
|
||||||
|
"""生成微信支付签名"""
|
||||||
|
# 过滤空值并排序
|
||||||
|
filtered = {k: v for k, v in params.items() if v and k != 'sign'}
|
||||||
|
sorted_params = sorted(filtered.items())
|
||||||
|
|
||||||
|
# 拼接字符串
|
||||||
|
string_a = "&".join([f"{k}={v}" for k, v in sorted_params])
|
||||||
|
string_sign_temp = f"{string_a}&key={api_key}"
|
||||||
|
|
||||||
|
# MD5 签名
|
||||||
|
sign = hashlib.md5(string_sign_temp.encode('utf-8')).hexdigest().upper()
|
||||||
|
return sign
|
||||||
|
|
||||||
|
|
||||||
|
def _dict_to_xml(data):
|
||||||
|
"""将字典转换为 XML"""
|
||||||
|
xml = ["<xml>"]
|
||||||
|
for key, value in data.items():
|
||||||
|
xml.append(f"<{key}><![CDATA[{value}]]></{key}>")
|
||||||
|
xml.append("</xml>")
|
||||||
|
return "".join(xml)
|
||||||
|
|
||||||
|
|
||||||
|
def _xml_to_dict(xml_data):
|
||||||
|
"""将 XML 转换为字典"""
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
try:
|
||||||
|
root = ET.fromstring(xml_data)
|
||||||
|
return {child.tag: child.text for child in root}
|
||||||
|
except Exception as e:
|
||||||
|
print(f"XML 解析失败: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
# ========================================
|
# ========================================
|
||||||
# 支付宝支付相关API
|
# 支付宝支付相关API
|
||||||
# ========================================
|
# ========================================
|
||||||
|
|||||||
@@ -270,6 +270,130 @@ export default function SubscriptionContentNew() {
|
|||||||
checkAlipayReturn();
|
checkAlipayReturn();
|
||||||
}, [toast]);
|
}, [toast]);
|
||||||
|
|
||||||
|
// 检查是否从微信 H5 支付授权返回
|
||||||
|
useEffect(() => {
|
||||||
|
const handleWechatH5PayReturn = async () => {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const wechatH5Pay = urlParams.get('wechat_h5_pay');
|
||||||
|
const token = urlParams.get('token');
|
||||||
|
const wechatPayError = urlParams.get('wechat_pay_error');
|
||||||
|
|
||||||
|
// 处理授权错误
|
||||||
|
if (wechatPayError) {
|
||||||
|
toast({
|
||||||
|
title: '微信授权失败',
|
||||||
|
description: wechatPayError === 'missing_code' ? '授权被取消' : wechatPayError,
|
||||||
|
status: 'error',
|
||||||
|
duration: 5000,
|
||||||
|
isClosable: true,
|
||||||
|
});
|
||||||
|
window.history.replaceState({}, document.title, window.location.pathname);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理授权成功,发起 JSAPI 支付
|
||||||
|
if (wechatH5Pay === 'ready' && token) {
|
||||||
|
console.log('[微信H5支付] 授权成功,准备发起支付');
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: '授权成功',
|
||||||
|
description: '正在发起微信支付...',
|
||||||
|
status: 'info',
|
||||||
|
duration: 2000,
|
||||||
|
isClosable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 调用 JSAPI 创建订单接口
|
||||||
|
const response = await fetch(`${getApiBase()}/api/payment/wechat/jsapi/create-order`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
credentials: 'include',
|
||||||
|
body: JSON.stringify({ token }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!data.success) {
|
||||||
|
throw new Error(data.error || '创建订单失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[微信H5支付] 订单创建成功,调用 WeixinJSBridge');
|
||||||
|
|
||||||
|
// 调用 WeixinJSBridge 发起支付
|
||||||
|
const paymentParams = data.data.payment_params;
|
||||||
|
|
||||||
|
// 等待 WeixinJSBridge 就绪
|
||||||
|
const invokePay = () => {
|
||||||
|
(window as any).WeixinJSBridge.invoke(
|
||||||
|
'getBrandWCPayRequest',
|
||||||
|
{
|
||||||
|
appId: paymentParams.appId,
|
||||||
|
timeStamp: paymentParams.timeStamp,
|
||||||
|
nonceStr: paymentParams.nonceStr,
|
||||||
|
package: paymentParams.package,
|
||||||
|
signType: paymentParams.signType,
|
||||||
|
paySign: paymentParams.paySign,
|
||||||
|
},
|
||||||
|
(res: any) => {
|
||||||
|
// 清除 URL 参数
|
||||||
|
window.history.replaceState({}, document.title, window.location.pathname);
|
||||||
|
|
||||||
|
if (res.err_msg === 'get_brand_wcpay_request:ok') {
|
||||||
|
toast({
|
||||||
|
title: '支付成功!',
|
||||||
|
description: '您的订阅已激活',
|
||||||
|
status: 'success',
|
||||||
|
duration: 5000,
|
||||||
|
isClosable: true,
|
||||||
|
});
|
||||||
|
setTimeout(() => window.location.reload(), 2000);
|
||||||
|
} else if (res.err_msg === 'get_brand_wcpay_request:cancel') {
|
||||||
|
toast({
|
||||||
|
title: '支付已取消',
|
||||||
|
description: '您可以稍后重新支付',
|
||||||
|
status: 'warning',
|
||||||
|
duration: 3000,
|
||||||
|
isClosable: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toast({
|
||||||
|
title: '支付失败',
|
||||||
|
description: res.err_msg || '请稍后重试',
|
||||||
|
status: 'error',
|
||||||
|
duration: 3000,
|
||||||
|
isClosable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof (window as any).WeixinJSBridge !== 'undefined') {
|
||||||
|
invokePay();
|
||||||
|
} else {
|
||||||
|
document.addEventListener('WeixinJSBridgeReady', invokePay, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('[微信H5支付] 支付失败:', error);
|
||||||
|
toast({
|
||||||
|
title: '支付失败',
|
||||||
|
description: error.message,
|
||||||
|
status: 'error',
|
||||||
|
duration: 5000,
|
||||||
|
isClosable: true,
|
||||||
|
});
|
||||||
|
window.history.replaceState({}, document.title, window.location.pathname);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleWechatH5PayReturn();
|
||||||
|
}, [toast]);
|
||||||
|
|
||||||
const fetchSubscriptionPlans = async () => {
|
const fetchSubscriptionPlans = async () => {
|
||||||
try {
|
try {
|
||||||
logger.debug('SubscriptionContentNew', '正在获取订阅套餐');
|
logger.debug('SubscriptionContentNew', '正在获取订阅套餐');
|
||||||
@@ -484,6 +608,54 @@ export default function SubscriptionContentNew() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检测是否在微信内置浏览器中
|
||||||
|
const isWechatBrowser = /MicroMessenger/i.test(navigator.userAgent);
|
||||||
|
|
||||||
|
// 微信浏览器内选择微信支付:使用 JSAPI 支付
|
||||||
|
if (paymentMethod === 'wechat' && isWechatBrowser) {
|
||||||
|
console.log('[微信H5支付] 检测到微信浏览器,跳转到 OAuth 授权');
|
||||||
|
|
||||||
|
subscriptionEvents.trackPaymentInitiated({
|
||||||
|
planName: selectedPlan.name,
|
||||||
|
paymentMethod: 'wechat_jsapi',
|
||||||
|
amount: price,
|
||||||
|
billingCycle: selectedCycle,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取微信 OAuth 授权 URL
|
||||||
|
const authUrlResponse = await fetch(
|
||||||
|
`${getApiBase()}/api/payment/wechat/h5/auth-url?` +
|
||||||
|
`plan_name=${encodeURIComponent(selectedPlan.name)}` +
|
||||||
|
`&billing_cycle=${encodeURIComponent(selectedCycle)}` +
|
||||||
|
`&promo_code=${encodeURIComponent(promoCodeApplied ? promoCode : '')}`,
|
||||||
|
{ credentials: 'include' }
|
||||||
|
);
|
||||||
|
|
||||||
|
const authUrlData = await authUrlResponse.json();
|
||||||
|
|
||||||
|
if (!authUrlData.success) {
|
||||||
|
throw new Error(authUrlData.error || '获取授权链接失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: '正在跳转微信授权',
|
||||||
|
description: '请在弹出的页面中确认授权',
|
||||||
|
status: 'info',
|
||||||
|
duration: 2000,
|
||||||
|
isClosable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 关闭支付弹窗
|
||||||
|
onClose();
|
||||||
|
|
||||||
|
// 跳转到微信授权页面
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = authUrlData.auth_url;
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const paymentMethodName = paymentMethod === 'alipay' ? 'alipay' : 'wechat_pay';
|
const paymentMethodName = paymentMethod === 'alipay' ? 'alipay' : 'wechat_pay';
|
||||||
|
|
||||||
subscriptionEvents.trackPaymentInitiated({
|
subscriptionEvents.trackPaymentInitiated({
|
||||||
|
|||||||
@@ -2,19 +2,9 @@
|
|||||||
// 使用 Ant Design Modal 保持与现有代码风格一致
|
// 使用 Ant Design Modal 保持与现有代码风格一致
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Modal as AntModal, Tag, ConfigProvider, theme } from "antd";
|
import { Modal as AntModal, Tag, ConfigProvider, theme } from "antd";
|
||||||
import { FileText, Clock } from "lucide-react";
|
import { FileText } from "lucide-react";
|
||||||
import { GLASS_BLUR } from "@/constants/glassConfig";
|
import { GLASS_BLUR } from "@/constants/glassConfig";
|
||||||
import dayjs from "dayjs";
|
import { getEventDetailUrl } from "@/utils/idEncoder";
|
||||||
|
|
||||||
/**
|
|
||||||
* 格式化事件时间
|
|
||||||
*/
|
|
||||||
const formatEventTime = (time) => {
|
|
||||||
if (!time) return null;
|
|
||||||
const d = dayjs(time);
|
|
||||||
if (!d.isValid()) return null;
|
|
||||||
return d.format("MM-DD HH:mm");
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取相关度颜色
|
* 获取相关度颜色
|
||||||
@@ -112,7 +102,7 @@ const RelatedEventsModal = ({
|
|||||||
transition: "all 0.2s",
|
transition: "all 0.2s",
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
window.open(`/community?event_id=${event.event_id}`, "_blank");
|
window.open(getEventDetailUrl(event.event_id), "_blank");
|
||||||
}}
|
}}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
e.currentTarget.style.background = "rgba(40,40,70,0.9)";
|
e.currentTarget.style.background = "rgba(40,40,70,0.9)";
|
||||||
@@ -126,34 +116,23 @@ const RelatedEventsModal = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: "12px" }}>
|
<div style={{ display: "flex", flexDirection: "column", gap: "12px" }}>
|
||||||
{/* 标题 + 时间 */}
|
{/* 标题 */}
|
||||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start" }}>
|
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start" }}>
|
||||||
<div style={{ display: "flex", gap: "8px", flex: 1 }}>
|
<div style={{ display: "flex", gap: "8px", flex: 1 }}>
|
||||||
<FileText size={16} color="#60A5FA" style={{ flexShrink: 0, marginTop: "2px" }} />
|
<FileText size={16} color="#60A5FA" style={{ flexShrink: 0 }} />
|
||||||
<div style={{ flex: 1 }}>
|
<span
|
||||||
<span
|
style={{
|
||||||
style={{
|
fontSize: "14px",
|
||||||
fontSize: "14px",
|
fontWeight: "600",
|
||||||
fontWeight: "600",
|
color: "#E0E0E0",
|
||||||
color: "#E0E0E0",
|
display: "-webkit-box",
|
||||||
display: "-webkit-box",
|
WebkitLineClamp: 2,
|
||||||
WebkitLineClamp: 2,
|
WebkitBoxOrient: "vertical",
|
||||||
WebkitBoxOrient: "vertical",
|
overflow: "hidden",
|
||||||
overflow: "hidden",
|
}}
|
||||||
}}
|
>
|
||||||
>
|
{event.title}
|
||||||
{event.title}
|
</span>
|
||||||
</span>
|
|
||||||
{/* 事件时间 */}
|
|
||||||
{(event.created_at || event.event_time || event.publish_time) && (
|
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: "4px", marginTop: "4px" }}>
|
|
||||||
<Clock size={12} color="rgba(255,255,255,0.4)" />
|
|
||||||
<span style={{ fontSize: "11px", color: "rgba(255,255,255,0.4)" }}>
|
|
||||||
{formatEventTime(event.created_at || event.event_time || event.publish_time)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
@@ -163,7 +142,6 @@ const RelatedEventsModal = ({
|
|||||||
padding: "2px 8px",
|
padding: "2px 8px",
|
||||||
borderRadius: "6px",
|
borderRadius: "6px",
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
marginLeft: "8px",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
相关度 {event.relevance_score || 0}
|
相关度 {event.relevance_score || 0}
|
||||||
|
|||||||
Reference in New Issue
Block a user