支付通路

This commit is contained in:
tianmo committed 2026-09-30 15:08:16 +08:00
1 parent c7f43e30a9
commit 85b0e70907
26 files changed
+1324 -74

No files matched your search

+109 -14
View File
@@ -1,14 +1,44 @@
// pages/buy/buy.js
//
// 商业化页面:所有购买入口都落到这里。
// 职责是「确认订单 → 拉起微信支付 → 确认发货」,课程详情不在这里展示。
// UI 待设计,wxml 现在只有一个占位块,支付链路已经就位。
// 职责是「确认订单 → 拉起微信支付 → 确认发货」。
//
// 两种进入方式:
// - 带 ?code=xxx(从课程详情页进来):只显示这一门
// - 不带参数:列出全部课程,一门一个购买按钮
const api = require('../../utils/api');
const format = require('../../utils/format');
const store = require('../../utils/store');
const POLL_INTERVAL = 1500;
const POLL_MAX = 8;
// 微信的发货推送实测要 13~17 秒才到,原来只等 12 秒,用户多半看到「可能稍有延迟」而不是「购买成功」
const POLL_MAX = 20;
/** iPhone 上跑 Apple 支付要求微信客户端 ≥ 这个版本 */
const IOS_MIN_WECHAT = '8.0.68';
/** 当前微信客户端版本号,取不到就返回空串。 */
function clientVersion() {
try {
const info = typeof wx.getAppBaseInfo === 'function' ? wx.getAppBaseInfo() : wx.getSystemInfoSync();
return (info && info.version) || '';
} catch (err) {
return '';
}
}
/** 版本号比大小:'8.0.68' vs '8.0.5' → 正数表示前者新。 */
function compareVersion(a, b) {
const left = String(a).split('.').map(Number);
const right = String(b).split('.').map(Number);
const len = Math.max(left.length, right.length);
for (let i = 0; i < len; i += 1) {
const l = left[i] || 0;
const r = right[i] || 0;
if (l !== r) return l - r;
}
return 0;
}
Page({
data: {
@@ -19,17 +49,17 @@ Page({
title: '',
coverUrl: '',
priceYuan: '',
list: [],
},
onLoad(options) {
const code = options && options.code;
// 没带课程号说明是误入(比如直接输路径打开),不该停在这个页面
if (!code) {
this.rejectOut('缺少课程参数');
if (code) {
this.setData({ code: code });
this.load(code);
return;
}
this.setData({ code: code });
this.load(code);
this.loadList();
},
load(code) {
@@ -53,6 +83,27 @@ Page({
});
},
/** 不带课程号进来:列出全部课程,让用户自己挑。 */
loadList() {
api
.getCourses()
.then((courses) => {
this.setData({
loading: false,
list: (courses || []).map((c) => ({
code: c.code,
title: c.title,
priceYuan: format.fenToYuan(c.priceFen),
owned: c.owned,
})),
});
})
.catch((err) => {
this.setData({ loading: false });
wx.showToast({ title: (err && err.message) || '课程加载失败', icon: 'none' });
});
},
/** 进错页面(没带课程、课程不存在、已经买过):提示一句然后退出去。 */
rejectOut(message) {
wx.showToast({ title: message, icon: 'none' });
@@ -68,9 +119,14 @@ Page({
wx.switchTab({ url: '/pages/kecheng/kecheng' });
},
/** 购买:下单 → 拉起支付 → 轮询订单状态确认发货。 */
buyNow() {
if (this.data.buying || !this.data.code) return;
/**
* 购买:下单 → 拉起支付 → 轮询订单状态确认发货。
* 课程号优先取按钮上的 data-code(列表模式),没有就用页面参数(单课程模式)。
*/
buyNow(e) {
const code =
(e && e.currentTarget && e.currentTarget.dataset && e.currentTarget.dataset.code) || this.data.code;
if (this.data.buying || !code) return;
// 购买的前置条件:用户主动使用过登录功能。静默登录不算。
if (!store.isLoggedIn()) {
@@ -92,11 +148,14 @@ Page({
return;
}
// iOS 版本预检放在下单之前:版本不够时不该先建出一笔待支付订单
if (!this.checkIosVersion()) return;
this.setData({ buying: true });
wx.showLoading({ title: '发起支付', mask: true });
api
.createOrder(this.data.code)
.createOrder(code)
.then((order) => {
wx.hideLoading();
return api.requestVirtualPayment(order.payData).then(() => order.outTradeNo);
@@ -107,10 +166,41 @@ Page({
this.setData({ buying: false });
// 用户主动取消不算错误
if (err && err.errMsg && err.errMsg.indexOf('cancel') >= 0) return;
wx.showToast({ title: (err && err.message) || '支付失败', icon: 'none' });
// 拉起支付失败时微信给的是 errMsg/errno,服务端报错给的是 message。
// 原来只弹「支付失败」,把排查线索全丢了,所以原样带出来。
const detail = (err && (err.errMsg || err.message)) || '未知错误';
const errno = err && (err.errno || err.errCode);
wx.showModal({
title: '支付失败',
content: errno ? detail + '\n错误码:' + errno : detail,
showCancel: false,
});
});
},
/**
* iPhone 上 Apple 支付要求微信 ≥ 8.0.68,低了拉不起来。
* 只在 iOS 上校验;安卓等没有这个限制,取不到版本号也不拦(交给微信自己报错)。
*/
checkIosVersion() {
if (api.devicePlatform() !== 'ios') return true;
const version = clientVersion();
if (!version || compareVersion(version, IOS_MIN_WECHAT) >= 0) return true;
wx.showModal({
title: '请先升级微信',
content:
'iPhone 上购买课程需要微信 ' +
IOS_MIN_WECHAT +
' 及以上版本,你当前是 ' +
version +
'。请到 App Store 更新微信后重试。',
showCancel: false,
confirmText: '知道了',
});
return false;
},
/**
* 支付成功后权益由服务端发货推送发放,存在秒级延迟,这里轮询确认。
* 即便轮询全部超时也不会丢单:服务端有定时查单兜底。
@@ -123,7 +213,12 @@ Page({
if (order.status === 2) {
this.setData({ buying: false, owned: true });
wx.showToast({ title: '购买成功', icon: 'success', mask: true });
setTimeout(() => this.back(), 1500);
// 单课程模式从详情页进来,买完退回去;列表模式留在原地刷新
if (this.data.code) {
setTimeout(() => this.back(), 1500);
} else {
this.loadList();
}
return;
}
if (attempts >= POLL_MAX || order.status === 3 || order.status === 4) {
+30 -3
View File
@@ -1,7 +1,34 @@
<!--pages/buy/buy.wxml-->
<!-- UI 待设计。支付链路(buyNow / waitForDelivery)已经在 buy.js 里就位,
设计稿定了直接替换下面这个占位块即可。 -->
<view class="buy-page">
<text class="buy-placeholder">购买页(待设计)</text>
<!-- 带 ?code= 进来就只买这一门,否则列出全部课程、一门一个购买按钮。
注意别在 wx:if 和 wx:else 之间插注释。 -->
<view class="buy-single" wx:if="{{code}}">
<text class="buy-title">{{title}}</text>
<text class="buy-price">¥{{priceYuan}}</text>
<button
class="buy-btn"
disabled="{{buying || owned}}"
bindtap="buyNow"
>{{owned ? '已拥有' : (buying ? '支付中…' : '立即购买')}}</button>
</view>
<view class="buy-list" wx:else>
<view class="buy-item" wx:for="{{list}}" wx:key="code">
<view class="buy-item-main">
<text class="buy-item-title">{{item.title}}</text>
<text class="buy-item-price">¥{{item.priceYuan}}</text>
</view>
<button
class="buy-btn buy-btn-mini"
size="mini"
disabled="{{buying || item.owned}}"
data-code="{{item.code}}"
bindtap="buyNow"
>{{item.owned ? '已拥有' : '购买'}}</button>
</view>
<view class="buy-empty" wx:if="{{!loading && list.length === 0}}">
<text>暂无可购买的课程</text>
</view>
</view>
</view>
+100 -8
View File
@@ -1,14 +1,106 @@
/* pages/buy/buy.wxss */
/* 占位样式,UI 定稿后连同 wxml 一起替换 */
.buy-page {
display: flex;
align-items: center;
justify-content: center;
min-height: 70vh;
min-height: 100vh;
background: #f6f7f9;
padding: 24rpx 24rpx 48rpx;
box-sizing: border-box;
}
.buy-placeholder {
color: #999;
font-size: 28rpx;
/* ── 单课程 ───────────────────────────────────────────── */
.buy-single {
background: #fff;
border-radius: 16rpx;
padding: 48rpx 32rpx;
display: flex;
flex-direction: column;
align-items: center;
}
.buy-title {
font-size: 34rpx;
font-weight: 600;
color: #222;
text-align: center;
}
.buy-price {
margin-top: 24rpx;
font-size: 44rpx;
color: #ff4d4f;
font-weight: 600;
}
/* ── 课程列表 ─────────────────────────────────────────── */
.buy-list {
background: #fff;
border-radius: 16rpx;
overflow: hidden;
}
.buy-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 28rpx 24rpx;
border-bottom: 1rpx solid #eee;
}
.buy-item:last-child {
border-bottom: none;
}
.buy-item-main {
flex: 1;
min-width: 0;
margin-right: 20rpx;
display: flex;
flex-direction: column;
}
.buy-item-title {
font-size: 28rpx;
color: #222;
line-height: 1.4;
}
.buy-item-price {
margin-top: 8rpx;
font-size: 26rpx;
color: #ff4d4f;
}
.buy-empty {
padding: 80rpx 0;
text-align: center;
color: #999;
font-size: 26rpx;
}
/* ── 按钮 ─────────────────────────────────────────────── */
.buy-btn {
margin-top: 40rpx;
background: #07c160;
color: #fff;
font-size: 30rpx;
border-radius: 44rpx;
line-height: 2.6;
}
.buy-btn-mini {
margin-top: 0;
flex: none;
font-size: 26rpx;
line-height: 2.4;
padding: 0 30rpx;
}
.buy-btn[disabled] {
background: #d8d8d8;
color: #fff;
}
/* 去掉微信按钮自带的那圈边框 */
.buy-btn::after {
border: none;
}