Files
NexusGate/lib/reset.js
T

97 lines
3.6 KiB
JavaScript

'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 };