quota.js
3.06 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
/**
* `quota.js` —— 免费额度切点判定(纯逻辑,零 IO)。
*
* 两类切点,皆数据驱动(规则从 `billingRepo.listPaywallRules` 取,用量从
* `getFreeQuotaUsage` 取,本文件只做纯计算):
* 1. **前 N 集免费**:第 1..N 集(key 为序号)免费,N+1 起收费。
* 2. **每日 K 次免费续写**:当日窗口内前 K 次 `continue` 免费,第 K+1 次起收费。
*
* 设计成纯函数让全表可直接单测:给定规则 + 当前用量,返回是否免费、剩余额度、价目。
*/
/**
* @typedef {object} PaywallRules
* @property {number} [free_episodes] 前 N 集免费(N)
* @property {number} [free_continue_per_day] 每日免费续写次数(K)
* @property {Record<string, number>} [prices] 各 kind 的解锁价目(积分),如 { episode:30, branch:50, ending:80, continue:5 }
*/
/**
* 判定「第 N 集」是否落在免费区间。
* @param {object} p
* @param {PaywallRules} p.rules
* @param {number} p.episode_number 1-based 集号
* @returns {{ free:boolean, price_points:number, free_quota_left:number }}
*/
export function evaluateEpisodeQuota({ rules, episode_number }) {
const free_episodes = numOr(rules.free_episodes, 0)
const price = priceFor(rules, 'episode')
const free = episode_number <= free_episodes
// 剩余免费集数:还能免费看到第几集(相对当前集),不为负。
const free_quota_left = Math.max(0, free_episodes - episode_number + 1)
return { free, price_points: free ? 0 : price, free_quota_left }
}
/**
* 判定一次续写是否落在「每日 K 次免费」区间。
* @param {object} p
* @param {PaywallRules} p.rules
* @param {number} p.used_today 当日已用免费续写次数(窗口内)
* @returns {{ free:boolean, price_points:number, free_quota_left:number }}
*/
export function evaluateContinueQuota({ rules, used_today }) {
const k = numOr(rules.free_continue_per_day, 0)
const price = priceFor(rules, 'continue')
const used = numOr(used_today, 0)
const free_quota_left = Math.max(0, k - used)
const free = free_quota_left > 0
return { free, price_points: free ? 0 : price, free_quota_left }
}
/**
* 解锁价目(无免费额度概念的 branch / ending)。
* @param {object} p
* @param {PaywallRules} p.rules
* @param {'branch'|'ending'} p.kind
* @returns {{ price_points:number }}
*/
export function priceForUnlock({ rules, kind }) {
return { price_points: priceFor(rules, kind) }
}
/**
* 计算「日窗口」起点(UTC 当日 0 点),供 repo 按窗口聚合用量(DATETIME(3) 列,Date 出入)。
* @param {Date} [now]
* @returns {Date}
*/
export function day_window_start(now = new Date()) {
const d = new Date(now)
d.setUTCHours(0, 0, 0, 0)
return d
}
/**
* @param {PaywallRules} rules
* @param {string} kind
*/
function priceFor(rules, kind) {
const prices = rules.prices || {}
return numOr(prices[kind], 0)
}
/**
* @param {unknown} v
* @param {number} fallback
*/
function numOr(v, fallback) {
return typeof v === 'number' && Number.isFinite(v) ? v : fallback
}