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