FIRST
CI / Go Backend (push) Canceled after 0s

This commit is contained in:
xyou
2026-08-07 23:39:47 +08:00
parent f2105bf14c
commit 9500c4c80a
52 changed files with 3801 additions and 0 deletions
+31
View File
@@ -0,0 +1,31 @@
.PHONY: build run-auth run-economy run-blueprint run-match run-social run-relay tidy lint test
build:
go build ./cmd/...
run-auth:
go run ./cmd/auth-service
run-economy:
go run ./cmd/economy-service
run-blueprint:
go run ./cmd/blueprint-service
run-match:
go run ./cmd/match-service
run-social:
go run ./cmd/social-service
run-relay:
go run ./cmd/relay-server
tidy:
go mod tidy
lint:
go vet ./...
test:
go test ./...
+26
View File
@@ -0,0 +1,26 @@
package main
import (
"log"
"mrcc/internal/config"
"mrcc/internal/httpserver"
"mrcc/internal/logger"
)
func main() {
cfg := config.Load("auth-service", "8081")
logger.WithService(cfg.Name).Info("starting service", "port", cfg.Port)
srv := httpserver.New(cfg)
// TODO: 注册认证路由
// r := srv.Group("/api/auth")
// r.POST("/register", ...)
// r.POST("/login", ...)
log.Printf("auth-service listening on %s", cfg.Address())
if err := srv.Run(cfg.Address()); err != nil {
log.Fatal(err)
}
}
+27
View File
@@ -0,0 +1,27 @@
package main
import (
"log"
"mrcc/internal/config"
"mrcc/internal/httpserver"
"mrcc/internal/logger"
)
func main() {
cfg := config.Load("blueprint-service", "8083")
logger.WithService(cfg.Name).Info("starting service", "port", cfg.Port)
srv := httpserver.New(cfg)
// TODO: 注册蓝图路由
// r := srv.Group("/api/blueprints")
// r.GET("/", ...)
// r.POST("/", ...)
// r.POST("/:id/buy", ...)
log.Printf("blueprint-service listening on %s", cfg.Address())
if err := srv.Run(cfg.Address()); err != nil {
log.Fatal(err)
}
}
+27
View File
@@ -0,0 +1,27 @@
package main
import (
"log"
"mrcc/internal/config"
"mrcc/internal/httpserver"
"mrcc/internal/logger"
)
func main() {
cfg := config.Load("economy-service", "8082")
logger.WithService(cfg.Name).Info("starting service", "port", cfg.Port)
srv := httpserver.New(cfg)
// TODO: 注册经济路由
// r := srv.Group("/api/user")
// r.GET("/balance", ...)
// r := srv.Group("/api/shop")
// r.GET("/items", ...)
log.Printf("economy-service listening on %s", cfg.Address())
if err := srv.Run(cfg.Address()); err != nil {
log.Fatal(err)
}
}
+27
View File
@@ -0,0 +1,27 @@
package main
import (
"log"
"mrcc/internal/config"
"mrcc/internal/httpserver"
"mrcc/internal/logger"
)
func main() {
cfg := config.Load("match-service", "8084")
logger.WithService(cfg.Name).Info("starting service", "port", cfg.Port)
srv := httpserver.New(cfg)
// TODO: 注册匹配路由
// r := srv.Group("/api/multiplayer")
// r.POST("/match", ...)
// r.POST("/join", ...)
// r.POST("/result", ...)
log.Printf("match-service listening on %s", cfg.Address())
if err := srv.Run(cfg.Address()); err != nil {
log.Fatal(err)
}
}
+167
View File
@@ -0,0 +1,167 @@
package main
import (
"log"
"net/http"
"sync"
"time"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"mrcc/internal/config"
"mrcc/internal/httpserver"
"mrcc/internal/logger"
"mrcc/pkg/response"
)
// === 数据结构骨架(对照开发文档 18.5 节)===
type RoomStatus string
const (
RoomCreated RoomStatus = "created"
RoomWaiting RoomStatus = "waiting"
RoomPlaying RoomStatus = "playing"
RoomFinished RoomStatus = "finished"
)
// Player 玩家连接
type Player struct {
UserID string `json:"user_id"`
Username string `json:"username"`
Conn *websocket.Conn `json:"-"`
IsReady bool `json:"is_ready"`
JoinedAt time.Time `json:"joined_at"`
SendQueue chan []byte `json:"-"`
}
// Room 房间
type Room struct {
ID string `json:"id"`
Status RoomStatus `json:"status"`
HostUserID string `json:"host_user_id"`
MaxPlayers int `json:"max_players"`
Players map[string]*Player `json:"players"`
CreatedAt time.Time `json:"created_at"`
mu sync.RWMutex `json:"-"`
}
// RelayServer 中转服务器
type RelayServer struct {
Rooms map[string]*Room
Players map[string]*Player
Register chan *Player
Unregister chan *Player
mu sync.RWMutex
}
func NewRelayServer() *RelayServer {
return &RelayServer{
Rooms: make(map[string]*Room),
Players: make(map[string]*Player),
Register: make(chan *Player, 64),
Unregister: make(chan *Player, 64),
}
}
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true // 开发阶段允许所有来源
},
}
// HandleWS 处理 WebSocket 连接升级
func HandleWS(relay *RelayServer) gin.HandlerFunc {
return func(c *gin.Context) {
userID := c.Query("user_id")
if userID == "" {
userID = "anonymous"
}
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
log.Printf("websocket upgrade failed: %v", err)
return
}
player := &Player{
UserID: userID,
Conn: conn,
JoinedAt: time.Now(),
SendQueue: make(chan []byte, 256),
}
relay.Register <- player
log.Printf("player connected: %s", userID)
// TODO: 实现读写 goroutine、房间管理、消息路由、断线重连
// 当前骨架仅保持连接并读取消息
go func() {
defer func() {
relay.Unregister <- player
conn.Close()
}()
for {
_, _, err := conn.ReadMessage()
if err != nil {
break
}
}
}()
}
}
func main() {
cfg := config.Load("relay-server", "8086")
logger.WithService(cfg.Name).Info("starting relay server", "port", cfg.Port)
relay := NewRelayServer()
// 后台处理注册/注销
go func() {
for {
select {
case player := <-relay.Register:
relay.mu.Lock()
relay.Players[player.UserID] = player
relay.mu.Unlock()
log.Printf("player registered: %s", player.UserID)
case player := <-relay.Unregister:
relay.mu.Lock()
delete(relay.Players, player.UserID)
relay.mu.Unlock()
log.Printf("player unregistered: %s", player.UserID)
}
}
}()
srv := httpserver.New(cfg)
// WebSocket 端点
srv.GET("/ws", HandleWS(relay))
// 房间信息端点(骨架)
srv.GET("/rooms", func(c *gin.Context) {
relay.mu.RLock()
defer relay.mu.RUnlock()
rooms := make([]gin.H, 0, len(relay.Rooms))
for id, room := range relay.Rooms {
rooms = append(rooms, gin.H{
"id": id,
"status": room.Status,
"playerCount": len(room.Players),
})
}
response.OK(c, gin.H{"rooms": rooms, "total": len(rooms)})
})
log.Printf("relay-server listening on %s (WS: /ws)", cfg.Address())
if err := srv.Run(cfg.Address()); err != nil {
log.Fatal(err)
}
}
+28
View File
@@ -0,0 +1,28 @@
package main
import (
"log"
"mrcc/internal/config"
"mrcc/internal/httpserver"
"mrcc/internal/logger"
)
func main() {
cfg := config.Load("social-service", "8085")
logger.WithService(cfg.Name).Info("starting service", "port", cfg.Port)
srv := httpserver.New(cfg)
// TODO: 注册社交路由
// r := srv.Group("/api/user")
// r.GET("/profile", ...)
// r.GET("/achievements", ...)
// r := srv.Group("/api/leaderboard")
// r.GET("/", ...)
log.Printf("social-service listening on %s", cfg.Address())
if err := srv.Run(cfg.Address()); err != nil {
log.Fatal(err)
}
}
+37
View File
@@ -0,0 +1,37 @@
module mrcc
go 1.22
require (
github.com/gin-gonic/gin v1.10.0
github.com/gorilla/websocket v1.5.3
)
require (
github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+58
View File
@@ -0,0 +1,58 @@
package config
import (
"fmt"
"os"
"strconv"
)
// ServiceConfig holds configuration shared across all microservices.
type ServiceConfig struct {
Name string // 服务名称,如 auth-service
Port string // 监听端口
// 基础设施连接(骨架阶段仅定义,暂不使用)
PostgresDSN string
RedisAddr string
MinIOAddr string
RabbitMQURL string
// 运行环境: debug / release
GinMode string
}
// Load 从环境变量加载配置,fallback 到默认值。
// serviceName 和 defaultPort 用于在没有设置环境变量时提供默认值。
func Load(serviceName, defaultPort string) *ServiceConfig {
cfg := &ServiceConfig{
Name: serviceName,
Port: envOr("SERVICE_PORT", defaultPort),
PostgresDSN: envOr("POSTGRES_DSN", "postgres://mrcc:mrcc_dev_2026@localhost:5432/mrcc?sslmode=disable"),
RedisAddr: envOr("REDIS_ADDR", "localhost:6379"),
MinIOAddr: envOr("MINIO_ADDR", "localhost:9000"),
RabbitMQURL: envOr("RABBITMQ_URL", "amqp://mrcc:mrcc_dev_2026@localhost:5672/"),
GinMode: envOr("GIN_MODE", "debug"),
}
return cfg
}
// Address 返回 :port 格式的监听地址
func (c *ServiceConfig) Address() string {
return fmt.Sprintf(":%s", c.Port)
}
func envOr(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}
func envIntOr(key string, fallback int) int {
if v := os.Getenv(key); v != "" {
if n, err := strconv.Atoi(v); err == nil {
return n
}
}
return fallback
}
+60
View File
@@ -0,0 +1,60 @@
package httpserver
import (
"time"
"github.com/gin-gonic/gin"
"mrcc/internal/config"
"mrcc/pkg/response"
)
// New 创建一个配置好中间件的 Gin 实例,并注册 /health 端点。
func New(cfg *config.ServiceConfig) *gin.Engine {
gin.SetMode(cfg.GinMode)
r := gin.New()
// 中间件
r.Use(gin.Recovery())
r.Use(gin.LoggerWithFormatter(func(p gin.LogFormatterParams) string {
return p.TimeStamp.Format(time.RFC3339) + " | " + p.Method + " | " + p.Path + " | " +
itoa(p.StatusCode) + " | " + p.Latency.String() + "\n"
}))
// CORS
r.Use(func(c *gin.Context) {
c.Header("Access-Control-Allow-Origin", "*")
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
})
// 健康检查
r.GET("/health", func(c *gin.Context) {
response.OK(c, gin.H{
"service": cfg.Name,
"status": "running",
})
})
return r
}
func itoa(n int) string {
if n == 0 {
return "0"
}
buf := [10]byte{}
pos := len(buf)
for n > 0 {
pos--
buf[pos] = byte('0' + n%10)
n /= 10
}
return string(buf[pos:])
}
+26
View File
@@ -0,0 +1,26 @@
package logger
import (
"log/slog"
"os"
)
// 全局默认 logger,使用 JSON handler 输出到 stdout。
var defaultLogger *slog.Logger
func init() {
defaultLogger = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
slog.SetDefault(defaultLogger)
}
// Default 返回全局 logger 实例
func Default() *slog.Logger {
return defaultLogger
}
// WithService 返回带有 service 字段的 logger
func WithService(name string) *slog.Logger {
return defaultLogger.With("service", name)
}
+27
View File
@@ -0,0 +1,27 @@
package response
import "github.com/gin-gonic/gin"
// 标准响应结构
// {"code": 0, "message": "ok", "data": ...}
const codeOK = 0
const codeError = 1
// OK 返回成功响应
func OK(c *gin.Context, data any) {
c.JSON(200, gin.H{
"code": codeOK,
"message": "ok",
"data": data,
})
}
// Error 返回错误响应
func Error(c *gin.Context, httpStatus int, message string) {
c.JSON(httpStatus, gin.H{
"code": codeError,
"message": message,
"data": nil,
})
}