fix: subscription extend instead of overwrite, improve placeholder pages

- Subscription purchase now extends expire_time when user has active
  subscription, instead of cancelling and replacing
- Admin subscription creation uses same extend logic
- Subscribe page shows "续费" button and extend message for renewals
- Help and contact pages: add pink header, centered icon + text
这个提交包含在:
Guoguo
2026-04-29 08:35:58 -07:00
父节点 9bdccdcec8
当前提交 1017fb5dae
修改 8 个文件,包含 114 行新增32 行删除
+6 -2
查看文件
@@ -1,9 +1,13 @@
<view class="page"> <view class="page">
<view class="page-header" style="padding-top: {{statusBarHeight + 24}}px;"> <view class="page-header page-header-pink" style="padding-top: {{statusBarHeight + 24}}px;">
<view class="nav-back" bindtap="onBack"> 返回</view> <view class="nav-back" bindtap="onBack"> 返回</view>
<view class="page-header-title">联系我们</view> <view class="page-header-title">联系我们</view>
</view> </view>
<view class="page-content"> <view class="page-content">
<view class="placeholder">内容建设中...</view> <view class="placeholder-container">
<view class="placeholder-icon">📬</view>
<view class="placeholder-text">内容建设中</view>
<view class="placeholder-sub">联系方式正在整理,敬请期待</view>
</view>
</view> </view>
</view> </view>
+22 -4
查看文件
@@ -1,6 +1,24 @@
.placeholder { .placeholder-container {
text-align: center; display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 40rpx;
}
.placeholder-icon {
font-size: 100rpx;
margin-bottom: 32rpx;
}
.placeholder-text {
font-size: 32rpx;
font-weight: 600;
color: #333333;
margin-bottom: 12rpx;
}
.placeholder-sub {
font-size: 26rpx;
color: #999999; color: #999999;
font-size: 28rpx;
padding: 80rpx 0;
} }
+6 -2
查看文件
@@ -1,9 +1,13 @@
<view class="page"> <view class="page">
<view class="page-header" style="padding-top: {{statusBarHeight + 24}}px;"> <view class="page-header page-header-pink" style="padding-top: {{statusBarHeight + 24}}px;">
<view class="nav-back" bindtap="onBack"> 返回</view> <view class="nav-back" bindtap="onBack"> 返回</view>
<view class="page-header-title">使用帮助</view> <view class="page-header-title">使用帮助</view>
</view> </view>
<view class="page-content"> <view class="page-content">
<view class="placeholder">内容建设中...</view> <view class="placeholder-container">
<view class="placeholder-icon">📖</view>
<view class="placeholder-text">内容建设中</view>
<view class="placeholder-sub">帮助文档正在编写,敬请期待</view>
</view>
</view> </view>
</view> </view>
+22 -4
查看文件
@@ -1,6 +1,24 @@
.placeholder { .placeholder-container {
text-align: center; display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 40rpx;
}
.placeholder-icon {
font-size: 100rpx;
margin-bottom: 32rpx;
}
.placeholder-text {
font-size: 32rpx;
font-weight: 600;
color: #333333;
margin-bottom: 12rpx;
}
.placeholder-sub {
font-size: 26rpx;
color: #999999; color: #999999;
font-size: 28rpx;
padding: 80rpx 0;
} }
@@ -118,9 +118,16 @@ Page({
return return
} }
var isRenew = self.data.subscription && self.data.subscription.status === 'active' && self.data.subRemaining > 0
var planDays = plan.key === 'yearly' ? 365 : 30
var title = isRenew ? '确认续费' : '确认支付'
var content = isRenew
? '在现有订阅基础上延长' + planDays + '天,模拟支付 ¥' + plan.price + '?(测试环境)'
: '模拟支付 ¥' + plan.price + '?(测试环境)'
wx.showModal({ wx.showModal({
title: '确认支付', title: title,
content: '模拟支付 ¥' + plan.price + '?(测试环境)', content: content,
success: function (res) { success: function (res) {
if (res.confirm) { if (res.confirm) {
self.doPurchase(plan) self.doPurchase(plan)
@@ -38,7 +38,7 @@
</view> </view>
<button class="btn-primary btn-primary-gold" bindtap="onPurchase" disabled="{{purchasing}}" loading="{{purchasing}}"> <button class="btn-primary btn-primary-gold" bindtap="onPurchase" disabled="{{purchasing}}" loading="{{purchasing}}">
确认支付 {{subscription && subscription.status === 'active' && subRemaining > 0 ? '续费' : '确认支付'}}
</button> </button>
<view class="agreement">支付即表示同意《订阅协议》</view> <view class="agreement">支付即表示同意《订阅协议》</view>
</view> </view>
+44 -15
查看文件
@@ -62,7 +62,9 @@ async function createTrial(userId, orderId) {
} }
/** /**
* Purchase / activate a subscription: expire old active subs, insert new one * Purchase / activate a subscription: extend existing or create new
* If user has an active subscription, add days to current expire_time.
* If no active subscription, insert a new one starting NOW().
* @param {number} userId * @param {number} userId
* @param {string} plan - 'monthly' | 'yearly' | 'trial' * @param {string} plan - 'monthly' | 'yearly' | 'trial'
* @param {number} amount * @param {number} amount
@@ -72,20 +74,34 @@ async function createTrial(userId, orderId) {
*/ */
async function purchase(userId, plan, amount, orderId, days) { async function purchase(userId, plan, amount, orderId, days) {
return transaction(async conn => { return transaction(async conn => {
await conn.execute( const [rows] = await conn.execute(
'UPDATE subscriptions SET status = 2 WHERE user_id = ? AND status = 1', 'SELECT subscription_id FROM subscriptions WHERE user_id = ? AND status = 1 AND expire_time > NOW() LIMIT 1',
[userId] [userId]
) )
await conn.execute( if (rows.length > 0) {
'INSERT INTO subscriptions (user_id, plan, status, amount, order_id, start_time, expire_time) VALUES (?, ?, 1, ?, ?, NOW(), DATE_ADD(NOW(), INTERVAL ? DAY))', // Extend existing active subscription
[userId, plan, amount, orderId, days] await conn.execute(
) 'UPDATE subscriptions SET expire_time = DATE_ADD(expire_time, INTERVAL ? DAY), plan = ?, amount = amount + ? WHERE subscription_id = ?',
[days, plan, amount, rows[0].subscription_id]
)
} else {
// Expire any stale rows (status=1 but already past expire_time), then insert new
await conn.execute(
'UPDATE subscriptions SET status = 2 WHERE user_id = ? AND status = 1',
[userId]
)
await conn.execute(
'INSERT INTO subscriptions (user_id, plan, status, amount, order_id, start_time, expire_time) VALUES (?, ?, 1, ?, ?, NOW(), DATE_ADD(NOW(), INTERVAL ? DAY))',
[userId, plan, amount, orderId, days]
)
}
}) })
} }
/** /**
* Admin-create a subscription (expire old, insert new) without transaction * Admin-create a subscription: extend existing or create new
* Used by admin subscription creation endpoint * If user has an active subscription, add days to current expire_time.
* If no active subscription, insert a new one starting NOW().
* @param {number} userId * @param {number} userId
* @param {string} plan * @param {string} plan
* @param {number} amount * @param {number} amount
@@ -94,14 +110,27 @@ async function purchase(userId, plan, amount, orderId, days) {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async function adminCreate(userId, plan, amount, orderId, days) { async function adminCreate(userId, plan, amount, orderId, days) {
await query( const active = await one(
'UPDATE subscriptions SET status = 2 WHERE user_id = :user_id AND status = 1', 'SELECT subscription_id FROM subscriptions WHERE user_id = :user_id AND status = 1 AND expire_time > NOW() LIMIT 1',
{ user_id: userId } { user_id: userId }
) )
await query( if (active) {
'INSERT INTO subscriptions (user_id, plan, status, amount, order_id, start_time, expire_time) VALUES (:user_id, :plan, 1, :amount, :order_id, NOW(), DATE_ADD(NOW(), INTERVAL :days DAY))', // Extend existing active subscription
{ user_id: userId, plan, amount, order_id: orderId, days } await query(
) 'UPDATE subscriptions SET expire_time = DATE_ADD(expire_time, INTERVAL :days DAY), plan = :plan, amount = amount + :amount WHERE subscription_id = :sid',
{ days, plan, amount, sid: active.subscription_id }
)
} else {
// Expire any stale rows, then insert new
await query(
'UPDATE subscriptions SET status = 2 WHERE user_id = :user_id AND status = 1',
{ user_id: userId }
)
await query(
'INSERT INTO subscriptions (user_id, plan, status, amount, order_id, start_time, expire_time) VALUES (:user_id, :plan, 1, :amount, :order_id, NOW(), DATE_ADD(NOW(), INTERVAL :days DAY))',
{ user_id: userId, plan, amount, order_id: orderId, days }
)
}
} }
/** /**
+4 -2
查看文件
@@ -55,7 +55,8 @@ router.post('/subscription/mock-purchase', requireUser, wrap(async (req, res) =>
const orderId = 'MOCK' + Date.now() const orderId = 'MOCK' + Date.now()
await subscriptionDao.purchase(req.user.user_id, plan, p.amount, orderId, p.days) await subscriptionDao.purchase(req.user.user_id, plan, p.amount, orderId, p.days)
await logDao.write({ user_id: req.user.user_id, action: 'subscription_mock_purchase', detail: '模拟购买: ' + plan, ip: req.ip }) await logDao.write({ user_id: req.user.user_id, action: 'subscription_mock_purchase', detail: '模拟购买: ' + plan, ip: req.ip })
res.json(ok({ status: 'active', plan, remaining_days: p.days })) const sub = await subscriptionDao.findActive(req.user.user_id)
res.json(ok({ status: 'active', plan, remaining_days: sub ? sub.remaining_days : p.days }))
})) }))
router.post('/subscription/trial', requireUser, wrap(async (req, res) => { router.post('/subscription/trial', requireUser, wrap(async (req, res) => {
@@ -76,7 +77,8 @@ router.post('/subscription/verify', requireAdmin, wrap(async (req, res) => {
const p = PLANS[plan] const p = PLANS[plan]
await subscriptionDao.purchase(userId, plan, p.amount, req.body.order_id || 'ORD' + Date.now(), p.days) await subscriptionDao.purchase(userId, plan, p.amount, req.body.order_id || 'ORD' + Date.now(), p.days)
await logDao.write({ admin_id: req.admin.admin_id, action: 'subscription_verify', detail: '订阅生效: ' + plan + ' user:' + userId, ip: req.ip }) await logDao.write({ admin_id: req.admin.admin_id, action: 'subscription_verify', detail: '订阅生效: ' + plan + ' user:' + userId, ip: req.ip })
res.json(ok({ status: 'active', plan, remaining_days: p.days })) const sub = await subscriptionDao.findActive(userId)
res.json(ok({ status: 'active', plan, remaining_days: sub ? sub.remaining_days : p.days }))
})) }))
module.exports = router module.exports = router