| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232 |
- package domain
- import (
- "strings"
- "time"
- infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
- )
- const (
- AnnouncementStatusDraft = "draft"
- AnnouncementStatusActive = "active"
- AnnouncementStatusArchived = "archived"
- )
- const (
- AnnouncementNotifyModeSilent = "silent"
- AnnouncementNotifyModePopup = "popup"
- )
- const (
- AnnouncementConditionTypeSubscription = "subscription"
- AnnouncementConditionTypeBalance = "balance"
- )
- const (
- AnnouncementOperatorIn = "in"
- AnnouncementOperatorGT = "gt"
- AnnouncementOperatorGTE = "gte"
- AnnouncementOperatorLT = "lt"
- AnnouncementOperatorLTE = "lte"
- AnnouncementOperatorEQ = "eq"
- )
- var (
- ErrAnnouncementNotFound = infraerrors.NotFound("ANNOUNCEMENT_NOT_FOUND", "announcement not found")
- ErrAnnouncementInvalidTarget = infraerrors.BadRequest("ANNOUNCEMENT_INVALID_TARGET", "invalid announcement targeting rules")
- )
- type AnnouncementTargeting struct {
- // AnyOf 表示 OR:任意一个条件组满足即可展示。
- AnyOf []AnnouncementConditionGroup `json:"any_of,omitempty"`
- }
- type AnnouncementConditionGroup struct {
- // AllOf 表示 AND:组内所有条件都满足才算命中该组。
- AllOf []AnnouncementCondition `json:"all_of,omitempty"`
- }
- type AnnouncementCondition struct {
- // Type: subscription | balance
- Type string `json:"type"`
- // Operator:
- // - subscription: in
- // - balance: gt/gte/lt/lte/eq
- Operator string `json:"operator"`
- // subscription 条件:匹配的订阅套餐(group_id)
- GroupIDs []int64 `json:"group_ids,omitempty"`
- // balance 条件:比较阈值
- Value float64 `json:"value,omitempty"`
- }
- func (t AnnouncementTargeting) Matches(balance float64, activeSubscriptionGroupIDs map[int64]struct{}) bool {
- // 空规则:展示给所有用户
- if len(t.AnyOf) == 0 {
- return true
- }
- for _, group := range t.AnyOf {
- if len(group.AllOf) == 0 {
- // 空条件组不命中(避免 OR 中出现无条件 “全命中”)
- continue
- }
- allMatched := true
- for _, cond := range group.AllOf {
- if !cond.Matches(balance, activeSubscriptionGroupIDs) {
- allMatched = false
- break
- }
- }
- if allMatched {
- return true
- }
- }
- return false
- }
- func (c AnnouncementCondition) Matches(balance float64, activeSubscriptionGroupIDs map[int64]struct{}) bool {
- switch c.Type {
- case AnnouncementConditionTypeSubscription:
- if c.Operator != AnnouncementOperatorIn {
- return false
- }
- if len(c.GroupIDs) == 0 {
- return false
- }
- if len(activeSubscriptionGroupIDs) == 0 {
- return false
- }
- for _, gid := range c.GroupIDs {
- if _, ok := activeSubscriptionGroupIDs[gid]; ok {
- return true
- }
- }
- return false
- case AnnouncementConditionTypeBalance:
- switch c.Operator {
- case AnnouncementOperatorGT:
- return balance > c.Value
- case AnnouncementOperatorGTE:
- return balance >= c.Value
- case AnnouncementOperatorLT:
- return balance < c.Value
- case AnnouncementOperatorLTE:
- return balance <= c.Value
- case AnnouncementOperatorEQ:
- return balance == c.Value
- default:
- return false
- }
- default:
- return false
- }
- }
- func (t AnnouncementTargeting) NormalizeAndValidate() (AnnouncementTargeting, error) {
- normalized := AnnouncementTargeting{AnyOf: make([]AnnouncementConditionGroup, 0, len(t.AnyOf))}
- // 允许空 targeting(展示给所有用户)
- if len(t.AnyOf) == 0 {
- return normalized, nil
- }
- if len(t.AnyOf) > 50 {
- return AnnouncementTargeting{}, ErrAnnouncementInvalidTarget
- }
- for _, g := range t.AnyOf {
- if len(g.AllOf) == 0 {
- return AnnouncementTargeting{}, ErrAnnouncementInvalidTarget
- }
- if len(g.AllOf) > 50 {
- return AnnouncementTargeting{}, ErrAnnouncementInvalidTarget
- }
- group := AnnouncementConditionGroup{AllOf: make([]AnnouncementCondition, 0, len(g.AllOf))}
- for _, c := range g.AllOf {
- cond := AnnouncementCondition{
- Type: strings.TrimSpace(c.Type),
- Operator: strings.TrimSpace(c.Operator),
- Value: c.Value,
- }
- for _, gid := range c.GroupIDs {
- if gid <= 0 {
- return AnnouncementTargeting{}, ErrAnnouncementInvalidTarget
- }
- cond.GroupIDs = append(cond.GroupIDs, gid)
- }
- if err := cond.validate(); err != nil {
- return AnnouncementTargeting{}, err
- }
- group.AllOf = append(group.AllOf, cond)
- }
- normalized.AnyOf = append(normalized.AnyOf, group)
- }
- return normalized, nil
- }
- func (c AnnouncementCondition) validate() error {
- switch c.Type {
- case AnnouncementConditionTypeSubscription:
- if c.Operator != AnnouncementOperatorIn {
- return ErrAnnouncementInvalidTarget
- }
- if len(c.GroupIDs) == 0 {
- return ErrAnnouncementInvalidTarget
- }
- return nil
- case AnnouncementConditionTypeBalance:
- switch c.Operator {
- case AnnouncementOperatorGT, AnnouncementOperatorGTE, AnnouncementOperatorLT, AnnouncementOperatorLTE, AnnouncementOperatorEQ:
- return nil
- default:
- return ErrAnnouncementInvalidTarget
- }
- default:
- return ErrAnnouncementInvalidTarget
- }
- }
- type Announcement struct {
- ID int64
- Title string
- Content string
- Status string
- NotifyMode string
- Targeting AnnouncementTargeting
- StartsAt *time.Time
- EndsAt *time.Time
- CreatedBy *int64
- UpdatedBy *int64
- CreatedAt time.Time
- UpdatedAt time.Time
- }
- func (a *Announcement) IsActiveAt(now time.Time) bool {
- if a == nil {
- return false
- }
- if a.Status != AnnouncementStatusActive {
- return false
- }
- if a.StartsAt != nil && now.Before(*a.StartsAt) {
- return false
- }
- if a.EndsAt != nil && !now.Before(*a.EndsAt) {
- // ends_at 语义:到点即下线
- return false
- }
- return true
- }
|