quota.js 3.06 KB
/**
 * `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
}