diff --git a/apis/admin/redemption.api b/apis/admin/redemption.api new file mode 100644 index 0000000..d424b61 --- /dev/null +++ b/apis/admin/redemption.api @@ -0,0 +1,86 @@ +syntax = "v1" + +info ( + title: "redemption API" + desc: "API for redemption code management" + author: "Tension" + email: "tension@ppanel.com" + version: "0.0.1" +) + +import "../types.api" + +type ( + CreateRedemptionCodeRequest { + TotalCount int64 `json:"total_count" validate:"required"` + SubscribePlan int64 `json:"subscribe_plan" validate:"required"` + UnitTime string `json:"unit_time" validate:"required,oneof=day month quarter half_year year"` + Quantity int64 `json:"quantity" validate:"required"` + BatchCount int64 `json:"batch_count" validate:"required,min=1"` + } + UpdateRedemptionCodeRequest { + Id int64 `json:"id" validate:"required"` + TotalCount int64 `json:"total_count,omitempty"` + SubscribePlan int64 `json:"subscribe_plan,omitempty"` + UnitTime string `json:"unit_time,omitempty" validate:"omitempty,oneof=day month quarter half_year year"` + Quantity int64 `json:"quantity,omitempty"` + } + DeleteRedemptionCodeRequest { + Id int64 `json:"id" validate:"required"` + } + BatchDeleteRedemptionCodeRequest { + Ids []int64 `json:"ids" validate:"required"` + } + GetRedemptionCodeListRequest { + Page int64 `form:"page" validate:"required"` + Size int64 `form:"size" validate:"required"` + SubscribePlan int64 `form:"subscribe_plan,omitempty"` + UnitTime string `form:"unit_time,omitempty"` + Code string `form:"code,omitempty"` + } + GetRedemptionCodeListResponse { + Total int64 `json:"total"` + List []RedemptionCode `json:"list"` + } + GetRedemptionRecordListRequest { + Page int64 `form:"page" validate:"required"` + Size int64 `form:"size" validate:"required"` + UserId int64 `form:"user_id,omitempty"` + CodeId int64 `form:"code_id,omitempty"` + } + GetRedemptionRecordListResponse { + Total int64 `json:"total"` + List []RedemptionRecord `json:"list"` + } +) + +@server ( + prefix: v1/admin/redemption + group: admin/redemption + middleware: AuthMiddleware +) +service ppanel { + @doc "Create redemption code" + @handler CreateRedemptionCode + post /code (CreateRedemptionCodeRequest) + + @doc "Update redemption code" + @handler UpdateRedemptionCode + put /code (UpdateRedemptionCodeRequest) + + @doc "Delete redemption code" + @handler DeleteRedemptionCode + delete /code (DeleteRedemptionCodeRequest) + + @doc "Batch delete redemption code" + @handler BatchDeleteRedemptionCode + delete /code/batch (BatchDeleteRedemptionCodeRequest) + + @doc "Get redemption code list" + @handler GetRedemptionCodeList + get /code/list (GetRedemptionCodeListRequest) returns (GetRedemptionCodeListResponse) + + @doc "Get redemption record list" + @handler GetRedemptionRecordList + get /record/list (GetRedemptionRecordListRequest) returns (GetRedemptionRecordListResponse) +} diff --git a/apis/public/redemption.api b/apis/public/redemption.api new file mode 100644 index 0000000..8bbb3ae --- /dev/null +++ b/apis/public/redemption.api @@ -0,0 +1,32 @@ +syntax = "v1" + +info ( + title: "redemption API" + desc: "API for redemption" + author: "Tension" + email: "tension@ppanel.com" + version: "0.0.1" +) + +import "../types.api" + +type ( + RedeemCodeRequest { + Code string `json:"code" validate:"required"` + } + RedeemCodeResponse { + Message string `json:"message"` + } +) + +@server ( + prefix: v1/public/redemption + group: public/redemption + jwt: JwtAuth + middleware: AuthMiddleware,DeviceMiddleware +) +service ppanel { + @doc "Redeem code" + @handler RedeemCode + post / (RedeemCodeRequest) returns (RedeemCodeResponse) +} diff --git a/apis/types.api b/apis/types.api index 7f4bc5a..ee5f56c 100644 --- a/apis/types.api +++ b/apis/types.api @@ -448,6 +448,27 @@ type ( CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` } + RedemptionCode { + Id int64 `json:"id"` + Code string `json:"code"` + TotalCount int64 `json:"total_count"` + UsedCount int64 `json:"used_count"` + SubscribePlan int64 `json:"subscribe_plan"` + UnitTime string `json:"unit_time"` + Quantity int64 `json:"quantity"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` + } + RedemptionRecord { + Id int64 `json:"id"` + RedemptionCodeId int64 `json:"redemption_code_id"` + UserId int64 `json:"user_id"` + SubscribeId int64 `json:"subscribe_id"` + UnitTime string `json:"unit_time"` + Quantity int64 `json:"quantity"` + RedeemedAt int64 `json:"redeemed_at"` + CreatedAt int64 `json:"created_at"` + } Announcement { Id int64 `json:"id"` Title string `json:"title"` diff --git a/initialize/migrate/database/02126_redemption.down.sql b/initialize/migrate/database/02126_redemption.down.sql new file mode 100644 index 0000000..3fdd96f --- /dev/null +++ b/initialize/migrate/database/02126_redemption.down.sql @@ -0,0 +1,5 @@ +-- Drop redemption_record table +DROP TABLE IF EXISTS `redemption_record`; + +-- Drop redemption_code table +DROP TABLE IF EXISTS `redemption_code`; diff --git a/initialize/migrate/database/02126_redemption.up.sql b/initialize/migrate/database/02126_redemption.up.sql new file mode 100644 index 0000000..e1e480d --- /dev/null +++ b/initialize/migrate/database/02126_redemption.up.sql @@ -0,0 +1,31 @@ +-- Create redemption_code table +CREATE TABLE IF NOT EXISTS `redemption_code` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'Primary Key', + `code` VARCHAR(255) NOT NULL COMMENT 'Redemption Code', + `total_count` BIGINT NOT NULL DEFAULT 0 COMMENT 'Total Redemption Count', + `used_count` BIGINT NOT NULL DEFAULT 0 COMMENT 'Used Redemption Count', + `subscribe_plan` BIGINT NOT NULL DEFAULT 0 COMMENT 'Subscribe Plan', + `unit_time` VARCHAR(50) NOT NULL DEFAULT 'month' COMMENT 'Unit Time: day, month, quarter, half_year, year', + `quantity` BIGINT NOT NULL DEFAULT 1 COMMENT 'Quantity', + `created_at` DATETIME NOT NULL COMMENT 'Creation Time', + `updated_at` DATETIME NOT NULL COMMENT 'Update Time', + `deleted_at` DATETIME DEFAULT NULL COMMENT 'Deletion Time', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_code` (`code`), + KEY `idx_deleted_at` (`deleted_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Redemption Code Table'; + +-- Create redemption_record table +CREATE TABLE IF NOT EXISTS `redemption_record` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'Primary Key', + `redemption_code_id` BIGINT NOT NULL DEFAULT 0 COMMENT 'Redemption Code Id', + `user_id` BIGINT NOT NULL DEFAULT 0 COMMENT 'User Id', + `subscribe_id` BIGINT NOT NULL DEFAULT 0 COMMENT 'Subscribe Id', + `unit_time` VARCHAR(50) NOT NULL DEFAULT 'month' COMMENT 'Unit Time', + `quantity` BIGINT NOT NULL DEFAULT 1 COMMENT 'Quantity', + `redeemed_at` DATETIME NOT NULL COMMENT 'Redeemed Time', + `created_at` DATETIME NOT NULL COMMENT 'Creation Time', + PRIMARY KEY (`id`), + KEY `idx_redemption_code_id` (`redemption_code_id`), + KEY `idx_user_id` (`user_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Redemption Record Table'; diff --git a/internal/handler/admin/redemption/batchDeleteRedemptionCodeHandler.go b/internal/handler/admin/redemption/batchDeleteRedemptionCodeHandler.go new file mode 100644 index 0000000..260fef8 --- /dev/null +++ b/internal/handler/admin/redemption/batchDeleteRedemptionCodeHandler.go @@ -0,0 +1,26 @@ +package redemption + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/redemption" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Batch delete redemption code +func BatchDeleteRedemptionCodeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.BatchDeleteRedemptionCodeRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := redemption.NewBatchDeleteRedemptionCodeLogic(c.Request.Context(), svcCtx) + err := l.BatchDeleteRedemptionCode(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/redemption/createRedemptionCodeHandler.go b/internal/handler/admin/redemption/createRedemptionCodeHandler.go new file mode 100644 index 0000000..787bc6b --- /dev/null +++ b/internal/handler/admin/redemption/createRedemptionCodeHandler.go @@ -0,0 +1,26 @@ +package redemption + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/redemption" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Create redemption code +func CreateRedemptionCodeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.CreateRedemptionCodeRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := redemption.NewCreateRedemptionCodeLogic(c.Request.Context(), svcCtx) + err := l.CreateRedemptionCode(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/redemption/deleteRedemptionCodeHandler.go b/internal/handler/admin/redemption/deleteRedemptionCodeHandler.go new file mode 100644 index 0000000..231c611 --- /dev/null +++ b/internal/handler/admin/redemption/deleteRedemptionCodeHandler.go @@ -0,0 +1,26 @@ +package redemption + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/redemption" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Delete redemption code +func DeleteRedemptionCodeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.DeleteRedemptionCodeRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := redemption.NewDeleteRedemptionCodeLogic(c.Request.Context(), svcCtx) + err := l.DeleteRedemptionCode(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/redemption/getRedemptionCodeListHandler.go b/internal/handler/admin/redemption/getRedemptionCodeListHandler.go new file mode 100644 index 0000000..d9f9f34 --- /dev/null +++ b/internal/handler/admin/redemption/getRedemptionCodeListHandler.go @@ -0,0 +1,26 @@ +package redemption + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/redemption" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Get redemption code list +func GetRedemptionCodeListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.GetRedemptionCodeListRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := redemption.NewGetRedemptionCodeListLogic(c.Request.Context(), svcCtx) + resp, err := l.GetRedemptionCodeList(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/redemption/getRedemptionRecordListHandler.go b/internal/handler/admin/redemption/getRedemptionRecordListHandler.go new file mode 100644 index 0000000..77ad6cb --- /dev/null +++ b/internal/handler/admin/redemption/getRedemptionRecordListHandler.go @@ -0,0 +1,26 @@ +package redemption + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/redemption" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Get redemption record list +func GetRedemptionRecordListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.GetRedemptionRecordListRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := redemption.NewGetRedemptionRecordListLogic(c.Request.Context(), svcCtx) + resp, err := l.GetRedemptionRecordList(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/redemption/updateRedemptionCodeHandler.go b/internal/handler/admin/redemption/updateRedemptionCodeHandler.go new file mode 100644 index 0000000..d5db12b --- /dev/null +++ b/internal/handler/admin/redemption/updateRedemptionCodeHandler.go @@ -0,0 +1,26 @@ +package redemption + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/redemption" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Update redemption code +func UpdateRedemptionCodeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.UpdateRedemptionCodeRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := redemption.NewUpdateRedemptionCodeLogic(c.Request.Context(), svcCtx) + err := l.UpdateRedemptionCode(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/public/redemption/redeemCodeHandler.go b/internal/handler/public/redemption/redeemCodeHandler.go new file mode 100644 index 0000000..eb76e5b --- /dev/null +++ b/internal/handler/public/redemption/redeemCodeHandler.go @@ -0,0 +1,26 @@ +package redemption + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/public/redemption" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Redeem code +func RedeemCodeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.RedeemCodeRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := redemption.NewRedeemCodeLogic(c.Request.Context(), svcCtx) + resp, err := l.RedeemCode(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/routes.go b/internal/handler/routes.go index fcd86fa..0f24078 100644 --- a/internal/handler/routes.go +++ b/internal/handler/routes.go @@ -16,6 +16,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" + 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" adminSystem "github.com/perfect-panel/server/internal/handler/admin/system" @@ -30,6 +31,7 @@ import ( publicOrder "github.com/perfect-panel/server/internal/handler/public/order" publicPayment "github.com/perfect-panel/server/internal/handler/public/payment" publicPortal "github.com/perfect-panel/server/internal/handler/public/portal" + publicRedemption "github.com/perfect-panel/server/internal/handler/public/redemption" publicSubscribe "github.com/perfect-panel/server/internal/handler/public/subscribe" publicTicket "github.com/perfect-panel/server/internal/handler/public/ticket" publicUser "github.com/perfect-panel/server/internal/handler/public/user" @@ -298,6 +300,29 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { adminPaymentGroupRouter.GET("/platform", adminPayment.GetPaymentPlatformHandler(serverCtx)) } + adminRedemptionGroupRouter := router.Group("/v1/admin/redemption") + adminRedemptionGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) + + { + // Create redemption code + adminRedemptionGroupRouter.POST("/code", adminRedemption.CreateRedemptionCodeHandler(serverCtx)) + + // Update redemption code + adminRedemptionGroupRouter.PUT("/code", adminRedemption.UpdateRedemptionCodeHandler(serverCtx)) + + // Delete redemption code + adminRedemptionGroupRouter.DELETE("/code", adminRedemption.DeleteRedemptionCodeHandler(serverCtx)) + + // Batch delete redemption code + adminRedemptionGroupRouter.DELETE("/code/batch", adminRedemption.BatchDeleteRedemptionCodeHandler(serverCtx)) + + // Get redemption code list + adminRedemptionGroupRouter.GET("/code/list", adminRedemption.GetRedemptionCodeListHandler(serverCtx)) + + // Get redemption record list + adminRedemptionGroupRouter.GET("/record/list", adminRedemption.GetRedemptionRecordListHandler(serverCtx)) + } + adminServerGroupRouter := router.Group("/v1/admin/server") adminServerGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) @@ -748,6 +773,14 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { publicPortalGroupRouter.GET("/subscribe", publicPortal.GetSubscriptionHandler(serverCtx)) } + publicRedemptionGroupRouter := router.Group("/v1/public/redemption") + publicRedemptionGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx)) + + { + // Redeem code + publicRedemptionGroupRouter.POST("/", publicRedemption.RedeemCodeHandler(serverCtx)) + } + publicSubscribeGroupRouter := router.Group("/v1/public/subscribe") publicSubscribeGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx)) diff --git a/internal/logic/admin/redemption/batchDeleteRedemptionCodeLogic.go b/internal/logic/admin/redemption/batchDeleteRedemptionCodeLogic.go new file mode 100644 index 0000000..ac4605a --- /dev/null +++ b/internal/logic/admin/redemption/batchDeleteRedemptionCodeLogic.go @@ -0,0 +1,36 @@ +package redemption + +import ( + "context" + + "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 BatchDeleteRedemptionCodeLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Batch delete redemption code +func NewBatchDeleteRedemptionCodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *BatchDeleteRedemptionCodeLogic { + return &BatchDeleteRedemptionCodeLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *BatchDeleteRedemptionCodeLogic) BatchDeleteRedemptionCode(req *types.BatchDeleteRedemptionCodeRequest) error { + err := l.svcCtx.RedemptionCodeModel.BatchDelete(l.ctx, req.Ids) + if err != nil { + l.Errorw("[BatchDeleteRedemptionCode] Database Error", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "batch delete redemption code error: %v", err.Error()) + } + + return nil +} diff --git a/internal/logic/admin/redemption/createRedemptionCodeLogic.go b/internal/logic/admin/redemption/createRedemptionCodeLogic.go new file mode 100644 index 0000000..dfd12e5 --- /dev/null +++ b/internal/logic/admin/redemption/createRedemptionCodeLogic.go @@ -0,0 +1,119 @@ +package redemption + +import ( + "context" + "crypto/rand" + "math/big" + + "github.com/perfect-panel/server/internal/model/redemption" + "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" + "gorm.io/gorm" +) + +type CreateRedemptionCodeLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Create redemption code +func NewCreateRedemptionCodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateRedemptionCodeLogic { + return &CreateRedemptionCodeLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// generateUniqueCode generates a unique redemption code +func (l *CreateRedemptionCodeLogic) generateUniqueCode() (string, error) { + const charset = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789" // Removed confusing characters like I, O, 0, 1 + const codeLength = 16 + + maxRetries := 10 + for i := 0; i < maxRetries; i++ { + code := make([]byte, codeLength) + for j := 0; j < codeLength; j++ { + num, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset)))) + if err != nil { + return "", err + } + code[j] = charset[num.Int64()] + } + + codeStr := string(code) + + // Check if code already exists + _, err := l.svcCtx.RedemptionCodeModel.FindOneByCode(l.ctx, codeStr) + if errors.Is(err, gorm.ErrRecordNotFound) { + return codeStr, nil + } else if err != nil { + return "", err + } + // Code exists, try again + } + + return "", errors.New("failed to generate unique code after maximum retries") +} + +func (l *CreateRedemptionCodeLogic) CreateRedemptionCode(req *types.CreateRedemptionCodeRequest) error { + // Check if subscribe plan is valid + if req.SubscribePlan == 0 { + l.Errorw("[CreateRedemptionCode] Subscribe plan cannot be empty") + return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "subscribe plan cannot be empty") + } + + // Verify subscribe plan exists + _, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, req.SubscribePlan) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + l.Errorw("[CreateRedemptionCode] Subscribe plan not found", logger.Field("subscribe_plan", req.SubscribePlan)) + return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "subscribe plan not found") + } + l.Errorw("[CreateRedemptionCode] Database Error", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find subscribe plan error: %v", err.Error()) + } + + // Validate batch count + if req.BatchCount < 1 { + l.Errorw("[CreateRedemptionCode] Batch count must be at least 1") + return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "batch count must be at least 1") + } + + // Generate redemption codes in batch + var createdCodes []string + for i := int64(0); i < req.BatchCount; i++ { + code, err := l.generateUniqueCode() + if err != nil { + l.Errorw("[CreateRedemptionCode] Failed to generate unique code", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "generate unique code error: %v", err.Error()) + } + + redemptionCode := &redemption.RedemptionCode{ + Code: code, + TotalCount: req.TotalCount, + UsedCount: 0, + SubscribePlan: req.SubscribePlan, + UnitTime: req.UnitTime, + Quantity: req.Quantity, + } + + err = l.svcCtx.RedemptionCodeModel.Insert(l.ctx, redemptionCode) + if err != nil { + l.Errorw("[CreateRedemptionCode] Database Error", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create redemption code error: %v", err.Error()) + } + + createdCodes = append(createdCodes, code) + } + + l.Infow("[CreateRedemptionCode] Successfully created redemption codes", + logger.Field("count", len(createdCodes)), + logger.Field("codes", createdCodes)) + + return nil +} diff --git a/internal/logic/admin/redemption/deleteRedemptionCodeLogic.go b/internal/logic/admin/redemption/deleteRedemptionCodeLogic.go new file mode 100644 index 0000000..4afbf72 --- /dev/null +++ b/internal/logic/admin/redemption/deleteRedemptionCodeLogic.go @@ -0,0 +1,36 @@ +package redemption + +import ( + "context" + + "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 DeleteRedemptionCodeLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Delete redemption code +func NewDeleteRedemptionCodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteRedemptionCodeLogic { + return &DeleteRedemptionCodeLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *DeleteRedemptionCodeLogic) DeleteRedemptionCode(req *types.DeleteRedemptionCodeRequest) error { + err := l.svcCtx.RedemptionCodeModel.Delete(l.ctx, req.Id) + if err != nil { + l.Errorw("[DeleteRedemptionCode] Database Error", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "delete redemption code error: %v", err.Error()) + } + + return nil +} diff --git a/internal/logic/admin/redemption/getRedemptionCodeListLogic.go b/internal/logic/admin/redemption/getRedemptionCodeListLogic.go new file mode 100644 index 0000000..3821e13 --- /dev/null +++ b/internal/logic/admin/redemption/getRedemptionCodeListLogic.go @@ -0,0 +1,61 @@ +package redemption + +import ( + "context" + + "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 GetRedemptionCodeListLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get redemption code list +func NewGetRedemptionCodeListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetRedemptionCodeListLogic { + return &GetRedemptionCodeListLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetRedemptionCodeListLogic) GetRedemptionCodeList(req *types.GetRedemptionCodeListRequest) (resp *types.GetRedemptionCodeListResponse, err error) { + total, list, err := l.svcCtx.RedemptionCodeModel.QueryRedemptionCodeListByPage( + l.ctx, + int(req.Page), + int(req.Size), + req.SubscribePlan, + req.UnitTime, + req.Code, + ) + if err != nil { + l.Errorw("[GetRedemptionCodeList] Database Error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get redemption code list error: %v", err.Error()) + } + + var redemptionCodes []types.RedemptionCode + for _, item := range list { + redemptionCodes = append(redemptionCodes, types.RedemptionCode{ + Id: item.Id, + Code: item.Code, + TotalCount: item.TotalCount, + UsedCount: item.UsedCount, + SubscribePlan: item.SubscribePlan, + UnitTime: item.UnitTime, + Quantity: item.Quantity, + CreatedAt: item.CreatedAt.Unix(), + UpdatedAt: item.UpdatedAt.Unix(), + }) + } + + return &types.GetRedemptionCodeListResponse{ + Total: total, + List: redemptionCodes, + }, nil +} diff --git a/internal/logic/admin/redemption/getRedemptionRecordListLogic.go b/internal/logic/admin/redemption/getRedemptionRecordListLogic.go new file mode 100644 index 0000000..5631fab --- /dev/null +++ b/internal/logic/admin/redemption/getRedemptionRecordListLogic.go @@ -0,0 +1,59 @@ +package redemption + +import ( + "context" + + "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 GetRedemptionRecordListLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get redemption record list +func NewGetRedemptionRecordListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetRedemptionRecordListLogic { + return &GetRedemptionRecordListLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetRedemptionRecordListLogic) GetRedemptionRecordList(req *types.GetRedemptionRecordListRequest) (resp *types.GetRedemptionRecordListResponse, err error) { + total, list, err := l.svcCtx.RedemptionRecordModel.QueryRedemptionRecordListByPage( + l.ctx, + int(req.Page), + int(req.Size), + req.UserId, + req.CodeId, + ) + if err != nil { + l.Errorw("[GetRedemptionRecordList] Database Error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get redemption record list error: %v", err.Error()) + } + + var redemptionRecords []types.RedemptionRecord + for _, item := range list { + redemptionRecords = append(redemptionRecords, types.RedemptionRecord{ + Id: item.Id, + RedemptionCodeId: item.RedemptionCodeId, + UserId: item.UserId, + SubscribeId: item.SubscribeId, + UnitTime: item.UnitTime, + Quantity: item.Quantity, + RedeemedAt: item.RedeemedAt.Unix(), + CreatedAt: item.CreatedAt.Unix(), + }) + } + + return &types.GetRedemptionRecordListResponse{ + Total: total, + List: redemptionRecords, + }, nil +} diff --git a/internal/logic/admin/redemption/updateRedemptionCodeLogic.go b/internal/logic/admin/redemption/updateRedemptionCodeLogic.go new file mode 100644 index 0000000..fab6102 --- /dev/null +++ b/internal/logic/admin/redemption/updateRedemptionCodeLogic.go @@ -0,0 +1,65 @@ +package redemption + +import ( + "context" + + "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 UpdateRedemptionCodeLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Update redemption code +func NewUpdateRedemptionCodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateRedemptionCodeLogic { + return &UpdateRedemptionCodeLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateRedemptionCodeLogic) UpdateRedemptionCode(req *types.UpdateRedemptionCodeRequest) error { + redemptionCode, err := l.svcCtx.RedemptionCodeModel.FindOne(l.ctx, req.Id) + if err != nil { + l.Errorw("[UpdateRedemptionCode] Find Redemption Code Error", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find redemption code error: %v", err.Error()) + } + + // Code is not allowed to be modified + if req.TotalCount != 0 { + // Total count cannot be less than used count + if req.TotalCount < redemptionCode.UsedCount { + l.Errorw("[UpdateRedemptionCode] Total count cannot be less than used count", + logger.Field("total_count", req.TotalCount), + logger.Field("used_count", redemptionCode.UsedCount)) + return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), + "total count cannot be less than used count: total_count=%d, used_count=%d", + req.TotalCount, redemptionCode.UsedCount) + } + redemptionCode.TotalCount = req.TotalCount + } + if req.SubscribePlan != 0 { + redemptionCode.SubscribePlan = req.SubscribePlan + } + if req.UnitTime != "" { + redemptionCode.UnitTime = req.UnitTime + } + if req.Quantity != 0 { + redemptionCode.Quantity = req.Quantity + } + + err = l.svcCtx.RedemptionCodeModel.Update(l.ctx, redemptionCode) + if err != nil { + l.Errorw("[UpdateRedemptionCode] Database Error", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update redemption code error: %v", err.Error()) + } + + return nil +} diff --git a/internal/logic/public/redemption/redeemCodeLogic.go b/internal/logic/public/redemption/redeemCodeLogic.go new file mode 100644 index 0000000..ca849d8 --- /dev/null +++ b/internal/logic/public/redemption/redeemCodeLogic.go @@ -0,0 +1,254 @@ +package redemption + +import ( + "context" + "fmt" + "time" + + "github.com/google/uuid" + "github.com/perfect-panel/server/internal/model/redemption" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/pkg/constant" + "github.com/perfect-panel/server/pkg/snowflake" + "github.com/perfect-panel/server/pkg/uuidx" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" + + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" +) + +type RedeemCodeLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Redeem code +func NewRedeemCodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RedeemCodeLogic { + return &RedeemCodeLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *RedeemCodeLogic) RedeemCode(req *types.RedeemCodeRequest) (resp *types.RedeemCodeResponse, err error) { + // Get user from context + u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) + if !ok { + logger.Error("current user is not found in context") + return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") + } + + // Find redemption code by code + redemptionCode, err := l.svcCtx.RedemptionCodeModel.FindOneByCode(l.ctx, req.Code) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + l.Errorw("[RedeemCode] Redemption code not found", logger.Field("code", req.Code)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "redemption code not found") + } + l.Errorw("[RedeemCode] Database Error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find redemption code error: %v", err.Error()) + } + + // Check if redemption code has remaining count + if redemptionCode.TotalCount > 0 && redemptionCode.UsedCount >= redemptionCode.TotalCount { + l.Errorw("[RedeemCode] Redemption code has been fully used", + logger.Field("code", req.Code), + logger.Field("total_count", redemptionCode.TotalCount), + logger.Field("used_count", redemptionCode.UsedCount)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "redemption code has been fully used") + } + + // Check if user has already redeemed this code + userRecords, err := l.svcCtx.RedemptionRecordModel.FindByUserId(l.ctx, u.Id) + if err != nil { + l.Errorw("[RedeemCode] Database Error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find redemption records error: %v", err.Error()) + } + for _, record := range userRecords { + if record.RedemptionCodeId == redemptionCode.Id { + l.Errorw("[RedeemCode] User has already redeemed this code", + logger.Field("user_id", u.Id), + logger.Field("code", req.Code)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "you have already redeemed this code") + } + } + + // Find subscribe plan from redemption code + subscribePlan, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, redemptionCode.SubscribePlan) + if err != nil { + l.Errorw("[RedeemCode] Subscribe plan not found", + logger.Field("subscribe_plan", redemptionCode.SubscribePlan), + logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "subscribe plan not found") + } + + // Check if subscribe plan is available + if !*subscribePlan.Sell { + l.Errorw("[RedeemCode] Subscribe plan is not available", + logger.Field("subscribe_plan", redemptionCode.SubscribePlan)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SubscribeNotAvailable), "subscribe plan is not available") + } + + // Start transaction + err = l.svcCtx.RedemptionCodeModel.Transaction(l.ctx, func(tx *gorm.DB) error { + // Find user's existing subscribe for this plan + var existingSubscribe *user.SubscribeDetails + userSubscribes, err := l.svcCtx.UserModel.QueryUserSubscribe(l.ctx, u.Id, 0, 1) + if err == nil { + for _, us := range userSubscribes { + if us.SubscribeId == redemptionCode.SubscribePlan { + existingSubscribe = us + break + } + } + } + + now := time.Now() + + if existingSubscribe != nil { + // Extend existing subscribe + var newExpireTime time.Time + if existingSubscribe.ExpireTime.After(now) { + newExpireTime = existingSubscribe.ExpireTime + } else { + newExpireTime = now + } + + // Calculate duration based on redemption code + duration, err := calculateDuration(redemptionCode.UnitTime, redemptionCode.Quantity) + if err != nil { + l.Errorw("[RedeemCode] Calculate duration error", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "calculate duration error: %v", err.Error()) + } + newExpireTime = newExpireTime.Add(duration) + + // Update subscribe + existingSubscribe.ExpireTime = newExpireTime + existingSubscribe.Status = 1 + + // Add traffic if needed + if subscribePlan.Traffic > 0 { + existingSubscribe.Traffic = subscribePlan.Traffic * 1024 * 1024 * 1024 + existingSubscribe.Download = 0 + existingSubscribe.Upload = 0 + } + + err = l.svcCtx.UserModel.UpdateSubscribe(l.ctx, &user.Subscribe{ + Id: existingSubscribe.Id, + UserId: existingSubscribe.UserId, + ExpireTime: existingSubscribe.ExpireTime, + Status: existingSubscribe.Status, + Traffic: existingSubscribe.Traffic, + Download: existingSubscribe.Download, + Upload: existingSubscribe.Upload, + }, tx) + if err != nil { + l.Errorw("[RedeemCode] Update subscribe error", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update subscribe error: %v", err.Error()) + } + } else { + // Create new subscribe + expireTime, traffic, err := calculateSubscribeTimeAndTraffic(redemptionCode.UnitTime, redemptionCode.Quantity, subscribePlan.Traffic) + if err != nil { + l.Errorw("[RedeemCode] Calculate subscribe time and traffic error", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "calculate subscribe time and traffic error: %v", err.Error()) + } + + newSubscribe := &user.Subscribe{ + Id: snowflake.GetID(), + UserId: u.Id, + OrderId: 0, + SubscribeId: redemptionCode.SubscribePlan, + StartTime: now, + ExpireTime: expireTime, + FinishedAt: nil, + Traffic: traffic, + Download: 0, + Upload: 0, + Token: uuidx.SubscribeToken(fmt.Sprintf("redemption:%d:%d", u.Id, time.Now().UnixMilli())), + UUID: uuid.New().String(), + Status: 1, + } + + err = l.svcCtx.UserModel.InsertSubscribe(l.ctx, newSubscribe, tx) + if err != nil { + l.Errorw("[RedeemCode] Insert subscribe error", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "insert subscribe error: %v", err.Error()) + } + } + + // Increment redemption code used count + err = l.svcCtx.RedemptionCodeModel.IncrementUsedCount(l.ctx, redemptionCode.Id) + if err != nil { + l.Errorw("[RedeemCode] Increment used count error", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "increment used count error: %v", err.Error()) + } + + // Create redemption record + redemptionRecord := &redemption.RedemptionRecord{ + Id: snowflake.GetID(), + RedemptionCodeId: redemptionCode.Id, + UserId: u.Id, + SubscribeId: redemptionCode.SubscribePlan, + UnitTime: redemptionCode.UnitTime, + Quantity: redemptionCode.Quantity, + RedeemedAt: now, + CreatedAt: now, + } + + err = l.svcCtx.RedemptionRecordModel.Insert(l.ctx, redemptionRecord) + if err != nil { + l.Errorw("[RedeemCode] Insert redemption record error", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "insert redemption record error: %v", err.Error()) + } + + return nil + }) + + if err != nil { + return nil, err + } + + return &types.RedeemCodeResponse{ + Message: "Redemption successful", + }, nil +} + +// calculateDuration calculates time duration based on unit time +func calculateDuration(unitTime string, quantity int64) (time.Duration, error) { + switch unitTime { + case "month": + return time.Duration(quantity*30*24) * time.Hour, nil + case "quarter": + return time.Duration(quantity*90*24) * time.Hour, nil + case "half_year": + return time.Duration(quantity*180*24) * time.Hour, nil + case "year": + return time.Duration(quantity*365*24) * time.Hour, nil + case "day": + return time.Duration(quantity*24) * time.Hour, nil + default: + return time.Duration(quantity*30*24) * time.Hour, nil + } +} + +// calculateSubscribeTimeAndTraffic calculates expire time and traffic based on subscribe plan +func calculateSubscribeTimeAndTraffic(unitTime string, quantity int64, traffic int64) (time.Time, int64, error) { + duration, err := calculateDuration(unitTime, quantity) + if err != nil { + return time.Time{}, 0, err + } + + expireTime := time.Now().Add(duration) + trafficBytes := int64(0) + if traffic > 0 { + trafficBytes = traffic * 1024 * 1024 * 1024 + } + + return expireTime, trafficBytes, nil +} diff --git a/internal/model/redemption/default.go b/internal/model/redemption/default.go new file mode 100644 index 0000000..3d8328b --- /dev/null +++ b/internal/model/redemption/default.go @@ -0,0 +1,279 @@ +package redemption + +import ( + "context" + "errors" + "fmt" + + "github.com/perfect-panel/server/pkg/cache" + "github.com/redis/go-redis/v9" + "gorm.io/gorm" +) + +var _ RedemptionCodeModel = (*customRedemptionCodeModel)(nil) +var _ RedemptionRecordModel = (*customRedemptionRecordModel)(nil) + +var ( + cacheRedemptionCodeIdPrefix = "cache:redemption_code:id:" + cacheRedemptionCodeCodePrefix = "cache:redemption_code:code:" + cacheRedemptionRecordIdPrefix = "cache:redemption_record:id:" +) + +type ( + RedemptionCodeModel interface { + Insert(ctx context.Context, data *RedemptionCode) error + FindOne(ctx context.Context, id int64) (*RedemptionCode, error) + FindOneByCode(ctx context.Context, code string) (*RedemptionCode, error) + Update(ctx context.Context, data *RedemptionCode) error + Delete(ctx context.Context, id int64) error + Transaction(ctx context.Context, fn func(db *gorm.DB) error) error + customRedemptionCodeLogicModel + } + + RedemptionRecordModel interface { + Insert(ctx context.Context, data *RedemptionRecord) error + FindOne(ctx context.Context, id int64) (*RedemptionRecord, error) + Update(ctx context.Context, data *RedemptionRecord) error + Delete(ctx context.Context, id int64) error + customRedemptionRecordLogicModel + } + + customRedemptionCodeLogicModel interface { + QueryRedemptionCodeListByPage(ctx context.Context, page, size int, subscribePlan int64, unitTime string, code string) (total int64, list []*RedemptionCode, err error) + BatchDelete(ctx context.Context, ids []int64) error + IncrementUsedCount(ctx context.Context, id int64) error + } + + customRedemptionRecordLogicModel interface { + QueryRedemptionRecordListByPage(ctx context.Context, page, size int, userId int64, codeId int64) (total int64, list []*RedemptionRecord, err error) + FindByUserId(ctx context.Context, userId int64) ([]*RedemptionRecord, error) + FindByCodeId(ctx context.Context, codeId int64) ([]*RedemptionRecord, error) + } + + customRedemptionCodeModel struct { + *defaultRedemptionCodeModel + } + defaultRedemptionCodeModel struct { + cache.CachedConn + table string + } + + customRedemptionRecordModel struct { + *defaultRedemptionRecordModel + } + defaultRedemptionRecordModel struct { + cache.CachedConn + table string + } +) + +func newRedemptionCodeModel(db *gorm.DB, c *redis.Client) *defaultRedemptionCodeModel { + return &defaultRedemptionCodeModel{ + CachedConn: cache.NewConn(db, c), + table: "`redemption_code`", + } +} + +func newRedemptionRecordModel(db *gorm.DB, c *redis.Client) *defaultRedemptionRecordModel { + return &defaultRedemptionRecordModel{ + CachedConn: cache.NewConn(db, c), + table: "`redemption_record`", + } +} + +// RedemptionCode cache methods +func (m *defaultRedemptionCodeModel) getCacheKeys(data *RedemptionCode) []string { + if data == nil { + return []string{} + } + codeIdKey := fmt.Sprintf("%s%v", cacheRedemptionCodeIdPrefix, data.Id) + codeCodeKey := fmt.Sprintf("%s%v", cacheRedemptionCodeCodePrefix, data.Code) + cacheKeys := []string{ + codeIdKey, + codeCodeKey, + } + return cacheKeys +} + +func (m *defaultRedemptionCodeModel) Insert(ctx context.Context, data *RedemptionCode) error { + err := m.ExecCtx(ctx, func(conn *gorm.DB) error { + return conn.Create(data).Error + }, m.getCacheKeys(data)...) + return err +} + +func (m *defaultRedemptionCodeModel) FindOne(ctx context.Context, id int64) (*RedemptionCode, error) { + codeIdKey := fmt.Sprintf("%s%v", cacheRedemptionCodeIdPrefix, id) + var resp RedemptionCode + err := m.QueryCtx(ctx, &resp, codeIdKey, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&RedemptionCode{}).Where("`id` = ?", id).First(&resp).Error + }) + switch { + case err == nil: + return &resp, nil + default: + return nil, err + } +} + +func (m *defaultRedemptionCodeModel) FindOneByCode(ctx context.Context, code string) (*RedemptionCode, error) { + codeCodeKey := fmt.Sprintf("%s%v", cacheRedemptionCodeCodePrefix, code) + var resp RedemptionCode + err := m.QueryCtx(ctx, &resp, codeCodeKey, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&RedemptionCode{}).Where("`code` = ?", code).First(&resp).Error + }) + switch { + case err == nil: + return &resp, nil + default: + return nil, err + } +} + +func (m *defaultRedemptionCodeModel) Update(ctx context.Context, data *RedemptionCode) error { + old, err := m.FindOne(ctx, data.Id) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + err = m.ExecCtx(ctx, func(conn *gorm.DB) error { + db := conn + return db.Save(data).Error + }, m.getCacheKeys(old)...) + return err +} + +func (m *defaultRedemptionCodeModel) Delete(ctx context.Context, id int64) error { + data, err := m.FindOne(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil + } + return err + } + err = m.ExecCtx(ctx, func(conn *gorm.DB) error { + db := conn + return db.Delete(&RedemptionCode{}, id).Error + }, m.getCacheKeys(data)...) + return err +} + +func (m *defaultRedemptionCodeModel) Transaction(ctx context.Context, fn func(db *gorm.DB) error) error { + return m.TransactCtx(ctx, fn) +} + +// RedemptionCode custom logic methods +func (m *customRedemptionCodeModel) QueryRedemptionCodeListByPage(ctx context.Context, page, size int, subscribePlan int64, unitTime string, code string) (total int64, list []*RedemptionCode, err error) { + err = m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error { + db := conn.Model(&RedemptionCode{}) + if subscribePlan != 0 { + db = db.Where("subscribe_plan = ?", subscribePlan) + } + if unitTime != "" { + db = db.Where("unit_time = ?", unitTime) + } + if code != "" { + db = db.Where("code like ?", "%"+code+"%") + } + return db.Count(&total).Limit(size).Offset((page - 1) * size).Order("created_at DESC").Find(v).Error + }) + return total, list, err +} + +func (m *customRedemptionCodeModel) BatchDelete(ctx context.Context, ids []int64) error { + var err error + for _, id := range ids { + if err = m.Delete(ctx, id); err != nil { + return err + } + } + return nil +} + +func (m *customRedemptionCodeModel) IncrementUsedCount(ctx context.Context, id int64) error { + data, err := m.FindOne(ctx, id) + if err != nil { + return err + } + data.UsedCount++ + return m.Update(ctx, data) +} + +// RedemptionRecord cache methods +func (m *defaultRedemptionRecordModel) getCacheKeys(data *RedemptionRecord) []string { + if data == nil { + return []string{} + } + recordIdKey := fmt.Sprintf("%s%v", cacheRedemptionRecordIdPrefix, data.Id) + cacheKeys := []string{ + recordIdKey, + } + return cacheKeys +} + +func (m *defaultRedemptionRecordModel) Insert(ctx context.Context, data *RedemptionRecord) error { + err := m.ExecCtx(ctx, func(conn *gorm.DB) error { + return conn.Create(data).Error + }, m.getCacheKeys(data)...) + return err +} + +func (m *defaultRedemptionRecordModel) FindOne(ctx context.Context, id int64) (*RedemptionRecord, error) { + recordIdKey := fmt.Sprintf("%s%v", cacheRedemptionRecordIdPrefix, id) + var resp RedemptionRecord + err := m.QueryCtx(ctx, &resp, recordIdKey, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&RedemptionRecord{}).Where("`id` = ?", id).First(&resp).Error + }) + switch { + case err == nil: + return &resp, nil + default: + return nil, err + } +} + +func (m *defaultRedemptionRecordModel) Update(ctx context.Context, data *RedemptionRecord) error { + err := m.ExecCtx(ctx, func(conn *gorm.DB) error { + db := conn + return db.Save(data).Error + }, m.getCacheKeys(data)...) + return err +} + +func (m *defaultRedemptionRecordModel) Delete(ctx context.Context, id int64) error { + err := m.ExecCtx(ctx, func(conn *gorm.DB) error { + db := conn + return db.Delete(&RedemptionRecord{}, id).Error + }, m.getCacheKeys(nil)...) + return err +} + +// RedemptionRecord custom logic methods +func (m *customRedemptionRecordModel) QueryRedemptionRecordListByPage(ctx context.Context, page, size int, userId int64, codeId int64) (total int64, list []*RedemptionRecord, err error) { + err = m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error { + db := conn.Model(&RedemptionRecord{}) + if userId != 0 { + db = db.Where("user_id = ?", userId) + } + if codeId != 0 { + db = db.Where("redemption_code_id = ?", codeId) + } + return db.Count(&total).Limit(size).Offset((page - 1) * size).Order("created_at DESC").Find(v).Error + }) + return total, list, err +} + +func (m *customRedemptionRecordModel) FindByUserId(ctx context.Context, userId int64) ([]*RedemptionRecord, error) { + var list []*RedemptionRecord + err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&RedemptionRecord{}).Where("user_id = ?", userId).Order("created_at DESC").Find(v).Error + }) + return list, err +} + +func (m *customRedemptionRecordModel) FindByCodeId(ctx context.Context, codeId int64) ([]*RedemptionRecord, error) { + var list []*RedemptionRecord + err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&RedemptionRecord{}).Where("redemption_code_id = ?", codeId).Order("created_at DESC").Find(v).Error + }) + return list, err +} diff --git a/internal/model/redemption/model.go b/internal/model/redemption/model.go new file mode 100644 index 0000000..ef7c6fe --- /dev/null +++ b/internal/model/redemption/model.go @@ -0,0 +1,20 @@ +package redemption + +import ( + "github.com/redis/go-redis/v9" + "gorm.io/gorm" +) + +// NewRedemptionCodeModel returns a model for the redemption_code table. +func NewRedemptionCodeModel(conn *gorm.DB, c *redis.Client) RedemptionCodeModel { + return &customRedemptionCodeModel{ + defaultRedemptionCodeModel: newRedemptionCodeModel(conn, c), + } +} + +// NewRedemptionRecordModel returns a model for the redemption_record table. +func NewRedemptionRecordModel(conn *gorm.DB, c *redis.Client) RedemptionRecordModel { + return &customRedemptionRecordModel{ + defaultRedemptionRecordModel: newRedemptionRecordModel(conn, c), + } +} diff --git a/internal/model/redemption/redemption.go b/internal/model/redemption/redemption.go new file mode 100644 index 0000000..1275496 --- /dev/null +++ b/internal/model/redemption/redemption.go @@ -0,0 +1,39 @@ +package redemption + +import ( + "time" + + "gorm.io/gorm" +) + +type RedemptionCode struct { + Id int64 `gorm:"primaryKey"` + Code string `gorm:"type:varchar(255);not null;unique;comment:Redemption Code"` + TotalCount int64 `gorm:"type:int;not null;default:0;comment:Total Redemption Count"` + UsedCount int64 `gorm:"type:int;not null;default:0;comment:Used Redemption Count"` + SubscribePlan int64 `gorm:"type:bigint;not null;default:0;comment:Subscribe Plan"` + UnitTime string `gorm:"type:varchar(50);not null;default:'month';comment:Unit Time: day, month, quarter, half_year, year"` + Quantity int64 `gorm:"type:int;not null;default:1;comment:Quantity"` + CreatedAt time.Time `gorm:"<-:create;comment:Create Time"` + UpdatedAt time.Time `gorm:"comment:Update Time"` + DeletedAt gorm.DeletedAt `gorm:"index;comment:Delete Time"` +} + +type RedemptionRecord struct { + Id int64 `gorm:"primaryKey"` + RedemptionCodeId int64 `gorm:"type:bigint;not null;default:0;comment:Redemption Code Id;index"` + UserId int64 `gorm:"type:bigint;not null;default:0;comment:User Id;index"` + SubscribeId int64 `gorm:"type:bigint;not null;default:0;comment:Subscribe Id"` + UnitTime string `gorm:"type:varchar(50);not null;default:'month';comment:Unit Time"` + Quantity int64 `gorm:"type:int;not null;default:1;comment:Quantity"` + RedeemedAt time.Time `gorm:"<-:create;comment:Redeemed Time"` + CreatedAt time.Time `gorm:"<-:create;comment:Create Time"` +} + +func (RedemptionCode) TableName() string { + return "redemption_code" +} + +func (RedemptionRecord) TableName() string { + return "redemption_record" +} diff --git a/internal/svc/serviceContext.go b/internal/svc/serviceContext.go index aa79ccc..5650b4b 100644 --- a/internal/svc/serviceContext.go +++ b/internal/svc/serviceContext.go @@ -5,6 +5,7 @@ import ( "github.com/perfect-panel/server/internal/model/client" "github.com/perfect-panel/server/internal/model/node" + "github.com/perfect-panel/server/internal/model/redemption" "github.com/perfect-panel/server/pkg/device" "github.com/perfect-panel/server/internal/config" @@ -49,13 +50,15 @@ type ServiceContext struct { ClientModel client.Model TicketModel ticket.Model //ServerModel server.Model - SystemModel system.Model - CouponModel coupon.Model - PaymentModel payment.Model - DocumentModel document.Model - SubscribeModel subscribe.Model - TrafficLogModel traffic.Model - AnnouncementModel announcement.Model + SystemModel system.Model + CouponModel coupon.Model + RedemptionCodeModel redemption.RedemptionCodeModel + RedemptionRecordModel redemption.RedemptionRecordModel + PaymentModel payment.Model + DocumentModel document.Model + SubscribeModel subscribe.Model + TrafficLogModel traffic.Model + AnnouncementModel announcement.Model Restart func() error TelegramBot *tgbotapi.BotAPI @@ -110,13 +113,15 @@ func NewServiceContext(c config.Config) *ServiceContext { ClientModel: client.NewSubscribeApplicationModel(db), TicketModel: ticket.NewModel(db, rds), //ServerModel: server.NewModel(db, rds), - SystemModel: system.NewModel(db, rds), - CouponModel: coupon.NewModel(db, rds), - PaymentModel: payment.NewModel(db, rds), - DocumentModel: document.NewModel(db, rds), - SubscribeModel: subscribe.NewModel(db, rds), - TrafficLogModel: traffic.NewModel(db), - AnnouncementModel: announcement.NewModel(db, rds), + SystemModel: system.NewModel(db, rds), + CouponModel: coupon.NewModel(db, rds), + RedemptionCodeModel: redemption.NewRedemptionCodeModel(db, rds), + RedemptionRecordModel: redemption.NewRedemptionRecordModel(db, rds), + PaymentModel: payment.NewModel(db, rds), + DocumentModel: document.NewModel(db, rds), + SubscribeModel: subscribe.NewModel(db, rds), + TrafficLogModel: traffic.NewModel(db), + AnnouncementModel: announcement.NewModel(db, rds), } srv.DeviceManager = NewDeviceManager(srv) return srv diff --git a/internal/types/types.go b/internal/types/types.go index 078a2fa..51f883a 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -146,6 +146,10 @@ type BatchDeleteDocumentRequest struct { Ids []int64 `json:"ids" validate:"required"` } +type BatchDeleteRedemptionCodeRequest struct { + Ids []int64 `json:"ids" validate:"required"` +} + type BatchDeleteSubscribeGroupRequest struct { Ids []int64 `json:"ids" validate:"required"` } @@ -367,6 +371,14 @@ type CreateQuotaTaskRequest struct { GiftValue uint64 `json:"gift_value"` } +type CreateRedemptionCodeRequest struct { + TotalCount int64 `json:"total_count" validate:"required"` + SubscribePlan int64 `json:"subscribe_plan" validate:"required"` + UnitTime string `json:"unit_time" validate:"required,oneof=day month quarter half_year year"` + Quantity int64 `json:"quantity" validate:"required"` + BatchCount int64 `json:"batch_count" validate:"required,min=1"` +} + type CreateServerRequest struct { Name string `json:"name"` Country string `json:"country,omitempty"` @@ -501,6 +513,10 @@ type DeletePaymentMethodRequest struct { Id int64 `json:"id" validate:"required"` } +type DeleteRedemptionCodeRequest struct { + Id int64 `json:"id" validate:"required"` +} + type DeleteServerRequest struct { Id int64 `json:"id"` } @@ -934,6 +950,31 @@ type GetPreSendEmailCountResponse struct { Count int64 `json:"count"` } +type GetRedemptionCodeListRequest struct { + Page int64 `form:"page" validate:"required"` + Size int64 `form:"size" validate:"required"` + SubscribePlan int64 `form:"subscribe_plan,omitempty"` + UnitTime string `form:"unit_time,omitempty"` + Code string `form:"code,omitempty"` +} + +type GetRedemptionCodeListResponse struct { + Total int64 `json:"total"` + List []RedemptionCode `json:"list"` +} + +type GetRedemptionRecordListRequest struct { + Page int64 `form:"page" validate:"required"` + Size int64 `form:"size" validate:"required"` + UserId int64 `form:"user_id,omitempty"` + CodeId int64 `form:"code_id,omitempty"` +} + +type GetRedemptionRecordListResponse struct { + Total int64 `json:"total"` + List []RedemptionRecord `json:"list"` +} + type GetServerConfigRequest struct { ServerCommon } @@ -1779,6 +1820,37 @@ type RechargeOrderResponse struct { OrderNo string `json:"order_no"` } +type RedeemCodeRequest struct { + Code string `json:"code" validate:"required"` +} + +type RedeemCodeResponse struct { + Message string `json:"message"` +} + +type RedemptionCode struct { + Id int64 `json:"id"` + Code string `json:"code"` + TotalCount int64 `json:"total_count"` + UsedCount int64 `json:"used_count"` + SubscribePlan int64 `json:"subscribe_plan"` + UnitTime string `json:"unit_time"` + Quantity int64 `json:"quantity"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` +} + +type RedemptionRecord struct { + Id int64 `json:"id"` + RedemptionCodeId int64 `json:"redemption_code_id"` + UserId int64 `json:"user_id"` + SubscribeId int64 `json:"subscribe_id"` + UnitTime string `json:"unit_time"` + Quantity int64 `json:"quantity"` + RedeemedAt int64 `json:"redeemed_at"` + CreatedAt int64 `json:"created_at"` +} + type RegisterConfig struct { StopRegister bool `json:"stop_register"` EnableTrial bool `json:"enable_trial"` @@ -2416,6 +2488,14 @@ type UpdatePaymentMethodRequest struct { Enable *bool `json:"enable" validate:"required"` } +type UpdateRedemptionCodeRequest struct { + Id int64 `json:"id" validate:"required"` + TotalCount int64 `json:"total_count,omitempty"` + SubscribePlan int64 `json:"subscribe_plan,omitempty"` + UnitTime string `json:"unit_time,omitempty" validate:"omitempty,oneof=day month quarter half_year year"` + Quantity int64 `json:"quantity,omitempty"` +} + type UpdateServerRequest struct { Id int64 `json:"id"` Name string `json:"name"` diff --git a/ppanel.api b/ppanel.api index 10c83c2..3e6f0d9 100644 --- a/ppanel.api +++ b/ppanel.api @@ -19,6 +19,7 @@ import ( "apis/admin/subscribe.api" "apis/admin/payment.api" "apis/admin/coupon.api" + "apis/admin/redemption.api" "apis/admin/order.api" "apis/admin/ticket.api" "apis/admin/announcement.api" @@ -31,6 +32,7 @@ import ( "apis/admin/application.api" "apis/public/user.api" "apis/public/subscribe.api" + "apis/public/redemption.api" "apis/public/order.api" "apis/public/announcement.api" "apis/public/ticket.api"