2026-06-11 13:33:44 +08:00

174 lines
7.5 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 gamemanagement
import (
"fmt"
"strings"
"hyapp-admin-server/internal/response"
gamev1 "hyapp.local/api/proto/game/v1"
"github.com/gin-gonic/gin"
)
func (h *Handler) GetRoomRPSConfig(c *gin.Context) {
// 后台查询不读本地缓存,也不直接查 game 表;所有配置事实统一从 game-service 管理 RPC 返回,避免 admin-server 和运行时出现双写。
resp, err := h.game.GetRoomRPSConfig(c.Request.Context(), &gamev1.GetRoomRPSConfigRequest{Meta: requestMeta(c)})
if err != nil {
response.ServerError(c, "获取房内猜拳配置失败")
return
}
response.OK(c, gin.H{"config": roomRPSConfigFromProto(resp.GetConfig()), "serverTimeMs": resp.GetServerTimeMs()})
}
func (h *Handler) UpdateRoomRPSConfig(c *gin.Context) {
var req roomRPSConfigRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "房内猜拳配置参数不正确")
return
}
// JSON 绑定只负责类型检查,业务边界统一放到 toProto默认启用、默认倒计时、固定四档礼物和礼物 ID 去重。
config, err := req.toProto()
if err != nil {
response.BadRequest(c, err.Error())
return
}
// admin-server 只校验后台可见的配置边界:开关、超时和 4 个礼物档位。
// 礼物名称、价格、图标、扣款、托管和结算幂等由 game-service/wallet-service 统一处理,避免后台绕过运行时校验写入脏配置。
resp, err := h.game.UpdateRoomRPSConfig(c.Request.Context(), &gamev1.UpdateRoomRPSConfigRequest{
Meta: requestMeta(c),
Config: config,
})
if err != nil {
response.BadRequest(c, err.Error())
return
}
item := roomRPSConfigFromProto(resp.GetConfig())
// 配置修改会影响所有房间入口和下注档位,必须写审计;审计目标使用 game_id便于后续按玩法追踪变更。
h.auditLog(c, "update-room-rps-config", "game_room_rps_configs", item.GameID, item.Status)
response.OK(c, gin.H{"config": item, "serverTimeMs": resp.GetServerTimeMs()})
}
func (h *Handler) ListRoomRPSChallenges(c *gin.Context) {
// 管理后台默认页大小比 App 大,但仍只允许正数;真正的上限和游标稳定性由 game-service 控制,防止 admin-server 复制分页规则。
pageSize := int32(parsePositiveInt(firstQuery(c, "pageSize", "page_size"), 50))
// 所有 ID 查询都兼容 camelCase 和 snake_case前端生成代码和手工调试都能命中同一套过滤逻辑。
initiatorUserID, ok := optionalInt64Query(c, "initiatorUserId", "initiator_user_id")
if !ok {
response.BadRequest(c, "发起人用户 ID 参数不正确")
return
}
challengerUserID, ok := optionalInt64Query(c, "challengerUserId", "challenger_user_id")
if !ok {
response.BadRequest(c, "应战人用户 ID 参数不正确")
return
}
startTimeMS, ok := optionalInt64Query(c, "startTimeMs", "start_time_ms")
if !ok {
response.BadRequest(c, "开始时间参数不正确")
return
}
endTimeMS, ok := optionalInt64Query(c, "endTimeMs", "end_time_ms")
if !ok {
response.BadRequest(c, "结束时间参数不正确")
return
}
// 列表接口只把后台筛选条件透传给 game-service订单可见性、游标语义、状态枚举和结算状态仍以 game-service 查询结果为准。
// admin-server 不做本地状态拼装,避免后续 game-service 增加 timeout/settlement_failed 补偿状态时后台漏同步。
resp, err := h.game.ListRoomRPSChallenges(c.Request.Context(), &gamev1.ListRoomRPSChallengesRequest{
Meta: requestMeta(c),
RoomId: strings.TrimSpace(firstQuery(c, "roomId", "room_id")),
Status: strings.TrimSpace(c.Query("status")),
InitiatorUserId: initiatorUserID,
ChallengerUserId: challengerUserID,
StartTimeMs: startTimeMS,
EndTimeMs: endTimeMS,
PageSize: pageSize,
Cursor: strings.TrimSpace(c.Query("cursor")),
})
if err != nil {
response.ServerError(c, "获取房内猜拳订单失败")
return
}
items := make([]roomRPSChallengeDTO, 0, len(resp.GetChallenges()))
for _, item := range resp.GetChallenges() {
// DTO 转换只做字段命名和大整数保护,不在后台列表里重新计算胜负或结算结果。
items = append(items, roomRPSChallengeFromProto(item))
}
response.OK(c, gin.H{"items": items, "nextCursor": resp.GetNextCursor(), "pageSize": pageSize, "serverTimeMs": resp.GetServerTimeMs()})
}
func (h *Handler) GetRoomRPSChallenge(c *gin.Context) {
challengeID := strings.TrimSpace(c.Param("challenge_id"))
if challengeID == "" {
response.BadRequest(c, "挑战单 ID 参数不正确")
return
}
// 详情接口不接收 room_id 或用户 ID 作为辅助条件challenge_id 是唯一事实入口,避免后台参数互相矛盾时误展示错误订单。
resp, err := h.game.GetRoomRPSChallenge(c.Request.Context(), &gamev1.GetRoomRPSChallengeRequest{
Meta: requestMeta(c),
ChallengeId: challengeID,
})
if err != nil {
response.BadRequest(c, err.Error())
return
}
response.OK(c, gin.H{"challenge": roomRPSChallengeFromProto(resp.GetChallenge()), "serverTimeMs": resp.GetServerTimeMs()})
}
func (h *Handler) RetryRoomRPSSettlement(c *gin.Context) {
challengeID := strings.TrimSpace(c.Param("challenge_id"))
if challengeID == "" {
response.BadRequest(c, "挑战单 ID 参数不正确")
return
}
// 重试入口只允许后台触发一次明确的运维命令。
// 是否处于 settlement_failed、是否已经结算过、钱包补偿是否可重放都由 game-service 按订单幂等事实判断admin-server 不能预读后再决定。
resp, err := h.game.RetryRoomRPSSettlement(c.Request.Context(), &gamev1.RetryRoomRPSSettlementRequest{
Meta: requestMeta(c),
ChallengeId: challengeID,
})
if err != nil {
response.BadRequest(c, err.Error())
return
}
item := roomRPSChallengeFromProto(resp.GetChallenge())
// 审计记录写回 game-service 返回的新结算状态,方便排查“点击重试但实际已被其它 worker 修复”的幂等场景。
h.auditLog(c, "retry-room-rps-settlement", "game_room_rps_challenges", item.ChallengeID, item.SettlementStatus)
response.OK(c, gin.H{"challenge": item, "serverTimeMs": resp.GetServerTimeMs()})
}
func (h *Handler) ExpireRoomRPSChallenge(c *gin.Context) {
challengeID := strings.TrimSpace(c.Param("challenge_id"))
if challengeID == "" {
response.BadRequest(c, "挑战单 ID 参数不正确")
return
}
// 手动过期只是运维兜底命令。
// 只有 pending 且未被应战的挑战能过期;这个判断必须在 game-service 事务内完成,避免后台读到 pending 后并发被应战。
resp, err := h.game.ExpireRoomRPSChallenge(c.Request.Context(), &gamev1.ExpireRoomRPSChallengeRequest{
Meta: requestMeta(c),
ChallengeId: challengeID,
})
if err != nil {
response.BadRequest(c, err.Error())
return
}
item := roomRPSChallengeFromProto(resp.GetChallenge())
// 审计记录写最终状态,而不是写“用户点击了过期”,这样能看出命令是否真的把订单推进到 timeout。
h.auditLog(c, "expire-room-rps-challenge", "game_room_rps_challenges", item.ChallengeID, item.Status)
response.OK(c, gin.H{"challenge": item, "serverTimeMs": resp.GetServerTimeMs()})
}
func optionalInt64Query(c *gin.Context, keys ...string) (int64, bool) {
raw := strings.TrimSpace(firstQuery(c, keys...))
if raw == "" {
// 空值表示不筛选,不是筛选用户 0game-service 看到 0 时按“无该条件”处理。
return 0, true
}
var value int64
if _, err := fmt.Sscan(raw, &value); err != nil || value < 0 {
return 0, false
}
return value, true
}