fix(user): expire renew only after expiry; preserve traffic on cycle enable; edit form fills group/tags/cycles
- MaybeApplyUserCycles no longer extends every tick inside a 24h pre-expiry window - First enable of traffic_reset_days records anchor without wiping counters - Users openEdit (and related setFieldsValue paths) populate group/tags/cycle fields so save does not clear them - Add unit tests for cycle behaviour fix(user): expire renew only after expiry; preserve traffic on cycle enable; edit form fills group/tags/cycles - MaybeApplyUserCycles no longer extends every tick inside a 24h pre-expiry window - First enable of traffic_reset_days records anchor without wiping counters - Users openEdit (and related setFieldsValue paths) populate group/tags/cycle fields so save does not clear them - Add unit tests for cycle behaviour fix(user): expire renew only after expiry; preserve traffic on cycle enable; edit form fills group/tags/cycles - MaybeApplyUserCycles no longer extends every tick inside a 24h pre-expiry window - First enable of traffic_reset_days records anchor without wiping counters - Users openEdit (and related setFieldsValue paths) populate group/tags/cycle fields so save does not clear them - Add unit tests for cycle behaviour fix(user): expire renew only after expiry; preserve traffic on cycle enable; edit form fills group/tags/cycles - MaybeApplyUserCycles no longer extends every tick inside a 24h pre-expiry window - First enable of traffic_reset_days records anchor without wiping counters - Users openEdit (and related setFieldsValue paths) populate group/tags/cycle fields so save does not clear them - Add unit tests for cycle behaviour fix(user): expire renew only after expiry; preserve traffic on cycle enable; edit form fills group/tags/cycles - MaybeApplyUserCycles no longer extends every tick inside a 24h pre-expiry window - First enable of traffic_reset_days records anchor without wiping counters - Users openEdit (and related setFieldsValue paths) populate group/tags/cycle fields so save does not clear them - Add unit tests for cycle behaviour fix(user): expire renew only after expiry; preserve traffic on cycle enable; edit form fills group/tags/cycles - MaybeApplyUserCycles no longer extends every tick inside a 24h pre-expiry window - First enable of traffic_reset_days records anchor without wiping counters - Users openEdit (and related setFieldsValue paths) populate group/tags/cycle fields so save does not clear them - Add unit tests for cycle behaviour fix(user): expire renew only after expiry; preserve traffic on cycle enable; edit form fills group/tags/cycles - MaybeApplyUserCycles no longer extends every tick inside a 24h pre-expiry window - First enable of traffic_reset_days records anchor without wiping counters - Users openEdit (and related setFieldsValue paths) populate group/tags/cycle fields so save does not clear them - Add unit tests for cycle behaviour fix(user): expire renew only after expiry; preserve traffic on cycle enable; edit form fills group/tags/cycles - MaybeApplyUserCycles no longer extends every tick inside a 24h pre-expiry window - First enable of traffic_reset_days records anchor without wiping counters - Users openEdit (and related setFieldsValue paths) populate group/tags/cycle fields so save does not clear them - Add unit tests for cycle behaviour fix(user): expire renew only after expiry; preserve traffic on cycle enable; edit form fills group/tags/cycles - MaybeApplyUserCycles no longer extends every tick inside a 24h pre-expiry window - First enable of traffic_reset_days records anchor without wiping counters - Users openEdit (and related setFieldsValue paths) populate group/tags/cycle fields so save does not clear them - Add unit tests for cycle behaviour fix(user): expire renew only after expiry; preserve traffic on cycle enable; edit form fills group/tags/cycles - MaybeApplyUserCycles no longer extends every tick inside a 24h pre-expiry window - First enable of traffic_reset_days records anchor without wiping counters - Users openEdit (and related setFieldsValue paths) populate group/tags/cycle fields so save does not clear them - Add unit tests for cycle behaviour fix(user): expire renew only after expiry; preserve traffic on cycle enable; edit form fills group/tags/cycles - MaybeApplyUserCycles no longer extends every tick inside a 24h pre-expiry window - First enable of traffic_reset_days records anchor without wiping counters - Users openEdit (and related setFieldsValue paths) populate group/tags/cycle fields so save does not clear them - Add unit tests for cycle behaviour fix(user): expire renew only after expiry; preserve traffic on cycle enable; edit form fills group/tags/cycles - MaybeApplyUserCycles no longer extends every tick inside a 24h pre-expiry window - First enable of traffic_reset_days records anchor without wiping counters - Users openEdit (and related setFieldsValue paths) populate group/tags/cycle fields so save does not clear them - Add unit tests for cycle behaviour
This commit is contained in:
1 parent
d150574ad3
commit
bf895d036c
3 files changed
+104
-19
No files matched your search
@@ -23,32 +23,23 @@ func MaybeApplyUserCycles(db *gorm.DB) {
|
||||
for _, u := range users {
|
||||
updates := map[string]interface{}{}
|
||||
if u.TrafficResetDays > 0 {
|
||||
need := false
|
||||
if u.LastTrafficCycleReset == nil {
|
||||
need = true
|
||||
// Enabling the cycle must not wipe current counters on the first tick.
|
||||
updates["last_traffic_cycle_reset"] = now
|
||||
} else {
|
||||
next := u.LastTrafficCycleReset.Add(time.Duration(u.TrafficResetDays) * 24 * time.Hour)
|
||||
if !now.Before(next) {
|
||||
need = true
|
||||
updates["traffic_used"] = 0
|
||||
updates["upload_bytes"] = 0
|
||||
updates["download_bytes"] = 0
|
||||
updates["last_traffic_cycle_reset"] = now
|
||||
}
|
||||
}
|
||||
if need {
|
||||
updates["traffic_used"] = 0
|
||||
updates["upload_bytes"] = 0
|
||||
updates["download_bytes"] = 0
|
||||
updates["last_traffic_cycle_reset"] = now
|
||||
}
|
||||
}
|
||||
if u.ExpireRenewDays > 0 && !u.ExpireTime.IsZero() {
|
||||
// Renew when expired or within 24h of expiry.
|
||||
window := u.ExpireTime.Add(-24 * time.Hour)
|
||||
if !now.Before(window) {
|
||||
base := u.ExpireTime
|
||||
if base.Before(now) {
|
||||
base = now
|
||||
}
|
||||
updates["expire_time"] = base.Add(time.Duration(u.ExpireRenewDays) * 24 * time.Hour)
|
||||
}
|
||||
if u.ExpireRenewDays > 0 && !u.ExpireTime.IsZero() && u.ExpireTime.Before(now) {
|
||||
// Only extend after the period has ended (avoids renewing every scheduler tick
|
||||
// while still inside a pre-expiry window).
|
||||
updates["expire_time"] = now.Add(time.Duration(u.ExpireRenewDays) * 24 * time.Hour)
|
||||
}
|
||||
if len(updates) == 0 {
|
||||
continue
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
package traffic
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/kazeyukiro/3m-ui/backend/internal/database/models"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
func testDB(t *testing.T) *gorm.DB {
|
||||
t.Helper()
|
||||
db, err := gorm.Open(sqlite.Open("file:cycle?mode=memory&cache=shared"), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Silent),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.AutoMigrate(&models.ProxyUser{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
func TestExpireRenewOnlyAfterExpiry(t *testing.T) {
|
||||
db := testDB(t)
|
||||
now := time.Now().UTC()
|
||||
// Within 24h window but not yet expired — must NOT extend.
|
||||
u := models.ProxyUser{
|
||||
Username: "soon", UUID: "u1", SubToken: "t1",
|
||||
ExpireTime: now.Add(12 * time.Hour), ExpireRenewDays: 30,
|
||||
Enabled: true,
|
||||
}
|
||||
if err := db.Create(&u).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
MaybeApplyUserCycles(db)
|
||||
var got models.ProxyUser
|
||||
if err := db.First(&got, u.ID).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !got.ExpireTime.Equal(u.ExpireTime) {
|
||||
t.Fatalf("pre-expiry renew mutated expire_time: %v -> %v", u.ExpireTime, got.ExpireTime)
|
||||
}
|
||||
|
||||
// Already expired — must extend from now by 30 days.
|
||||
past := now.Add(-time.Hour)
|
||||
if err := db.Model(&got).Update("expire_time", past).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
MaybeApplyUserCycles(db)
|
||||
if err := db.First(&got, u.ID).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !got.ExpireTime.After(now.Add(29 * 24 * time.Hour)) {
|
||||
t.Fatalf("expected ~30d extension, got %v", got.ExpireTime)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrafficCycleDoesNotWipeOnFirstEnable(t *testing.T) {
|
||||
db := testDB(t)
|
||||
u := models.ProxyUser{
|
||||
Username: "tuser", UUID: "u2", SubToken: "t2",
|
||||
TrafficUsed: 12345, TrafficResetDays: 7, Enabled: true,
|
||||
}
|
||||
if err := db.Create(&u).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
MaybeApplyUserCycles(db)
|
||||
var got models.ProxyUser
|
||||
if err := db.First(&got, u.ID).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.TrafficUsed != 12345 {
|
||||
t.Fatalf("first tick wiped traffic: %d", got.TrafficUsed)
|
||||
}
|
||||
if got.LastTrafficCycleReset == nil {
|
||||
t.Fatal("expected last_traffic_cycle_reset to be set")
|
||||
}
|
||||
}
|
||||
@@ -140,6 +140,10 @@ const Users: React.FC = () => {
|
||||
ip_limit: record.ip_limit || 0,
|
||||
hwid_limit: record.hwid_limit || 0,
|
||||
remark: record.remark || '',
|
||||
group: record.group || '',
|
||||
tags: record.tags || '',
|
||||
traffic_reset_days: record.traffic_reset_days || 0,
|
||||
expire_renew_days: record.expire_renew_days || 0,
|
||||
});
|
||||
setModalOpen(true);
|
||||
};
|
||||
@@ -513,6 +517,10 @@ const Users: React.FC = () => {
|
||||
ip_limit: record.ip_limit || 0,
|
||||
hwid_limit: record.hwid_limit || 0,
|
||||
remark: record.remark,
|
||||
group: record.group || '',
|
||||
tags: record.tags || '',
|
||||
traffic_reset_days: record.traffic_reset_days || 0,
|
||||
expire_renew_days: record.expire_renew_days || 0,
|
||||
});
|
||||
setModalOpen(true);
|
||||
}}
|
||||
@@ -559,6 +567,10 @@ const Users: React.FC = () => {
|
||||
ip_limit: record.ip_limit || 0,
|
||||
hwid_limit: record.hwid_limit || 0,
|
||||
remark: record.remark,
|
||||
group: record.group || '',
|
||||
tags: record.tags || '',
|
||||
traffic_reset_days: record.traffic_reset_days || 0,
|
||||
expire_renew_days: record.expire_renew_days || 0,
|
||||
});
|
||||
setModalOpen(true);
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user