'use strict'; // Monthly traffic reset — Node port of panel-go/reset.go (the two panel // implementations must stay behavior-identical). A customer with // trafficResetDay (1-31) gets the cycle counters zeroed once per monthly // period. Due-ness derives only from the persisted trafficResetPeriod // marker, so a reset missed while the panel was down fires at the first // housekeeping pass after it is back. All dates are panel-local. function daysInMonth(year, monthIndex) { return new Date(Date.UTC(year, monthIndex + 1, 0)).getUTCDate(); } function resetInstant(year, monthIndex, day) { const dim = daysInMonth(year, monthIndex); return new Date(year, monthIndex, Math.min(day, dim), 0, 0, 0, 0); } function periodKeyOf(date) { const pad = (n) => String(n).padStart(2, '0'); return `${date.getFullYear()}-${pad(date.getMonth() + 1)}`; } // The "YYYY-MM" of the most recent scheduled reset instant at or before now. function trafficResetPeriodKey(now, day) { let year = now.getFullYear(); let month = now.getMonth(); let cand = resetInstant(year, month, day); if (now.getTime() < cand.getTime()) { month -= 1; if (month < 0) { month = 11; year -= 1; } cand = resetInstant(year, month, day); } return periodKeyOf(cand); } // (Re)arm a customer's period marker for its current reset day WITHOUT // zeroing usage: the schedule takes effect from the next reset instant. function armTrafficReset(customer, now = new Date()) { const day = Number(customer.trafficResetDay || 0); if (!Number.isInteger(day) || day < 1 || day > 31) { delete customer.trafficResetPeriod; return; } customer.trafficResetPeriod = trafficResetPeriodKey(now, day); } // Validate a trafficResetDay request value: 0 disables, 1-31 is the day of // month. null/absent means 0. Mirrors panel-go ParseResetDay. function parseResetDay(value) { if (value == null) return 0; const number = Number(value); if (!Number.isFinite(number) || number < 0 || !Number.isInteger(number)) { const error = new Error('每月重置日必须是非负整数'); error.statusCode = 400; throw error; } if (number > 31) { const error = new Error('每月重置日须在 0–31 之间(0 表示不重置)'); error.statusCode = 400; throw error; } return number; } // Perform due monthly resets for all customers in data as of now. Returns // the list of customers whose counters were zeroed (the caller mirrors the // manual reset-usage semantics: a traffic_limit suspension may lift). // auditFn(data, customer, previousUsed, day, period) records the event. function applyMonthlyTrafficResets(data, now, auditFn) { const reset = []; for (const customer of data.customers) { const day = Number(customer.trafficResetDay || 0); if (!Number.isInteger(day) || day < 1 || day > 31) continue; const key = trafficResetPeriodKey(now, day); if (!customer.trafficResetPeriod) { // First sight: arm only — usage already accrued in the current // period must survive until the next reset day. customer.trafficResetPeriod = key; continue; } if (customer.trafficResetPeriod === key) continue; const previousUsed = customer.usedBytes || 0; customer.usedBytes = 0; customer.usedUplinkBytes = 0; customer.usedDownlinkBytes = 0; customer.trafficResetPeriod = key; customer.trafficResetAt = now.toISOString(); customer.updatedAt = now.toISOString(); if (auditFn) auditFn(data, customer, previousUsed, day, key); reset.push(customer); } return reset; } module.exports = { trafficResetPeriodKey, armTrafficReset, parseResetDay, applyMonthlyTrafficResets, daysInMonth, resetInstant };