// Package domain 存放与存储无关的纯业务计算:日期、剩余时长、状态判定、追加换算。 // // lite 版只支持「按日(包时)」一种套餐:订阅永远有开始日与结束日, // 剩余量永远是天数,因此这里不再有次数、周期、通行证等分支。 package domain import "time" const ( DateLayout = "2006-01-02" StampLayout = "2006-01-02 15:04:05" ) // Today 返回本地时区当天零点。 func Today() time.Time { now := time.Now() return time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) } // ParseDate 解析 YYYY-MM-DD。 func ParseDate(s string) (time.Time, error) { return time.ParseInLocation(DateLayout, s, time.Local) } // ParseDateOr 解析失败时返回默认值。 func ParseDateOr(s string, fallback time.Time) time.Time { if t, err := ParseDate(s); err == nil { return t } return fallback } // Date 格式化为 YYYY-MM-DD。 func Date(t time.Time) string { return t.Format(DateLayout) } // DayDiff 返回 b-a 的自然日天数(不受夏令时影响)。 func DayDiff(a, b time.Time) int { ay, am, ad := a.Date() by, bm, bd := b.Date() ua := time.Date(ay, am, ad, 0, 0, 0, 0, time.UTC) ub := time.Date(by, bm, bd, 0, 0, 0, 0, time.UTC) return int(ub.Sub(ua).Hours() / 24.0) } // AddDays 加 n 天。 func AddDays(t time.Time, n int) time.Time { return t.AddDate(0, 0, n) } // MonthStart 返回当月 1 日。 func MonthStart(y int, m time.Month) time.Time { return time.Date(y, m, 1, 0, 0, 0, 0, time.Local) } // MonthEnd 返回当月最后一天。 func MonthEnd(y int, m time.Month) time.Time { return time.Date(y, m, 1, 0, 0, 0, 0, time.Local).AddDate(0, 1, -1) } // WeekdayIndex 返回以周一为 0 的星期索引。 func WeekdayIndex(t time.Time) int { wd := int(t.Weekday()) if wd == 0 { return 6 } return wd - 1 } // 订阅状态(存储值) const ( StatusActive = "active" StatusPaused = "paused" StatusClosed = "closed" ) // 派生展示状态 const ( StateActive = "active" // 进行中 StatePending = "pending" // 未开始 StatePaused = "paused" // 暂停中 StateExpired = "expired" // 已到期 StateClosed = "closed" // 已结束 ) // 到期预警等级 const ( LevelOK = "ok" LevelWarn = "warn" LevelToday = "today" LevelExpired = "expired" LevelNone = "none" // 无结束日(历史数据兜底) ) // Eval 是订阅的计算结果。 type Eval struct { State string DaysLeft int // 距结束日的天数:0=今天到期,>0=未来,-1=无结束日 RemainingDays int Level string } // EvalSub 计算订阅的展示状态与剩余天数。 // 剩余天数 = end - today + 1(含当天)。 func EvalSub(status, startDate, endDate string, today time.Time, warnDays int) Eval { e := Eval{Level: LevelNone, DaysLeft: -1} if warnDays <= 0 { warnDays = 3 } start := ParseDateOr(startDate, today) end, hasEnd := time.Time{}, false if endDate != "" { if t, err := ParseDate(endDate); err == nil { end, hasEnd = t, true e.DaysLeft = DayDiff(today, end) } } if hasEnd { e.RemainingDays = e.DaysLeft + 1 } if e.RemainingDays < 0 { e.RemainingDays = 0 } expired := hasEnd && end.Before(today) switch { case status == StatusClosed: e.State = StateClosed case status == StatusPaused: e.State = StatePaused case expired: e.State = StateExpired case start.After(today): e.State = StatePending default: e.State = StateActive } // 预警等级只对「进行中 / 未开始」有意义 switch e.State { case StateExpired: e.Level = LevelExpired case StateActive, StatePending: switch { case !hasEnd: e.Level = LevelNone case e.DaysLeft <= 0: e.Level = LevelToday case e.DaysLeft <= warnDays: e.Level = LevelWarn default: e.Level = LevelOK } } return e } // ExtendPlan 描述一次「按日追加」的换算结果。 type ExtendPlan struct { Base time.Time // 计算起点 OldEnd time.Time NewEnd time.Time Days int SameDay bool // 新结束日等于原结束日(未增加) Backwards bool // 新结束日早于计算起点 } // PlanExtend 根据原结束日、今天与新结束日,算出计算起点与追加天数。 // 规则(PRD R4):已到期从今天起算,未到期从原结束日次日起算。 func PlanExtend(oldEnd, newEnd, today time.Time) ExtendPlan { p := ExtendPlan{OldEnd: oldEnd, NewEnd: newEnd} if !oldEnd.Before(today) { p.Base = AddDays(oldEnd, 1) } else { p.Base = today } p.Days = DayDiff(p.Base, newEnd) + 1 p.SameDay = DayDiff(oldEnd, newEnd) == 0 p.Backwards = p.Days <= 0 if p.Days < 0 { p.Days = 0 } return p }