mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-17 15:47:14 +00:00
feat(sub): add opt-in month-end expiry presentation (#6517)
Offer monthly calendar subscriptions an explicit last-valid-second display without moving their real billing boundary or spending renewal allowances. Keep the option off by default and limit conversion to a shared fixed day-1 midnight cutoff at an actual month transition in the panel timezone. Use the authoritative client calendar mode when aggregating node traffic, and share the header formatter across raw, JSON, and Clash exports. Expose the setting in the existing settings API/UI, regenerate its schemas, and document that clients may report expiry one second early or format the date differently in another timezone. Add HTTP, settings, and DST coverage. Stored deadlines, access enforcement, info/remark expiry values, and renewal accounting remain unchanged. Refs: #6516 Co-authored-by: JacktheRanger <219502738+JacktheRanger@users.noreply.github.com>
This commit is contained in:
+20
-5
@@ -48,6 +48,8 @@ type SubService struct {
|
||||
usageShown map[string]bool
|
||||
showIdentityOnAllLinks bool
|
||||
subInfoNodeEnable bool
|
||||
subCalendarExpireInclusive bool
|
||||
calendarExpireLocation *time.Location
|
||||
subExpiredTemplate string
|
||||
subTrafficDepletedTemplate string
|
||||
inboundService service.InboundService
|
||||
@@ -111,6 +113,11 @@ func (s *SubService) PrepareForRequest(host string) {
|
||||
s.settingsByInbound = map[int]map[string]any{}
|
||||
s.loadNodes()
|
||||
s.loadRemarkSettings()
|
||||
s.subCalendarExpireInclusive, _ = s.settingService.GetSubCalendarExpireInclusive()
|
||||
s.calendarExpireLocation = nil
|
||||
if s.subCalendarExpireInclusive {
|
||||
s.calendarExpireLocation, _ = s.settingService.GetTimeLocation()
|
||||
}
|
||||
}
|
||||
|
||||
// primeLinkClients caches clients (first occurrence per email, matching the
|
||||
@@ -544,13 +551,13 @@ func (s *SubService) AggregateTrafficByEmails(emails []string) (xray.ClientTraff
|
||||
// runtime traffic rows. In a multi-node setup the node snapshot can reset
|
||||
// client_traffics.total/expiry_time to 0, so fall back to the clients
|
||||
// table to keep the Subscription-Userinfo header in sync with the UI (#4645).
|
||||
limits := make(map[string][2]int64, len(emails))
|
||||
limits := make(map[string]model.ClientRecord, len(emails))
|
||||
var records []model.ClientRecord
|
||||
if err := db.Model(&model.ClientRecord{}).Where("email IN ?", emails).Find(&records).Error; err != nil {
|
||||
logger.Warning("SubService - AggregateTrafficByEmails: load client limits:", err)
|
||||
} else {
|
||||
for _, r := range records {
|
||||
limits[r.Email] = [2]int64{r.TotalGB, r.ExpiryTime}
|
||||
limits[r.Email] = r
|
||||
}
|
||||
}
|
||||
|
||||
@@ -560,25 +567,33 @@ func (s *SubService) AggregateTrafficByEmails(emails []string) (xray.ClientTraff
|
||||
if ct.LastOnline > lastOnline {
|
||||
lastOnline = ct.LastOnline
|
||||
}
|
||||
total, expiry := ct.Total, ct.ExpiryTime
|
||||
total, expiry, resetDay := ct.Total, ct.ExpiryTime, ct.ResetDay
|
||||
if lim, ok := limits[ct.Email]; ok {
|
||||
resetDay = lim.ResetDay
|
||||
if total == 0 {
|
||||
total = lim[0]
|
||||
total = lim.TotalGB
|
||||
}
|
||||
if expiry == 0 {
|
||||
expiry = lim[1]
|
||||
expiry = lim.ExpiryTime
|
||||
}
|
||||
}
|
||||
if expiry <= 0 {
|
||||
resetDay = 0
|
||||
}
|
||||
if first {
|
||||
agg.Up = ct.Up
|
||||
agg.Down = ct.Down
|
||||
agg.Total = total
|
||||
agg.ExpiryTime = subscriptionExpiryFromClient(now, expiry)
|
||||
agg.ResetDay = resetDay
|
||||
first = false
|
||||
continue
|
||||
}
|
||||
agg.Up += ct.Up
|
||||
agg.Down += ct.Down
|
||||
if resetDay != agg.ResetDay {
|
||||
agg.ResetDay = 0
|
||||
}
|
||||
if agg.Total == 0 || total == 0 {
|
||||
agg.Total = 0
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user