inbounds: allow custom monthly traffic reset days (#6071)

This commit is contained in:
Shichao Song
2026-07-29 05:27:09 +08:00
committed by GitHub
parent 34d2591e50
commit 17e6b5a460
37 changed files with 179 additions and 16 deletions

View File

@@ -369,8 +369,8 @@ All registered in `web.go` → `startTask()`. Each is a struct with a `Run()` me
| `@every 5m` | `outbound_subscription_job` | Refresh outbound provider configs |
| `@every 10m` | `clear_logs_job` (`PruneXrayLogsJob`) | Truncate Xray access/error logs once either exceeds 64 MiB |
| `@hourly` | `warp_ip_job`, `periodic_traffic_reset_job("hourly")` | WARP IP rotation; traffic resets |
| `@daily` | `clear_logs_job`, `periodic_traffic_reset_job("daily")` | IP-limit and Xray access/error log cleanup; traffic resets |
| `@weekly` / `@monthly` | `periodic_traffic_reset_job(...)` | Weekly/monthly traffic resets |
| `@daily` | `clear_logs_job`, `periodic_traffic_reset_job("daily")`, `periodic_traffic_reset_job("monthly")` | IP-limit and Xray access/error log cleanup; daily resets and due monthly resets |
| `@weekly` | `periodic_traffic_reset_job("weekly")` | Weekly traffic resets |
| default `@every 1m` | `ldap_sync_job` | Only if LDAP enabled; schedule configurable |
| default `@daily` | `stats_notify_job` | Only if TG bot enabled; schedule configurable |
| `@every 2m` | `check_hash_storage` | Only if TG bot enabled; expires bot callback hashes |

View File

@@ -40,6 +40,9 @@ See [Clients](/docs/config/clients).
Optionally cap total traffic and set an expiry date for the inbound, and choose a
periodic **traffic reset** schedule: `never` (default), `hourly`, `daily`,
`weekly`, or `monthly`.
For `monthly` resets, select a day from 1 to 31. If the selected day does not
exist in a shorter month, the reset runs on that month's last day.
</Step>
</Steps>

View File

@@ -40,6 +40,9 @@ TLS یا REALITY) را انتخاب کنید. به [انتقال‌ها](/docs/c
به‌صورت اختیاری می‌توانید کل ترافیک را محدود کنید و یک تاریخ انقضا برای ورودی
تعیین کنید، و یک زمان‌بندی **بازنشانی ترافیک** دوره‌ای انتخاب کنید: `never`
(پیش‌فرض)، `hourly`، `daily`، `weekly` یا `monthly`.
برای بازنشانی `monthly`، روزی از ۱ تا ۳۱ انتخاب کنید. اگر آن روز در ماهی کوتاه‌تر
وجود نداشته باشد، بازنشانی در آخرین روز همان ماه انجام می‌شود.
</Step>
</Steps>

View File

@@ -41,6 +41,9 @@ icon: ArrowDownToLine
При необходимости ограничьте общий объём трафика и установите дату истечения для
входящего подключения, а также выберите расписание периодического **сброса трафика**:
`never` (по умолчанию), `hourly`, `daily`, `weekly` или `monthly`.
Для сброса `monthly` выберите день от 1 до 31. Если выбранного дня нет в более
коротком месяце, сброс выполняется в последний день этого месяца.
</Step>
</Steps>

View File

@@ -37,6 +37,9 @@ icon: ArrowDownToLine
可选地为入站设置总流量上限和到期日期,并选择一个周期性的**流量重置**计划:
`never`(默认)、`hourly`、`daily`、`weekly` 或 `monthly`。
选择 `monthly` 时,可以指定每月 1 至 31 日重置。如果当月没有指定日期,
则在该月最后一天重置。
</Step>
</Steps>

View File

@@ -1868,6 +1868,13 @@
],
"type": "string"
},
"trafficResetDay": {
"description": "Day of month for monthly traffic resets",
"example": 1,
"maximum": 31,
"minimum": 1,
"type": "integer"
},
"up": {
"description": "Upload traffic in bytes",
"format": "int64",
@@ -1894,6 +1901,7 @@
"tag",
"total",
"trafficReset",
"trafficResetDay",
"up"
],
"type": "object"
@@ -3159,6 +3167,7 @@
"tag": "in-443-tcp",
"total": 0,
"trafficReset": "never",
"trafficResetDay": 1,
"up": 0
}
]

View File

@@ -450,6 +450,7 @@ export const EXAMPLES: Record<string, unknown> = {
"tag": "in-443-tcp",
"total": 0,
"trafficReset": "never",
"trafficResetDay": 1,
"up": 0
},
"InboundClientIps": {

View File

@@ -1842,6 +1842,13 @@ export const SCHEMAS: Record<string, unknown> = {
],
"type": "string"
},
"trafficResetDay": {
"description": "Day of month for monthly traffic resets",
"example": 1,
"maximum": 31,
"minimum": 1,
"type": "integer"
},
"up": {
"description": "Upload traffic in bytes",
"format": "int64",
@@ -1868,6 +1875,7 @@ export const SCHEMAS: Record<string, unknown> = {
"tag",
"total",
"trafficReset",
"trafficResetDay",
"up"
],
"type": "object"

View File

@@ -428,6 +428,7 @@ export interface Inbound {
tag: string;
total: number;
trafficReset: string;
trafficResetDay: number;
up: number;
}

View File

@@ -453,6 +453,7 @@ export const InboundSchema = z.object({
tag: z.string(),
total: z.number().int(),
trafficReset: z.enum(['never', 'hourly', 'daily', 'weekly', 'monthly']),
trafficResetDay: z.number().int().min(1).max(31),
up: z.number().int(),
});
export type Inbound = z.infer<typeof InboundSchema>;

View File

@@ -41,6 +41,7 @@ export interface RawInboundRow {
enable?: boolean;
expiryTime?: number;
trafficReset?: string;
trafficResetDay?: number;
lastTrafficResetTime?: number;
nodeId?: number | null;
shareAddrStrategy?: string;
@@ -60,6 +61,7 @@ export interface WireInboundPayload {
enable: boolean;
expiryTime: number;
trafficReset: TrafficReset;
trafficResetDay: number;
lastTrafficResetTime: number;
listen: string;
port: number;
@@ -202,6 +204,7 @@ export function rawInboundToFormValues(row: RawInboundRow): InboundFormValues {
down: row.down ?? 0,
total: row.total ?? 0,
trafficReset: coerceTrafficReset(row.trafficReset),
trafficResetDay: Math.min(31, Math.max(1, row.trafficResetDay ?? 1)),
lastTrafficResetTime: row.lastTrafficResetTime ?? 0,
nodeId: row.nodeId ?? null,
shareAddrStrategy: coerceShareAddrStrategy(row.shareAddrStrategy),
@@ -344,6 +347,7 @@ export function formValuesToWirePayload(values: InboundFormValues): WireInboundP
enable: values.enable,
expiryTime: values.expiryTime,
trafficReset: values.trafficReset,
trafficResetDay: values.trafficResetDay,
lastTrafficResetTime: values.lastTrafficResetTime,
listen: values.listen,
port: values.port,

View File

@@ -30,6 +30,7 @@ export type DBInboundInit = Partial<{
enable: boolean;
expiryTime: number;
trafficReset: string;
trafficResetDay: number;
lastTrafficResetTime: number;
listen: string;
port: number;
@@ -76,6 +77,7 @@ export class DBInbound {
enable: boolean;
expiryTime: number;
trafficReset: string;
trafficResetDay: number;
lastTrafficResetTime: number;
listen: string;
@@ -105,6 +107,7 @@ export class DBInbound {
this.enable = true;
this.expiryTime = 0;
this.trafficReset = "never";
this.trafficResetDay = 1;
this.lastTrafficResetTime = 0;
this.listen = "";

View File

@@ -33,6 +33,7 @@ import {
isSS2022,
} from '@/lib/xray/protocol-capabilities';
import {
InboundDbFieldsSchema,
InboundFormBaseSchema,
InboundFormSchema,
type InboundFormValues,
@@ -255,6 +256,7 @@ export default function InboundFormModal({
const wTunnelNetwork = useWatch({ control, name: 'settings.allowedNetwork' });
const wTotal = (useWatch({ control, name: 'total' }) as number | undefined) ?? 0;
const wExpiry = (useWatch({ control, name: 'expiryTime' }) as number | undefined) ?? 0;
const trafficReset = useWatch({ control, name: 'trafficReset' }) ?? 'never';
const autoTagRef = useRef(true);
const lastWrittenTagRef = useRef('');
const currentTagInput = (): InboundTagInput => ({
@@ -619,6 +621,16 @@ export default function InboundFormModal({
/>
</FormField>
{trafficReset === 'monthly' && (
<FormField
name="trafficResetDay"
label={t('pages.inbounds.periodicTrafficResetDay')}
rules={{ validate: rhfZodValidate(InboundDbFieldsSchema.shape.trafficResetDay) }}
>
<InputNumber min={1} max={31} />
</FormField>
)}
<Form.Item
label={
<Tooltip title={t('pages.inbounds.leaveBlankToNeverExpire')}>

View File

@@ -22,6 +22,7 @@ export const InboundDbFieldsSchema = z.object({
down: z.number().int().min(0).default(0),
total: z.number().int().min(0).default(0),
trafficReset: TrafficResetSchema.default('never'),
trafficResetDay: z.number().int().min(1).max(31).default(1),
lastTrafficResetTime: z.number().int().default(0),
nodeId: z.number().int().nullable().optional(),
shareAddrStrategy: ShareAddrStrategySchema.default('node'),

View File

@@ -32,6 +32,7 @@ const vlessRow: RawInboundRow = {
total: 1_000_000_000,
expiryTime: 0,
trafficReset: 'monthly',
trafficResetDay: 15,
lastTrafficResetTime: 0,
tag: 'inbound-1',
nodeId: null,
@@ -267,6 +268,7 @@ describe('formValuesToWirePayload', () => {
enable: payload.enable,
expiryTime: payload.expiryTime,
trafficReset: payload.trafficReset,
trafficResetDay: payload.trafficResetDay,
lastTrafficResetTime: payload.lastTrafficResetTime,
nodeId: payload.nodeId ?? null,
});
@@ -276,8 +278,13 @@ describe('formValuesToWirePayload', () => {
expect(replay.listen).toBe(original.listen);
expect(replay.up).toBe(original.up);
expect(replay.down).toBe(original.down);
expect(replay.trafficResetDay).toBe(original.trafficResetDay);
expect(replay.streamSettings).toEqual(original.streamSettings);
});
it('defaults a missing monthly reset day to the first', () => {
expect(rawInboundToFormValues({ ...vlessRow, trafficResetDay: undefined }).trafficResetDay).toBe(1);
});
});
describe('subSortIndex', () => {

View File

@@ -54,6 +54,7 @@ type Inbound struct {
Enable bool `json:"enable" form:"enable" gorm:"index:idx_enable_traffic_reset,priority:1" example:"true"` // Whether the inbound is enabled
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` // Expiration timestamp
TrafficReset string `json:"trafficReset" form:"trafficReset" gorm:"default:never;index:idx_enable_traffic_reset,priority:2" validate:"omitempty,oneof=never hourly daily weekly monthly"` // Traffic reset schedule
TrafficResetDay int `json:"trafficResetDay" form:"trafficResetDay" gorm:"default:1" validate:"omitempty,gte=1,lte=31" example:"1"` // Day of month for monthly traffic resets
LastTrafficResetTime int64 `json:"lastTrafficResetTime" form:"lastTrafficResetTime" gorm:"default:0"` // Last traffic reset timestamp
ClientStats []xray.ClientTraffic `gorm:"foreignKey:InboundId;references:Id" json:"clientStats" form:"clientStats"` // Client traffic statistics

View File

@@ -1,6 +1,8 @@
package job
import (
"time"
"github.com/mhsanaei/3x-ui/v3/internal/logger"
"github.com/mhsanaei/3x-ui/v3/internal/web/service"
)
@@ -13,15 +15,25 @@ type PeriodicTrafficResetJob struct {
inboundService service.InboundService
clientService service.ClientService
period Period
location *time.Location
}
// NewPeriodicTrafficResetJob creates a new periodic traffic reset job for the specified period.
func NewPeriodicTrafficResetJob(period Period) *PeriodicTrafficResetJob {
func NewPeriodicTrafficResetJob(period Period, location *time.Location) *PeriodicTrafficResetJob {
return &PeriodicTrafficResetJob{
period: period,
period: period,
location: location,
}
}
func monthlyResetDue(resetDay int, now time.Time) bool {
if resetDay < 1 {
resetDay = 1
}
lastDay := time.Date(now.Year(), now.Month()+1, 0, 0, 0, 0, 0, now.Location()).Day()
return now.Day() == min(resetDay, lastDay)
}
// Run resets traffic statistics for all inbounds that match the configured reset period.
func (j *PeriodicTrafficResetJob) Run() {
inbounds, err := j.inboundService.GetInboundsByTrafficReset(string(j.period))
@@ -30,13 +42,22 @@ func (j *PeriodicTrafficResetJob) Run() {
return
}
if j.period == "monthly" {
now := time.Now().In(j.location)
due := inbounds[:0]
for _, inbound := range inbounds {
if monthlyResetDue(inbound.TrafficResetDay, now) {
due = append(due, inbound)
}
}
inbounds = due
}
if len(inbounds) == 0 {
return
}
logger.Infof("Running periodic traffic reset job for period: %s (%d matching inbounds)", j.period, len(inbounds))
resetCount := 0
for _, inbound := range inbounds {
resetInboundErr := j.inboundService.ResetInboundTraffic(inbound.Id)
if resetInboundErr != nil {

View File

@@ -0,0 +1,31 @@
package job
import (
"testing"
"time"
)
func TestMonthlyResetDue(t *testing.T) {
cases := []struct {
name string
resetDay int
now time.Time
want bool
}{
{"legacy default on first", 0, time.Date(2026, time.July, 1, 0, 0, 0, 0, time.UTC), true},
{"configured day", 15, time.Date(2026, time.July, 15, 0, 0, 0, 0, time.UTC), true},
{"before configured day", 15, time.Date(2026, time.July, 14, 0, 0, 0, 0, time.UTC), false},
{"month end", 31, time.Date(2026, time.January, 31, 0, 0, 0, 0, time.UTC), true},
{"short month fallback", 31, time.Date(2026, time.February, 28, 0, 0, 0, 0, time.UTC), true},
{"leap year fallback", 31, time.Date(2028, time.February, 29, 0, 0, 0, 0, time.UTC), true},
{"not before short month end", 31, time.Date(2028, time.February, 28, 0, 0, 0, 0, time.UTC), false},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := monthlyResetDue(tc.resetDay, tc.now); got != tc.want {
t.Fatalf("monthlyResetDue(%d, %s) = %v, want %v", tc.resetDay, tc.now.Format(time.DateOnly), got, tc.want)
}
})
}
}

View File

@@ -770,6 +770,9 @@ func wireInbound(ib *model.Inbound, remoteNodeID int) url.Values {
if ib.TrafficReset != "" {
v.Set("trafficReset", ib.TrafficReset)
}
if ib.TrafficResetDay > 0 {
v.Set("trafficResetDay", strconv.Itoa(ib.TrafficResetDay))
}
return v
}

View File

@@ -252,9 +252,12 @@ func TestIsNonEmptySlice(t *testing.T) {
}
func TestWireInboundTrafficReset(t *testing.T) {
with := wireInbound(&model.Inbound{TrafficReset: "daily"}, 0)
if got := with.Get("trafficReset"); got != "daily" {
t.Fatalf("trafficReset = %q, want daily", got)
with := wireInbound(&model.Inbound{TrafficReset: "monthly", TrafficResetDay: 15}, 0)
if got := with.Get("trafficReset"); got != "monthly" {
t.Fatalf("trafficReset = %q, want monthly", got)
}
if got := with.Get("trafficResetDay"); got != "15" {
t.Fatalf("trafficResetDay = %q, want 15", got)
}
// Empty TrafficReset must be omitted entirely, not sent as an empty field.
without := wireInbound(&model.Inbound{}, 0)

View File

@@ -34,6 +34,13 @@ type InboundService struct {
fallbackService FallbackService
}
func normalizeTrafficResetDay(day int) int {
if day < 1 {
return 1
}
return min(day, 31)
}
func normalizeInboundShareAddrStrategy(strategy string) string {
strategy = strings.TrimSpace(strategy)
switch strategy {
@@ -905,6 +912,7 @@ func (s *InboundService) normalizeMtprotoXrayPort(inbound *model.Inbound, oldSet
// Returns the created inbound, whether Xray needs restart, and any error.
func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, bool, error) {
inbound.Id = 0
inbound.TrafficResetDay = normalizeTrafficResetDay(inbound.TrafficResetDay)
// Normalize streamSettings based on protocol
s.normalizeStreamSettings(inbound)
if err := validateFinalMaskRealityCombo(inbound.StreamSettings); err != nil {
@@ -1331,6 +1339,7 @@ func (s *InboundService) SetInboundEnable(id int, enable bool) (bool, error) {
}
func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound, bool, error) {
inbound.TrafficResetDay = normalizeTrafficResetDay(inbound.TrafficResetDay)
// Normalize streamSettings based on protocol
s.normalizeStreamSettings(inbound)
if err := validateFinalMaskRealityCombo(inbound.StreamSettings); err != nil {
@@ -1460,6 +1469,7 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
oldInbound.Enable = inbound.Enable
oldInbound.ExpiryTime = inbound.ExpiryTime
oldInbound.TrafficReset = inbound.TrafficReset
oldInbound.TrafficResetDay = inbound.TrafficResetDay
oldInbound.Listen = inbound.Listen
oldInbound.Port = inbound.Port
oldInbound.Protocol = inbound.Protocol

View File

@@ -311,7 +311,8 @@ func adoptedWireChanged(c, snapIb *model.Inbound, adoptedSettings string) bool {
c.ExpiryTime != snapIb.ExpiryTime ||
c.StreamSettings != snapIb.StreamSettings ||
c.Sniffing != snapIb.Sniffing ||
c.TrafficReset != snapIb.TrafficReset
c.TrafficReset != snapIb.TrafficReset ||
c.TrafficResetDay != normalizeTrafficResetDay(snapIb.TrafficResetDay)
}
// adoptedWireInbound is the central inbound as it reads after adopting the
@@ -330,6 +331,7 @@ func adoptedWireInbound(c, snapIb *model.Inbound, adoptedSettings string) *model
a.StreamSettings = snapIb.StreamSettings
a.Sniffing = snapIb.Sniffing
a.TrafficReset = snapIb.TrafficReset
a.TrafficResetDay = normalizeTrafficResetDay(snapIb.TrafficResetDay)
return &a
}
@@ -561,6 +563,7 @@ func (s *InboundService) setRemoteTrafficLocked(nodeID int, snap *runtime.Traffi
StreamSettings: snapIb.StreamSettings,
Sniffing: snapIb.Sniffing,
TrafficReset: snapIb.TrafficReset,
TrafficResetDay: normalizeTrafficResetDay(snapIb.TrafficResetDay),
LastTrafficResetTime: snapIb.LastTrafficResetTime,
Enable: snapIb.Enable,
Remark: snapIb.Remark,
@@ -616,6 +619,7 @@ func (s *InboundService) setRemoteTrafficLocked(nodeID int, snap *runtime.Traffi
updates["stream_settings"] = snapIb.StreamSettings
updates["sniffing"] = snapIb.Sniffing
updates["traffic_reset"] = snapIb.TrafficReset
updates["traffic_reset_day"] = normalizeTrafficResetDay(snapIb.TrafficResetDay)
updates["last_traffic_reset_time"] = snapIb.LastTrafficResetTime
if adoptedWireChanged(c, snapIb, adoptedSettings) {
adoptedInbounds = append(adoptedInbounds, adoptedWireInbound(c, snapIb, adoptedSettings))

View File

@@ -0,0 +1,18 @@
package service
import "testing"
func TestNormalizeTrafficResetDay(t *testing.T) {
tests := map[int]int{
0: 1,
1: 1,
15: 15,
31: 31,
32: 31,
}
for input, want := range tests {
if got := normalizeTrafficResetDay(input); got != want {
t.Errorf("normalizeTrafficResetDay(%d) = %d, want %d", input, got, want)
}
}
}

View File

@@ -451,6 +451,7 @@
"importInbound": "استيراد إدخال",
"periodicTrafficResetTitle": "إعادة تعيين حركة المرور",
"periodicTrafficResetDesc": "إعادة تعيين عداد حركة المرور تلقائيًا في فترات محددة",
"periodicTrafficResetDay": "يوم إعادة التعيين الشهري",
"lastReset": "آخر إعادة تعيين",
"periodicTrafficReset": {
"never": "أبداً",

View File

@@ -451,6 +451,7 @@
"importInbound": "Import an Inbound",
"periodicTrafficResetTitle": "Traffic Reset",
"periodicTrafficResetDesc": "Automatically reset traffic counter at specified intervals",
"periodicTrafficResetDay": "Monthly reset day",
"lastReset": "Last Reset",
"periodicTrafficReset": {
"never": "Never",

View File

@@ -451,6 +451,7 @@
"importInbound": "Importar un entrante",
"periodicTrafficResetTitle": "Reset de Tráfico",
"periodicTrafficResetDesc": "Reiniciar automáticamente el contador de tráfico en intervalos especificados",
"periodicTrafficResetDay": "Día de reinicio mensual",
"lastReset": "Último reinicio",
"periodicTrafficReset": {
"never": "Nunca",

View File

@@ -451,6 +451,7 @@
"importInbound": "افزودن یک ورودی",
"periodicTrafficResetTitle": "بازنشانی ترافیک",
"periodicTrafficResetDesc": "بازنشانی خودکار شمارنده ترافیک در فواصل زمانی مشخص",
"periodicTrafficResetDay": "روز بازنشانی ماهانه",
"lastReset": "آخرین بازنشانی",
"periodicTrafficReset": {
"never": "هرگز",

View File

@@ -451,6 +451,7 @@
"importInbound": "Impor Masuk",
"periodicTrafficResetTitle": "Reset Trafik Berkala",
"periodicTrafficResetDesc": "Reset otomatis penghitung trafik pada interval tertentu",
"periodicTrafficResetDay": "Hari reset bulanan",
"lastReset": "Reset Terakhir",
"periodicTrafficReset": {
"never": "Tidak Pernah",

View File

@@ -451,6 +451,7 @@
"importInbound": "インバウンドルールをインポート",
"periodicTrafficResetTitle": "トラフィックリセット",
"periodicTrafficResetDesc": "指定された間隔でトラフィックカウンタを自動的にリセット",
"periodicTrafficResetDay": "毎月のリセット日",
"lastReset": "最後のリセット",
"periodicTrafficReset": {
"never": "なし",

View File

@@ -451,6 +451,7 @@
"importInbound": "Importar um Inbound",
"periodicTrafficResetTitle": "Reset de Tráfego",
"periodicTrafficResetDesc": "Reinicia automaticamente o contador de tráfego em intervalos especificados",
"periodicTrafficResetDay": "Dia da redefinição mensal",
"lastReset": "Último Reset",
"periodicTrafficReset": {
"never": "Nunca",

View File

@@ -451,6 +451,7 @@
"importInbound": "Импорт подключений",
"periodicTrafficResetTitle": "Сброс трафика",
"periodicTrafficResetDesc": "Автоматический сброс счетчика трафика через указанные интервалы",
"periodicTrafficResetDay": "День ежемесячного сброса",
"lastReset": "Последний сброс",
"periodicTrafficReset": {
"never": "Никогда",

View File

@@ -451,6 +451,7 @@
"importInbound": "Gelen Bağlantı İçe Aktar",
"periodicTrafficResetTitle": "Trafik Sıfırlama",
"periodicTrafficResetDesc": "Belirtilen aralıklarla trafik sayacını otomatik olarak sıfırla",
"periodicTrafficResetDay": "Aylık sıfırlama günü",
"lastReset": "Son Sıfırlama",
"periodicTrafficReset": {
"never": "Asla",

View File

@@ -451,6 +451,7 @@
"importInbound": "Імпортувати вхідний",
"periodicTrafficResetTitle": "Скидання трафіку",
"periodicTrafficResetDesc": "Автоматично скидати лічильник трафіку через певні проміжки часу",
"periodicTrafficResetDay": "День щомісячного скидання",
"lastReset": "Останнє скидання",
"periodicTrafficReset": {
"never": "Ніколи",

View File

@@ -451,6 +451,7 @@
"importInbound": "Nhập inbound",
"periodicTrafficResetTitle": "Đặt lại lưu lượng",
"periodicTrafficResetDesc": "Tự động đặt lại bộ đếm lưu lượng theo khoảng thời gian xác định",
"periodicTrafficResetDay": "Ngày đặt lại hàng tháng",
"lastReset": "Đặt lại lần cuối",
"periodicTrafficReset": {
"never": "Không bao giờ",

View File

@@ -451,6 +451,7 @@
"importInbound": "导入入站规则",
"periodicTrafficResetTitle": "流量重置",
"periodicTrafficResetDesc": "按指定间隔自动重置流量计数器",
"periodicTrafficResetDay": "每月重置日",
"lastReset": "上次重置",
"periodicTrafficReset": {
"never": "从不",

View File

@@ -451,6 +451,7 @@
"importInbound": "匯入入站規則",
"periodicTrafficResetTitle": "流量重置",
"periodicTrafficResetDesc": "按指定間隔自動重置流量計數器",
"periodicTrafficResetDay": "每月重置日",
"lastReset": "上次重置",
"periodicTrafficReset": {
"never": "從不",

View File

@@ -302,7 +302,7 @@ const (
// startTask schedules background jobs (Xray checks, traffic jobs, cron
// jobs) which the panel relies on for periodic maintenance and monitoring.
func (s *Server) startTask(restartXray bool) {
func (s *Server) startTask(restartXray bool, loc *time.Location) {
if restartXray {
err := s.xrayService.RestartXray(true)
if err != nil {
@@ -344,13 +344,13 @@ func (s *Server) startTask(restartXray bool) {
// Inbound traffic reset jobs
// Run every hour
_, _ = s.cron.AddJob("@hourly", job.NewPeriodicTrafficResetJob("hourly"))
_, _ = s.cron.AddJob("@hourly", job.NewPeriodicTrafficResetJob("hourly", loc))
// Run once a day, midnight
_, _ = s.cron.AddJob("@daily", job.NewPeriodicTrafficResetJob("daily"))
_, _ = s.cron.AddJob("@daily", job.NewPeriodicTrafficResetJob("daily", loc))
// Run once a week, midnight between Sat/Sun
_, _ = s.cron.AddJob("@weekly", job.NewPeriodicTrafficResetJob("weekly"))
// Run once a month, midnight, first of month
_, _ = s.cron.AddJob("@monthly", job.NewPeriodicTrafficResetJob("monthly"))
_, _ = s.cron.AddJob("@weekly", job.NewPeriodicTrafficResetJob("weekly", loc))
// Check monthly reset days at midnight
_, _ = s.cron.AddJob("@daily", job.NewPeriodicTrafficResetJob("monthly", loc))
// LDAP sync scheduling
if ldapEnabled, _ := s.settingService.GetLdapEnable(); ldapEnabled {
@@ -651,7 +651,7 @@ func (s *Server) start(restartXray bool, startTgBot bool) (err error) {
}
})
s.startTask(restartXray)
s.startTask(restartXray, loc)
if startTgBot {
isTgbotenabled, err := s.settingService.GetTgbotEnabled()