feat: WeChat Pay V3 integration (fill credentials to activate)
Server: - New lib/wxpay.js: native crypto RSA-SHA256 signing, JSAPI prepay, AES-256-GCM notify decryption, order query (no npm deps) - New dao/payment-order.dao.js: createOrder, markPrepay, markPaidAndActivateSubscription (idempotent + transactional) - New routes/payment.js: GET order status, POST order sync - New routes/payment-notify.js: WeChat async callback handler with signature verification, amount/appid/mchid validation - Modified subscription/purchase: auto-detects wxpay config, returns real payment_params or mock fallback - Schema: payment_orders table with out_trade_no unique key - app.js: express.raw() for notify path, payment routes mounted - config.js: wxpay block with 7 env vars - .env.example: all WeChat Pay fields documented - .gitignore: certs/, *.pem, *.p12 Miniprogram: - subscribe-plans doPurchase: calls real purchase API, falls back to mockPurchase only when server returns mock:true - Added syncAndRedirect for post-payment order confirmation - api.js: getPaymentOrder, syncPaymentOrder - Removed "模拟支付"/"测试环境" from UI text
这个提交包含在:
@@ -130,8 +130,8 @@ Page({
|
||||
var planDays = plan.key === 'yearly' ? 365 : 30
|
||||
var title = isRenew ? '确认续费' : '确认支付'
|
||||
var content = isRenew
|
||||
? '在现有订阅基础上延长' + planDays + '天,模拟支付 ¥' + plan.price + '?(测试环境)'
|
||||
: '模拟支付 ¥' + plan.price + '?(测试环境)'
|
||||
? '在现有订阅基础上延长' + planDays + '天,支付 ¥' + plan.price
|
||||
: '支付 ¥' + plan.price
|
||||
|
||||
wx.showModal({
|
||||
title: title,
|
||||
@@ -162,15 +162,65 @@ Page({
|
||||
doPurchase: function (plan) {
|
||||
var self = this
|
||||
self.setData({ purchasing: true })
|
||||
api.mockPurchase(plan.key).then(function (order) {
|
||||
self.setData({ purchasing: false })
|
||||
wx.showToast({ title: '支付成功', icon: 'success' })
|
||||
setTimeout(function () {
|
||||
wx.redirectTo({ url: '/pages/subscribe-success/subscribe-success?plan=' + plan.key })
|
||||
}, 1000)
|
||||
api.purchase(plan.key).then(function (data) {
|
||||
if (data.mock || !data.payment_params) {
|
||||
// Dev fallback: use mock purchase
|
||||
return api.mockPurchase(plan.key).then(function () {
|
||||
self.setData({ purchasing: false })
|
||||
wx.showToast({ title: '支付成功', icon: 'success' })
|
||||
setTimeout(function () {
|
||||
wx.redirectTo({ url: '/pages/subscribe-success/subscribe-success?plan=' + plan.key })
|
||||
}, 1000)
|
||||
})
|
||||
}
|
||||
// Real payment
|
||||
var params = data.payment_params
|
||||
self._currentOrderId = data.order_id
|
||||
wx.requestPayment({
|
||||
timeStamp: params.timeStamp,
|
||||
nonceStr: params.nonceStr,
|
||||
package: params.package,
|
||||
signType: params.signType,
|
||||
paySign: params.paySign,
|
||||
success: function () {
|
||||
// Payment dialog succeeded, sync order to confirm
|
||||
self.syncAndRedirect(data.order_id, plan.key)
|
||||
},
|
||||
fail: function (err) {
|
||||
self.setData({ purchasing: false })
|
||||
var msg = (err.errMsg || '').indexOf('cancel') > -1 ? '已取消支付' : '支付失败'
|
||||
wx.showToast({ title: msg, icon: 'none' })
|
||||
}
|
||||
})
|
||||
}).catch(function (err) {
|
||||
self.setData({ purchasing: false })
|
||||
wx.showToast({ title: err.message || '支付失败', icon: 'none' })
|
||||
wx.showToast({ title: err.message || '创建订单失败', icon: 'none' })
|
||||
})
|
||||
},
|
||||
|
||||
syncAndRedirect: function (orderId, planKey) {
|
||||
var self = this
|
||||
api.syncPaymentOrder(orderId).then(function (result) {
|
||||
self.setData({ purchasing: false })
|
||||
if (result.status === 'paid') {
|
||||
wx.showToast({ title: '支付成功', icon: 'success' })
|
||||
setTimeout(function () {
|
||||
wx.redirectTo({ url: '/pages/subscribe-success/subscribe-success?plan=' + planKey })
|
||||
}, 1000)
|
||||
} else {
|
||||
// Callback may not have arrived yet, still redirect optimistically
|
||||
wx.showToast({ title: '支付处理中', icon: 'none' })
|
||||
setTimeout(function () {
|
||||
wx.redirectTo({ url: '/pages/subscribe-success/subscribe-success?plan=' + planKey })
|
||||
}, 2000)
|
||||
}
|
||||
}).catch(function () {
|
||||
self.setData({ purchasing: false })
|
||||
// Even if sync fails, payment may still succeed via callback
|
||||
wx.showToast({ title: '支付处理中,请稍后查看', icon: 'none' })
|
||||
setTimeout(function () {
|
||||
wx.redirectTo({ url: '/pages/subscribe-success/subscribe-success?plan=' + planKey })
|
||||
}, 2000)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -23,6 +23,10 @@ module.exports = {
|
||||
purchase: function (plan) { return http.post('/api/v1/subscription/purchase', { plan: plan }) },
|
||||
mockPurchase: function (plan) { return http.post('/api/v1/subscription/mock-purchase', { plan: plan }) },
|
||||
|
||||
// Payment
|
||||
getPaymentOrder: function (orderId) { return http.get('/api/v1/payment/orders/' + orderId) },
|
||||
syncPaymentOrder: function (orderId) { return http.post('/api/v1/payment/orders/' + orderId + '/sync') },
|
||||
|
||||
// Treatment
|
||||
getRecords: function (params) { return http.get('/api/v1/treatment/history', params) },
|
||||
syncTreatment: function (data) { return http.post('/api/v1/treatment/sync', data) },
|
||||
|
||||
在新工单中引用
屏蔽一个用户