announcement.go 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. package domain
  2. import (
  3. "strings"
  4. "time"
  5. infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
  6. )
  7. const (
  8. AnnouncementStatusDraft = "draft"
  9. AnnouncementStatusActive = "active"
  10. AnnouncementStatusArchived = "archived"
  11. )
  12. const (
  13. AnnouncementNotifyModeSilent = "silent"
  14. AnnouncementNotifyModePopup = "popup"
  15. )
  16. const (
  17. AnnouncementConditionTypeSubscription = "subscription"
  18. AnnouncementConditionTypeBalance = "balance"
  19. )
  20. const (
  21. AnnouncementOperatorIn = "in"
  22. AnnouncementOperatorGT = "gt"
  23. AnnouncementOperatorGTE = "gte"
  24. AnnouncementOperatorLT = "lt"
  25. AnnouncementOperatorLTE = "lte"
  26. AnnouncementOperatorEQ = "eq"
  27. )
  28. var (
  29. ErrAnnouncementNotFound = infraerrors.NotFound("ANNOUNCEMENT_NOT_FOUND", "announcement not found")
  30. ErrAnnouncementInvalidTarget = infraerrors.BadRequest("ANNOUNCEMENT_INVALID_TARGET", "invalid announcement targeting rules")
  31. )
  32. type AnnouncementTargeting struct {
  33. // AnyOf 表示 OR:任意一个条件组满足即可展示。
  34. AnyOf []AnnouncementConditionGroup `json:"any_of,omitempty"`
  35. }
  36. type AnnouncementConditionGroup struct {
  37. // AllOf 表示 AND:组内所有条件都满足才算命中该组。
  38. AllOf []AnnouncementCondition `json:"all_of,omitempty"`
  39. }
  40. type AnnouncementCondition struct {
  41. // Type: subscription | balance
  42. Type string `json:"type"`
  43. // Operator:
  44. // - subscription: in
  45. // - balance: gt/gte/lt/lte/eq
  46. Operator string `json:"operator"`
  47. // subscription 条件:匹配的订阅套餐(group_id)
  48. GroupIDs []int64 `json:"group_ids,omitempty"`
  49. // balance 条件:比较阈值
  50. Value float64 `json:"value,omitempty"`
  51. }
  52. func (t AnnouncementTargeting) Matches(balance float64, activeSubscriptionGroupIDs map[int64]struct{}) bool {
  53. // 空规则:展示给所有用户
  54. if len(t.AnyOf) == 0 {
  55. return true
  56. }
  57. for _, group := range t.AnyOf {
  58. if len(group.AllOf) == 0 {
  59. // 空条件组不命中(避免 OR 中出现无条件 “全命中”)
  60. continue
  61. }
  62. allMatched := true
  63. for _, cond := range group.AllOf {
  64. if !cond.Matches(balance, activeSubscriptionGroupIDs) {
  65. allMatched = false
  66. break
  67. }
  68. }
  69. if allMatched {
  70. return true
  71. }
  72. }
  73. return false
  74. }
  75. func (c AnnouncementCondition) Matches(balance float64, activeSubscriptionGroupIDs map[int64]struct{}) bool {
  76. switch c.Type {
  77. case AnnouncementConditionTypeSubscription:
  78. if c.Operator != AnnouncementOperatorIn {
  79. return false
  80. }
  81. if len(c.GroupIDs) == 0 {
  82. return false
  83. }
  84. if len(activeSubscriptionGroupIDs) == 0 {
  85. return false
  86. }
  87. for _, gid := range c.GroupIDs {
  88. if _, ok := activeSubscriptionGroupIDs[gid]; ok {
  89. return true
  90. }
  91. }
  92. return false
  93. case AnnouncementConditionTypeBalance:
  94. switch c.Operator {
  95. case AnnouncementOperatorGT:
  96. return balance > c.Value
  97. case AnnouncementOperatorGTE:
  98. return balance >= c.Value
  99. case AnnouncementOperatorLT:
  100. return balance < c.Value
  101. case AnnouncementOperatorLTE:
  102. return balance <= c.Value
  103. case AnnouncementOperatorEQ:
  104. return balance == c.Value
  105. default:
  106. return false
  107. }
  108. default:
  109. return false
  110. }
  111. }
  112. func (t AnnouncementTargeting) NormalizeAndValidate() (AnnouncementTargeting, error) {
  113. normalized := AnnouncementTargeting{AnyOf: make([]AnnouncementConditionGroup, 0, len(t.AnyOf))}
  114. // 允许空 targeting(展示给所有用户)
  115. if len(t.AnyOf) == 0 {
  116. return normalized, nil
  117. }
  118. if len(t.AnyOf) > 50 {
  119. return AnnouncementTargeting{}, ErrAnnouncementInvalidTarget
  120. }
  121. for _, g := range t.AnyOf {
  122. if len(g.AllOf) == 0 {
  123. return AnnouncementTargeting{}, ErrAnnouncementInvalidTarget
  124. }
  125. if len(g.AllOf) > 50 {
  126. return AnnouncementTargeting{}, ErrAnnouncementInvalidTarget
  127. }
  128. group := AnnouncementConditionGroup{AllOf: make([]AnnouncementCondition, 0, len(g.AllOf))}
  129. for _, c := range g.AllOf {
  130. cond := AnnouncementCondition{
  131. Type: strings.TrimSpace(c.Type),
  132. Operator: strings.TrimSpace(c.Operator),
  133. Value: c.Value,
  134. }
  135. for _, gid := range c.GroupIDs {
  136. if gid <= 0 {
  137. return AnnouncementTargeting{}, ErrAnnouncementInvalidTarget
  138. }
  139. cond.GroupIDs = append(cond.GroupIDs, gid)
  140. }
  141. if err := cond.validate(); err != nil {
  142. return AnnouncementTargeting{}, err
  143. }
  144. group.AllOf = append(group.AllOf, cond)
  145. }
  146. normalized.AnyOf = append(normalized.AnyOf, group)
  147. }
  148. return normalized, nil
  149. }
  150. func (c AnnouncementCondition) validate() error {
  151. switch c.Type {
  152. case AnnouncementConditionTypeSubscription:
  153. if c.Operator != AnnouncementOperatorIn {
  154. return ErrAnnouncementInvalidTarget
  155. }
  156. if len(c.GroupIDs) == 0 {
  157. return ErrAnnouncementInvalidTarget
  158. }
  159. return nil
  160. case AnnouncementConditionTypeBalance:
  161. switch c.Operator {
  162. case AnnouncementOperatorGT, AnnouncementOperatorGTE, AnnouncementOperatorLT, AnnouncementOperatorLTE, AnnouncementOperatorEQ:
  163. return nil
  164. default:
  165. return ErrAnnouncementInvalidTarget
  166. }
  167. default:
  168. return ErrAnnouncementInvalidTarget
  169. }
  170. }
  171. type Announcement struct {
  172. ID int64
  173. Title string
  174. Content string
  175. Status string
  176. NotifyMode string
  177. Targeting AnnouncementTargeting
  178. StartsAt *time.Time
  179. EndsAt *time.Time
  180. CreatedBy *int64
  181. UpdatedBy *int64
  182. CreatedAt time.Time
  183. UpdatedAt time.Time
  184. }
  185. func (a *Announcement) IsActiveAt(now time.Time) bool {
  186. if a == nil {
  187. return false
  188. }
  189. if a.Status != AnnouncementStatusActive {
  190. return false
  191. }
  192. if a.StartsAt != nil && now.Before(*a.StartsAt) {
  193. return false
  194. }
  195. if a.EndsAt != nil && !now.Before(*a.EndsAt) {
  196. // ends_at 语义:到点即下线
  197. return false
  198. }
  199. return true
  200. }