feat(panel): reskin backend — node sortIndex, presence/speed, /api/system, credentials, subscription aggregation (Node+Go parity)
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
'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 };
|
||||
Reference in New Issue
Block a user