207 lines
4.9 KiB
Go
207 lines
4.9 KiB
Go
// Package domain 存放与存储无关的纯业务计算:日期、剩余时长、状态判定、追加换算。
|
||
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 (
|
||
UnitDay = "day"
|
||
UnitCount = "count"
|
||
)
|
||
|
||
// 派生展示状态
|
||
const (
|
||
StateActive = "active" // 进行中
|
||
StatePending = "pending" // 未开始
|
||
StatePaused = "paused" // 暂停中
|
||
StateExpired = "expired" // 已到期
|
||
StateUsedUp = "usedup" // 次数用尽
|
||
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
|
||
RemainingCount int
|
||
Level string
|
||
}
|
||
|
||
// EvalSub 计算订阅的展示状态与剩余量。
|
||
// - unit=day:剩余天数 = end - today + 1(含当天)
|
||
// - unit=count:剩余次数 = total - used;end 为可选有效期
|
||
func EvalSub(unit, status, startDate, endDate string, total, used int, 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 unit == UnitDay {
|
||
if hasEnd {
|
||
e.RemainingDays = e.DaysLeft + 1
|
||
}
|
||
if e.RemainingDays < 0 {
|
||
e.RemainingDays = 0
|
||
}
|
||
} else {
|
||
e.RemainingCount = total - used
|
||
if e.RemainingCount < 0 {
|
||
e.RemainingCount = 0
|
||
}
|
||
}
|
||
|
||
expired := false
|
||
if hasEnd && end.Before(today) {
|
||
expired = true
|
||
}
|
||
if unit == UnitCount && e.RemainingCount <= 0 {
|
||
expired = true
|
||
}
|
||
|
||
switch {
|
||
case status == StatusClosed:
|
||
e.State = StateClosed
|
||
case status == StatusPaused:
|
||
e.State = StatePaused
|
||
case expired:
|
||
if unit == UnitCount && e.RemainingCount <= 0 {
|
||
e.State = StateUsedUp
|
||
} else {
|
||
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
|
||
}
|