@@ -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 ./...
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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:])
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user