bindbox-game/internal/api/user/game_passes_app.go

281 lines
8.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package app
import (
"fmt"
"net/http"
"time"
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/validation"
"bindbox-game/internal/repository/mysql/model"
)
// ==================== 用户次数卡 API ====================
type getGamePassesRequest struct {
ActivityID *int64 `form:"activity_id"`
}
type userGamePassItem struct {
ID int64 `json:"id"`
ActivityID int64 `json:"activity_id"`
ActivityName string `json:"activity_name"`
Remaining int32 `json:"remaining"`
ExpiredAt string `json:"expired_at"`
Source string `json:"source"`
}
type getGamePassesResponse struct {
TotalRemaining int32 `json:"total_remaining"`
GlobalRemaining int32 `json:"global_remaining"` // 全局通用次数
Passes []userGamePassItem `json:"passes"`
}
// GetGamePasses 获取用户可用的游戏次数卡
// @Summary 获取用户次数卡
// @Description 查询当前用户可用的游戏次数卡
// @Tags APP端.用户
// @Accept json
// @Produce json
// @Security LoginVerifyToken
// @Param activity_id query integer false "活动ID不传返回所有可用次数卡"
// @Success 200 {object} getGamePassesResponse
// @Failure 400 {object} code.Failure
// @Router /api/app/game-passes/available [get]
func (h *handler) GetGamePasses() core.HandlerFunc {
return func(ctx core.Context) {
req := new(getGamePassesRequest)
res := new(getGamePassesResponse)
if err := ctx.ShouldBindForm(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
userID := int64(ctx.SessionUserInfo().Id)
now := time.Now()
// 查询用户的次数卡已过滤过期和剩余为0的
q := h.readDB.UserGamePasses.WithContext(ctx.RequestContext()).
Where(h.readDB.UserGamePasses.UserID.Eq(userID)).
Where(h.readDB.UserGamePasses.Remaining.Gt(0))
passes, err := q.Find()
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ServerError, err.Error()))
return
}
// 获取活动名称
activityIDs := make([]int64, 0)
for _, p := range passes {
if p.ActivityID > 0 {
activityIDs = append(activityIDs, p.ActivityID)
}
}
activityMap := make(map[int64]string)
if len(activityIDs) > 0 {
activities, _ := h.readDB.Activities.WithContext(ctx.RequestContext()).
Where(h.readDB.Activities.ID.In(activityIDs...)).Find()
for _, a := range activities {
activityMap[a.ID] = a.Name
}
}
res.Passes = make([]userGamePassItem, 0)
for _, p := range passes {
// 检查是否过期
if !p.ExpiredAt.IsZero() && p.ExpiredAt.Before(now) {
continue
}
// 如果指定了活动ID只返回全局通用的和该活动的
if req.ActivityID != nil && *req.ActivityID > 0 {
if p.ActivityID != 0 && p.ActivityID != *req.ActivityID {
continue
}
}
expiredAt := ""
if !p.ExpiredAt.IsZero() {
expiredAt = p.ExpiredAt.Format("2006-01-02 15:04:05")
}
item := userGamePassItem{
ID: p.ID,
ActivityID: p.ActivityID,
ActivityName: activityMap[p.ActivityID],
Remaining: p.Remaining,
ExpiredAt: expiredAt,
Source: p.Source,
}
res.TotalRemaining += p.Remaining
if p.ActivityID == 0 {
res.GlobalRemaining += p.Remaining
}
res.Passes = append(res.Passes, item)
}
ctx.Payload(res)
}
}
// ==================== 套餐列表 API ====================
type getPackagesRequest struct {
ActivityID *int64 `form:"activity_id"`
}
type packageItem struct {
ID int64 `json:"id"`
Name string `json:"name"`
PassCount int32 `json:"pass_count"`
Price int64 `json:"price"`
OriginalPrice int64 `json:"original_price"`
ValidDays int32 `json:"valid_days"`
ActivityID int64 `json:"activity_id"`
}
type getPackagesResponse struct {
Packages []packageItem `json:"packages"`
}
// GetGamePassPackages 获取可购买的套餐列表
// @Summary 获取次数卡套餐
// @Description 获取可购买的次数卡套餐列表
// @Tags APP端.用户
// @Accept json
// @Produce json
// @Param activity_id query integer false "活动ID不传返回全局套餐"
// @Success 200 {object} getPackagesResponse
// @Failure 400 {object} code.Failure
// @Router /api/app/game-passes/packages [get]
func (h *handler) GetGamePassPackages() core.HandlerFunc {
return func(ctx core.Context) {
req := new(getPackagesRequest)
res := new(getPackagesResponse)
if err := ctx.ShouldBindForm(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
q := h.readDB.GamePassPackages.WithContext(ctx.RequestContext()).
Where(h.readDB.GamePassPackages.Status.Eq(1)) // 只返回上架的
// 如果指定了活动ID返回全局套餐(activity_id=0)和该活动的专属套餐
if req.ActivityID != nil && *req.ActivityID > 0 {
q = q.Where(h.readDB.GamePassPackages.ActivityID.In(0, *req.ActivityID))
} else {
// 只返回全局套餐
q = q.Where(h.readDB.GamePassPackages.ActivityID.Eq(0))
}
packages, err := q.Order(h.readDB.GamePassPackages.SortOrder.Desc(), h.readDB.GamePassPackages.ID.Asc()).Find()
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ServerError, err.Error()))
return
}
res.Packages = make([]packageItem, len(packages))
for i, p := range packages {
res.Packages[i] = packageItem{
ID: p.ID,
Name: p.Name,
PassCount: p.PassCount,
Price: p.Price,
OriginalPrice: p.OriginalPrice,
ValidDays: p.ValidDays,
ActivityID: p.ActivityID,
}
}
ctx.Payload(res)
}
}
// ==================== 购买套餐 API ====================
type purchasePackageRequest struct {
PackageID int64 `json:"package_id" binding:"required"`
Count int32 `json:"count"` // 购买数量
}
type purchasePackageResponse struct {
OrderNo string `json:"order_no"`
Message string `json:"message"`
}
// PurchaseGamePassPackage 购买次数卡套餐(创建订单)
// @Summary 购买次数卡套餐
// @Description 购买次数卡套餐,创建订单等待支付
// @Tags APP端.用户
// @Accept json
// @Produce json
// @Security LoginVerifyToken
// @Param RequestBody body purchasePackageRequest true "请求参数"
// @Success 200 {object} purchasePackageResponse
// @Failure 400 {object} code.Failure
// @Router /api/app/game-passes/purchase [post]
func (h *handler) PurchaseGamePassPackage() core.HandlerFunc {
return func(ctx core.Context) {
req := new(purchasePackageRequest)
res := new(purchasePackageResponse)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
if req.Count <= 0 {
req.Count = 1
}
userID := int64(ctx.SessionUserInfo().Id)
// 查询套餐信息
pkg, err := h.readDB.GamePassPackages.WithContext(ctx.RequestContext()).
Where(h.readDB.GamePassPackages.ID.Eq(req.PackageID)).
Where(h.readDB.GamePassPackages.Status.Eq(1)).
First()
if err != nil || pkg == nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "套餐不存在或已下架"))
return
}
// Calculate total price
totalPrice := pkg.Price * int64(req.Count)
// 创建订单
now := time.Now()
orderNo := now.Format("20060102150405") + fmt.Sprintf("%04d", now.UnixNano()%10000)
order := &model.Orders{
UserID: userID,
OrderNo: "GP" + orderNo,
SourceType: 4, // 次数卡购买
TotalAmount: totalPrice,
ActualAmount: totalPrice,
Status: 1, // 待支付
Remark: fmt.Sprintf("game_pass_package:%s|count:%d", pkg.Name, req.Count),
CreatedAt: now,
UpdatedAt: now,
}
if err := h.writeDB.Orders.WithContext(ctx.RequestContext()).
Omit(h.writeDB.Orders.PaidAt, h.writeDB.Orders.CancelledAt).
Create(order); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ServerError, err.Error()))
return
}
// 在备注中记录套餐ID和数量
remark := fmt.Sprintf("%s|pkg_id:%d|count:%d", order.Remark, pkg.ID, req.Count)
h.writeDB.Orders.WithContext(ctx.RequestContext()).
Where(h.writeDB.Orders.ID.Eq(order.ID)).
Updates(map[string]any{"remark": remark})
res.OrderNo = order.OrderNo
res.Message = "订单创建成功,请完成支付"
ctx.Payload(res)
}
}