207 lines
6.2 KiB
JavaScript
207 lines
6.2 KiB
JavaScript
/**
|
|
* 章节权限管理器
|
|
* 统一管理章节权限判断、状态流转、异常处理
|
|
*/
|
|
|
|
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('/api/miniprogram/config', { timeout: 3000 })
|
|
if (res.success && res.freeChapters) {
|
|
return {
|
|
freeChapters: res.freeChapters,
|
|
prices: res.prices || { section: 1, fullbook: 9.9 },
|
|
userDiscount: (typeof res.userDiscount === 'number' ? res.userDiscount : 5)
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.warn('[AccessManager] 获取配置失败,使用默认配置:', e)
|
|
}
|
|
|
|
return {
|
|
freeChapters: ['preface', 'epilogue', '1.1', 'appendix-1', 'appendix-2', 'appendix-3'],
|
|
prices: { section: 1, fullbook: 9.9 },
|
|
userDiscount: 5
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 判断章节是否免费
|
|
*/
|
|
isFreeChapter(sectionId, freeList) {
|
|
return freeList.includes(sectionId)
|
|
}
|
|
|
|
/**
|
|
* 【核心方法】确定章节权限状态
|
|
* @param {string} sectionId - 章节ID
|
|
* @param {Array} freeList - 免费章节列表
|
|
* @returns {Promise<string>} accessState
|
|
*/
|
|
async determineAccessState(sectionId, freeList) {
|
|
try {
|
|
// 1. 检查是否免费
|
|
if (this.isFreeChapter(sectionId, freeList)) {
|
|
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 || []
|
|
app.globalData.sectionMidMap = res.data.sectionMidMap || {}
|
|
app.globalData.matchCount = res.data.matchCount ?? 0
|
|
app.globalData.matchQuota = res.data.matchQuota || null
|
|
|
|
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,
|
|
matchCount: res.data.matchCount
|
|
})
|
|
}
|
|
} 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
|