首次推送
build / build-and-test (push) Failing after 1m24s

This commit is contained in:
2026-10-18 19:44:38 +08:00
commit f047662129
64 changed files with 10454 additions and 0 deletions
+206
View File
@@ -0,0 +1,206 @@
// 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
}