Files
soul-yongping/miniprogram/utils/chapterAccessManager.js
2026-03-07 22:58:43 +08:00

201 lines
6.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 章节权限管理器
* 统一管理章节权限判断、状态流转、异常处理
*/
const app = getApp()
class ChapterAccessManager {
constructor() {
this.accessStates = {
UNKNOWN: 'unknown',
FREE: 'free',
LOCKED_NOT_LOGIN: 'locked_not_login',
LOCKED_NOT_PURCHASED: 'locked_not_purchased',
UNLOCKED_PURCHASED: 'unlocked_purchased',
ERROR: 'error'
}
}
/**
* 拉取最新配置(价格等,免费以章节数据为准)
*/
async fetchLatestConfig() {
try {
const res = await app.request({ url: '/api/miniprogram/config', silent: true, timeout: 3000 })
if (res.success && res.prices) {
return {
prices: res.prices || { section: 1, fullbook: 9.9 }
}
}
} catch (e) {
console.warn('[AccessManager] 获取配置失败,使用默认配置:', e)
}
return {
prices: { section: 1, fullbook: 9.9 }
}
}
/**
* 判断章节是否免费(统一:章节 isFree 或 price===0
*/
isFreeFromChapterData(chapterData) {
if (!chapterData) return false
if (chapterData.isFree === true) return true
if (chapterData.price !== undefined && chapterData.price === 0) return true
return false
}
/**
* 【核心方法】确定章节权限状态(统一以章节数据 isFree/price 为准)
* @param {string} sectionId - 章节ID
* @param {object} chapterData - 章节接口返回 { isFree, price, ... },免费 = isFree 或 price===0
* @returns {Promise<string>} accessState
*/
async determineAccessState(sectionId, chapterData) {
try {
// 1. 检查是否免费(统一:章节 isFree 或 price===0
if (this.isFreeFromChapterData(chapterData)) {
console.log('[AccessManager] 免费章节:', sectionId)
return this.accessStates.FREE
}
// 2. 检查是否登录
const userId = app.globalData.userInfo?.id
if (!userId) {
console.log('[AccessManager] 未登录,需要登录:', sectionId)
return this.accessStates.LOCKED_NOT_LOGIN
}
// 3. 请求服务端校验是否已购买(带重试)
const res = await this.requestWithRetry(
`/api/miniprogram/user/check-purchased?userId=${encodeURIComponent(userId)}&type=section&productId=${encodeURIComponent(sectionId)}`,
{ timeout: 5000 },
2 // 最多重试2次
)
if (res.success && res.data?.isPurchased) {
console.log('[AccessManager] 已购买:', sectionId, res.data.reason)
// 同步更新本地缓存(仅用于展示,不作权限依据)
this.syncLocalCache(sectionId, res.data)
return this.accessStates.UNLOCKED_PURCHASED
}
console.log('[AccessManager] 未购买:', sectionId)
return this.accessStates.LOCKED_NOT_PURCHASED
} catch (error) {
console.error('[AccessManager] 权限判断失败:', error)
// 网络/服务端错误 → 保守策略:返回错误状态
return this.accessStates.ERROR
}
}
/**
* 带重试的请求
*/
async requestWithRetry(url, options = {}, maxRetries = 3) {
let lastError = null
for (let i = 0; i < maxRetries; i++) {
try {
const res = await app.request(url, options)
return res
} catch (e) {
lastError = e
console.warn(`[AccessManager] 第 ${i+1} 次请求失败:`, url, e.message)
// 如果不是最后一次,等待后重试(指数退避)
if (i < maxRetries - 1) {
await this.sleep(1000 * (i + 1))
}
}
}
throw lastError
}
/**
* 同步更新本地购买缓存(仅用于展示,不作权限依据)
*/
syncLocalCache(sectionId, purchaseData) {
if (purchaseData.reason === 'has_full_book') {
app.globalData.hasFullBook = true
}
if (!app.globalData.purchasedSections.includes(sectionId)) {
app.globalData.purchasedSections = [...app.globalData.purchasedSections, sectionId]
}
// 更新 storage
const userInfo = app.globalData.userInfo || {}
userInfo.hasFullBook = app.globalData.hasFullBook
userInfo.purchasedSections = app.globalData.purchasedSections
wx.setStorageSync('userInfo', userInfo)
}
/**
* 刷新用户购买状态(从 orders 表拉取最新)
*/
async refreshUserPurchaseStatus() {
const userId = app.globalData.userInfo?.id
if (!userId) return
try {
const res = await app.request(`/api/miniprogram/user/purchase-status?userId=${encodeURIComponent(userId)}`)
if (res.success && res.data) {
app.globalData.hasFullBook = res.data.hasFullBook || false
app.globalData.purchasedSections = res.data.purchasedSections || []
const userInfo = app.globalData.userInfo || {}
userInfo.hasFullBook = res.data.hasFullBook
userInfo.purchasedSections = res.data.purchasedSections
wx.setStorageSync('userInfo', userInfo)
console.log('[AccessManager] 购买状态已刷新:', {
hasFullBook: res.data.hasFullBook,
purchasedCount: res.data.purchasedSections.length
})
}
} catch (e) {
console.error('[AccessManager] 刷新购买状态失败:', e)
}
}
/**
* 获取状态对应的用户提示文案
*/
getStateMessage(accessState) {
const messages = {
[this.accessStates.UNKNOWN]: '加载中...',
[this.accessStates.FREE]: '免费阅读',
[this.accessStates.LOCKED_NOT_LOGIN]: '登录后继续阅读',
[this.accessStates.LOCKED_NOT_PURCHASED]: '购买后继续阅读',
[this.accessStates.UNLOCKED_PURCHASED]: '已解锁',
[this.accessStates.ERROR]: '网络异常,请重试'
}
return messages[accessState] || '未知状态'
}
/**
* 判断是否可访问全文
*/
canAccessFullContent(accessState) {
return [this.accessStates.FREE, this.accessStates.UNLOCKED_PURCHASED].includes(accessState)
}
/**
* 工具:延迟
*/
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
}
// 导出单例
const accessManager = new ChapterAccessManager()
export default accessManager