promo_code_repo.go 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. package repository
  2. import (
  3. "context"
  4. "strings"
  5. dbent "github.com/Wei-Shaw/sub2api/ent"
  6. "github.com/Wei-Shaw/sub2api/ent/promocode"
  7. "github.com/Wei-Shaw/sub2api/ent/promocodeusage"
  8. "github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
  9. "github.com/Wei-Shaw/sub2api/internal/service"
  10. entsql "entgo.io/ent/dialect/sql"
  11. )
  12. type promoCodeRepository struct {
  13. client *dbent.Client
  14. }
  15. func NewPromoCodeRepository(client *dbent.Client) service.PromoCodeRepository {
  16. return &promoCodeRepository{client: client}
  17. }
  18. func (r *promoCodeRepository) Create(ctx context.Context, code *service.PromoCode) error {
  19. client := clientFromContext(ctx, r.client)
  20. builder := client.PromoCode.Create().
  21. SetCode(code.Code).
  22. SetBonusAmount(code.BonusAmount).
  23. SetMaxUses(code.MaxUses).
  24. SetUsedCount(code.UsedCount).
  25. SetStatus(code.Status).
  26. SetNotes(code.Notes)
  27. if code.ExpiresAt != nil {
  28. builder.SetExpiresAt(*code.ExpiresAt)
  29. }
  30. created, err := builder.Save(ctx)
  31. if err != nil {
  32. return err
  33. }
  34. code.ID = created.ID
  35. code.CreatedAt = created.CreatedAt
  36. code.UpdatedAt = created.UpdatedAt
  37. return nil
  38. }
  39. func (r *promoCodeRepository) GetByID(ctx context.Context, id int64) (*service.PromoCode, error) {
  40. m, err := r.client.PromoCode.Query().
  41. Where(promocode.IDEQ(id)).
  42. Only(ctx)
  43. if err != nil {
  44. if dbent.IsNotFound(err) {
  45. return nil, service.ErrPromoCodeNotFound
  46. }
  47. return nil, err
  48. }
  49. return promoCodeEntityToService(m), nil
  50. }
  51. func (r *promoCodeRepository) GetByCode(ctx context.Context, code string) (*service.PromoCode, error) {
  52. m, err := r.client.PromoCode.Query().
  53. Where(promocode.CodeEqualFold(code)).
  54. Only(ctx)
  55. if err != nil {
  56. if dbent.IsNotFound(err) {
  57. return nil, service.ErrPromoCodeNotFound
  58. }
  59. return nil, err
  60. }
  61. return promoCodeEntityToService(m), nil
  62. }
  63. func (r *promoCodeRepository) GetByCodeForUpdate(ctx context.Context, code string) (*service.PromoCode, error) {
  64. client := clientFromContext(ctx, r.client)
  65. m, err := client.PromoCode.Query().
  66. Where(promocode.CodeEqualFold(code)).
  67. ForUpdate().
  68. Only(ctx)
  69. if err != nil {
  70. if dbent.IsNotFound(err) {
  71. return nil, service.ErrPromoCodeNotFound
  72. }
  73. return nil, err
  74. }
  75. return promoCodeEntityToService(m), nil
  76. }
  77. // Update 写入管理员可编辑的字段。
  78. //
  79. // 这里刻意不写 used_count:它由兑换路径的 IncrementUsedCount 原子递增,
  80. // 而 used_count >= max_uses 正是"优惠码用完了"的判定依据。若管理员编辑
  81. // (改有效期、改额度……)时按快照把 used_count 回写,并发的兑换计数就会被抹掉,
  82. // 兑换次数统计随之失真。PromoService.Update 也从不修改该字段。
  83. func (r *promoCodeRepository) Update(ctx context.Context, code *service.PromoCode) error {
  84. client := clientFromContext(ctx, r.client)
  85. builder := client.PromoCode.UpdateOneID(code.ID).
  86. SetCode(code.Code).
  87. SetBonusAmount(code.BonusAmount).
  88. SetMaxUses(code.MaxUses).
  89. SetStatus(code.Status).
  90. SetNotes(code.Notes)
  91. if code.ExpiresAt != nil {
  92. builder.SetExpiresAt(*code.ExpiresAt)
  93. } else {
  94. builder.ClearExpiresAt()
  95. }
  96. updated, err := builder.Save(ctx)
  97. if err != nil {
  98. if dbent.IsNotFound(err) {
  99. return service.ErrPromoCodeNotFound
  100. }
  101. return err
  102. }
  103. code.UpdatedAt = updated.UpdatedAt
  104. return nil
  105. }
  106. func (r *promoCodeRepository) Delete(ctx context.Context, id int64) error {
  107. client := clientFromContext(ctx, r.client)
  108. _, err := client.PromoCode.Delete().Where(promocode.IDEQ(id)).Exec(ctx)
  109. return err
  110. }
  111. func (r *promoCodeRepository) List(ctx context.Context, params pagination.PaginationParams) ([]service.PromoCode, *pagination.PaginationResult, error) {
  112. return r.ListWithFilters(ctx, params, "", "")
  113. }
  114. func (r *promoCodeRepository) ListWithFilters(ctx context.Context, params pagination.PaginationParams, status, search string) ([]service.PromoCode, *pagination.PaginationResult, error) {
  115. q := r.client.PromoCode.Query()
  116. if status != "" {
  117. q = q.Where(promocode.StatusEQ(status))
  118. }
  119. if search != "" {
  120. q = q.Where(promocode.CodeContainsFold(search))
  121. }
  122. total, err := q.Clone().Count(ctx)
  123. if err != nil {
  124. return nil, nil, err
  125. }
  126. codesQuery := q.
  127. Offset(params.Offset()).
  128. Limit(params.Limit())
  129. for _, order := range promoCodeListOrder(params) {
  130. codesQuery = codesQuery.Order(order)
  131. }
  132. codes, err := codesQuery.All(ctx)
  133. if err != nil {
  134. return nil, nil, err
  135. }
  136. outCodes := promoCodeEntitiesToService(codes)
  137. return outCodes, paginationResultFromTotal(int64(total), params), nil
  138. }
  139. func promoCodeListOrder(params pagination.PaginationParams) []func(*entsql.Selector) {
  140. sortBy := strings.ToLower(strings.TrimSpace(params.SortBy))
  141. sortOrder := params.NormalizedSortOrder(pagination.SortOrderDesc)
  142. var field string
  143. switch sortBy {
  144. case "bonus_amount":
  145. field = promocode.FieldBonusAmount
  146. case "status":
  147. field = promocode.FieldStatus
  148. case "expires_at":
  149. field = promocode.FieldExpiresAt
  150. case "created_at":
  151. field = promocode.FieldCreatedAt
  152. case "code":
  153. field = promocode.FieldCode
  154. default:
  155. field = promocode.FieldID
  156. }
  157. if sortOrder == pagination.SortOrderAsc {
  158. return []func(*entsql.Selector){dbent.Asc(field), dbent.Asc(promocode.FieldID)}
  159. }
  160. return []func(*entsql.Selector){dbent.Desc(field), dbent.Desc(promocode.FieldID)}
  161. }
  162. func (r *promoCodeRepository) CreateUsage(ctx context.Context, usage *service.PromoCodeUsage) error {
  163. client := clientFromContext(ctx, r.client)
  164. created, err := client.PromoCodeUsage.Create().
  165. SetPromoCodeID(usage.PromoCodeID).
  166. SetUserID(usage.UserID).
  167. SetBonusAmount(usage.BonusAmount).
  168. SetUsedAt(usage.UsedAt).
  169. Save(ctx)
  170. if err != nil {
  171. return err
  172. }
  173. usage.ID = created.ID
  174. return nil
  175. }
  176. func (r *promoCodeRepository) GetUsageByPromoCodeAndUser(ctx context.Context, promoCodeID, userID int64) (*service.PromoCodeUsage, error) {
  177. m, err := r.client.PromoCodeUsage.Query().
  178. Where(
  179. promocodeusage.PromoCodeIDEQ(promoCodeID),
  180. promocodeusage.UserIDEQ(userID),
  181. ).
  182. Only(ctx)
  183. if err != nil {
  184. if dbent.IsNotFound(err) {
  185. return nil, nil
  186. }
  187. return nil, err
  188. }
  189. return promoCodeUsageEntityToService(m), nil
  190. }
  191. func (r *promoCodeRepository) ListUsagesByPromoCode(ctx context.Context, promoCodeID int64, params pagination.PaginationParams) ([]service.PromoCodeUsage, *pagination.PaginationResult, error) {
  192. q := r.client.PromoCodeUsage.Query().
  193. Where(promocodeusage.PromoCodeIDEQ(promoCodeID))
  194. total, err := q.Clone().Count(ctx)
  195. if err != nil {
  196. return nil, nil, err
  197. }
  198. usages, err := q.
  199. WithUser().
  200. Offset(params.Offset()).
  201. Limit(params.Limit()).
  202. Order(dbent.Desc(promocodeusage.FieldID)).
  203. All(ctx)
  204. if err != nil {
  205. return nil, nil, err
  206. }
  207. outUsages := promoCodeUsageEntitiesToService(usages)
  208. return outUsages, paginationResultFromTotal(int64(total), params), nil
  209. }
  210. func (r *promoCodeRepository) IncrementUsedCount(ctx context.Context, id int64) error {
  211. client := clientFromContext(ctx, r.client)
  212. _, err := client.PromoCode.UpdateOneID(id).
  213. AddUsedCount(1).
  214. Save(ctx)
  215. return err
  216. }
  217. // Entity to Service conversions
  218. func promoCodeEntityToService(m *dbent.PromoCode) *service.PromoCode {
  219. if m == nil {
  220. return nil
  221. }
  222. return &service.PromoCode{
  223. ID: m.ID,
  224. Code: m.Code,
  225. BonusAmount: m.BonusAmount,
  226. MaxUses: m.MaxUses,
  227. UsedCount: m.UsedCount,
  228. Status: m.Status,
  229. ExpiresAt: m.ExpiresAt,
  230. Notes: derefString(m.Notes),
  231. CreatedAt: m.CreatedAt,
  232. UpdatedAt: m.UpdatedAt,
  233. }
  234. }
  235. func promoCodeEntitiesToService(models []*dbent.PromoCode) []service.PromoCode {
  236. out := make([]service.PromoCode, 0, len(models))
  237. for i := range models {
  238. if s := promoCodeEntityToService(models[i]); s != nil {
  239. out = append(out, *s)
  240. }
  241. }
  242. return out
  243. }
  244. func promoCodeUsageEntityToService(m *dbent.PromoCodeUsage) *service.PromoCodeUsage {
  245. if m == nil {
  246. return nil
  247. }
  248. out := &service.PromoCodeUsage{
  249. ID: m.ID,
  250. PromoCodeID: m.PromoCodeID,
  251. UserID: m.UserID,
  252. BonusAmount: m.BonusAmount,
  253. UsedAt: m.UsedAt,
  254. }
  255. if m.Edges.User != nil {
  256. out.User = userEntityToService(m.Edges.User)
  257. }
  258. return out
  259. }
  260. func promoCodeUsageEntitiesToService(models []*dbent.PromoCodeUsage) []service.PromoCodeUsage {
  261. out := make([]service.PromoCodeUsage, 0, len(models))
  262. for i := range models {
  263. if s := promoCodeUsageEntityToService(models[i]); s != nil {
  264. out = append(out, *s)
  265. }
  266. }
  267. return out
  268. }