refactor: update match configuration and user matching logic

- Changed the daily free match limit to a lifetime limit, aligning with backend configurations.
- Updated the match price display to include original pricing for better user clarity.
- Refactored match quota handling to sync with the server, ensuring accurate match counts and purchase statuses.
- Enhanced UI messages to reflect changes in match availability and pricing.
- Removed deprecated logic related to daily match counts, streamlining the matching process.
This commit is contained in:
Alex-larget
2026-04-01 17:09:45 +08:00
parent bdd0795434
commit 360bba697b
26 changed files with 374 additions and 781 deletions

View File

@@ -32,7 +32,7 @@ let MATCH_TYPES = [
{ id: 'team', label: '团队招募', matchLabel: '团队招募', icon: 'gamepad', matchFromDB: true, showJoinAfterMatch: true }
]
let FREE_MATCH_LIMIT = 3 // 每日免费匹配次数
let FREE_MATCH_LIMIT = 1 // 终身免费匹配次数(不按日重置,与后端 match_config 一致)
Page({
data: {
@@ -48,8 +48,7 @@ Page({
hasPurchased: false,
hasFullBook: false,
// 匹配次数
todayMatchCount: 0,
// 匹配次数(以服务端 purchase-status.matchQuota 为准)
totalMatchesAllowed: FREE_MATCH_LIMIT,
matchesRemaining: FREE_MATCH_LIMIT,
needPayToMatch: false,
@@ -86,20 +85,21 @@ Page({
contactWechat: '',
contactSaving: false,
// 匹配价格(可配置)
// 匹配价格(可配置)matchPriceOriginal 为划线原价展示
matchPrice: 1,
matchPriceOriginal: 9.9,
extraMatches: 0
},
onLoad() {
async onLoad() {
wx.showShareMenu({ withShareTimeline: true })
this.setData({
statusBarHeight: app.globalData.statusBarHeight || 44
})
this.loadMatchConfig()
await this.loadMatchConfig()
this.loadStoredContact()
this.loadTodayMatchCount()
this.initUserStatus()
await this.refreshMatchQuotaFromServer()
},
onShow() {
@@ -112,6 +112,7 @@ Page({
}
}
this.initUserStatus()
this.refreshMatchQuotaFromServer()
},
// 加载匹配配置
@@ -132,12 +133,15 @@ Page({
})
MATCH_TYPES = types
FREE_MATCH_LIMIT = res.data.freeMatchLimit || FREE_MATCH_LIMIT
if (FREE_MATCH_LIMIT > 1) FREE_MATCH_LIMIT = 1
const matchPrice = res.data.matchPrice || 1
const matchPriceOriginal = res.data.matchPriceOriginal != null ? res.data.matchPriceOriginal : 9.9
this.setData({
matchTypes: MATCH_TYPES,
totalMatchesAllowed: FREE_MATCH_LIMIT,
matchPrice: matchPrice
matchPrice: matchPrice,
matchPriceOriginal: matchPriceOriginal
})
console.log('[Match] 加载匹配配置成功:', {
@@ -162,49 +166,64 @@ 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 })
}
}
} catch (e) {
console.error('加载匹配次数失败:', e)
/** 从服务端同步匹配配额(终身免费 + 已购次数,不按自然日重置) */
async refreshMatchQuotaFromServer() {
const userId = app.globalData.userInfo?.id
const hasFullBookGlobal = !!app.globalData.hasFullBook
if (!userId) {
this.setData({
matchesRemaining: FREE_MATCH_LIMIT,
needPayToMatch: false,
extraMatches: 0,
totalMatchesAllowed: FREE_MATCH_LIMIT
})
return
}
try {
const res = await app.request({
url: `/api/miniprogram/user/purchase-status?userId=${encodeURIComponent(userId)}`,
silent: true
})
if (!res.success || !res.data) return
const hasFullBook = res.data.hasFullBook === true
const mq = res.data.matchQuota || {}
const remain = mq.remainToday != null ? Number(mq.remainToday) : 0
const purchasedRemain = mq.purchasedRemain != null ? Number(mq.purchasedRemain) : 0
const purchasedTotal = mq.purchasedTotal != null ? Number(mq.purchasedTotal) : 0
if (hasFullBook) {
this.setData({
hasFullBook: true,
matchesRemaining: 999999,
needPayToMatch: false,
extraMatches: purchasedRemain,
totalMatchesAllowed: 999999
})
return
}
this.setData({
hasFullBook: false,
matchesRemaining: Math.max(0, remain),
needPayToMatch: remain <= 0,
extraMatches: purchasedRemain,
totalMatchesAllowed: FREE_MATCH_LIMIT + purchasedTotal
})
} catch (e) {
console.log('[Match] 同步匹配配额失败', e)
this.setData({
hasFullBook: hasFullBookGlobal,
matchesRemaining: hasFullBookGlobal ? 999999 : this.data.matchesRemaining,
needPayToMatch: !hasFullBookGlobal && this.data.matchesRemaining <= 0
})
}
},
// 保存今日匹配次数
saveTodayMatchCount(count) {
const today = new Date().toISOString().split('T')[0]
wx.setStorageSync('match_count_data', { date: today, count })
},
// 初始化用户状态
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 { isLoggedIn, hasFullBook } = app.globalData
this.setData({
isLoggedIn,
hasFullBook,
hasPurchased: true, // 所有用户都可以使用匹配功能
totalMatchesAllowed,
matchesRemaining,
needPayToMatch,
extraMatches
hasPurchased: true
})
},
@@ -473,7 +492,7 @@ Page({
} else if (res && !res.success) {
matchFailHint = res.message || res.error || ''
if (res.code === 'QUOTA_EXCEEDED') {
matchFailHint = matchFailHint || '今日免费次数已用完,可购买额外匹配次数后再试'
matchFailHint = matchFailHint || '免费次数已用完,可付费购买匹配次数后再试'
} else if (res.code === 'NO_USERS') {
matchFailHint = matchFailHint || '当前流量池暂无可匹配用户,可稍后再试;补全档案后匹配范围通常更大。'
}
@@ -500,18 +519,11 @@ Page({
return
}
// 增加今日匹配次数
const newCount = this.data.todayMatchCount + 1
const matchesRemaining = this.data.hasFullBook ? 999999 : Math.max(0, this.data.totalMatchesAllowed - newCount)
this.setData({
isMatching: false,
currentMatch: matchedUser,
todayMatchCount: newCount,
matchesRemaining,
needPayToMatch: !this.data.hasFullBook && matchesRemaining <= 0
currentMatch: matchedUser
})
this.saveTodayMatchCount(newCount)
this.refreshMatchQuotaFromServer()
// 上报匹配行为到存客宝
this.reportMatch(matchedUser)
@@ -729,12 +741,8 @@ Page({
})
})
// 支付成功,增加匹配次数
const extraMatches = (wx.getStorageSync('extra_match_count') || 0) + 1
wx.setStorageSync('extra_match_count', extraMatches)
wx.showToast({ title: '购买成功', icon: 'success' })
this.initUserStatus()
await this.refreshMatchQuotaFromServer()
} else {
throw new Error(res.error || '创建订单失败')
}

View File

@@ -15,9 +15,9 @@
<view style="height: 16rpx;"></view>
<!-- 匹配提示条 - 简化显示 -->
<view class="match-tip-bar" wx:if="{{matchesRemaining <= 0 && !hasFullBook}}">
<view class="match-tip-bar" wx:if="{{isLoggedIn && matchesRemaining <= 0 && !hasFullBook}}">
<icon name="zap" size="36" color="#FFD700" customClass="tip-icon"></icon>
<text class="tip-text">今日免费次数已用完</text>
<text class="tip-text">免费次数已用完,可付费继续匹配</text>
<view class="tip-btn" bindtap="showUnlockModal">购买次数</view>
</view>
@@ -302,22 +302,25 @@
<view class="modal-content unlock-modal" catchtap="preventBubble">
<view class="unlock-icon"><icon name="zap" size="64" color="#FFD700"></icon></view>
<text class="unlock-title">购买匹配次数</text>
<text class="unlock-desc">今日3次免费匹配已用完可付费购买额外次数</text>
<text class="unlock-desc">每位用户仅有 1 次免费匹配(不按天重置),之后每次 ¥{{matchPrice || 1}}</text>
<view class="unlock-info">
<view class="info-row">
<view class="info-row info-row-price">
<text class="info-label">单价</text>
<text class="info-value text-brand">¥{{matchPrice || 1}} / 次</text>
<view class="info-value price-line">
<text class="price-original">¥{{matchPriceOriginal}}</text>
<text class="text-brand">¥{{matchPrice || 1}} / 次</text>
</view>
</view>
<view class="info-row">
<text class="info-label">已购</text>
<text class="info-label">已购未用</text>
<text class="info-value">{{extraMatches || 0}} 次</text>
</view>
</view>
<view class="unlock-buttons">
<view class="btn-gold" bindtap="buyMatchCount">立即购买 ¥{{matchPrice || 1}}</view>
<view class="btn-ghost" bindtap="closeUnlockModal">明天再来</view>
<view class="btn-gold" bindtap="buyMatchCount">立即购买 ¥{{matchPrice || 1}} / 次</view>
<view class="btn-ghost" bindtap="closeUnlockModal">稍后再说</view>
</view>
</view>
</view>

View File

@@ -80,6 +80,25 @@
border-radius: 20rpx;
}
.info-row-price {
align-items: flex-start;
}
.price-line {
display: flex;
flex-direction: row;
align-items: baseline;
flex-wrap: wrap;
justify-content: flex-end;
gap: 12rpx;
}
.price-original {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.45);
text-decoration: line-through;
}
.text-brand {
color: #00CED1;
}

View File

@@ -146,6 +146,15 @@ function appendQueryToPath(path, key, value) {
return `${base}${sep}${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`
}
/** 本小程序 wx.navigateTo / switchTab 用的路径:补全前导 / */
function normalizeMiniProgramNavPath(p) {
if (p == null || typeof p !== 'string') return ''
let s = p.trim()
if (!s) return ''
if (!s.startsWith('/')) s = '/' + s.replace(/^\/+/, '')
return s
}
/** 当前用户已绑定手机号(与 app 内登录态一致) */
function getLoggedInUserPhone() {
const u = app.globalData.userInfo || {}
@@ -903,6 +912,27 @@ Page({
return
}
// 本小程序内页(链接标签类型 internal#标签 跳转当前小程序页面)
if (tagType === 'internal') {
const path = normalizeMiniProgramNavPath(pagePath || url)
if (!path) {
wx.showToast({ title: '未配置页面路径', icon: 'none' })
return
}
wx.navigateTo({
url: path,
fail: () => {
wx.switchTab({
url: path,
fail: (err) => {
wx.showToast({ title: err.errMsg || '跳转失败', icon: 'none' })
},
})
},
})
return
}
// 小程序类型:用密钥查 linkedMiniprograms 得 appId再唤醒需在 app.json 的 navigateToMiniProgramAppIdList 中配置)
if (tagType === 'miniprogram') {
if (!mpKey && label) {
@@ -930,10 +960,22 @@ Page({
if (mpKey) wx.showToast({ title: '未找到关联小程序配置', icon: 'none' })
}
// 小程序内部路径(pagePath 或 url 以 /pages/ 开头
const internalPath = pagePath || (url.startsWith('/pages/') ? url : '')
// 兼容:仅填 pagePath或 url 为内页路径(非 internal 类型也可走此分支
const rawInternal =
pagePath || (url.startsWith('/pages/') || url.startsWith('pages/') ? url : '')
const internalPath = normalizeMiniProgramNavPath(rawInternal)
if (internalPath) {
wx.navigateTo({ url: internalPath, fail: () => wx.switchTab({ url: internalPath }) })
wx.navigateTo({
url: internalPath,
fail: () => {
wx.switchTab({
url: internalPath,
fail: (err) => {
wx.showToast({ title: err.errMsg || '跳转失败', icon: 'none' })
},
})
},
})
return
}