- service/xray.go 1741->573行: 只剩 agent 配置构建(GetXrayConfigForAgent/buildAgentXrayConfig=模板+节点入站+egress+转发编译); 删进程生命周期/状态机/RestartXray/StopXray/SetToNeedRestart/GetXrayTraffic/GetOnlineUsers/balancer/TestRoute 与 injectPanelEgress/injectNodeEgresses/injectMtproto/injectAmneziawg/mergeSubscriptionOutbounds
- controller: 43处 SetToNeedRestart 与各 RestartXray 调用清除; 删 balancerStatus/balancerOverride/routeTest/getXrayResult 路由与handler(纯本地核心操作); 前端删 RouteTester 页签与组件
- server service: GetConfigJson 改模板直读; 状态Xray块恒 stopped/空版本; Stop/RestartXrayService 改 no-op(DB导入/恢复流程保留形状)
- setting service: PanelEgressProxyURL/NodeEgressProxyURL 恒直连(桥在本地核心内,已不存在); NodeService.withOutboundBridge 恒直连回退
- runtime: Local 改为全 no-op 桩(DB即真源); NewManager() 去 LocalDeps(APIPort/SetNeedRestart)
- job: 删 XrayTrafficJob/CheckXrayRunningJob/CheckClientIpJob 三文件(共享声明 EventBus/IPWithTimestamp/clientStatsSnapshotMaxClients 迁 shared.go); web.go 不再调度 MtprotoJob/AmneziaWGJob, 删 OnCrash 接线与 stop() 的 sidecar StopAll
- MTProto/AmneziaWG 下线: AddInbound/UpdateInbound 对两协议直接拒绝(存量行保留可删); TG bot 的 restart 命令与在线查询按无本地核心收口
- 测试: GetXrayConfig 系列移植到 agent 构建(finalmask/clients/wireguard/scale); 删退役行为测试(lifecycle/restart/mtproto本地应用/client-ip job/inject系列); 共享测试helper迁 mtproto_test_helpers_test.go 与 job/integration_helpers_test.go
- 既有红(非本轮引入,已在 f6d6520 复现): TestGetXrayConfig_EnabledClientsStillEmitted、TestRouteRegistryContract(P6删 endpoints.ts 遗留)
140 lines
4.3 KiB
Go
140 lines
4.3 KiB
Go
package job
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/logger"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/web/service"
|
|
)
|
|
|
|
// Period represents the time period for traffic resets.
|
|
type Period string
|
|
|
|
// PeriodicTrafficResetJob resets traffic statistics for inbounds based on their configured reset period.
|
|
type PeriodicTrafficResetJob struct {
|
|
inboundService service.InboundService
|
|
clientService service.ClientService
|
|
xrayService service.XrayService
|
|
period Period
|
|
location *time.Location
|
|
}
|
|
|
|
// NewPeriodicTrafficResetJob creates a new periodic traffic reset job for the specified period.
|
|
func NewPeriodicTrafficResetJob(period Period, location *time.Location) *PeriodicTrafficResetJob {
|
|
return &PeriodicTrafficResetJob{
|
|
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, then for the clients carrying that period on their own (#5497).
|
|
func (j *PeriodicTrafficResetJob) Run() {
|
|
j.resetInboundsOnSchedule()
|
|
j.resetClientsOnTheirOwnCycle()
|
|
}
|
|
|
|
func (j *PeriodicTrafficResetJob) resetInboundsOnSchedule() {
|
|
inbounds, err := j.inboundService.GetInboundsByTrafficReset(string(j.period))
|
|
if err != nil {
|
|
logger.Warning("Failed to get inbounds for traffic reset:", err)
|
|
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 {
|
|
logger.Warning("Failed to reset traffic for inbound", inbound.Id, ":", resetInboundErr)
|
|
}
|
|
|
|
resetClientErr := j.clientService.ResetAllClientTraffics(&j.inboundService, inbound.Id)
|
|
if resetClientErr != nil {
|
|
logger.Warning("Failed to reset traffic for all users of inbound", inbound.Id, ":", resetClientErr)
|
|
}
|
|
|
|
if resetInboundErr == nil && resetClientErr == nil {
|
|
resetCount++
|
|
}
|
|
}
|
|
|
|
if resetCount > 0 {
|
|
logger.Infof("Periodic traffic reset completed: %d inbounds reset", resetCount)
|
|
}
|
|
}
|
|
|
|
// resetClientsOnTheirOwnCycle resets clients whose cycle is set individually. A
|
|
// client inside an inbound on the same cycle is reset twice, which is harmless.
|
|
func (j *PeriodicTrafficResetJob) resetClientsOnTheirOwnCycle() {
|
|
cycles, err := j.clientService.GetClientsByTrafficReset(string(j.period))
|
|
if err != nil {
|
|
logger.Warning("Failed to get clients for traffic reset:", err)
|
|
return
|
|
}
|
|
|
|
now := time.Now().In(j.location)
|
|
due := make([]service.ClientResetCycle, 0, len(cycles))
|
|
for _, c := range cycles {
|
|
// Monthly clients come due on their own day, the rule the inbound-level
|
|
// schedule already follows.
|
|
if j.period == "monthly" && !monthlyResetDue(c.TrafficResetDay, now) {
|
|
continue
|
|
}
|
|
// A reset re-enables, which is right for a client the quota switched off
|
|
// and wrong for one an operator switched off by hand.
|
|
if !c.Enable && !c.Depleted() {
|
|
continue
|
|
}
|
|
due = append(due, c)
|
|
}
|
|
if len(due) == 0 {
|
|
return
|
|
}
|
|
logger.Infof("Running periodic traffic reset job for period: %s (%d matching clients)", j.period, len(due))
|
|
|
|
resetCount := 0
|
|
needRestart := false
|
|
for _, c := range due {
|
|
// ResetTrafficByEmail rather than a bulk UPDATE: it is the path that also
|
|
// propagates to the client's node and clears the MTProto sidecar quota.
|
|
nr, resetErr := j.clientService.ResetTrafficByEmail(&j.inboundService, c.Email)
|
|
if resetErr != nil {
|
|
logger.Warning("Failed to reset traffic for client", c.Email, ":", resetErr)
|
|
continue
|
|
}
|
|
needRestart = needRestart || nr
|
|
resetCount++
|
|
}
|
|
// Dropping this leaves a re-enabled client absent from the running core until
|
|
// something unrelated restarts it.
|
|
if needRestart {
|
|
}
|
|
|
|
if resetCount > 0 {
|
|
logger.Infof("Periodic traffic reset completed: %d clients reset", resetCount)
|
|
}
|
|
}
|