From a4d9c287027c1c05c2d55ac119c5c8d446421231 Mon Sep 17 00:00:00 2001 From: luorijun Date: Wed, 29 Apr 2026 16:59:14 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E5=B7=B2=E5=8F=91=E6=94=BE?= =?UTF-8?q?=E4=BC=98=E6=83=A0=E5=88=B8=E7=9A=84=E7=AE=A1=E7=90=86=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/sql/fill.sql | 12 +- web/core/scopes.go | 5 + web/handlers/coupon_user.go | 330 ++++++++++++++++++++++++++++++++++++ web/models/coupon_user.go | 5 +- web/routes.go | 14 ++ web/services/coupon.go | 51 +----- web/services/coupon_user.go | 255 ++++++++++++++++++++++++++++ 7 files changed, 622 insertions(+), 50 deletions(-) create mode 100644 web/handlers/coupon_user.go create mode 100644 web/services/coupon_user.go diff --git a/scripts/sql/fill.sql b/scripts/sql/fill.sql index 1ac333e..3f9a67b 100644 --- a/scripts/sql/fill.sql +++ b/scripts/sql/fill.sql @@ -127,7 +127,8 @@ insert into permission (name, description, sort) values ('trade', '交易', 12), ('bill', '账单', 13), ('balance_activity', '余额变动', 14), - ('proxy', '代理', 15); + ('proxy', '代理', 15), + ('coupon_user', '已发放优惠券', 16); -- -------------------------- -- level 2 @@ -209,6 +210,11 @@ insert into permission (parent_id, name, description, sort) values insert into permission (parent_id, name, description, sort) values ((select id from permission where name = 'balance_activity' and deleted_at is null), 'balance_activity:read', '读取余额变动列表', 1); +-- coupon_user 子权限 +insert into permission (parent_id, name, description, sort) values + ((select id from permission where name = 'coupon_user' and deleted_at is null), 'coupon_user:read', '读取已发放优惠券列表', 1), + ((select id from permission where name = 'coupon_user' and deleted_at is null), 'coupon_user:write', '编辑已发放优惠券', 2); + -- -------------------------- -- level 3 -- -------------------------- @@ -263,6 +269,10 @@ insert into permission (parent_id, name, description, sort) values insert into permission (parent_id, name, description, sort) values ((select id from permission where name = 'coupon:write' and deleted_at is null), 'coupon:write:assign', '发放优惠券', 1); +-- coupon_user:read 子权限 +insert into permission (parent_id, name, description, sort) values + ((select id from permission where name = 'coupon_user:read' and deleted_at is null), 'coupon_user:read:of_user', '读取指定用户的已发放优惠券列表', 1); + -- -------------------------- -- level 4 -- -------------------------- diff --git a/web/core/scopes.go b/web/core/scopes.go index 2b1c909..30770e2 100644 --- a/web/core/scopes.go +++ b/web/core/scopes.go @@ -53,6 +53,11 @@ const ( ScopeCouponWrite = string("coupon:write") // 写入优惠券 ScopeCouponWriteAssign = string("coupon:write:assign") // 发放优惠券 + ScopeCouponUser = string("coupon_user") // 用户优惠券 + ScopeCouponUserRead = string("coupon_user:read") // 读取用户优惠券列表 + ScopeCouponUserReadOfUser = string("coupon_user:read:of_user") // 读取指定用户的用户优惠券列表 + ScopeCouponUserWrite = string("coupon_user:write") // 写入用户优惠券 + ScopeBatch = string("batch") // 批次 ScopeBatchRead = string("batch:read") // 读取批次列表 ScopeBatchReadOfUser = string("batch:read:of_user") // 读取指定用户的批次列表 diff --git a/web/handlers/coupon_user.go b/web/handlers/coupon_user.go new file mode 100644 index 0000000..5309d30 --- /dev/null +++ b/web/handlers/coupon_user.go @@ -0,0 +1,330 @@ +package handlers + +import ( + "errors" + "platform/pkg/u" + "platform/web/auth" + "platform/web/core" + g "platform/web/globals" + m "platform/web/models" + q "platform/web/queries" + s "platform/web/services" + "time" + + "github.com/gofiber/fiber/v2" + "gorm.io/gen" + "gorm.io/gen/field" + "gorm.io/gorm" +) + +// PageCouponUser 分页查询当前用户已发放优惠券 +func PageCouponUser(c *fiber.Ctx) error { + authCtx, err := auth.GetAuthCtx(c).PermitUser() + if err != nil { + return err + } + + var req PageCouponUserReq + if err := g.Validator.ParseBody(c, &req); err != nil { + return err + } + + conds := couponUserPageConditions(req.CouponUserPageFilter) + conds = append(conds, q.CouponUser.UserID.Eq(authCtx.User.ID)) + + list, total, err := q.CouponUser. + Joins(q.CouponUser.Coupon). + Select(couponUserSelect(false)...). + Where(conds...). + Order(q.CouponUser.CreatedAt.Desc()). + FindByPage(req.GetOffset(), req.GetLimit()) + if err != nil { + return core.NewBizErr("获取数据失败", err) + } + + return c.JSON(core.PageResp{ + List: list, + Total: int(total), + Page: req.GetPage(), + Size: req.GetSize(), + }) +} + +type PageCouponUserReq struct { + core.PageReq + CouponUserPageFilter +} + +// GetCouponUser 获取当前用户已发放优惠券详情 +func GetCouponUser(c *fiber.Ctx) error { + authCtx, err := auth.GetAuthCtx(c).PermitUser() + if err != nil { + return err + } + + var req core.IdReq + if err := g.Validator.ParseBody(c, &req); err != nil { + return err + } + + item, err := q.CouponUser. + Joins(q.CouponUser.Coupon). + Select(couponUserSelect(false)...). + Where( + q.CouponUser.ID.Eq(req.Id), + q.CouponUser.UserID.Eq(authCtx.User.ID), + ). + Take() + if errors.Is(err, gorm.ErrRecordNotFound) { + return core.NewBizErr("已发放优惠券不存在") + } + if err != nil { + return core.NewBizErr("获取数据失败", err) + } + + return c.JSON(item) +} + +// PageCouponUserByAdmin 分页查询全部已发放优惠券 +func PageCouponUserByAdmin(c *fiber.Ctx) error { + _, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeCouponUserRead) + if err != nil { + return err + } + + var req PageCouponUserByAdminReq + if err := g.Validator.ParseBody(c, &req); err != nil { + return err + } + + conds := couponUserPageConditions(req.CouponUserPageFilter) + if req.UserID != nil { + conds = append(conds, q.CouponUser.UserID.Eq(*req.UserID)) + } + if req.UserPhone != nil { + conds = append(conds, q.User.As("User").Phone.Eq(*req.UserPhone)) + } + + list, total, err := q.CouponUser. + Joins(q.CouponUser.Coupon, q.CouponUser.User). + Select(couponUserSelect(true)...). + Where(conds...). + Order(q.CouponUser.CreatedAt.Desc()). + FindByPage(req.GetOffset(), req.GetLimit()) + if err != nil { + return core.NewBizErr("获取数据失败", err) + } + + return c.JSON(core.PageResp{ + List: list, + Total: int(total), + Page: req.GetPage(), + Size: req.GetSize(), + }) +} + +type PageCouponUserByAdminReq struct { + core.PageReq + CouponUserPageFilter + UserID *int32 `json:"user_id,omitempty"` + UserPhone *string `json:"user_phone,omitempty"` +} + +// PageCouponUserOfUserByAdmin 分页查询指定用户已发放优惠券 +func PageCouponUserOfUserByAdmin(c *fiber.Ctx) error { + _, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeCouponUserReadOfUser) + if err != nil { + return err + } + + var req PageCouponUserOfUserByAdminReq + if err := g.Validator.ParseBody(c, &req); err != nil { + return err + } + + conds := couponUserPageConditions(req.CouponUserPageFilter) + conds = append(conds, q.CouponUser.UserID.Eq(req.UserID)) + + list, total, err := q.CouponUser. + Joins(q.CouponUser.Coupon, q.CouponUser.User). + Select(couponUserSelect(true)...). + Where(conds...). + Order(q.CouponUser.CreatedAt.Desc()). + FindByPage(req.GetOffset(), req.GetLimit()) + if err != nil { + return core.NewBizErr("获取数据失败", err) + } + + return c.JSON(core.PageResp{ + List: list, + Total: int(total), + Page: req.GetPage(), + Size: req.GetSize(), + }) +} + +type PageCouponUserOfUserByAdminReq struct { + core.PageReq + CouponUserPageFilter + UserID int32 `json:"user_id" validate:"required"` +} + +// GetCouponUserByAdmin 获取已发放优惠券详情 +func GetCouponUserByAdmin(c *fiber.Ctx) error { + _, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeCouponUserRead) + if err != nil { + return err + } + + var req core.IdReq + if err := g.Validator.ParseBody(c, &req); err != nil { + return err + } + + item, err := q.CouponUser. + Joins(q.CouponUser.Coupon, q.CouponUser.User). + Select(couponUserSelect(true)...). + Where(q.CouponUser.ID.Eq(req.Id)). + Take() + if errors.Is(err, gorm.ErrRecordNotFound) { + return core.NewBizErr("已发放优惠券不存在") + } + if err != nil { + return core.NewBizErr("获取数据失败", err) + } + + return c.JSON(item) +} + +func CreateCouponUserByAdmin(c *fiber.Ctx) error { + _, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeCouponUserWrite) + if err != nil { + return err + } + + var req s.CreateCouponUserData + if err := g.Validator.ParseBody(c, &req); err != nil { + return err + } + + if err := s.CouponUser.Create(req); err != nil { + return err + } + + return nil +} + +func UpdateCouponUserByAdmin(c *fiber.Ctx) error { + _, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeCouponUserWrite) + if err != nil { + return err + } + + var req s.UpdateCouponUserData + if err := g.Validator.ParseBody(c, &req); err != nil { + return err + } + + if err := s.CouponUser.Update(req); err != nil { + return err + } + + return nil +} + +func DeleteCouponUserByAdmin(c *fiber.Ctx) error { + _, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeCouponUserWrite) + if err != nil { + return err + } + + var req core.IdReq + if err := g.Validator.ParseBody(c, &req); err != nil { + return err + } + + if err := s.CouponUser.Delete(req.Id); err != nil { + return err + } + + return nil +} + +type CouponUserPageFilter struct { + CouponID *int32 `json:"coupon_id,omitempty"` + CouponName *string `json:"coupon_name,omitempty"` + Status *m.CouponUserStatus `json:"status,omitempty"` + Expired *bool `json:"expired,omitempty"` + CreatedAtStart *time.Time `json:"created_at_start,omitempty"` + CreatedAtEnd *time.Time `json:"created_at_end,omitempty"` + ExpireAtStart *time.Time `json:"expire_at_start,omitempty"` + ExpireAtEnd *time.Time `json:"expire_at_end,omitempty"` + UsedAtStart *time.Time `json:"used_at_start,omitempty"` + UsedAtEnd *time.Time `json:"used_at_end,omitempty"` +} + +func couponUserPageConditions(req CouponUserPageFilter) []gen.Condition { + conds := make([]gen.Condition, 0) + if req.CouponID != nil { + conds = append(conds, q.CouponUser.CouponID.Eq(*req.CouponID)) + } + if req.CouponName != nil { + conds = append(conds, q.Coupon.As("Coupon").Name.Like("%"+*req.CouponName+"%")) + } + if req.Status != nil { + conds = append(conds, q.CouponUser.Status.Eq(int(*req.Status))) + } + if req.Expired != nil { + if *req.Expired { + conds = append(conds, q.CouponUser.ExpireAt.IsNotNull(), q.CouponUser.ExpireAt.Lte(time.Now())) + } else { + conds = append(conds, q.CouponUser.Where(q.CouponUser.ExpireAt.IsNull()).Or(q.CouponUser.ExpireAt.Gt(time.Now()))) + } + } + if req.CreatedAtStart != nil { + conds = append(conds, q.CouponUser.CreatedAt.Gte(u.DateHead(*req.CreatedAtStart))) + } + if req.CreatedAtEnd != nil { + conds = append(conds, q.CouponUser.CreatedAt.Lte(u.DateTail(*req.CreatedAtEnd))) + } + if req.ExpireAtStart != nil { + conds = append(conds, q.CouponUser.ExpireAt.Gte(u.DateHead(*req.ExpireAtStart))) + } + if req.ExpireAtEnd != nil { + conds = append(conds, q.CouponUser.ExpireAt.Lte(u.DateTail(*req.ExpireAtEnd))) + } + if req.UsedAtStart != nil { + conds = append(conds, q.CouponUser.UsedAt.Gte(u.DateHead(*req.UsedAtStart))) + } + if req.UsedAtEnd != nil { + conds = append(conds, q.CouponUser.UsedAt.Lte(u.DateTail(*req.UsedAtEnd))) + } + return conds +} + +func couponUserSelect(includeUser bool) []field.Expr { + cols := []field.Expr{ + q.CouponUser.ALL, + q.Coupon.As("Coupon").ID.As("Coupon__id"), + q.Coupon.As("Coupon").Name.As("Coupon__name"), + q.Coupon.As("Coupon").Amount.As("Coupon__amount"), + q.Coupon.As("Coupon").MinAmount.As("Coupon__min_amount"), + q.Coupon.As("Coupon").Count_.As("Coupon__count"), + q.Coupon.As("Coupon").Status.As("Coupon__status"), + q.Coupon.As("Coupon").ExpireType.As("Coupon__expire_type"), + q.Coupon.As("Coupon").ExpireAt.As("Coupon__expire_at"), + q.Coupon.As("Coupon").ExpireIn.As("Coupon__expire_in"), + q.Coupon.As("Coupon").CreatedAt.As("Coupon__created_at"), + q.Coupon.As("Coupon").UpdatedAt.As("Coupon__updated_at"), + } + + if includeUser { + cols = append(cols, + q.User.As("User").ID.As("User__id"), + q.User.As("User").Phone.As("User__phone"), + q.User.As("User").Name.As("User__name"), + ) + } + + return cols +} diff --git a/web/models/coupon_user.go b/web/models/coupon_user.go index db4dc08..3d1ad2e 100644 --- a/web/models/coupon_user.go +++ b/web/models/coupon_user.go @@ -20,6 +20,7 @@ type CouponUser struct { type CouponUserStatus int const ( - CouponUserStatusUnused CouponUserStatus = 0 // 未使用 - CouponUserStatusUsed CouponUserStatus = 1 // 已使用 + CouponUserStatusUnused CouponUserStatus = 0 // 未使用 + CouponUserStatusUsed CouponUserStatus = 1 // 已使用 + CouponUserStatusDisabled CouponUserStatus = 2 // 已禁用 ) diff --git a/web/routes.go b/web/routes.go index 09a9c9f..27a18f3 100644 --- a/web/routes.go +++ b/web/routes.go @@ -95,6 +95,11 @@ func userRouter(api fiber.Router) { balance := api.Group("/balance") balance.Post("/page", handlers.PageBalanceActivity) + // 已发放优惠券 + couponUser := api.Group("/coupon-user") + couponUser.Post("/page", handlers.PageCouponUser) + couponUser.Post("/get", handlers.GetCouponUser) + // 公告 announcement := api.Group("/announcement") announcement.Post("/list", handlers.ListAnnouncements) @@ -254,4 +259,13 @@ func adminRouter(api fiber.Router) { coupon.Post("/update", handlers.UpdateCoupon) coupon.Post("/remove", handlers.DeleteCoupon) coupon.Post("/update/assign", handlers.AssignCoupon) + + // coupon-user 已发放优惠券 + var couponUser = api.Group("/coupon-user") + couponUser.Post("/page", handlers.PageCouponUserByAdmin) + couponUser.Post("/page/of-user", handlers.PageCouponUserOfUserByAdmin) + couponUser.Post("/get", handlers.GetCouponUserByAdmin) + couponUser.Post("/create", handlers.CreateCouponUserByAdmin) + couponUser.Post("/update", handlers.UpdateCouponUserByAdmin) + couponUser.Post("/remove", handlers.DeleteCouponUserByAdmin) } diff --git a/web/services/coupon.go b/web/services/coupon.go index ca34d88..8234ec7 100644 --- a/web/services/coupon.go +++ b/web/services/coupon.go @@ -108,54 +108,11 @@ func (s *couponService) Delete(id int32) error { return err } -// 发放优惠券 func (s *couponService) Assign(couponID int32, userID int32) error { - // 获取优惠券信息 - coupon, err := q.Coupon.Where(q.Coupon.ID.Eq(couponID)).Take() - if errors.Is(err, gorm.ErrRecordNotFound) { - return core.NewBizErr("优惠券不存在") - } - if err != nil { - return core.NewBizErr("获取优惠券数据失败", err) - } - - // 检查优惠券是否可用 - // 1. 状态必须为正常 - if coupon.Status != m.CouponStatusEnabled { - return core.NewBizErr("优惠券不可用") - } - // 2. 数量必须大于0 - if coupon.Count <= 0 { - return core.NewBizErr("优惠券已发放完") - } - - // 发放优惠券 - err = q.Q.Transaction(func(q *q.Query) error { - // 创建用户优惠券记录 - err := q.CouponUser.Create(&m.CouponUser{ - UserID: userID, - CouponID: couponID, - Status: m.CouponUserStatusUnused, - }) - if err != nil { - return err - } - - // 扣减优惠券数量 - _, err = q.Coupon. - Where(q.Coupon.ID.Eq(couponID), q.Coupon.Count_.Eq(coupon.Count)). - UpdateSimple(q.Coupon.Count_.Value(coupon.Count - 1)) - if err != nil { - return err - } - - return nil + return CouponUser.Create(CreateCouponUserData{ + CouponID: couponID, + UserID: userID, }) - if err != nil { - return core.NewBizErr("发放优惠券失败", err) - } - - return nil } // GetUserCoupon 获取用户的指定优惠券 @@ -165,7 +122,7 @@ func (s *couponService) GetUserCoupon(uid int32, cuid int32, amount decimal.Deci q.CouponUser.ID.Eq(cuid), q.CouponUser.UserID.Eq(uid), q.CouponUser.Status.Eq(int(m.CouponUserStatusUnused)), - q.CouponUser.ExpireAt.Gt(time.Now()), + q.CouponUser.Where(q.CouponUser.ExpireAt.IsNull()).Or(q.CouponUser.ExpireAt.Gt(time.Now())), ).Take() if errors.Is(err, gorm.ErrRecordNotFound) { return nil, core.NewBizErr("优惠券不存在或已失效") diff --git a/web/services/coupon_user.go b/web/services/coupon_user.go new file mode 100644 index 0000000..3901b75 --- /dev/null +++ b/web/services/coupon_user.go @@ -0,0 +1,255 @@ +package services + +import ( + "errors" + "platform/pkg/u" + "platform/web/core" + m "platform/web/models" + q "platform/web/queries" + "time" + + "gorm.io/gen/field" + "gorm.io/gorm" +) + +var CouponUser = &couponUserService{} + +type couponUserService struct{} + +func (s *couponUserService) Create(data CreateCouponUserData) error { + now := time.Now() + status := u.Else(data.Status, m.CouponUserStatusUnused) + if err := validateCouponUserStatus(status); err != nil { + return err + } + + return q.Q.Transaction(func(tx *q.Query) error { + coupon, err := tx.Coupon.Where(tx.Coupon.ID.Eq(data.CouponID)).Take() + if errors.Is(err, gorm.ErrRecordNotFound) { + return core.NewBizErr("优惠券不存在") + } + if err != nil { + return core.NewServErr("获取优惠券数据失败", err) + } + if coupon.Status != m.CouponStatusEnabled { + return core.NewBizErr("优惠券不可用") + } + if coupon.Count <= 0 { + return core.NewBizErr("优惠券已发放完") + } + + _, err = tx.User.Where(tx.User.ID.Eq(data.UserID)).Take() + if errors.Is(err, gorm.ErrRecordNotFound) { + return core.NewBizErr("用户不存在") + } + if err != nil { + return core.NewServErr("获取用户数据失败", err) + } + + expireAt := data.ExpireAt + if expireAt == nil { + expireAt = couponUserExpireAt(coupon, now) + } + + usedAt := data.UsedAt + if status == m.CouponUserStatusUsed && usedAt == nil { + usedAt = &now + } + if status == m.CouponUserStatusUnused { + usedAt = nil + } + + err = tx.CouponUser.Create(&m.CouponUser{ + UserID: data.UserID, + CouponID: data.CouponID, + Status: status, + ExpireAt: expireAt, + UsedAt: usedAt, + }) + if err != nil { + return core.NewServErr("发放优惠券失败", err) + } + + return adjustCouponCount(tx, coupon.ID, -1) + }) +} + +type CreateCouponUserData struct { + CouponID int32 `json:"coupon_id" validate:"required"` + UserID int32 `json:"user_id" validate:"required"` + Status *m.CouponUserStatus `json:"status"` + ExpireAt *time.Time `json:"expire_at"` + UsedAt *time.Time `json:"used_at"` +} + +func (s *couponUserService) Update(data UpdateCouponUserData) error { + return q.Q.Transaction(func(tx *q.Query) error { + current, err := tx.CouponUser.Where(tx.CouponUser.ID.Eq(data.ID)).Take() + if errors.Is(err, gorm.ErrRecordNotFound) { + return core.NewBizErr("已发放优惠券不存在") + } + if err != nil { + return core.NewServErr("获取已发放优惠券失败", err) + } + + do := make([]field.AssignExpr, 0) + if data.ExpireAtClear != nil && *data.ExpireAtClear { + do = append(do, tx.CouponUser.ExpireAt.Null()) + } else if data.ExpireAt != nil { + do = append(do, tx.CouponUser.ExpireAt.Value(*data.ExpireAt)) + } + + if data.UsedAtClear != nil && *data.UsedAtClear { + do = append(do, tx.CouponUser.UsedAt.Null()) + } else if data.UsedAt != nil { + do = append(do, tx.CouponUser.UsedAt.Value(*data.UsedAt)) + } + + if data.Status != nil { + if err := validateCouponUserStatus(*data.Status); err != nil { + return err + } + if current.Status != *data.Status { + if current.Status == m.CouponUserStatusUsed { + return core.NewBizErr("已使用的优惠券不能修改状态") + } + if current.Status == m.CouponUserStatusDisabled && *data.Status == m.CouponUserStatusUsed { + return core.NewBizErr("已禁用的优惠券不能标记为已使用") + } + + switch *data.Status { + case m.CouponUserStatusUnused: + if current.Status == m.CouponUserStatusDisabled { + if err := adjustCouponCount(tx, current.CouponID, -1); err != nil { + return err + } + } + if data.UsedAt == nil && (data.UsedAtClear == nil || !*data.UsedAtClear) { + do = append(do, tx.CouponUser.UsedAt.Null()) + } + + case m.CouponUserStatusUsed: + if data.UsedAt == nil && (data.UsedAtClear == nil || !*data.UsedAtClear) { + do = append(do, tx.CouponUser.UsedAt.Value(time.Now())) + } + + case m.CouponUserStatusDisabled: + if current.Status == m.CouponUserStatusUnused { + if err := adjustCouponCount(tx, current.CouponID, 1); err != nil { + return err + } + } + } + do = append(do, tx.CouponUser.Status.Value(int(*data.Status))) + } + } + + if len(do) == 0 { + return nil + } + + result, err := tx.CouponUser. + Where( + tx.CouponUser.ID.Eq(data.ID), + tx.CouponUser.Status.Eq(int(current.Status)), + ). + UpdateSimple(do...) + if err != nil { + return core.NewServErr("更新已发放优惠券失败", err) + } + if result.RowsAffected == 0 { + return core.NewBizErr("已发放优惠券状态已变化,请重试") + } + return nil + }) +} + +type UpdateCouponUserData struct { + ID int32 `json:"id" validate:"required"` + Status *m.CouponUserStatus `json:"status"` + ExpireAt *time.Time `json:"expire_at"` + ExpireAtClear *bool `json:"expire_at_clear"` + UsedAt *time.Time `json:"used_at"` + UsedAtClear *bool `json:"used_at_clear"` +} + +func (s *couponUserService) Delete(id int32) error { + status := m.CouponUserStatusDisabled + return s.Update(UpdateCouponUserData{ + ID: id, + Status: &status, + }) +} + +func (s *couponUserService) DeleteOfUser(id int32, userID int32) error { + assigned, err := q.CouponUser.Where( + q.CouponUser.ID.Eq(id), + q.CouponUser.UserID.Eq(userID), + ).Take() + if errors.Is(err, gorm.ErrRecordNotFound) { + return core.NewBizErr("已发放优惠券不存在") + } + if err != nil { + return core.NewServErr("获取已发放优惠券失败", err) + } + if assigned.Status != m.CouponUserStatusUnused { + return core.NewBizErr("只能撤销未使用的优惠券") + } + + return s.Delete(id) +} + +func couponUserExpireAt(coupon *m.Coupon, now time.Time) *time.Time { + if coupon == nil { + return nil + } + + switch coupon.ExpireType { + case m.CouponExpireTypeFixed: + return coupon.ExpireAt + case m.CouponExpireTypeRelative: + if coupon.ExpireIn == nil { + return nil + } + expireAt := now.Add(time.Duration(*coupon.ExpireIn) * 24 * time.Hour) + return &expireAt + default: + return nil + } +} + +func validateCouponUserStatus(status m.CouponUserStatus) error { + switch status { + case m.CouponUserStatusUnused, m.CouponUserStatusUsed, m.CouponUserStatusDisabled: + return nil + default: + return core.NewBizErr("优惠券发放状态无效") + } +} + +func adjustCouponCount(tx *q.Query, couponID int32, delta int32) error { + coupon, err := tx.Coupon.Where(tx.Coupon.ID.Eq(couponID)).Take() + if errors.Is(err, gorm.ErrRecordNotFound) { + return core.NewBizErr("优惠券不存在") + } + if err != nil { + return core.NewServErr("获取优惠券数据失败", err) + } + + next := coupon.Count + delta + if next < 0 { + return core.NewBizErr("优惠券已发放完") + } + + result, err := tx.Coupon. + Where(tx.Coupon.ID.Eq(couponID), tx.Coupon.Count_.Eq(coupon.Count)). + UpdateSimple(tx.Coupon.Count_.Value(next)) + if err != nil { + return core.NewServErr("更新优惠券数量失败", err) + } + if result.RowsAffected == 0 { + return core.NewBizErr("优惠券库存已变化,请重试") + } + + return nil +}