From ea586e3ba210520171624932e1a151c931c5c973 Mon Sep 17 00:00:00 2001 From: shanshanzhong Date: Tue, 26 May 2026 22:49:46 -0700 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D(#84):=20=E8=A1=A5=E9=BD=90?= =?UTF-8?q?=E4=BF=83=E9=94=80=E7=AE=A1=E7=90=86API=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: multica-agent --- apis/admin/promo.api | 155 ++++++++++++++++ apis/swagger_admin.api | 3 +- .../admin/promo/createPromoPriceHandler.go | 25 +++ .../admin/promo/createPromoRuleHandler.go | 25 +++ .../admin/promo/deletePromoPriceHandler.go | 35 ++++ .../admin/promo/deletePromoRuleHandler.go | 35 ++++ .../admin/promo/getPromoPriceListHandler.go | 25 +++ .../admin/promo/getPromoRuleDetailHandler.go | 35 ++++ .../admin/promo/getPromoRuleListHandler.go | 25 +++ .../admin/promo/getPromoUsageListHandler.go | 25 +++ .../admin/promo/updatePromoRuleHandler.go | 35 ++++ internal/handler/routes.go | 33 ++++ internal/logic/admin/promo/common.go | 128 +++++++++++++ .../admin/promo/createPromoPriceLogic.go | 62 +++++++ .../logic/admin/promo/createPromoRuleLogic.go | 51 ++++++ .../admin/promo/deletePromoPriceLogic.go | 45 +++++ .../logic/admin/promo/deletePromoRuleLogic.go | 39 ++++ .../admin/promo/getPromoPriceListLogic.go | 50 ++++++ .../admin/promo/getPromoRuleDetailLogic.go | 39 ++++ .../admin/promo/getPromoRuleListLogic.go | 47 +++++ .../admin/promo/getPromoUsageListLogic.go | 48 +++++ .../logic/admin/promo/updatePromoRuleLogic.go | 59 ++++++ .../logic/public/order/promoPricing_test.go | 40 +++++ internal/model/promo/model.go | 170 ++++++++++++++++++ internal/types/types.go | 114 ++++++++++++ ppanel.api | 2 +- 26 files changed, 1348 insertions(+), 2 deletions(-) create mode 100644 apis/admin/promo.api create mode 100644 internal/handler/admin/promo/createPromoPriceHandler.go create mode 100644 internal/handler/admin/promo/createPromoRuleHandler.go create mode 100644 internal/handler/admin/promo/deletePromoPriceHandler.go create mode 100644 internal/handler/admin/promo/deletePromoRuleHandler.go create mode 100644 internal/handler/admin/promo/getPromoPriceListHandler.go create mode 100644 internal/handler/admin/promo/getPromoRuleDetailHandler.go create mode 100644 internal/handler/admin/promo/getPromoRuleListHandler.go create mode 100644 internal/handler/admin/promo/getPromoUsageListHandler.go create mode 100644 internal/handler/admin/promo/updatePromoRuleHandler.go create mode 100644 internal/logic/admin/promo/common.go create mode 100644 internal/logic/admin/promo/createPromoPriceLogic.go create mode 100644 internal/logic/admin/promo/createPromoRuleLogic.go create mode 100644 internal/logic/admin/promo/deletePromoPriceLogic.go create mode 100644 internal/logic/admin/promo/deletePromoRuleLogic.go create mode 100644 internal/logic/admin/promo/getPromoPriceListLogic.go create mode 100644 internal/logic/admin/promo/getPromoRuleDetailLogic.go create mode 100644 internal/logic/admin/promo/getPromoRuleListLogic.go create mode 100644 internal/logic/admin/promo/getPromoUsageListLogic.go create mode 100644 internal/logic/admin/promo/updatePromoRuleLogic.go diff --git a/apis/admin/promo.api b/apis/admin/promo.api new file mode 100644 index 0000000..727b451 --- /dev/null +++ b/apis/admin/promo.api @@ -0,0 +1,155 @@ +syntax = "v1" + +info ( + title: "Promo API" + desc: "API for ppanel" + author: "Tension" + email: "tension@ppanel.com" + version: "0.0.1" +) + +import "../types.api" + +type ( + PromoRule { + Id int64 `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Params interface{} `json:"params"` + Priority int64 `json:"priority"` + Enabled bool `json:"enabled"` + StartTime int64 `json:"start_time"` + EndTime int64 `json:"end_time"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` + } + CreatePromoRuleRequest { + Name string `json:"name" validate:"required,max=100"` + Type string `json:"type" validate:"required,oneof=new_user inactive_user campaign"` + Params interface{} `json:"params" validate:"required"` + Priority int64 `json:"priority" validate:"gte=0"` + Enabled *bool `json:"enabled" validate:"required"` + StartTime int64 `json:"start_time" validate:"required"` + EndTime int64 `json:"end_time" validate:"required"` + } + UpdatePromoRuleRequest { + Id int64 `json:"id" validate:"required"` + Name string `json:"name" validate:"required,max=100"` + Type string `json:"type" validate:"required,oneof=new_user inactive_user campaign"` + Params interface{} `json:"params" validate:"required"` + Priority int64 `json:"priority" validate:"gte=0"` + Enabled *bool `json:"enabled" validate:"required"` + StartTime int64 `json:"start_time" validate:"required"` + EndTime int64 `json:"end_time" validate:"required"` + } + DeletePromoRuleRequest { + Id int64 `path:"id" validate:"required"` + } + GetPromoRuleDetailRequest { + Id int64 `path:"id" validate:"required"` + } + GetPromoRuleListRequest { + Page int64 `form:"page" validate:"required"` + Size int64 `form:"size" validate:"required"` + Type string `form:"type,omitempty" validate:"omitempty,oneof=new_user inactive_user campaign"` + Enabled *bool `form:"enabled,omitempty"` + Search string `form:"search,omitempty"` + } + GetPromoRuleListResponse { + Total int64 `json:"total"` + List []PromoRule `json:"list"` + } + PromoPriceInput { + SubscribeId int64 `json:"subscribe_id" validate:"required"` + PromoPrice int64 `json:"promo_price" validate:"required,gt=0"` + } + PromoPrice { + Id int64 `json:"id"` + SubscribeId int64 `json:"subscribe_id"` + PromoRuleId int64 `json:"promo_rule_id"` + PromoPrice int64 `json:"promo_price"` + UnitPrice int64 `json:"unit_price"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` + } + CreatePromoPriceRequest { + PromoRuleId int64 `json:"promo_rule_id" validate:"required"` + Items []PromoPriceInput `json:"items" validate:"required,dive"` + } + GetPromoPriceListRequest { + Page int64 `form:"page" validate:"required"` + Size int64 `form:"size" validate:"required"` + PromoRuleId int64 `form:"promo_rule_id" validate:"required"` + } + GetPromoPriceListResponse { + Total int64 `json:"total"` + List []PromoPrice `json:"list"` + } + DeletePromoPriceRequest { + Id int64 `path:"id" validate:"required"` + } + PromoUsage { + Id int64 `json:"id"` + UserId int64 `json:"user_id"` + PromoRuleId int64 `json:"promo_rule_id"` + SubscribeId int64 `json:"subscribe_id"` + OrderNo string `json:"order_no"` + PromoPrice int64 `json:"promo_price"` + CreatedAt int64 `json:"created_at"` + } + GetPromoUsageListRequest { + Page int64 `form:"page" validate:"required"` + Size int64 `form:"size" validate:"required"` + PromoRuleId int64 `form:"rule_id,omitempty"` + UserId int64 `form:"user_id,omitempty"` + SubscribeId int64 `form:"subscribe_id,omitempty"` + OrderNo string `form:"order_no,omitempty"` + } + GetPromoUsageListResponse { + Total int64 `json:"total"` + List []PromoUsage `json:"list"` + } +) + +@server ( + prefix: v1/admin/promo + group: admin/promo + middleware: AuthMiddleware +) +service ppanel { + @doc "Create promo rule" + @handler CreatePromoRule + post /rule (CreatePromoRuleRequest) returns (PromoRule) + + @doc "Get promo rule list" + @handler GetPromoRuleList + get /rule/list (GetPromoRuleListRequest) returns (GetPromoRuleListResponse) + + @doc "Get promo rule detail" + @handler GetPromoRuleDetail + get /rule/:id (GetPromoRuleDetailRequest) returns (PromoRule) + + @doc "Update promo rule" + @handler UpdatePromoRule + put /rule/:id (UpdatePromoRuleRequest) returns (PromoRule) + + @doc "Delete promo rule" + @handler DeletePromoRule + delete /rule/:id (DeletePromoRuleRequest) + + @doc "Batch set promo price" + @handler CreatePromoPrice + post /price (CreatePromoPriceRequest) + + @doc "Get promo price list" + @handler GetPromoPriceList + get /price/list (GetPromoPriceListRequest) returns (GetPromoPriceListResponse) + + @doc "Delete promo price" + @handler DeletePromoPrice + delete /price/:id (DeletePromoPriceRequest) + + @doc "Get promo usage list" + @handler GetPromoUsageList + get /usage/list (GetPromoUsageListRequest) returns (GetPromoUsageListResponse) +} diff --git a/apis/swagger_admin.api b/apis/swagger_admin.api index e61a903..9913b0d 100644 --- a/apis/swagger_admin.api +++ b/apis/swagger_admin.api @@ -26,5 +26,6 @@ import ( "./admin/ads.api" "./admin/marketing.api" "./admin/application.api" + "./admin/group.api" + "./admin/promo.api" ) - diff --git a/internal/handler/admin/promo/createPromoPriceHandler.go b/internal/handler/admin/promo/createPromoPriceHandler.go new file mode 100644 index 0000000..7227c88 --- /dev/null +++ b/internal/handler/admin/promo/createPromoPriceHandler.go @@ -0,0 +1,25 @@ +package promo + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/promo" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Batch set promo price +func CreatePromoPriceHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.CreatePromoPriceRequest + _ = c.ShouldBind(&req) + if err := svcCtx.Validate(&req); err != nil { + result.ParamErrorResult(c, err) + return + } + + l := promo.NewCreatePromoPriceLogic(c.Request.Context(), svcCtx) + err := l.CreatePromoPrice(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/promo/createPromoRuleHandler.go b/internal/handler/admin/promo/createPromoRuleHandler.go new file mode 100644 index 0000000..b21ab11 --- /dev/null +++ b/internal/handler/admin/promo/createPromoRuleHandler.go @@ -0,0 +1,25 @@ +package promo + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/promo" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Create promo rule +func CreatePromoRuleHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.CreatePromoRuleRequest + _ = c.ShouldBind(&req) + if err := svcCtx.Validate(&req); err != nil { + result.ParamErrorResult(c, err) + return + } + + l := promo.NewCreatePromoRuleLogic(c.Request.Context(), svcCtx) + resp, err := l.CreatePromoRule(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/promo/deletePromoPriceHandler.go b/internal/handler/admin/promo/deletePromoPriceHandler.go new file mode 100644 index 0000000..0b4d48b --- /dev/null +++ b/internal/handler/admin/promo/deletePromoPriceHandler.go @@ -0,0 +1,35 @@ +package promo + +import ( + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/promo" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Delete promo price +func DeletePromoPriceHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.DeletePromoPriceRequest + id, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + c.String(http.StatusBadRequest, "Invalid Params") + c.Abort() + return + } + req.Id = id + _ = c.ShouldBind(&req) + if err := svcCtx.Validate(&req); err != nil { + result.ParamErrorResult(c, err) + return + } + + l := promo.NewDeletePromoPriceLogic(c.Request.Context(), svcCtx) + err = l.DeletePromoPrice(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/promo/deletePromoRuleHandler.go b/internal/handler/admin/promo/deletePromoRuleHandler.go new file mode 100644 index 0000000..a784a86 --- /dev/null +++ b/internal/handler/admin/promo/deletePromoRuleHandler.go @@ -0,0 +1,35 @@ +package promo + +import ( + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/promo" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Delete promo rule +func DeletePromoRuleHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.DeletePromoRuleRequest + id, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + c.String(http.StatusBadRequest, "Invalid Params") + c.Abort() + return + } + req.Id = id + _ = c.ShouldBind(&req) + if err := svcCtx.Validate(&req); err != nil { + result.ParamErrorResult(c, err) + return + } + + l := promo.NewDeletePromoRuleLogic(c.Request.Context(), svcCtx) + err = l.DeletePromoRule(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/promo/getPromoPriceListHandler.go b/internal/handler/admin/promo/getPromoPriceListHandler.go new file mode 100644 index 0000000..733d3a0 --- /dev/null +++ b/internal/handler/admin/promo/getPromoPriceListHandler.go @@ -0,0 +1,25 @@ +package promo + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/promo" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Get promo price list +func GetPromoPriceListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.GetPromoPriceListRequest + _ = c.ShouldBind(&req) + if err := svcCtx.Validate(&req); err != nil { + result.ParamErrorResult(c, err) + return + } + + l := promo.NewGetPromoPriceListLogic(c.Request.Context(), svcCtx) + resp, err := l.GetPromoPriceList(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/promo/getPromoRuleDetailHandler.go b/internal/handler/admin/promo/getPromoRuleDetailHandler.go new file mode 100644 index 0000000..e02faa3 --- /dev/null +++ b/internal/handler/admin/promo/getPromoRuleDetailHandler.go @@ -0,0 +1,35 @@ +package promo + +import ( + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/promo" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Get promo rule detail +func GetPromoRuleDetailHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.GetPromoRuleDetailRequest + id, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + c.String(http.StatusBadRequest, "Invalid Params") + c.Abort() + return + } + req.Id = id + _ = c.ShouldBind(&req) + if err := svcCtx.Validate(&req); err != nil { + result.ParamErrorResult(c, err) + return + } + + l := promo.NewGetPromoRuleDetailLogic(c.Request.Context(), svcCtx) + resp, err := l.GetPromoRuleDetail(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/promo/getPromoRuleListHandler.go b/internal/handler/admin/promo/getPromoRuleListHandler.go new file mode 100644 index 0000000..a3e7900 --- /dev/null +++ b/internal/handler/admin/promo/getPromoRuleListHandler.go @@ -0,0 +1,25 @@ +package promo + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/promo" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Get promo rule list +func GetPromoRuleListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.GetPromoRuleListRequest + _ = c.ShouldBind(&req) + if err := svcCtx.Validate(&req); err != nil { + result.ParamErrorResult(c, err) + return + } + + l := promo.NewGetPromoRuleListLogic(c.Request.Context(), svcCtx) + resp, err := l.GetPromoRuleList(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/promo/getPromoUsageListHandler.go b/internal/handler/admin/promo/getPromoUsageListHandler.go new file mode 100644 index 0000000..e157391 --- /dev/null +++ b/internal/handler/admin/promo/getPromoUsageListHandler.go @@ -0,0 +1,25 @@ +package promo + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/promo" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Get promo usage list +func GetPromoUsageListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.GetPromoUsageListRequest + _ = c.ShouldBind(&req) + if err := svcCtx.Validate(&req); err != nil { + result.ParamErrorResult(c, err) + return + } + + l := promo.NewGetPromoUsageListLogic(c.Request.Context(), svcCtx) + resp, err := l.GetPromoUsageList(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/promo/updatePromoRuleHandler.go b/internal/handler/admin/promo/updatePromoRuleHandler.go new file mode 100644 index 0000000..eaa84bc --- /dev/null +++ b/internal/handler/admin/promo/updatePromoRuleHandler.go @@ -0,0 +1,35 @@ +package promo + +import ( + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/promo" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Update promo rule +func UpdatePromoRuleHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.UpdatePromoRuleRequest + id, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + c.String(http.StatusBadRequest, "Invalid Params") + c.Abort() + return + } + _ = c.ShouldBind(&req) + req.Id = id + if err := svcCtx.Validate(&req); err != nil { + result.ParamErrorResult(c, err) + return + } + + l := promo.NewUpdatePromoRuleLogic(c.Request.Context(), svcCtx) + resp, err := l.UpdatePromoRule(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/routes.go b/internal/handler/routes.go index 7da38a6..0de4eae 100644 --- a/internal/handler/routes.go +++ b/internal/handler/routes.go @@ -17,6 +17,7 @@ import ( adminMarketing "github.com/perfect-panel/server/internal/handler/admin/marketing" adminOrder "github.com/perfect-panel/server/internal/handler/admin/order" adminPayment "github.com/perfect-panel/server/internal/handler/admin/payment" + adminPromo "github.com/perfect-panel/server/internal/handler/admin/promo" adminRedemption "github.com/perfect-panel/server/internal/handler/admin/redemption" adminServer "github.com/perfect-panel/server/internal/handler/admin/server" adminSubscribe "github.com/perfect-panel/server/internal/handler/admin/subscribe" @@ -374,6 +375,38 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { adminPaymentGroupRouter.GET("/platform", adminPayment.GetPaymentPlatformHandler(serverCtx)) } + adminPromoGroupRouter := router.Group("/v1/admin/promo") + adminPromoGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) + + { + // Create promo rule + adminPromoGroupRouter.POST("/rule", adminPromo.CreatePromoRuleHandler(serverCtx)) + + // Get promo rule list + adminPromoGroupRouter.GET("/rule/list", adminPromo.GetPromoRuleListHandler(serverCtx)) + + // Get promo rule detail + adminPromoGroupRouter.GET("/rule/:id", adminPromo.GetPromoRuleDetailHandler(serverCtx)) + + // Update promo rule + adminPromoGroupRouter.PUT("/rule/:id", adminPromo.UpdatePromoRuleHandler(serverCtx)) + + // Delete promo rule + adminPromoGroupRouter.DELETE("/rule/:id", adminPromo.DeletePromoRuleHandler(serverCtx)) + + // Batch set promo price + adminPromoGroupRouter.POST("/price", adminPromo.CreatePromoPriceHandler(serverCtx)) + + // Get promo price list + adminPromoGroupRouter.GET("/price/list", adminPromo.GetPromoPriceListHandler(serverCtx)) + + // Delete promo price + adminPromoGroupRouter.DELETE("/price/:id", adminPromo.DeletePromoPriceHandler(serverCtx)) + + // Get promo usage list + adminPromoGroupRouter.GET("/usage/list", adminPromo.GetPromoUsageListHandler(serverCtx)) + } + adminRedemptionGroupRouter := router.Group("/v1/admin/redemption") adminRedemptionGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) diff --git a/internal/logic/admin/promo/common.go b/internal/logic/admin/promo/common.go new file mode 100644 index 0000000..48f95bf --- /dev/null +++ b/internal/logic/admin/promo/common.go @@ -0,0 +1,128 @@ +package promo + +import ( + "context" + "encoding/json" + "fmt" + "time" + + promoModel "github.com/perfect-panel/server/internal/model/promo" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +const ( + enabledRulesCacheKey = "promo:rules:enabled" + subscribePromoKeyFmt = "promo:subscribe:%d" +) + +type ruleParams struct { + WindowHours int `json:"window_hours"` + InactiveMonths int `json:"inactive_months"` +} + +func validateRulePayload(ruleType string, params json.RawMessage, priority, startTime, endTime int64) error { + if priority < 0 { + return xerr.NewErrCodeMsg(xerr.InvalidParams, "priority must be greater than or equal to 0") + } + if startTime >= endTime { + return xerr.NewErrCodeMsg(xerr.InvalidParams, "start_time must be less than end_time") + } + + var p ruleParams + if len(params) == 0 || !json.Valid(params) { + return xerr.NewErrCodeMsg(xerr.InvalidParams, "params must be valid json") + } + if err := json.Unmarshal(params, &p); err != nil { + return xerr.NewErrCodeMsg(xerr.InvalidParams, "params must be valid json object") + } + + switch ruleType { + case promoModel.RuleTypeNewUser: + if p.WindowHours <= 0 { + return xerr.NewErrCodeMsg(xerr.InvalidParams, "params.window_hours must be greater than 0") + } + case promoModel.RuleTypeInactiveUser: + if p.InactiveMonths <= 0 { + return xerr.NewErrCodeMsg(xerr.InvalidParams, "params.inactive_months must be greater than 0") + } + case promoModel.RuleTypeCampaign: + return nil + default: + return xerr.NewErrCodeMsg(xerr.InvalidParams, "type must be new_user, inactive_user or campaign") + } + return nil +} + +func toRuleResponse(data *promoModel.Rule) types.PromoRule { + resp := types.PromoRule{ + Id: data.Id, + Name: data.Name, + Type: data.Type, + Params: json.RawMessage(data.Params), + Priority: data.Priority, + Enabled: data.Enabled, + CreatedAt: data.CreatedAt.UnixMilli(), + UpdatedAt: data.UpdatedAt.UnixMilli(), + } + if data.StartTime != nil { + resp.StartTime = data.StartTime.UnixMilli() + } + if data.EndTime != nil { + resp.EndTime = data.EndTime.UnixMilli() + } + return resp +} + +func toPriceResponse(data *promoModel.SubscribePromo, unitPrice int64) types.PromoPrice { + return types.PromoPrice{ + Id: data.Id, + SubscribeId: data.SubscribeId, + PromoRuleId: data.PromoRuleId, + PromoPrice: data.PromoPrice, + UnitPrice: unitPrice, + CreatedAt: data.CreatedAt.UnixMilli(), + UpdatedAt: data.UpdatedAt.UnixMilli(), + } +} + +func toUsageResponse(data *promoModel.Usage) types.PromoUsage { + return types.PromoUsage{ + Id: data.Id, + UserId: data.UserId, + PromoRuleId: data.PromoRuleId, + SubscribeId: data.SubscribeId, + OrderNo: data.OrderNo, + PromoPrice: data.PromoPrice, + CreatedAt: data.CreatedAt.UnixMilli(), + } +} + +func ruleTimes(startTime, endTime int64) (*time.Time, *time.Time) { + start := time.UnixMilli(startTime) + end := time.UnixMilli(endTime) + return &start, &end +} + +func clearRuleCache(ctx context.Context, svcCtx *svc.ServiceContext) { + if svcCtx != nil && svcCtx.Redis != nil { + _ = svcCtx.Redis.Del(ctx, enabledRulesCacheKey).Err() + } +} + +func clearSubscribePromoCache(ctx context.Context, svcCtx *svc.ServiceContext, ids ...int64) { + if svcCtx == nil || svcCtx.Redis == nil || len(ids) == 0 { + return + } + keys := make([]string, 0, len(ids)) + for _, id := range ids { + keys = append(keys, fmt.Sprintf(subscribePromoKeyFmt, id)) + } + _ = svcCtx.Redis.Del(ctx, keys...).Err() +} + +func wrapQueryError(msg string, err error) error { + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "%s: %v", msg, err.Error()) +} diff --git a/internal/logic/admin/promo/createPromoPriceLogic.go b/internal/logic/admin/promo/createPromoPriceLogic.go new file mode 100644 index 0000000..0fef805 --- /dev/null +++ b/internal/logic/admin/promo/createPromoPriceLogic.go @@ -0,0 +1,62 @@ +package promo + +import ( + "context" + + promoModel "github.com/perfect-panel/server/internal/model/promo" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type CreatePromoPriceLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Batch set promo price +func NewCreatePromoPriceLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreatePromoPriceLogic { + return &CreatePromoPriceLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *CreatePromoPriceLogic) CreatePromoPrice(req *types.CreatePromoPriceRequest) error { + if _, err := l.svcCtx.PromoModel.FindRule(l.ctx, req.PromoRuleId); err != nil { + if promoModel.IsNotFound(err) { + return xerr.NewErrCodeMsg(xerr.InvalidParams, "promo rule not found") + } + l.Errorw("[CreatePromoPrice] Query Rule Error", logger.Field("error", err.Error())) + return wrapQueryError("get promo rule failed", err) + } + + data := make([]*promoModel.SubscribePromo, 0, len(req.Items)) + subscribeIds := make([]int64, 0, len(req.Items)) + for _, item := range req.Items { + sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, item.SubscribeId) + if err != nil { + return xerr.NewErrCodeMsg(xerr.InvalidParams, "subscribe not found") + } + if item.PromoPrice >= sub.UnitPrice { + return xerr.NewErrCodeMsg(xerr.InvalidParams, "promo_price must be less than unit_price") + } + data = append(data, &promoModel.SubscribePromo{ + SubscribeId: item.SubscribeId, + PromoRuleId: req.PromoRuleId, + PromoPrice: item.PromoPrice, + }) + subscribeIds = append(subscribeIds, item.SubscribeId) + } + + if err := l.svcCtx.PromoModel.UpsertSubscribePromos(l.ctx, data); err != nil { + l.Errorw("[CreatePromoPrice] Database Error", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "set promo price failed: %v", err.Error()) + } + clearSubscribePromoCache(l.ctx, l.svcCtx, subscribeIds...) + return nil +} diff --git a/internal/logic/admin/promo/createPromoRuleLogic.go b/internal/logic/admin/promo/createPromoRuleLogic.go new file mode 100644 index 0000000..add0f05 --- /dev/null +++ b/internal/logic/admin/promo/createPromoRuleLogic.go @@ -0,0 +1,51 @@ +package promo + +import ( + "context" + + promoModel "github.com/perfect-panel/server/internal/model/promo" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type CreatePromoRuleLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Create promo rule +func NewCreatePromoRuleLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreatePromoRuleLogic { + return &CreatePromoRuleLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *CreatePromoRuleLogic) CreatePromoRule(req *types.CreatePromoRuleRequest) (*types.PromoRule, error) { + if err := validateRulePayload(req.Type, req.Params, req.Priority, req.StartTime, req.EndTime); err != nil { + return nil, err + } + + start, end := ruleTimes(req.StartTime, req.EndTime) + data := &promoModel.Rule{ + Name: req.Name, + Type: req.Type, + Params: string(req.Params), + Priority: req.Priority, + Enabled: *req.Enabled, + StartTime: start, + EndTime: end, + } + if err := l.svcCtx.PromoModel.InsertRule(l.ctx, data); err != nil { + l.Errorw("[CreatePromoRule] Database Error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create promo rule failed: %v", err.Error()) + } + clearRuleCache(l.ctx, l.svcCtx) + resp := toRuleResponse(data) + return &resp, nil +} diff --git a/internal/logic/admin/promo/deletePromoPriceLogic.go b/internal/logic/admin/promo/deletePromoPriceLogic.go new file mode 100644 index 0000000..fdb4605 --- /dev/null +++ b/internal/logic/admin/promo/deletePromoPriceLogic.go @@ -0,0 +1,45 @@ +package promo + +import ( + "context" + + promoModel "github.com/perfect-panel/server/internal/model/promo" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type DeletePromoPriceLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Delete promo price +func NewDeletePromoPriceLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeletePromoPriceLogic { + return &DeletePromoPriceLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *DeletePromoPriceLogic) DeletePromoPrice(req *types.DeletePromoPriceRequest) error { + data, err := l.svcCtx.PromoModel.FindSubscribePromo(l.ctx, req.Id) + if err != nil { + if promoModel.IsNotFound(err) { + return xerr.NewErrCodeMsg(xerr.InvalidParams, "promo price not found") + } + l.Errorw("[DeletePromoPrice] Database Query Error", logger.Field("error", err.Error())) + return wrapQueryError("get promo price failed", err) + } + + if err = l.svcCtx.PromoModel.DeleteSubscribePromo(l.ctx, req.Id); err != nil { + l.Errorw("[DeletePromoPrice] Database Delete Error", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "delete promo price failed: %v", err.Error()) + } + clearSubscribePromoCache(l.ctx, l.svcCtx, data.SubscribeId) + return nil +} diff --git a/internal/logic/admin/promo/deletePromoRuleLogic.go b/internal/logic/admin/promo/deletePromoRuleLogic.go new file mode 100644 index 0000000..2fa6b22 --- /dev/null +++ b/internal/logic/admin/promo/deletePromoRuleLogic.go @@ -0,0 +1,39 @@ +package promo + +import ( + "context" + + promoModel "github.com/perfect-panel/server/internal/model/promo" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type DeletePromoRuleLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Delete promo rule +func NewDeletePromoRuleLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeletePromoRuleLogic { + return &DeletePromoRuleLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *DeletePromoRuleLogic) DeletePromoRule(req *types.DeletePromoRuleRequest) error { + if err := l.svcCtx.PromoModel.DeleteRule(l.ctx, req.Id); err != nil { + if promoModel.IsNotFound(err) { + return xerr.NewErrCodeMsg(xerr.InvalidParams, "promo rule not found") + } + l.Errorw("[DeletePromoRule] Database Delete Error", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "delete promo rule failed: %v", err.Error()) + } + clearRuleCache(l.ctx, l.svcCtx) + return nil +} diff --git a/internal/logic/admin/promo/getPromoPriceListLogic.go b/internal/logic/admin/promo/getPromoPriceListLogic.go new file mode 100644 index 0000000..e60997d --- /dev/null +++ b/internal/logic/admin/promo/getPromoPriceListLogic.go @@ -0,0 +1,50 @@ +package promo + +import ( + "context" + + promoModel "github.com/perfect-panel/server/internal/model/promo" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" +) + +type GetPromoPriceListLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get promo price list +func NewGetPromoPriceListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetPromoPriceListLogic { + return &GetPromoPriceListLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetPromoPriceListLogic) GetPromoPriceList(req *types.GetPromoPriceListRequest) (*types.GetPromoPriceListResponse, error) { + total, list, err := l.svcCtx.PromoModel.QuerySubscribePromoList(l.ctx, promoModel.SubscribePromoFilter{ + Page: int(req.Page), + Size: int(req.Size), + PromoRuleId: req.PromoRuleId, + }) + if err != nil { + l.Errorw("[GetPromoPriceList] Database Error", logger.Field("error", err.Error())) + return nil, wrapQueryError("get promo price list failed", err) + } + resp := &types.GetPromoPriceListResponse{ + Total: total, + List: make([]types.PromoPrice, 0, len(list)), + } + for _, item := range list { + var unitPrice int64 + sub, subErr := l.svcCtx.SubscribeModel.FindOne(l.ctx, item.SubscribeId) + if subErr == nil { + unitPrice = sub.UnitPrice + } + resp.List = append(resp.List, toPriceResponse(item, unitPrice)) + } + return resp, nil +} diff --git a/internal/logic/admin/promo/getPromoRuleDetailLogic.go b/internal/logic/admin/promo/getPromoRuleDetailLogic.go new file mode 100644 index 0000000..95dc3e9 --- /dev/null +++ b/internal/logic/admin/promo/getPromoRuleDetailLogic.go @@ -0,0 +1,39 @@ +package promo + +import ( + "context" + + promoModel "github.com/perfect-panel/server/internal/model/promo" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" +) + +type GetPromoRuleDetailLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get promo rule detail +func NewGetPromoRuleDetailLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetPromoRuleDetailLogic { + return &GetPromoRuleDetailLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetPromoRuleDetailLogic) GetPromoRuleDetail(req *types.GetPromoRuleDetailRequest) (*types.PromoRule, error) { + data, err := l.svcCtx.PromoModel.FindRule(l.ctx, req.Id) + if err != nil { + if promoModel.IsNotFound(err) { + return nil, xerr.NewErrCodeMsg(xerr.InvalidParams, "promo rule not found") + } + l.Errorw("[GetPromoRuleDetail] Database Error", logger.Field("error", err.Error())) + return nil, wrapQueryError("get promo rule detail failed", err) + } + resp := toRuleResponse(data) + return &resp, nil +} diff --git a/internal/logic/admin/promo/getPromoRuleListLogic.go b/internal/logic/admin/promo/getPromoRuleListLogic.go new file mode 100644 index 0000000..41581f0 --- /dev/null +++ b/internal/logic/admin/promo/getPromoRuleListLogic.go @@ -0,0 +1,47 @@ +package promo + +import ( + "context" + + promoModel "github.com/perfect-panel/server/internal/model/promo" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" +) + +type GetPromoRuleListLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get promo rule list +func NewGetPromoRuleListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetPromoRuleListLogic { + return &GetPromoRuleListLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetPromoRuleListLogic) GetPromoRuleList(req *types.GetPromoRuleListRequest) (*types.GetPromoRuleListResponse, error) { + total, list, err := l.svcCtx.PromoModel.QueryRuleList(l.ctx, promoModel.RuleFilter{ + Page: int(req.Page), + Size: int(req.Size), + Type: req.Type, + Enabled: req.Enabled, + Search: req.Search, + }) + if err != nil { + l.Errorw("[GetPromoRuleList] Database Error", logger.Field("error", err.Error())) + return nil, wrapQueryError("get promo rule list failed", err) + } + resp := &types.GetPromoRuleListResponse{ + Total: total, + List: make([]types.PromoRule, 0, len(list)), + } + for _, item := range list { + resp.List = append(resp.List, toRuleResponse(item)) + } + return resp, nil +} diff --git a/internal/logic/admin/promo/getPromoUsageListLogic.go b/internal/logic/admin/promo/getPromoUsageListLogic.go new file mode 100644 index 0000000..7675aff --- /dev/null +++ b/internal/logic/admin/promo/getPromoUsageListLogic.go @@ -0,0 +1,48 @@ +package promo + +import ( + "context" + + promoModel "github.com/perfect-panel/server/internal/model/promo" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" +) + +type GetPromoUsageListLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get promo usage list +func NewGetPromoUsageListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetPromoUsageListLogic { + return &GetPromoUsageListLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetPromoUsageListLogic) GetPromoUsageList(req *types.GetPromoUsageListRequest) (*types.GetPromoUsageListResponse, error) { + total, list, err := l.svcCtx.PromoModel.QueryUsageList(l.ctx, promoModel.UsageFilter{ + Page: int(req.Page), + Size: int(req.Size), + PromoRuleId: req.PromoRuleId, + UserId: req.UserId, + SubscribeId: req.SubscribeId, + OrderNo: req.OrderNo, + }) + if err != nil { + l.Errorw("[GetPromoUsageList] Database Error", logger.Field("error", err.Error())) + return nil, wrapQueryError("get promo usage list failed", err) + } + resp := &types.GetPromoUsageListResponse{ + Total: total, + List: make([]types.PromoUsage, 0, len(list)), + } + for _, item := range list { + resp.List = append(resp.List, toUsageResponse(item)) + } + return resp, nil +} diff --git a/internal/logic/admin/promo/updatePromoRuleLogic.go b/internal/logic/admin/promo/updatePromoRuleLogic.go new file mode 100644 index 0000000..be51dd9 --- /dev/null +++ b/internal/logic/admin/promo/updatePromoRuleLogic.go @@ -0,0 +1,59 @@ +package promo + +import ( + "context" + + promoModel "github.com/perfect-panel/server/internal/model/promo" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type UpdatePromoRuleLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Update promo rule +func NewUpdatePromoRuleLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdatePromoRuleLogic { + return &UpdatePromoRuleLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdatePromoRuleLogic) UpdatePromoRule(req *types.UpdatePromoRuleRequest) (*types.PromoRule, error) { + if err := validateRulePayload(req.Type, req.Params, req.Priority, req.StartTime, req.EndTime); err != nil { + return nil, err + } + + data, err := l.svcCtx.PromoModel.FindRule(l.ctx, req.Id) + if err != nil { + if promoModel.IsNotFound(err) { + return nil, xerr.NewErrCodeMsg(xerr.InvalidParams, "promo rule not found") + } + l.Errorw("[UpdatePromoRule] Database Query Error", logger.Field("error", err.Error())) + return nil, wrapQueryError("get promo rule failed", err) + } + + start, end := ruleTimes(req.StartTime, req.EndTime) + data.Name = req.Name + data.Type = req.Type + data.Params = string(req.Params) + data.Priority = req.Priority + data.Enabled = *req.Enabled + data.StartTime = start + data.EndTime = end + + if err = l.svcCtx.PromoModel.UpdateRule(l.ctx, data); err != nil { + l.Errorw("[UpdatePromoRule] Database Update Error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update promo rule failed: %v", err.Error()) + } + clearRuleCache(l.ctx, l.svcCtx) + resp := toRuleResponse(data) + return &resp, nil +} diff --git a/internal/logic/public/order/promoPricing_test.go b/internal/logic/public/order/promoPricing_test.go index 2c3f579..a41510b 100644 --- a/internal/logic/public/order/promoPricing_test.go +++ b/internal/logic/public/order/promoPricing_test.go @@ -22,6 +22,46 @@ func (m fakePromoModel) InsertUsage(context.Context, *promo.Usage, ...*gorm.DB) return nil } +func (m fakePromoModel) InsertRule(context.Context, *promo.Rule) error { + return nil +} + +func (m fakePromoModel) FindRule(context.Context, int64) (*promo.Rule, error) { + return nil, nil +} + +func (m fakePromoModel) UpdateRule(context.Context, *promo.Rule) error { + return nil +} + +func (m fakePromoModel) DeleteRule(context.Context, int64) error { + return nil +} + +func (m fakePromoModel) QueryRuleList(context.Context, promo.RuleFilter) (int64, []*promo.Rule, error) { + return 0, nil, nil +} + +func (m fakePromoModel) UpsertSubscribePromos(context.Context, []*promo.SubscribePromo) error { + return nil +} + +func (m fakePromoModel) FindSubscribePromo(context.Context, int64) (*promo.SubscribePromo, error) { + return nil, nil +} + +func (m fakePromoModel) DeleteSubscribePromo(context.Context, int64) error { + return nil +} + +func (m fakePromoModel) QuerySubscribePromoList(context.Context, promo.SubscribePromoFilter) (int64, []*promo.SubscribePromo, error) { + return 0, nil, nil +} + +func (m fakePromoModel) QueryUsageList(context.Context, promo.UsageFilter) (int64, []*promo.Usage, error) { + return 0, nil, nil +} + func TestCalculatePurchasePricePromoSkipsPercentDiscount(t *testing.T) { svcCtx := &svc.ServiceContext{ DB: &gorm.DB{}, diff --git a/internal/model/promo/model.go b/internal/model/promo/model.go index ff48935..37a6774 100644 --- a/internal/model/promo/model.go +++ b/internal/model/promo/model.go @@ -2,9 +2,11 @@ package promo import ( "context" + "errors" "github.com/redis/go-redis/v9" "gorm.io/gorm" + "gorm.io/gorm/clause" ) type RuleWithPrice struct { @@ -12,9 +14,42 @@ type RuleWithPrice struct { PromoPrice int64 `gorm:"column:promo_price"` } +type RuleFilter struct { + Page int + Size int + Type string + Enabled *bool + Search string +} + +type SubscribePromoFilter struct { + Page int + Size int + PromoRuleId int64 +} + +type UsageFilter struct { + Page int + Size int + PromoRuleId int64 + UserId int64 + SubscribeId int64 + OrderNo string +} + type Model interface { QueryEligibleRules(ctx context.Context, subscribeId int64) ([]*RuleWithPrice, error) InsertUsage(ctx context.Context, data *Usage, tx ...*gorm.DB) error + InsertRule(ctx context.Context, data *Rule) error + FindRule(ctx context.Context, id int64) (*Rule, error) + UpdateRule(ctx context.Context, data *Rule) error + DeleteRule(ctx context.Context, id int64) error + QueryRuleList(ctx context.Context, filter RuleFilter) (int64, []*Rule, error) + UpsertSubscribePromos(ctx context.Context, data []*SubscribePromo) error + FindSubscribePromo(ctx context.Context, id int64) (*SubscribePromo, error) + DeleteSubscribePromo(ctx context.Context, id int64) error + QuerySubscribePromoList(ctx context.Context, filter SubscribePromoFilter) (int64, []*SubscribePromo, error) + QueryUsageList(ctx context.Context, filter UsageFilter) (int64, []*Usage, error) } type defaultPromoModel struct { @@ -25,6 +60,19 @@ func NewModel(db *gorm.DB, _ *redis.Client) Model { return &defaultPromoModel{db: db} } +func normalizePage(page, size int) (int, int) { + if page <= 0 { + page = 1 + } + if size <= 0 { + size = 10 + } + if size > 100 { + size = 100 + } + return page, size +} + func (m *defaultPromoModel) QueryEligibleRules(ctx context.Context, subscribeId int64) ([]*RuleWithPrice, error) { var list []*RuleWithPrice err := m.db.WithContext(ctx). @@ -46,3 +94,125 @@ func (m *defaultPromoModel) InsertUsage(ctx context.Context, data *Usage, tx ... } return db.Model(&Usage{}).Create(data).Error } + +func (m *defaultPromoModel) InsertRule(ctx context.Context, data *Rule) error { + return m.db.WithContext(ctx).Create(data).Error +} + +func (m *defaultPromoModel) FindRule(ctx context.Context, id int64) (*Rule, error) { + var data Rule + err := m.db.WithContext(ctx).Model(&Rule{}).Where("id = ?", id).First(&data).Error + return &data, err +} + +func (m *defaultPromoModel) UpdateRule(ctx context.Context, data *Rule) error { + return m.db.WithContext(ctx).Save(data).Error +} + +func (m *defaultPromoModel) DeleteRule(ctx context.Context, id int64) error { + result := m.db.WithContext(ctx).Delete(&Rule{}, id) + if result.Error != nil { + return result.Error + } + if result.RowsAffected == 0 { + return gorm.ErrRecordNotFound + } + return nil +} + +func (m *defaultPromoModel) QueryRuleList(ctx context.Context, filter RuleFilter) (int64, []*Rule, error) { + page, size := normalizePage(filter.Page, filter.Size) + var total int64 + var list []*Rule + + query := m.db.WithContext(ctx).Model(&Rule{}) + if filter.Type != "" { + query = query.Where("type = ?", filter.Type) + } + if filter.Enabled != nil { + query = query.Where("enabled = ?", *filter.Enabled) + } + if filter.Search != "" { + search := "%" + filter.Search + "%" + query = query.Where("name LIKE ?", search) + } + if err := query.Count(&total).Error; err != nil { + return 0, nil, err + } + err := query.Order("priority DESC").Order("id DESC"). + Limit(size).Offset((page - 1) * size). + Find(&list).Error + return total, list, err +} + +func (m *defaultPromoModel) UpsertSubscribePromos(ctx context.Context, data []*SubscribePromo) error { + if len(data) == 0 { + return nil + } + return m.db.WithContext(ctx).Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "subscribe_id"}, {Name: "promo_rule_id"}}, + DoUpdates: clause.AssignmentColumns([]string{"promo_price", "updated_at"}), + }).Create(&data).Error +} + +func (m *defaultPromoModel) FindSubscribePromo(ctx context.Context, id int64) (*SubscribePromo, error) { + var data SubscribePromo + err := m.db.WithContext(ctx).Model(&SubscribePromo{}).Where("id = ?", id).First(&data).Error + return &data, err +} + +func (m *defaultPromoModel) DeleteSubscribePromo(ctx context.Context, id int64) error { + result := m.db.WithContext(ctx).Delete(&SubscribePromo{}, id) + if result.Error != nil { + return result.Error + } + if result.RowsAffected == 0 { + return gorm.ErrRecordNotFound + } + return nil +} + +func (m *defaultPromoModel) QuerySubscribePromoList(ctx context.Context, filter SubscribePromoFilter) (int64, []*SubscribePromo, error) { + page, size := normalizePage(filter.Page, filter.Size) + var total int64 + var list []*SubscribePromo + + query := m.db.WithContext(ctx).Model(&SubscribePromo{}) + if filter.PromoRuleId > 0 { + query = query.Where("promo_rule_id = ?", filter.PromoRuleId) + } + if err := query.Count(&total).Error; err != nil { + return 0, nil, err + } + err := query.Order("id DESC").Limit(size).Offset((page - 1) * size).Find(&list).Error + return total, list, err +} + +func (m *defaultPromoModel) QueryUsageList(ctx context.Context, filter UsageFilter) (int64, []*Usage, error) { + page, size := normalizePage(filter.Page, filter.Size) + var total int64 + var list []*Usage + + query := m.db.WithContext(ctx).Model(&Usage{}) + if filter.PromoRuleId > 0 { + query = query.Where("promo_rule_id = ?", filter.PromoRuleId) + } + if filter.UserId > 0 { + query = query.Where("user_id = ?", filter.UserId) + } + if filter.SubscribeId > 0 { + query = query.Where("subscribe_id = ?", filter.SubscribeId) + } + if filter.OrderNo != "" { + query = query.Where("order_no LIKE ?", "%"+filter.OrderNo+"%") + } + if err := query.Count(&total).Error; err != nil { + return 0, nil, err + } + err := query.Order("id DESC").Limit(size).Offset((page - 1) * size).Find(&list).Error + return total, list, err +} + +func IsNotFound(err error) bool { + return errors.Is(err, gorm.ErrRecordNotFound) +} diff --git a/internal/types/types.go b/internal/types/types.go index 1e282c3..48883e1 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -436,6 +436,21 @@ type CreatePaymentMethodRequest struct { Enable *bool `json:"enable" validate:"required"` } +type CreatePromoRuleRequest struct { + Name string `json:"name" validate:"required,max=100"` + Type string `json:"type" validate:"required,oneof=new_user inactive_user campaign"` + Params json.RawMessage `json:"params" validate:"required"` + Priority int64 `json:"priority" validate:"gte=0"` + Enabled *bool `json:"enabled" validate:"required"` + StartTime int64 `json:"start_time" validate:"required"` + EndTime int64 `json:"end_time" validate:"required"` +} + +type CreatePromoPriceRequest struct { + PromoRuleId int64 `json:"promo_rule_id" validate:"required"` + Items []PromoPriceInput `json:"items" validate:"required,dive"` +} + type CreateQuotaTaskRequest struct { Subscribers []int64 `json:"subscribers"` IsActive *bool `json:"is_active"` @@ -619,6 +634,14 @@ type DeletePaymentMethodRequest struct { Id int64 `json:"id" validate:"required"` } +type DeletePromoPriceRequest struct { + Id int64 `path:"id" validate:"required"` +} + +type DeletePromoRuleRequest struct { + Id int64 `path:"id" validate:"required"` +} + type DeleteRedemptionCodeRequest struct { Id int64 `json:"id" validate:"required"` } @@ -1327,6 +1350,48 @@ type GetPaymentMethodListResponse struct { List []PaymentMethodDetail `json:"list"` } +type GetPromoPriceListRequest struct { + Page int64 `form:"page" validate:"required"` + Size int64 `form:"size" validate:"required"` + PromoRuleId int64 `form:"promo_rule_id" validate:"required"` +} + +type GetPromoPriceListResponse struct { + Total int64 `json:"total"` + List []PromoPrice `json:"list"` +} + +type GetPromoRuleDetailRequest struct { + Id int64 `path:"id" validate:"required"` +} + +type GetPromoRuleListRequest struct { + Page int64 `form:"page" validate:"required"` + Size int64 `form:"size" validate:"required"` + Type string `form:"type,omitempty" validate:"omitempty,oneof=new_user inactive_user campaign"` + Enabled *bool `form:"enabled,omitempty"` + Search string `form:"search,omitempty"` +} + +type GetPromoRuleListResponse struct { + Total int64 `json:"total"` + List []PromoRule `json:"list"` +} + +type GetPromoUsageListRequest struct { + Page int64 `form:"page" validate:"required"` + Size int64 `form:"size" validate:"required"` + PromoRuleId int64 `form:"rule_id,omitempty"` + UserId int64 `form:"user_id,omitempty"` + SubscribeId int64 `form:"subscribe_id,omitempty"` + OrderNo string `form:"order_no,omitempty"` +} + +type GetPromoUsageListResponse struct { + Total int64 `json:"total"` + List []PromoUsage `json:"list"` +} + type GetPreSendEmailCountRequest struct { Scope int8 `json:"scope"` RegisterStartTime int64 `json:"register_start_time,omitempty"` @@ -2039,6 +2104,44 @@ type PrePurchaseOrderResponse struct { FeeAmount int64 `json:"fee_amount"` } +type PromoPrice struct { + Id int64 `json:"id"` + SubscribeId int64 `json:"subscribe_id"` + PromoRuleId int64 `json:"promo_rule_id"` + PromoPrice int64 `json:"promo_price"` + UnitPrice int64 `json:"unit_price"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` +} + +type PromoPriceInput struct { + SubscribeId int64 `json:"subscribe_id" validate:"required"` + PromoPrice int64 `json:"promo_price" validate:"required,gt=0"` +} + +type PromoRule struct { + Id int64 `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Params json.RawMessage `json:"params"` + Priority int64 `json:"priority"` + Enabled bool `json:"enabled"` + StartTime int64 `json:"start_time"` + EndTime int64 `json:"end_time"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` +} + +type PromoUsage struct { + Id int64 `json:"id"` + UserId int64 `json:"user_id"` + PromoRuleId int64 `json:"promo_rule_id"` + SubscribeId int64 `json:"subscribe_id"` + OrderNo string `json:"order_no"` + PromoPrice int64 `json:"promo_price"` + CreatedAt int64 `json:"created_at"` +} + type PreRenewalOrderResponse struct { OrderNo string `json:"orderNo"` } @@ -3207,6 +3310,17 @@ type UpdatePaymentMethodRequest struct { Enable *bool `json:"enable" validate:"required"` } +type UpdatePromoRuleRequest struct { + Id int64 `json:"id" validate:"required"` + Name string `json:"name" validate:"required,max=100"` + Type string `json:"type" validate:"required,oneof=new_user inactive_user campaign"` + Params json.RawMessage `json:"params" validate:"required"` + Priority int64 `json:"priority" validate:"gte=0"` + Enabled *bool `json:"enabled" validate:"required"` + StartTime int64 `json:"start_time" validate:"required"` + EndTime int64 `json:"end_time" validate:"required"` +} + type UpdateRedemptionCodeRequest struct { Id int64 `json:"id" validate:"required"` TotalCount int64 `json:"total_count,omitempty"` diff --git a/ppanel.api b/ppanel.api index b6d8726..c92c25d 100644 --- a/ppanel.api +++ b/ppanel.api @@ -31,6 +31,7 @@ import ( "apis/admin/marketing.api" "apis/admin/application.api" "apis/admin/group.api" + "apis/admin/promo.api" "apis/public/user.api" "apis/public/subscribe.api" "apis/public/redemption.api" @@ -44,4 +45,3 @@ import ( "apis/public/portal.api" "apis/public/iap.api" ) -