小游戏服务平台 - 接口详细设计(入参/出参)
1. 用户服务
1.1 登录接口
POST /api/v1/auth/login
入参
{
"login_type": "password", // password / oauth
"username": "test_user", // 当 login_type=password
"password": "123456",
"oauth_provider": "wechat", // 当 login_type=oauth
"oauth_token": "wx_xxx"
}
出参
{
"code": 0,
"message": "success",
"data": {
"user_id": "100928374923",
"access_token": "eyJhbGciOiJIUzI1...",
"refresh_token": "eyJhbGciOiJIUzI1...",
"expires_in": 1800
}
}
1.2 刷新 Token
POST /api/v1/auth/refresh
入参
{
"refresh_token": "eyJhbGciOiJIUzI1..."
}
出参
{
"code": 0,
"message": "success",
"data": {
"access_token": "new_token_xxx",
"expires_in": 1800
}
}
1.3 获取用户信息
GET /api/v1/users/{user_id}
出参
{
"code": 0,
"message": "success",
"data": {
"user_id": "100928374923",
"username": "test_user",
"age": 22,
"gender": 1,
"region": "Beijing",
"status": 1,
"created_at": "2025-09-26T10:00:00Z"
}
}
2. 游戏服务
2.1 上传游戏
POST /api/v1/games/upload
入参
multipart/form-data
file: game.zip
metadata: {
"name": "Jump Game",
"description": "A casual jump game",
"version": "1.0.0",
"tags": ["casual","jump"]
}
出参
{
"code": 0,
"message": "upload success",
"data": {
"game_id": "200123891237",
"status": "pending_review"
}
}
2.2 获取游戏详情
GET /api/v1/games/{id}
出参
{
"code": 0,
"message": "success",
"data": {
"game_id": "200123891237",
"name": "Jump Game",
"description": "A casual jump game",
"version": "1.0.0",
"status": "online",
"cdn_url": "https://cdn.xxx.com/games/200123891237/v1/index.html",
"tags": ["casual","jump"],
"created_at": "2025-09-26T12:00:00Z"
}
}
2.3 搜索游戏
GET /api/v1/games?tag=casual&keyword=jump&page=1&size=10
出参
{
"code": 0,
"message": "success",
"data": {
"total": 125,
"list": [
{
"game_id": "200123891237",
"name": "Jump Game",
"tags": ["casual","jump"],
"status": "online"
}
]
}
}
3. 广告服务
3.1 请求广告
GET /api/v1/ads/request?slot_id=3001&user_id=100928374923
出参
{
"code": 0,
"message": "success",
"data": {
"ad_id": "ad_789123",
"type": "reward",
"material_url": "https://cdn.xxx.com/ads/ad_789123.mp4",
"duration": 30,
"click_url": "https://track.xxx.com/click?ad_id=ad_789123&user_id=100928374923"
}
}
3.2 上报广告事件
POST /api/v1/ads/report
入参
{
"user_id": "100928374923",
"game_id": "200123891237",
"ad_id": "ad_789123",
"event_type": "click",
"ts": "2025-09-26T12:00:00Z"
}
出参
{
"code": 0,
"message": "event logged"
}
4. 支付服务
4.1 创建订单
POST /api/v1/payments/create
入参
{
"user_id": "100928374923",
"game_id": "200123891237",
"amount": 9.99,
"currency": "USD",
"pay_method": "wechat" // wechat / alipay / stripe / paypal
}
出参
{
"code": 0,
"message": "order created",
"data": {
"order_id": "500123981273",
"out_trade_no": "wx202509261200xx",
"pay_url": "https://pay.wechat.com/order/wx202509261200xx"
}
}
4.2 支付回调
POST /api/v1/payments/notify
入参(由支付网关推送)
{
"out_trade_no": "wx202509261200xx",
"status": "paid",
"transaction_id": "wx_txn_001",
"paid_at": "2025-09-26T12:01:00Z"
}
出参
{
"code": 0,
"message": "callback processed"
}
4.3 开发者提现
POST /api/v1/developers/withdraw
入参
{
"developer_id": "dev_1001",
"amount": 500.00,
"currency": "USD",
"account": {
"method": "paypal",
"account_id": "dev@example.com"
}
}
出参
{
"code": 0,
"message": "withdraw request submitted",
"data": {
"settlement_id": "600918237182",
"status": "pending"
}
}
总结
我为 用户、游戏、广告、支付 四大模块整理了接口的 详细入参/出参 JSON 格式,同时包含:
- 错误码规范(
code) - 状态字段(status)
- 时间戳统一 ISO8601 UTC
- 幂等保障字段(如订单 out_trade_no)
5 代码实践:Go DTO 与统一响应封装
本节将 API 文档中的 JSON 格式转化为 Go struct DTO 与 Gin 路由 + 中间件,可直接用于前后端联调。
5.1 请求/响应 DTO
package dto
import "time"
type LoginRequest struct {
LoginType string `json:"login_type"`
Username string `json:"username"`
Password string `json:"password"`
OAuthProvider string `json:"oauth_provider"`
OAuthToken string `json:"oauth_token"`
}
type LoginResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data struct {
UserID uint64 `json:"user_id"`
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int `json:"expires_in"`
} `json:"data"`
TraceID string `json:"trace_id"`
}
type GameDetailResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data struct {
GameID uint64 `json:"game_id"`
Name string `json:"name"`
Description string `json:"description"`
Version string `json:"version"`
Status string `json:"status"`
CDNURL string `json:"cdn_url"`
Tags []string `json:"tags"`
CreatedAt time.Time `json:"created_at"`
} `json:"data"`
}
type AdRequest struct {
SlotID int `form:"slot_id"`
UserID uint64 `form:"user_id"`
}
5.2 统一响应与错误封装
package response
import (
"github.com/gin-gonic/gin"
"net/http"
)
type APIResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
TraceID string `json:"trace_id"`
}
func Success(c *gin.Context, data interface{}) {
c.JSON(http.StatusOK, APIResponse{Code: 0, Message: "success", Data: data, TraceID: c.GetString("trace_id")})
}
func Error(c *gin.Context, code int, message string) {
c.JSON(http.StatusOK, APIResponse{Code: code, Message: message, TraceID: c.GetString("trace_id")})
}
5.3 路由注册示例
package routes
import (
"github.com/gin-gonic/gin"
"playnow/handler"
"playnow/middleware"
)
func Register(r *gin.Engine) {
api := r.Group("/api/v1")
{
api.POST("/auth/login", handler.Login)
api.POST("/auth/refresh", handler.RefreshToken)
authorized := api.Group("", middleware.JWT(), middleware.VerifySig())
{
authorized.GET("/users/:id", handler.GetUser)
authorized.GET("/games/:id", handler.GetGame)
authorized.GET("/ads/request", handler.RequestAd)
authorized.POST("/payments", handler.CreatePayment)
}
}
}
6. API 设计原则与 RESTful 最佳实践
在小游戏服务平台这种多租户、并发高的业务场景中,API 设计不仅仅是约定 URI 和 JSON 格式,更需要从幂等性、版本控制、限流熔断三个维度构建高可用、可演进的接口体系。
6.1 幂等性设计
幂等性(Idempotency)是指同一操作执行一次与执行多次产生的效果相同。在支付创建订单、广告事件上报等场景中,网络超时或客户端重试可能导致同一请求被重复处理,引发重复扣款或重复发放奖励。
核心策略有两种:
- 客户端幂等键(Idempotency-Key):客户端在请求头中携带唯一键(如 UUID),服务端以
user_id + idempotency_key作为 Redis 分布式锁的键,首次处理时写入处理结果并设置 TTL(如 24 小时),后续相同键直接返回缓存结果。 - 业务字段天然幂等:如
out_trade_no是支付网关的唯一订单号,数据库对该字段设置唯一索引,重复插入会触发唯一约束冲突,服务端捕获后返回幂等成功。
以下为基于 Gin + Redis 的幂等中间件实现:
package middleware
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/redis/go-redis/v9"
)
func Idempotency(rdb *redis.Client) gin.HandlerFunc {
return func(c *gin.Context) {
key := c.GetHeader("X-Idempotency-Key")
if key == "" {
c.Next()
return
}
// 以 user_id + key 组合避免跨用户碰撞
userID, _ := c.Get("user_id")
lockKey := fmt.Sprintf("idem:%v:%s", userID, key)
// SET NX EX 实现原子加锁
ok, err := rdb.SetNX(c.Request.Context(), lockKey, "1", 24*time.Hour).Result()
if err != nil || !ok {
c.AbortWithStatusJSON(http.StatusConflict, gin.H{
"code": 409, "message": "duplicate request",
})
return
}
c.Next()
}
}
// IdempotencyKey 从请求体生成确定性幂等键(用于无可用的客户端 UUID 时)
func IdempotencyKey(userID string, body []byte) string {
h := sha256.New()
h.Write([]byte(userID))
h.Write(body)
return hex.EncodeToString(h.Sum(nil))[:32]
}
6.2 API 版本控制
小游戏平台更新迭代频繁,API 的版本控制策略直接影响向后兼容性。我们采用 URL Path 版本号(如 /api/v1/、/api/v2/),相比 Header 版本或 Media Type 版本更加直观、易于 Nginx/网关层路由分流。
版本演进实践:
- 大版本变更:URI 路径升级,如支付接口从
v1升级到v2时修改请求体结构。 - 小版本变更:在同一版本内通过加字段实现向前兼容(只扩展、不删除、不改含义)。
- 弃用策略:旧版本保留 6 个月,在响应头中附加
Deprecation: true和Sunset日期提示客户端迁移。
// 版本路由注册
func RegisterV1(r *gin.Engine) {
v1 := r.Group("/api/v1")
{
v1.POST("/payments/create", handler.CreatePaymentV1)
}
}
func RegisterV2(r *gin.Engine) {
v2 := r.Group("/api/v2")
{
v2.POST("/payments/create", handler.CreatePaymentV2)
}
}
6.3 限流熔断
平台广告请求和支付回调容易因流量突增导致上游服务雪崩。我们在网关层引入令牌桶限流保护核心接口,在服务内部使用自适应熔断器隔离故障依赖。
限流中间件基于 Redis 实现分布式令牌桶:
package middleware
import (
"math"
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
"github.com/redis/go-redis/v9"
)
// RateLimit 基于 Redis 的滑动窗口限流,key 按 user_id + path 区分
func RateLimit(rdb *redis.Client, maxQPS int, window time.Duration) gin.HandlerFunc {
return func(c *gin.Context) {
key := "ratelimit:" + c.ClientIP() + ":" + c.Request.URL.Path
now := time.Now().Unix()
windowStart := now - int64(window.Seconds())
pipe := rdb.TxPipeline()
pipe.ZRemRangeByScore(c.Request.Context(), key, "-inf", strconv.FormatInt(windowStart, 10))
pipe.ZCard(c.Request.Context(), key)
pipe.ZAdd(c.Request.Context(), key, redis.Z{Score: float64(now), Member: now})
pipe.Expire(c.Request.Context(), key, window)
cmders, err := pipe.Exec(c.Request.Context())
if err != nil {
c.Next()
return
}
count := cmders[1].(*redis.IntCmd).Val()
if int(count) > maxQPS {
c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{
"code": 429, "message": "rate limit exceeded",
})
return
}
c.Next()
}
}
熔断器则采用类似 Hystrix 的计数窗口算法:统计最近 N 次调用的失败率,超过阈值后进入 Open 状态,短时间直接拒绝请求,避免持续调用故障服务。
7. JWT 认证与安全中间件实现
小游戏平台的用户服务以 JWT(JSON Web Token)作为无状态认证方案,access token 负责接口鉴权,refresh token 负责会话续期。
7.1 双 Token 架构与密钥轮换
access token 有效期短(15-30 分钟),纯内存校验即可;refresh token 有效期长(7-30 天),需要持久化到 Redis 以便实现吊销和单设备登录限制。密钥轮换(Key Rotation)是降低密钥泄露风险的核心手段:平台维护一个密钥版本标识 kid(Key ID),JWT 头部携带 kid,服务端根据 kid 查找对应密钥。轮换时先生成新密钥,让旧密钥继续可用一个周期(如 24 小时),再通知所有服务更新。
package auth
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"fmt"
"time"
"github.com/golang-jwt/jwt/v5"
)
type JWTManager struct {
keys map[string]*ecdsa.PrivateKey // kid -> privateKey
current string // 当前生效的 kid
}
func NewJWTManager() *JWTManager {
m := &JWTManager{keys: make(map[string]*ecdsa.PrivateKey)}
m.RotateKey()
return m
}
// RotateKey 生成新的 ECDSA 密钥对并切换 current kid
func (m *JWTManager) RotateKey() {
key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
kid := fmt.Sprintf("kid_%d", time.Now().Unix())
m.keys[kid] = key
m.current = kid
}
func (m *JWTManager) Sign(userID string, ttl time.Duration) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodES256, jwt.MapClaims{
"sub": userID,
"iat": time.Now().Unix(),
"exp": time.Now().Add(ttl).Unix(),
"kid": m.current,
})
token.Header["kid"] = m.current
return token.SignedString(m.keys[m.current])
}
func (m *JWTManager) Parse(tokenStr string) (*jwt.Token, error) {
return jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) {
kid, ok := token.Header["kid"].(string)
if !ok {
return nil, fmt.Errorf("missing kid")
}
key, exists := m.keys[kid]
if !exists {
return nil, fmt.Errorf("unknown kid: %s", kid)
}
return &key.PublicKey, nil
}, jwt.WithValidMethods([]string{"ES256"}))
}
7.2 Refresh Token 黑名单与单设备登录
当用户修改密码或管理员强制下线时,需要吊销已下发的 refresh token。我们将 refresh token 的 jti(JWT ID)存储到 Redis,同时建立 user_id -> jti set 的反向索引。黑名单直接删除反向索引中的对应 jti 即可。单设备登录限制则在生成新 token 时,清除该用户之前的所有 jti。
package auth
import (
"context"
"fmt"
"time"
"github.com/redis/go-redis/v9"
)
type TokenStore struct {
rdb *redis.Client
}
func (s *TokenStore) Save(ctx context.Context, userID, jti string, ttl time.Duration) error {
pipe := s.rdb.TxPipeline()
pipe.Set(ctx, "rt:jti:"+jti, userID, ttl)
pipe.SAdd(ctx, "rt:user:"+userID, jti)
pipe.Expire(ctx, "rt:user:"+userID, ttl)
_, err := pipe.Exec(ctx)
return err
}
func (s *TokenStore) Revoke(ctx context.Context, userID string) error {
jtiSet, err := s.rdb.SMembers(ctx, "rt:user:"+userID).Result()
if err != nil {
return err
}
pipe := s.rdb.TxPipeline()
for _, jti := range jtiSet {
pipe.Del(ctx, "rt:jti:"+jti)
}
pipe.Del(ctx, "rt:user:"+userID)
_, err = pipe.Exec(ctx)
return err
}
func (s *TokenStore) IsValid(ctx context.Context, jti string) bool {
exists, _ := s.rdb.Exists(ctx, "rt:jti:"+jti).Result()
return exists == 1
}
func (s *TokenStore) LimitSingleDevice(ctx context.Context, userID, newJTI string, ttl time.Duration) error {
// 吊销旧设备
if err := s.Revoke(ctx, userID); err != nil {
return err
}
// 绑定新设备
return s.Save(ctx, userID, newJTI, ttl)
}
7.3 Gin 鉴权中间件
将 JWT 解析与 refresh token 黑名单校验封装为可复用的 Gin HandlerFunc:
package middleware
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
"playnow/auth"
)
func JWT(jwtManager *auth.JWTManager, store *auth.TokenStore) gin.HandlerFunc {
return func(c *gin.Context) {
header := c.GetHeader("Authorization")
tokenStr := strings.TrimPrefix(header, "Bearer ")
if tokenStr == header {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"code": 401, "message": "missing token"})
return
}
token, err := jwtManager.Parse(tokenStr)
if err != nil || !token.Valid {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"code": 401, "message": "invalid token"})
return
}
claims := token.Claims.(jwt.MapClaims)
c.Set("user_id", claims["sub"])
c.Set("jti", claims["jti"])
c.Next()
}
}
8. 请求参数校验与 OpenAPI3/Swagger 文档自动生成
手动维护 API 文档容易与代码脱节,引入自动生成工具是工程化产出的必经之路。
8.1 Validator 标签校验与自定义规则
Go 生态中 go-playground/validator 是最通用的校验库,通过 struct tag 声明规则。在创建订单接口中,金额必须为正数、支付渠道必须在白名单中,这些约束可直接标注在 DTO 上。
package dto
import "github.com/go-playground/validator/v10"
type CreateOrderRequest struct {
UserID string `json:"user_id" validate:"required,len=12"`
GameID string `json:"game_id" validate:"required,len=12"`
Amount float64 `json:"amount" validate:"required,gt=0"`
Currency string `json:"currency" validate:"required,oneof=USD CNY EUR"`
PayMethod string `json:"pay_method" validate:"required,oneof=wechat alipay stripe paypal"`
}
func Validate(r interface{}) error {
validate := validator.New()
return validate.Struct(r)
}
自定义校验规则示例:验证游戏 ID 必须以 20 开头:
func init() {
validate.RegisterValidation("gameid_prefix", func(fl validator.FieldLevel) bool {
return strings.HasPrefix(fl.Field().String(), "20")
})
}
8.2 Gin 集成 Validator 中间件
统一封装校验失败响应,避免在每个 Handler 中重复编写错误处理:
package middleware
import (
"net/http"
"reflect"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"playnow/response"
)
func BindAndValidate(obj interface{}) gin.HandlerFunc {
return func(c *gin.Context) {
// 根据请求方法选择绑定方式
if err := c.ShouldBind(obj); err != nil {
response.Error(c, 400, "invalid request: "+err.Error())
c.Abort()
return
}
if err := dto.Validate(obj); err != nil {
if ve, ok := err.(validator.ValidationErrors); ok {
fields := make(map[string]string)
for _, fe := range ve {
fields[fe.Field()] = fe.Tag()
}
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
"code": 400, "message": "validation failed", "fields": fields,
})
return
}
}
// 将绑定后的对象存入上下文
c.Set(reflect.TypeOf(obj).Elem().Name(), obj)
c.Next()
}
}
8.3 swaggo 自动生成 Swagger 文档
使用 swaggo/swag 在代码中通过注释生成 OpenAPI 2.0/3.0 文档,配合 gin-swagger 在中间件挂载 Swagger UI。
Handler 注释规范:
// Login godoc
// @Summary 用户登录
// @Description 支持密码登录与第三方 OAuth 登录
// @Tags auth
// @Accept json
// @Produce json
// @Param body body dto.LoginRequest true "登录参数"
// @Success 200 {object} response.APIResponse
// @Failure 400 {object} response.APIResponse
// @Router /api/v1/auth/login [post]
func Login(c *gin.Context) {
var req dto.LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, 400, err.Error())
return
}
// ... 业务逻辑
response.Success(c, tokenData)
}
生成与挂载命令:
# 安装 swag 命令行工具
go install github.com/swaggo/swag/cmd/swag@latest
# 扫描注释生成 docs 目录
swag init -g cmd/api/main.go --parseDependency
# main.go 中挂载 Swagger UI
import swaggerFiles "github.com/swaggo/files"
import ginSwagger "github.com/swaggo/gin-swagger"
func main() {
r := gin.Default()
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
// ... 注册路由
}
9. 支付回调防重放与安全验签设计
支付回调(Webhook)是支付服务最核心的安全入口。外部支付网关向平台推送支付结果时,攻击者可能伪造请求、篡改参数或重放历史请求,因此必须从签名验证、防重放攻击、幂等处理三个层面进行防御。
9.1 签名算法与验签中间件
绝大多数支付网关采用 HMAC-SHA256 或 RSA-SHA256 对请求体加签。以微信支付为例,平台与网关预先共享 APIv3 密钥,回调请求头中携带 Wechatpay-Signature、Wechatpay-Timestamp、Wechatpay-Nonce、Wechatpay-Serial。验签步骤为:
- 提取时间戳和随机串,构造待验签串
timestamp + "\n" + nonce + "\n" + body + "\n"。 - 从平台证书序列号查找对应公钥。
- 使用 RSA 公钥验证签名。
- 校验时间戳与服务器当前时间差是否在合理窗口内(如 5 分钟)。
以下为通用 HMAC-SHA256 验签中间件,可配置到任何支付网关:
package middleware
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
)
type WebhookConfig struct {
Secret string // 共享密钥
Tolerance time.Duration // 时间戳容差
HeaderSig string // 存放签名的 Header 名
HeaderTS string // 存放时间戳的 Header 名
}
func VerifyWebhook(cfg WebhookConfig) gin.HandlerFunc {
return func(c *gin.Context) {
tsStr := c.GetHeader(cfg.HeaderTS)
sig := c.GetHeader(cfg.HeaderSig)
if tsStr == "" || sig == "" {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"code": 400, "message": "missing signature headers"})
return
}
ts, err := strconv.ParseInt(tsStr, 10, 64)
if err != nil || time.Since(time.Unix(ts, 0)) > cfg.Tolerance {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"code": 400, "message": "timestamp expired"})
return
}
body, _ := io.ReadAll(c.Request.Body)
c.Request.Body = io.NopCloser(bytes.NewBuffer(body)) // 还原 Body 供下游读取
mac := hmac.New(sha256.New, []byte(cfg.Secret))
mac.Write([]byte(tsStr + "."))
mac.Write(body)
expected := strings.ToLower(hex.EncodeToString(mac.Sum(nil)))
if !hmac.Equal([]byte(expected), []byte(strings.ToLower(sig))) {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"code": 403, "message": "signature mismatch"})
return
}
c.Next()
}
}
9.2 重放攻击防御
即使签名正确,攻击者仍可能截获合法请求在容差时间内多次重发。防御方案是在 Redis 中以 nonce + timestamp 为键,设置时长等于容差窗口的分布式锁,重复 nonce 直接拒绝。
func AntiReplay(rdb *redis.Client, ttl time.Duration) gin.HandlerFunc {
return func(c *gin.Context) {
nonce := c.GetHeader("X-Nonce")
if nonce == "" {
c.Next()
return
}
key := "nonce:" + nonce
ok, _ := rdb.SetNX(c.Request.Context(), key, "1", ttl).Result()
if !ok {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"code": 403, "message": "replay detected"})
return
}
c.Next()
}
}
9.3 Webhook 幂等处理与状态机
支付回调的处理必须幂等,因为网关可能因未收到 HTTP 200 而重复推送。我们使用 out_trade_no + transaction_id 作为唯一键,在处理回调时先查询订单状态:
- 如果状态已为
paid,直接返回成功,不再执行发放道具或通知下游逻辑。 - 如果状态为
pending,使用数据库乐观锁WHERE status = 'pending'更新为paid,更新成功后才执行后续业务。 - 如果更新失败(已被其他进程处理),则捕获行冲突并返回幂等成功。
package handler
import (
"context"
"fmt"
"gorm.io/gorm"
"playnow/model"
)
func ProcessPaymentNotify(ctx context.Context, db *gorm.DB, notify model.PaymentNotify) error {
return db.Transaction(func(tx *gorm.DB) error {
var order model.Order
if err := tx.WithContext(ctx).Where("out_trade_no = ?", notify.OutTradeNo).First(&order).Error; err != nil {
return err
}
if order.Status == "paid" {
return nil // 已处理,幂等返回
}
// 乐观锁更新
result := tx.Model(&order).
Where("id = ? AND status = ?", order.ID, "pending").
Updates(model.Order{
Status: "paid",
TransactionID: notify.TransactionID,
PaidAt: notify.PaidAt,
})
if result.RowsAffected == 0 {
return fmt.Errorf("order already processed concurrently")
}
// 发放道具、通知游戏服务器等后续逻辑
return DeliverGameAsset(ctx, tx, order.UserID, order.GameID)
})
}
10. API 可观测性:日志链路追踪与 Prometheus 监控指标
在生产环境中,API 的**可观测性(Observability)是快速定位故障、分析性能瓶颈的关键。小游戏平台围绕日志(Logs)、指标(Metrics)、链路(Traces)**三大支柱构建观测体系。
10.1 结构化日志与 Trace ID 传递
传统文本日志难以在分布式系统中聚合查询,我们采用 uber-go/zap 输出 JSON 结构化日志,每条日志包含 trace_id、user_id、latency_ms、status_code 等字段,直接对接 ELK/Loki 等日志平台。
Trace ID 由 API 网关生成,通过 HTTP Header X-Trace-ID 透传到所有下游服务,确保一次请求在日志系统中可被完整串联。
package middleware
import (
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"go.uber.org/zap"
)
func TraceAndLog(logger *zap.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
traceID := c.GetHeader("X-Trace-ID")
if traceID == "" {
traceID = uuid.New().String()
}
c.Set("trace_id", traceID)
c.Header("X-Trace-ID", traceID)
start := time.Now()
c.Next()
latency := time.Since(start)
logger.Info("api request",
zap.String("trace_id", traceID),
zap.String("method", c.Request.Method),
zap.String("path", c.Request.URL.Path),
zap.Int("status", c.Writer.Status()),
zap.String("client_ip", c.ClientIP()),
zap.Duration("latency", latency),
zap.Int("body_size", c.Writer.Size()),
)
}
}
10.2 Prometheus 自定义业务指标
在基础系统指标(CPU、内存、QPS)之外,我们针对小游戏业务定义了以下关键业务指标:
api_requests_total:按method、path、status分维度的请求总量计数器。api_request_duration_seconds:按路径分维度的请求延迟直方图。payment_orders_total:按status、pay_method分维度的订单数计数器。ad_requests_total:按slot_id、ad_type分维度的广告请求数。
package metrics
import (
"strconv"
"time"
"github.com/gin-gonic/gin"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
var (
RequestTotal = prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "api_requests_total",
Help: "Total number of API requests",
}, []string{"method", "path", "status"})
RequestDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{
Name: "api_request_duration_seconds",
Help: "API request latency in seconds",
Buckets: prometheus.DefBuckets,
}, []string{"method", "path"})
)
func init() {
prometheus.MustRegister(RequestTotal, RequestDuration)
}
func Prometheus() gin.HandlerFunc {
return func(c *gin.Context) {
if c.Request.URL.Path == "/metrics" {
c.Next()
return
}
start := time.Now()
c.Next()
duration := time.Since(start).Seconds()
status := strconv.Itoa(c.Writer.Status())
RequestTotal.WithLabelValues(c.Request.Method, c.FullPath(), status).Inc()
RequestDuration.WithLabelValues(c.Request.Method, c.FullPath()).Observe(duration)
}
}
// RegisterPrometheus 在 Gin 路由中暴露 /metrics 端点
func RegisterPrometheus(r *gin.Engine) {
r.GET("/metrics", gin.WrapH(promhttp.Handler()))
}
在 main.go 中依次挂载中间件即可自动采集所有请求的指标:
func main() {
r := gin.New()
logger, _ := zap.NewProduction()
r.Use(middleware.TraceAndLog(logger))
r.Use(middleware.Prometheus())
metrics.RegisterPrometheus(r)
routes.Register(r)
r.Run(":8080")
}
10.3 分布式链路追踪(OpenTelemetry + Jaeger)
小游戏平台涉及用户服务、游戏服务、支付服务、广告服务等多个微服务,一次完整请求可能跨越多个进程。通过 OpenTelemetry SDK 在 Gin 中间件中自动创建 Span,并将 Trace Context 通过 HTTP Header 传递到下游,即可在 Jaeger UI 中还原完整调用链。
package middleware
import (
"github.com/gin-gonic/gin"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/trace"
)
var tracer = otel.Tracer("minigame-api")
func OpenTelemetry() gin.HandlerFunc {
return func(c *gin.Context) {
ctx := otel.GetTextMapPropagator().Extract(c.Request.Context(), propagation.HeaderCarrier(c.Request.Header))
ctx, span := tracer.Start(ctx, c.FullPath(),
trace.WithAttributes(
attribute.String("http.method", c.Request.Method),
attribute.String("http.path", c.Request.URL.Path),
attribute.String("http.client_ip", c.ClientIP()),
),
)
defer span.End()
c.Set("otel_ctx", ctx)
c.Next()
span.SetAttributes(attribute.Int("http.status_code", c.Writer.Status()))
}
}
下游服务通过 c.MustGet("otel_ctx") 获取 Span Context,继续创建子 Span。搭配 otelgorm 等插件,还能将数据库查询也纳入链路中,实现从 API 入口到 SQL 执行的全链路可视化。
本章小结
详细设计了用户服务、游戏服务、支付服务、广告服务等全模块的 RESTful API 入参/出参与数据结构,为前后端联调提供统一契约。新增扩写了 API 设计原则与 RESTful 最佳实践、JWT 认证与安全中间件、请求参数校验与 Swagger 自动生成、支付回调防重放与验签、以及 API 可观测性等五大深度章节,包含可直接运行的 Go 代码实现,覆盖高并发小游戏平台在实际生产中的完整技术闭环。
延伸阅读
相关专题
本文属于 「小游戏服务平台」完整知识体系 。
如果你正在构建一个 SaaS 小游戏平台,可参考 后端架构白皮书 了解多租户、缓存、高可用等通用架构模式。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。