feat(panel-go): monthly traffic reset for customers
- trafficResetDay/Period/At on the shared customer record (omitempty, inert for the Node panel, which preserves unknown fields) - housekeeping applies due resets before quota evaluation: period-key idempotency, short-month clamping (31st fires Feb 28), missed-period catch-up after panel downtime, traffic suspensions auto-resume - first sight of a schedule only arms the period marker, never zeroes; API create/PATCH accept trafficResetDay (0=off) and re-arm on change - UI: reset-day input + last-reset display in the customer form - tests: calendar math, apply pass, API lifecycle, persistence; full Go/Node suites green, difftest formal 50/50
This commit is contained in:
+2
-1
@@ -335,6 +335,7 @@ function customerForm(item = null) {
|
||||
<label>流量上限(GB,0 不限)<input name="trafficGb" type="number" min="0" step="0.01" value="${Number(customer.trafficLimitBytes || 0) / 1024 ** 3}"></label><label>快速到期<select data-expiry-quick><option value="">自定义 / 不限</option><option value="7">7 天后</option><option value="30">30 天后</option><option value="90">90 天后</option><option value="365">1 年后</option></select></label>
|
||||
<label>到期日期<input name="expiryDate" type="date" value="${esc(expiry.date)}"><small>留空表示不限期,不再需要手工输入日期格式。</small></label><label>到期时间<select name="expiryTime">${timeOptions(expiry.time)}</select></label>
|
||||
<label>滚动节点 IP 上限<input name="ipLimit" type="number" min="0" step="1" value="${Number(customer.ipLimit || 0)}"><small>0 不限;Agent 每分钟上报,超限后暂停客户和节点。</small></label><label>订阅客户端估计上限<input name="deviceLimit" type="number" min="0" step="1" value="${Number(customer.deviceLimit || 0)}"><small>0 不限;24 小时内按客户端标识或 User-Agent 计数,只拦截新订阅请求,无法限制已导入的节点。</small></label>
|
||||
<label>每月流量重置日<input name="trafficResetDay" type="number" min="0" max="31" step="1" value="${Number(customer.trafficResetDay || 0)}"><small>0 不重置;1–31 为每月重置日(超过当月天数按月末),到日自动清零已用流量。${customer.trafficResetAt ? `上次重置 ${fmtDate(customer.trafficResetAt)}。` : ''}</small></label>
|
||||
<label class="wide">标签(逗号分隔)<input name="tags" value="${esc((customer.tags || []).join(', '))}"></label><label class="wide">备注<textarea name="notes">${esc(customer.notes || '')}</textarea></label>
|
||||
<div class="form-actions"><button type="button" data-close>取消</button><button class="primary" type="submit">${item ? '保存修改' : '创建客户'}</button></div></form>`);
|
||||
}
|
||||
@@ -475,7 +476,7 @@ document.addEventListener('submit', async (event) => {
|
||||
$('#modal').close(); toast(resourceId ? '设备信息已更新' : '设备已添加'); await load('servers');
|
||||
} else if (form.id === 'customer-form') {
|
||||
const resourceId = data.get('resourceId');
|
||||
const payload = { name:data.get('name'), group:data.get('group'), trafficLimitBytes:Number(data.get('trafficGb') || 0) * 1024 ** 3, expiresAt:expiryIso(data), ipLimit:Number(data.get('ipLimit') || 0), deviceLimit:Number(data.get('deviceLimit') || 0), tags:splitList(data.get('tags')), notes:data.get('notes') };
|
||||
const payload = { name:data.get('name'), group:data.get('group'), trafficLimitBytes:Number(data.get('trafficGb') || 0) * 1024 ** 3, expiresAt:expiryIso(data), ipLimit:Number(data.get('ipLimit') || 0), deviceLimit:Number(data.get('deviceLimit') || 0), trafficResetDay:Number(data.get('trafficResetDay') || 0), tags:splitList(data.get('tags')), notes:data.get('notes') };
|
||||
await api(resourceId ? `/api/customers/${resourceId}` : '/api/customers', { method:resourceId ? 'PATCH' : 'POST', body:JSON.stringify(payload) });
|
||||
$('#modal').close(); toast(resourceId ? '客户信息已更新' : '客户已创建'); await load('customers');
|
||||
} else if (form.id === 'node-form') {
|
||||
|
||||
Reference in New Issue
Block a user