feat: Redemption Code

This commit is contained in:
EUForest 2026-01-05 17:53:31 +08:00
parent 5beff61e91
commit 518595b058
26 changed files with 1459 additions and 14 deletions

86
apis/admin/redemption.api Normal file
View File

@ -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)
}

View File

@ -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)
}

View File

@ -448,6 +448,27 @@ type (
CreatedAt int64 `json:"created_at"` CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_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 { Announcement {
Id int64 `json:"id"` Id int64 `json:"id"`
Title string `json:"title"` Title string `json:"title"`

View File

@ -0,0 +1,5 @@
-- Drop redemption_record table
DROP TABLE IF EXISTS `redemption_record`;
-- Drop redemption_code table
DROP TABLE IF EXISTS `redemption_code`;

View File

@ -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';

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -16,6 +16,7 @@ import (
adminMarketing "github.com/perfect-panel/server/internal/handler/admin/marketing" adminMarketing "github.com/perfect-panel/server/internal/handler/admin/marketing"
adminOrder "github.com/perfect-panel/server/internal/handler/admin/order" adminOrder "github.com/perfect-panel/server/internal/handler/admin/order"
adminPayment "github.com/perfect-panel/server/internal/handler/admin/payment" 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" adminServer "github.com/perfect-panel/server/internal/handler/admin/server"
adminSubscribe "github.com/perfect-panel/server/internal/handler/admin/subscribe" adminSubscribe "github.com/perfect-panel/server/internal/handler/admin/subscribe"
adminSystem "github.com/perfect-panel/server/internal/handler/admin/system" 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" publicOrder "github.com/perfect-panel/server/internal/handler/public/order"
publicPayment "github.com/perfect-panel/server/internal/handler/public/payment" publicPayment "github.com/perfect-panel/server/internal/handler/public/payment"
publicPortal "github.com/perfect-panel/server/internal/handler/public/portal" 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" publicSubscribe "github.com/perfect-panel/server/internal/handler/public/subscribe"
publicTicket "github.com/perfect-panel/server/internal/handler/public/ticket" publicTicket "github.com/perfect-panel/server/internal/handler/public/ticket"
publicUser "github.com/perfect-panel/server/internal/handler/public/user" 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)) 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 := router.Group("/v1/admin/server")
adminServerGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) adminServerGroupRouter.Use(middleware.AuthMiddleware(serverCtx))
@ -748,6 +773,14 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
publicPortalGroupRouter.GET("/subscribe", publicPortal.GetSubscriptionHandler(serverCtx)) 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 := router.Group("/v1/public/subscribe")
publicSubscribeGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx)) publicSubscribeGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx))

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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),
}
}

View File

@ -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"
}

View File

@ -5,6 +5,7 @@ import (
"github.com/perfect-panel/server/internal/model/client" "github.com/perfect-panel/server/internal/model/client"
"github.com/perfect-panel/server/internal/model/node" "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/pkg/device"
"github.com/perfect-panel/server/internal/config" "github.com/perfect-panel/server/internal/config"
@ -51,6 +52,8 @@ type ServiceContext struct {
//ServerModel server.Model //ServerModel server.Model
SystemModel system.Model SystemModel system.Model
CouponModel coupon.Model CouponModel coupon.Model
RedemptionCodeModel redemption.RedemptionCodeModel
RedemptionRecordModel redemption.RedemptionRecordModel
PaymentModel payment.Model PaymentModel payment.Model
DocumentModel document.Model DocumentModel document.Model
SubscribeModel subscribe.Model SubscribeModel subscribe.Model
@ -112,6 +115,8 @@ func NewServiceContext(c config.Config) *ServiceContext {
//ServerModel: server.NewModel(db, rds), //ServerModel: server.NewModel(db, rds),
SystemModel: system.NewModel(db, rds), SystemModel: system.NewModel(db, rds),
CouponModel: coupon.NewModel(db, rds), CouponModel: coupon.NewModel(db, rds),
RedemptionCodeModel: redemption.NewRedemptionCodeModel(db, rds),
RedemptionRecordModel: redemption.NewRedemptionRecordModel(db, rds),
PaymentModel: payment.NewModel(db, rds), PaymentModel: payment.NewModel(db, rds),
DocumentModel: document.NewModel(db, rds), DocumentModel: document.NewModel(db, rds),
SubscribeModel: subscribe.NewModel(db, rds), SubscribeModel: subscribe.NewModel(db, rds),

View File

@ -146,6 +146,10 @@ type BatchDeleteDocumentRequest struct {
Ids []int64 `json:"ids" validate:"required"` Ids []int64 `json:"ids" validate:"required"`
} }
type BatchDeleteRedemptionCodeRequest struct {
Ids []int64 `json:"ids" validate:"required"`
}
type BatchDeleteSubscribeGroupRequest struct { type BatchDeleteSubscribeGroupRequest struct {
Ids []int64 `json:"ids" validate:"required"` Ids []int64 `json:"ids" validate:"required"`
} }
@ -367,6 +371,14 @@ type CreateQuotaTaskRequest struct {
GiftValue uint64 `json:"gift_value"` 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 { type CreateServerRequest struct {
Name string `json:"name"` Name string `json:"name"`
Country string `json:"country,omitempty"` Country string `json:"country,omitempty"`
@ -501,6 +513,10 @@ type DeletePaymentMethodRequest struct {
Id int64 `json:"id" validate:"required"` Id int64 `json:"id" validate:"required"`
} }
type DeleteRedemptionCodeRequest struct {
Id int64 `json:"id" validate:"required"`
}
type DeleteServerRequest struct { type DeleteServerRequest struct {
Id int64 `json:"id"` Id int64 `json:"id"`
} }
@ -934,6 +950,31 @@ type GetPreSendEmailCountResponse struct {
Count int64 `json:"count"` 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 { type GetServerConfigRequest struct {
ServerCommon ServerCommon
} }
@ -1779,6 +1820,37 @@ type RechargeOrderResponse struct {
OrderNo string `json:"order_no"` 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 { type RegisterConfig struct {
StopRegister bool `json:"stop_register"` StopRegister bool `json:"stop_register"`
EnableTrial bool `json:"enable_trial"` EnableTrial bool `json:"enable_trial"`
@ -2416,6 +2488,14 @@ type UpdatePaymentMethodRequest struct {
Enable *bool `json:"enable" validate:"required"` 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 { type UpdateServerRequest struct {
Id int64 `json:"id"` Id int64 `json:"id"`
Name string `json:"name"` Name string `json:"name"`

View File

@ -19,6 +19,7 @@ import (
"apis/admin/subscribe.api" "apis/admin/subscribe.api"
"apis/admin/payment.api" "apis/admin/payment.api"
"apis/admin/coupon.api" "apis/admin/coupon.api"
"apis/admin/redemption.api"
"apis/admin/order.api" "apis/admin/order.api"
"apis/admin/ticket.api" "apis/admin/ticket.api"
"apis/admin/announcement.api" "apis/admin/announcement.api"
@ -31,6 +32,7 @@ import (
"apis/admin/application.api" "apis/admin/application.api"
"apis/public/user.api" "apis/public/user.api"
"apis/public/subscribe.api" "apis/public/subscribe.api"
"apis/public/redemption.api"
"apis/public/order.api" "apis/public/order.api"
"apis/public/announcement.api" "apis/public/announcement.api"
"apis/public/ticket.api" "apis/public/ticket.api"