新增匹配次数管理功能,优化用户匹配体验。通过服务端计算用户的匹配配额,更新用户状态以反映剩余匹配次数。同时,调整匹配页面逻辑,确保在匹配次数用尽时提示用户购买更多次数。更新相关API以支持匹配记录的存储与查询,提升系统稳定性。

This commit is contained in:
2026-02-11 16:53:17 +08:00
parent ecee1bb2bb
commit a1dcf599ee
18 changed files with 422 additions and 229 deletions

View File

@@ -38,6 +38,7 @@ Page({
todayMatchCount: 0,
totalMatchesAllowed: FREE_MATCH_LIMIT,
matchesRemaining: FREE_MATCH_LIMIT,
showQuotaExhausted: false,
needPayToMatch: false,
// 匹配状态
@@ -88,8 +89,7 @@ Page({
})
this.loadMatchConfig()
this.loadStoredContact()
this.loadTodayMatchCount()
this.initUserStatus()
this.refreshMatchCountAndStatus()
},
onShow() {
@@ -102,7 +102,7 @@ Page({
}
}
this.loadStoredContact()
this.initUserStatus()
this.refreshMatchCountAndStatus()
},
// 加载匹配配置
@@ -147,49 +147,46 @@ Page({
})
},
// 加载今日匹配次数
loadTodayMatchCount() {
try {
const today = new Date().toISOString().split('T')[0]
const stored = wx.getStorageSync('match_count_data')
if (stored) {
const data = typeof stored === 'string' ? JSON.parse(stored) : stored
if (data.date === today) {
this.setData({ todayMatchCount: data.count })
// 从服务端刷新匹配配额并初始化用户状态(前后端双向校验,服务端为权威)
async refreshMatchCountAndStatus() {
if (app.globalData.isLoggedIn && app.globalData.userInfo?.id) {
try {
const res = await app.request(`/api/miniprogram/user/purchase-status?userId=${encodeURIComponent(app.globalData.userInfo.id)}`)
if (res.success && res.data) {
app.globalData.matchCount = res.data.matchCount ?? 0
app.globalData.matchQuota = res.data.matchQuota || null
}
} catch (e) {
console.log('[Match] 拉取 matchQuota 失败:', e)
}
} catch (e) {
console.error('加载匹配次数失败:', e)
}
this.initUserStatus()
},
// 保存今日匹配次数
saveTodayMatchCount(count) {
const today = new Date().toISOString().split('T')[0]
wx.setStorageSync('match_count_data', { date: today, count })
},
// 初始化用户状态
// 初始化用户状态matchQuota 服务端纯计算:订单+match_records
initUserStatus() {
const { isLoggedIn, hasFullBook, purchasedSections } = app.globalData
// 获取额外购买的匹配次数
const extraMatches = wx.getStorageSync('extra_match_count') || 0
// 总匹配次数 = 每日免费(3) + 额外购买次数
// 全书用户无限制
const totalMatchesAllowed = hasFullBook ? 999999 : FREE_MATCH_LIMIT + extraMatches
const matchesRemaining = hasFullBook ? 999999 : Math.max(0, totalMatchesAllowed - this.data.todayMatchCount)
const needPayToMatch = !hasFullBook && matchesRemaining <= 0
const quota = app.globalData.matchQuota
// 今日剩余次数、今日已用:来自服务端 matchQuota未登录无法计算不能显示已用完
const remainToday = quota?.remainToday ?? 0
const matchesUsedToday = quota?.matchesUsedToday ?? 0
const purchasedRemain = quota?.purchasedRemain ?? 0
const totalMatchesAllowed = hasFullBook ? 999999 : (quota ? remainToday + matchesUsedToday : FREE_MATCH_LIMIT)
// 仅登录且服务端返回配额时,才判断是否已用完;未登录时显示「开始匹配」
const needPayToMatch = isLoggedIn && !hasFullBook && (quota ? remainToday <= 0 : false)
const showQuotaExhausted = isLoggedIn && !hasFullBook && (quota ? remainToday <= 0 : false)
this.setData({
isLoggedIn,
hasFullBook,
hasPurchased: true, // 所有用户都可以使用匹配功能
hasPurchased: true,
todayMatchCount: matchesUsedToday,
totalMatchesAllowed,
matchesRemaining,
matchesRemaining: hasFullBook ? 999999 : (isLoggedIn && quota ? remainToday : (isLoggedIn ? 0 : FREE_MATCH_LIMIT)),
needPayToMatch,
extraMatches
showQuotaExhausted,
extraMatches: purchasedRemain
})
},
@@ -224,7 +221,7 @@ Page({
confirmText: '去购买',
success: (res) => {
if (res.confirm) {
wx.switchTab({ url: '/pages/catalog/catalog' })
wx.switchTab({ url: '/pages/chapters/chapters' })
}
}
})
@@ -257,12 +254,12 @@ Page({
return
}
// 创业合伙类型 - 真正的匹配功能
if (this.data.needPayToMatch) {
// 创业合伙类型 - 真正的匹配功能(仅当登录且服务端确认次数用尽时,弹出购买)
if (this.data.showQuotaExhausted) {
this.setData({ showUnlockModal: true })
return
}
this.startMatch()
},
@@ -314,6 +311,7 @@ Page({
// 开始匹配 - 只匹配数据库中的真实用户
async startMatch() {
this.loadStoredContact()
this.setData({
isMatching: true,
matchAttempts: 0,
@@ -325,20 +323,28 @@ Page({
this.setData({ matchAttempts: this.data.matchAttempts + 1 })
}, 1000)
// 从数据库获取真实用户匹配
// 从数据库获取真实用户匹配(后端会校验剩余次数)
let matchedUser = null
let quotaExceeded = false
try {
const ui = app.globalData.userInfo || {}
const phone = (wx.getStorageSync('user_phone') || ui.phone || this.data.phoneNumber || '').trim()
const wechatId = (wx.getStorageSync('user_wechat') || ui.wechat || ui.wechatId || this.data.wechatId || '').trim()
const res = await app.request('/api/miniprogram/match/users', {
method: 'POST',
data: {
matchType: this.data.selectedType,
userId: app.globalData.userInfo?.id || ''
userId: app.globalData.userInfo?.id || '',
phone,
wechatId
}
})
if (res.success && res.data) {
matchedUser = res.data
console.log('[Match] 从数据库匹配到用户:', matchedUser.nickname)
} else if (res.code === 'QUOTA_EXCEEDED') {
quotaExceeded = true
}
} catch (e) {
console.log('[Match] 数据库匹配失败:', e)
@@ -348,7 +354,22 @@ Page({
const delay = Math.random() * 2000 + 2000
setTimeout(() => {
clearInterval(timer)
// 次数用尽(后端校验)
if (quotaExceeded) {
this.setData({ isMatching: false })
wx.showModal({
title: '今日匹配次数已用完',
content: '请购买更多次数继续匹配',
confirmText: '去购买',
success: (r) => {
if (r.confirm) this.setData({ showUnlockModal: true })
}
})
this.refreshMatchCountAndStatus()
return
}
// 如果没有匹配到用户,提示用户
if (!matchedUser) {
this.setData({ isMatching: false })
@@ -360,23 +381,17 @@ Page({
})
return
}
// 增加今日匹配次数
const newCount = this.data.todayMatchCount + 1
const matchesRemaining = this.data.hasFullBook ? 999999 : Math.max(0, this.data.totalMatchesAllowed - newCount)
// 匹配成功:从服务端刷新配额(后端已写入 match_records
this.setData({
isMatching: false,
currentMatch: matchedUser,
todayMatchCount: newCount,
matchesRemaining,
needPayToMatch: !this.data.hasFullBook && matchesRemaining <= 0
needPayToMatch: false
})
this.saveTodayMatchCount(newCount)
this.refreshMatchCountAndStatus()
// 上报匹配行为到存客宝
this.reportMatch(matchedUser)
}, delay)
},
@@ -665,7 +680,8 @@ Page({
try {
const result = await app.login()
if (result) {
this.initUserStatus()
// 登录成功后必须拉取 matchQuota否则无法正确显示剩余次数
await this.refreshMatchCountAndStatus()
this.setData({ showLoginModal: false, agreeProtocol: false })
wx.showToast({ title: '登录成功', icon: 'success' })
} else {
@@ -777,7 +793,24 @@ Page({
this.setData({ showUnlockModal: false })
},
// 购买匹配次数
// 支付成功后立即查询订单状态并刷新(首轮 0 延迟,之后每 800ms 重试)
async pollOrderAndRefresh(orderSn) {
const maxAttempts = 12
const interval = 800
for (let i = 0; i < maxAttempts; i++) {
try {
const r = await app.request(`/api/miniprogram/pay?orderSn=${encodeURIComponent(orderSn)}`, { method: 'GET', silent: true })
if (r?.data?.status === 'paid') {
await this.refreshMatchCountAndStatus()
return
}
} catch (_) {}
if (i < maxAttempts - 1) await new Promise(r => setTimeout(r, interval))
}
await this.refreshMatchCountAndStatus()
},
// 购买匹配次数(与购买章节逻辑一致,写入订单)
async buyMatchCount() {
this.setData({ showUnlockModal: false })
@@ -793,16 +826,17 @@ Page({
return
}
const matchPrice = this.data.matchPrice || 1
// 邀请码:与章节支付一致,写入订单便于分销归属与对账
const referralCode = wx.getStorageSync('referral_code') || ''
// 调用支付接口购买匹配次数
// 调用支付接口购买匹配次数productType: match订单类型购买匹配次数
const res = await app.request('/api/miniprogram/pay', {
method: 'POST',
data: {
openId,
productType: 'match',
productId: 'match_1',
amount: 1,
amount: matchPrice,
description: '匹配次数x1',
userId: app.globalData.userInfo?.id || '',
referralCode: referralCode || undefined
@@ -810,6 +844,7 @@ Page({
})
if (res.success && res.data?.payParams) {
const orderSn = res.data.orderSn
// 调用微信支付
await new Promise((resolve, reject) => {
wx.requestPayment({
@@ -818,13 +853,9 @@ Page({
fail: reject
})
})
// 支付成功,增加匹配次数
const extraMatches = (wx.getStorageSync('extra_match_count') || 0) + 1
wx.setStorageSync('extra_match_count', extraMatches)
wx.showToast({ title: '购买成功', icon: 'success' })
this.initUserStatus()
// 轮询订单状态,确认已支付后再刷新(不依赖 PayNotify 回调时机)
this.pollOrderAndRefresh(orderSn)
} else {
throw new Error(res.error || '创建订单失败')
}
@@ -832,14 +863,13 @@ Page({
if (e.errMsg && e.errMsg.includes('cancel')) {
wx.showToast({ title: '已取消', icon: 'none' })
} else {
// 测试模式
// 测试模式(无支付环境时本地模拟)
wx.showModal({
title: '支付服务暂不可用',
content: '是否使用测试模式购买?',
success: (res) => {
if (res.confirm) {
const extraMatches = (wx.getStorageSync('extra_match_count') || 0) + 1
wx.setStorageSync('extra_match_count', extraMatches)
app.globalData.matchCount = (app.globalData.matchCount ?? 0) + 1
wx.showToast({ title: '测试购买成功', icon: 'success' })
this.initUserStatus()
}