174 lines
7.5 KiB
Go
174 lines
7.5 KiB
Go
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 == "" {
|
||
// 空值表示不筛选,不是筛选用户 0;game-service 看到 0 时按“无该条件”处理。
|
||
return 0, true
|
||
}
|
||
var value int64
|
||
if _, err := fmt.Sscan(raw, &value); err != nil || value < 0 {
|
||
return 0, false
|
||
}
|
||
return value, true
|
||
}
|