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"`
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"`

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"
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))

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/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

View File

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

View File

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