Some checks failed
Build docker and publish / linux (1.24.5) (push) Failing after 48s
381 lines
11 KiB
Go
381 lines
11 KiB
Go
package game
|
|
|
|
import (
|
|
"bindbox-game/internal/code"
|
|
"bindbox-game/internal/pkg/core"
|
|
"bindbox-game/internal/pkg/logger"
|
|
"bindbox-game/internal/pkg/validation"
|
|
"bindbox-game/internal/repository/mysql"
|
|
"bindbox-game/internal/repository/mysql/dao"
|
|
"bindbox-game/internal/service/game"
|
|
usersvc "bindbox-game/internal/service/user"
|
|
"encoding/json"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/redis/go-redis/v9"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
type handler struct {
|
|
logger logger.CustomLogger
|
|
db mysql.Repo
|
|
redis *redis.Client
|
|
ticketSvc game.TicketService
|
|
userSvc usersvc.Service
|
|
readDB *dao.Query
|
|
}
|
|
|
|
func New(l logger.CustomLogger, db mysql.Repo, rdb *redis.Client, userSvc usersvc.Service) *handler {
|
|
return &handler{
|
|
logger: l,
|
|
db: db,
|
|
redis: rdb,
|
|
ticketSvc: game.NewTicketService(l, db),
|
|
userSvc: userSvc,
|
|
readDB: dao.Use(db.GetDbR()),
|
|
}
|
|
}
|
|
|
|
// ========== Admin API ==========
|
|
|
|
type grantTicketRequest struct {
|
|
GameCode string `json:"game_code" binding:"required"`
|
|
Amount int `json:"amount" binding:"required,min=1"`
|
|
Remark string `json:"remark"`
|
|
}
|
|
|
|
// GrantUserTicket Admin为用户发放游戏资格
|
|
// @Summary 发放游戏资格
|
|
// @Tags 管理端.游戏
|
|
// @Param user_id path int true "用户ID"
|
|
// @Param RequestBody body grantTicketRequest true "请求参数"
|
|
// @Success 200 {object} map[string]any
|
|
// @Router /api/admin/users/{user_id}/game_tickets [post]
|
|
func (h *handler) GrantUserTicket() core.HandlerFunc {
|
|
return func(ctx core.Context) {
|
|
userID, err := strconv.ParseInt(ctx.Param("user_id"), 10, 64)
|
|
if err != nil || userID <= 0 {
|
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "invalid user_id"))
|
|
return
|
|
}
|
|
|
|
req := new(grantTicketRequest)
|
|
if err := ctx.ShouldBindJSON(req); err != nil {
|
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
|
|
return
|
|
}
|
|
|
|
err = h.ticketSvc.GrantTicket(ctx.RequestContext(), userID, req.GameCode, req.Amount, "admin", 0, req.Remark)
|
|
if err != nil {
|
|
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, err.Error()))
|
|
return
|
|
}
|
|
|
|
ctx.Payload(map[string]any{"success": true})
|
|
}
|
|
}
|
|
|
|
// ListUserTickets Admin查询用户游戏资格日志
|
|
// @Summary 查询用户游戏资格日志
|
|
// @Tags 管理端.游戏
|
|
// @Param user_id path int true "用户ID"
|
|
// @Param page query int false "页码"
|
|
// @Param page_size query int false "每页数量"
|
|
// @Success 200 {object} map[string]any
|
|
// @Router /api/admin/users/{user_id}/game_tickets [get]
|
|
func (h *handler) ListUserTickets() core.HandlerFunc {
|
|
return func(ctx core.Context) {
|
|
userID, err := strconv.ParseInt(ctx.Param("user_id"), 10, 64)
|
|
if err != nil || userID <= 0 {
|
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "invalid user_id"))
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
Page int `form:"page"`
|
|
PageSize int `form:"page_size"`
|
|
}
|
|
_ = ctx.ShouldBindQuery(&req)
|
|
|
|
if req.Page <= 0 {
|
|
req.Page = 1
|
|
}
|
|
if req.PageSize <= 0 {
|
|
req.PageSize = 20
|
|
}
|
|
|
|
logs, total, err := h.ticketSvc.GetTicketLogs(ctx.RequestContext(), userID, req.Page, req.PageSize)
|
|
if err != nil {
|
|
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, err.Error()))
|
|
return
|
|
}
|
|
|
|
ctx.Payload(map[string]any{
|
|
"list": logs,
|
|
"total": total,
|
|
"page": req.Page,
|
|
"page_size": req.PageSize,
|
|
})
|
|
}
|
|
}
|
|
|
|
// ========== App API ==========
|
|
|
|
// GetMyTickets App获取我的游戏资格
|
|
// @Summary 获取我的游戏资格
|
|
// @Tags APP端.游戏
|
|
// @Param user_id path int true "用户ID"
|
|
// @Success 200 {object} map[string]int
|
|
// @Router /api/app/users/{user_id}/game_tickets [get]
|
|
func (h *handler) GetMyTickets() core.HandlerFunc {
|
|
return func(ctx core.Context) {
|
|
userID := int64(ctx.SessionUserInfo().Id)
|
|
|
|
tickets, err := h.ticketSvc.GetUserTickets(ctx.RequestContext(), userID)
|
|
if err != nil {
|
|
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, err.Error()))
|
|
return
|
|
}
|
|
|
|
ctx.Payload(tickets)
|
|
}
|
|
}
|
|
|
|
type enterGameRequest struct {
|
|
GameCode string `json:"game_code" binding:"required"`
|
|
}
|
|
|
|
type enterGameResponse struct {
|
|
TicketToken string `json:"ticket_token"`
|
|
NakamaServer string `json:"nakama_server"`
|
|
NakamaKey string `json:"nakama_key"`
|
|
RemainingTimes int `json:"remaining_times"`
|
|
}
|
|
|
|
// EnterGame App进入游戏(消耗资格)
|
|
// @Summary 进入游戏
|
|
// @Tags APP端.游戏
|
|
// @Param RequestBody body enterGameRequest true "请求参数"
|
|
// @Success 200 {object} enterGameResponse
|
|
// @Router /api/app/games/enter [post]
|
|
func (h *handler) EnterGame() core.HandlerFunc {
|
|
return func(ctx core.Context) {
|
|
userID := int64(ctx.SessionUserInfo().Id)
|
|
|
|
req := new(enterGameRequest)
|
|
if err := ctx.ShouldBindJSON(req); err != nil {
|
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
|
|
return
|
|
}
|
|
|
|
// 扣减资格
|
|
if err := h.ticketSvc.UseTicket(ctx.RequestContext(), userID, req.GameCode); err != nil {
|
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, 180001, "游戏次数不足"))
|
|
return
|
|
}
|
|
|
|
// 生成临时token并存入Redis
|
|
ticketToken := generateTicketToken(userID)
|
|
h.redis.Set(ctx.RequestContext(), "game:ticket:"+ticketToken, userID, 30*60*1000000000) // 30分钟
|
|
|
|
// 查询剩余次数
|
|
ticket, _ := h.ticketSvc.GetUserTicketByGame(ctx.RequestContext(), userID, req.GameCode)
|
|
remaining := 0
|
|
if ticket != nil {
|
|
remaining = int(ticket.Available)
|
|
}
|
|
|
|
// TODO: 从配置读取Nakama服务器信息
|
|
ctx.Payload(&enterGameResponse{
|
|
TicketToken: ticketToken,
|
|
NakamaServer: "ws://localhost:7350",
|
|
NakamaKey: "defaultkey",
|
|
RemainingTimes: remaining,
|
|
})
|
|
}
|
|
}
|
|
|
|
// ========== Internal API (Nakama调用) ==========
|
|
|
|
type verifyRequest struct {
|
|
UserID string `json:"user_id"`
|
|
Ticket string `json:"ticket"`
|
|
}
|
|
|
|
type verifyResponse struct {
|
|
Valid bool `json:"valid"`
|
|
UserID string `json:"user_id"`
|
|
GameConfig map[string]any `json:"game_config,omitempty"`
|
|
}
|
|
|
|
// VerifyTicket Internal验证游戏票据
|
|
// @Summary 验证票据
|
|
// @Tags Internal.游戏
|
|
// @Param RequestBody body verifyRequest true "请求参数"
|
|
// @Success 200 {object} verifyResponse
|
|
// @Router /internal/game/verify [post]
|
|
func (h *handler) VerifyTicket() core.HandlerFunc {
|
|
return func(ctx core.Context) {
|
|
req := new(verifyRequest)
|
|
if err := ctx.ShouldBindJSON(req); err != nil {
|
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
|
|
return
|
|
}
|
|
|
|
// 从Redis验证token
|
|
storedUserID, err := h.redis.Get(ctx.RequestContext(), "game:ticket:"+req.Ticket).Result()
|
|
if err != nil || storedUserID != req.UserID {
|
|
ctx.Payload(&verifyResponse{Valid: false})
|
|
return
|
|
}
|
|
|
|
// 获取游戏配置
|
|
gameConfig := make(map[string]any)
|
|
configKey := "game_minesweeper_config"
|
|
conf, err := h.readDB.SystemConfigs.WithContext(ctx.RequestContext()).Where(h.readDB.SystemConfigs.ConfigKey.Eq(configKey)).First()
|
|
if err == nil && conf != nil {
|
|
json.Unmarshal([]byte(conf.ConfigValue), &gameConfig)
|
|
}
|
|
|
|
ctx.Payload(&verifyResponse{Valid: true, UserID: req.UserID, GameConfig: gameConfig})
|
|
}
|
|
}
|
|
|
|
type settleRequest struct {
|
|
UserID string `json:"user_id"`
|
|
Ticket string `json:"ticket"`
|
|
MatchID string `json:"match_id"`
|
|
Win bool `json:"win"`
|
|
Score int `json:"score"`
|
|
}
|
|
|
|
type settleResponse struct {
|
|
Success bool `json:"success"`
|
|
Reward string `json:"reward,omitempty"`
|
|
}
|
|
|
|
// SettleGame Internal游戏结算
|
|
// @Summary 游戏结算
|
|
// @Tags Internal.游戏
|
|
// @Param RequestBody body settleRequest true "请求参数"
|
|
// @Success 200 {object} settleResponse
|
|
// @Router /internal/game/settle [post]
|
|
func (h *handler) SettleGame() core.HandlerFunc {
|
|
return func(ctx core.Context) {
|
|
req := new(settleRequest)
|
|
if err := ctx.ShouldBindJSON(req); err != nil {
|
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
|
|
return
|
|
}
|
|
|
|
// 验证token
|
|
storedUserID, err := h.redis.Get(ctx.RequestContext(), "game:ticket:"+req.Ticket).Result()
|
|
if err != nil || storedUserID != req.UserID {
|
|
ctx.Payload(&settleResponse{Success: false})
|
|
return
|
|
}
|
|
|
|
// 删除token防止重复使用
|
|
h.redis.Del(ctx.RequestContext(), "game:ticket:"+req.Ticket)
|
|
|
|
// 奖品发放逻辑
|
|
var rewardMsg string
|
|
var msConfig struct {
|
|
WinnerRewardPoints int64 `json:"winner_reward_points"`
|
|
ParticipationRewardPoints int64 `json:"participation_reward_points"`
|
|
WinnerRewardProductID int64 `json:"winner_reward_product_id"`
|
|
ParticipationRewardProductID int64 `json:"participation_reward_product_id"`
|
|
}
|
|
|
|
// 1. 读取配置
|
|
configKey := "game_minesweeper_config"
|
|
conf, err := h.readDB.SystemConfigs.WithContext(ctx.RequestContext()).Where(h.readDB.SystemConfigs.ConfigKey.Eq(configKey)).First()
|
|
if err == nil && conf != nil {
|
|
json.Unmarshal([]byte(conf.ConfigValue), &msConfig)
|
|
}
|
|
|
|
uid, _ := strconv.ParseInt(req.UserID, 10, 64)
|
|
|
|
// 2. 确定奖励内容
|
|
var targetProductID int64
|
|
var targetPoints int64
|
|
|
|
if req.Win {
|
|
targetProductID = msConfig.WinnerRewardProductID
|
|
targetPoints = msConfig.WinnerRewardPoints
|
|
if targetPoints == 0 && targetProductID == 0 {
|
|
targetPoints = 100 // 兜底
|
|
}
|
|
} else {
|
|
targetProductID = msConfig.ParticipationRewardProductID
|
|
targetPoints = msConfig.ParticipationRewardPoints
|
|
if targetPoints == 0 && targetProductID == 0 {
|
|
targetPoints = 10 // 兜底
|
|
}
|
|
}
|
|
|
|
// 3. 发放奖励
|
|
if targetProductID > 0 {
|
|
res, err := h.userSvc.GrantReward(ctx.RequestContext(), uid, usersvc.GrantRewardRequest{
|
|
ProductID: targetProductID,
|
|
Quantity: 1,
|
|
Remark: "扫雷游戏奖励",
|
|
})
|
|
if err != nil || !res.Success {
|
|
h.logger.Error("Failed to grant game product reward", zap.Error(err), zap.String("msg", res.Message))
|
|
rewardMsg = "奖励发放失败"
|
|
} else {
|
|
rewardMsg = "获得奖品"
|
|
}
|
|
} else if targetPoints > 0 {
|
|
err := h.userSvc.AddPointsWithAction(ctx.RequestContext(), uid, targetPoints, "game_reward", "扫雷游戏奖励", "minesweeper_settle", nil, nil)
|
|
if err != nil {
|
|
h.logger.Error("Failed to grant game points", zap.Error(err))
|
|
}
|
|
rewardMsg = strconv.FormatInt(targetPoints, 10) + "积分"
|
|
}
|
|
|
|
ctx.Payload(&settleResponse{Success: true, Reward: rewardMsg})
|
|
}
|
|
}
|
|
|
|
// GetMinesweeperConfig Internal获取扫雷配置
|
|
// @Summary 获取扫雷配置
|
|
// @Tags Internal.游戏
|
|
// @Success 200 {object} map[string]interface{}
|
|
// @Router /internal/game/minesweeper/config [get]
|
|
func (h *handler) GetMinesweeperConfig() core.HandlerFunc {
|
|
return func(ctx core.Context) {
|
|
configKey := "game_minesweeper_config"
|
|
conf, err := h.readDB.SystemConfigs.WithContext(ctx.RequestContext()).Where(h.readDB.SystemConfigs.ConfigKey.Eq(configKey)).First()
|
|
if err != nil {
|
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ServerError, "获取配置失败"))
|
|
return
|
|
}
|
|
|
|
var gameConfig map[string]interface{}
|
|
if err := json.Unmarshal([]byte(conf.ConfigValue), &gameConfig); err != nil {
|
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ServerError, "解析配置失败"))
|
|
return
|
|
}
|
|
|
|
ctx.Payload(gameConfig)
|
|
}
|
|
}
|
|
|
|
// ========== Helpers ==========
|
|
|
|
func generateTicketToken(userID int64) string {
|
|
return "GT" + randomString(16)
|
|
}
|
|
|
|
func randomString(n int) string {
|
|
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
|
b := make([]byte, n)
|
|
for i := range b {
|
|
b[i] = letters[i%len(letters)]
|
|
}
|
|
return string(b)
|
|
}
|