增加用户管理
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.21-alpine AS builder
|
||||
FROM golang:1.24-alpine AS builder
|
||||
ENV GOPROXY=https://goproxy.cn,direct
|
||||
WORKDIR /app
|
||||
COPY go.mod go.sum ./
|
||||
|
||||
@@ -2,6 +2,8 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"doudizhu-server/internal/captcha"
|
||||
"doudizhu-server/internal/db"
|
||||
"doudizhu-server/internal/game"
|
||||
"doudizhu-server/internal/handlers"
|
||||
"doudizhu-server/internal/redis"
|
||||
@@ -10,15 +12,13 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
redisAddr := os.Getenv("REDIS_ADDR")
|
||||
if redisAddr == "" {
|
||||
redisAddr = "localhost:6379"
|
||||
}
|
||||
redisAddr := getEnv("REDIS_ADDR", "localhost:6379")
|
||||
|
||||
redisClient, err := redis.NewClient(redis.Config{
|
||||
Addr: redisAddr,
|
||||
@@ -31,13 +31,74 @@ func main() {
|
||||
defer redisClient.Close()
|
||||
log.Println("Connected to Redis:", redisAddr)
|
||||
|
||||
dbHost := getEnv("DB_HOST", "localhost")
|
||||
dbPort, _ := strconv.Atoi(getEnv("DB_PORT", "5432"))
|
||||
dbUser := getEnv("DB_USER", "postgres")
|
||||
dbPassword := getEnv("DB_PASSWORD", "postgres")
|
||||
dbName := getEnv("DB_NAME", "doudizhu")
|
||||
|
||||
database, err := db.New(db.Config{
|
||||
Host: dbHost,
|
||||
Port: dbPort,
|
||||
User: dbUser,
|
||||
Password: dbPassword,
|
||||
Database: dbName,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to connect to PostgreSQL: %v", err)
|
||||
}
|
||||
defer database.Close()
|
||||
log.Println("Connected to PostgreSQL:", dbHost)
|
||||
|
||||
gameMgr := game.NewGameManager(redisClient)
|
||||
hub := ws.NewHub(gameMgr)
|
||||
go hub.Run()
|
||||
|
||||
h := handlers.NewHandler(gameMgr, hub)
|
||||
captchaMgr := captcha.NewManager(redisClient)
|
||||
h := handlers.NewHandler(gameMgr, hub, redisClient, database, captchaMgr)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
|
||||
mux.HandleFunc("/api/auth/captcha", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "GET" {
|
||||
h.GetCaptcha(w, r)
|
||||
} else {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
})
|
||||
|
||||
mux.HandleFunc("/api/auth/register", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "POST" {
|
||||
h.Register(w, r)
|
||||
} else {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
})
|
||||
|
||||
mux.HandleFunc("/api/auth/login", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "POST" {
|
||||
h.Login(w, r)
|
||||
} else {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
})
|
||||
|
||||
mux.HandleFunc("/api/auth/validate", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "GET" {
|
||||
h.ValidateToken(w, r)
|
||||
} else {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
})
|
||||
|
||||
mux.HandleFunc("/api/auth/logout", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "POST" {
|
||||
h.Logout(w, r)
|
||||
} else {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
})
|
||||
|
||||
mux.HandleFunc("/api/rooms", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "POST" {
|
||||
h.CreateRoom(w, r)
|
||||
@@ -45,6 +106,23 @@ func main() {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
})
|
||||
|
||||
mux.HandleFunc("/api/rooms/current", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "GET" {
|
||||
h.GetCurrentRoom(w, r)
|
||||
} else {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
})
|
||||
|
||||
mux.HandleFunc("/api/rooms/leave", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "POST" {
|
||||
h.LeaveRoom(w, r)
|
||||
} else {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
})
|
||||
|
||||
mux.HandleFunc("/api/rooms/", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "POST" {
|
||||
h.JoinRoom(w, r)
|
||||
@@ -52,6 +130,7 @@ func main() {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
})
|
||||
|
||||
mux.HandleFunc("/api/ws", h.WebSocket)
|
||||
|
||||
srv := &http.Server{
|
||||
@@ -82,3 +161,10 @@ func main() {
|
||||
gameMgr.Stop()
|
||||
log.Println("Server stopped")
|
||||
}
|
||||
|
||||
func getEnv(key, defaultVal string) string {
|
||||
if val := os.Getenv(key); val != "" {
|
||||
return val
|
||||
}
|
||||
return defaultVal
|
||||
}
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
module doudizhu-server
|
||||
|
||||
go 1.21
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
github.com/gorilla/websocket v1.5.1
|
||||
github.com/jackc/pgx/v5 v5.8.0
|
||||
github.com/redis/go-redis/v9 v9.5.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
golang.org/x/net v0.17.0 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/text v0.29.0 // indirect
|
||||
)
|
||||
|
||||
26
app/go.sum
26
app/go.sum
@@ -4,11 +4,37 @@ github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo=
|
||||
github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8=
|
||||
github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
230
app/internal/captcha/captcha.go
Normal file
230
app/internal/captcha/captcha.go
Normal file
@@ -0,0 +1,230 @@
|
||||
package captcha
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"doudizhu-server/internal/redis"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
"image/png"
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
charset = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
|
||||
captchaLen = 4
|
||||
width = 120
|
||||
height = 40
|
||||
CaptchaTTL = 5 * time.Minute
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
rdb *redis.Client
|
||||
}
|
||||
|
||||
func NewManager(rdb *redis.Client) *Manager {
|
||||
return &Manager{rdb: rdb}
|
||||
}
|
||||
|
||||
func (m *Manager) Generate(ctx context.Context) (captchaID string, imageBase64 string, err error) {
|
||||
code := generateCode(captchaLen)
|
||||
captchaID = generateID()
|
||||
|
||||
img := createImage(code)
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := png.Encode(&buf, img); err != nil {
|
||||
return "", "", fmt.Errorf("failed to encode captcha image: %w", err)
|
||||
}
|
||||
|
||||
imageBase64 = "data:image/png;base64," + base64.StdEncoding.EncodeToString(buf.Bytes())
|
||||
|
||||
if err := m.rdb.Set(ctx, redis.CaptchaKey(captchaID), code, CaptchaTTL); err != nil {
|
||||
return "", "", fmt.Errorf("failed to store captcha: %w", err)
|
||||
}
|
||||
|
||||
return captchaID, imageBase64, nil
|
||||
}
|
||||
|
||||
func (m *Manager) Verify(ctx context.Context, captchaID, code string) bool {
|
||||
var storedCode string
|
||||
if err := m.rdb.Get(ctx, redis.CaptchaKey(captchaID), &storedCode); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
m.rdb.Delete(ctx, redis.CaptchaKey(captchaID))
|
||||
|
||||
return storedCode == code
|
||||
}
|
||||
|
||||
func generateCode(length int) string {
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
b := make([]byte, length)
|
||||
for i := range b {
|
||||
b[i] = charset[r.Intn(len(charset))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func generateID() string {
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
const idCharset = "abcdefghijklmnopqrstuvwxyz0123456789"
|
||||
b := make([]byte, 16)
|
||||
for i := range b {
|
||||
b[i] = idCharset[r.Intn(len(idCharset))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func createImage(code string) image.Image {
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
|
||||
img := image.NewRGBA(image.Rect(0, 0, width, height))
|
||||
|
||||
bgColor := color.RGBA{
|
||||
R: uint8(200 + r.Intn(56)),
|
||||
G: uint8(200 + r.Intn(56)),
|
||||
B: uint8(200 + r.Intn(56)),
|
||||
A: 255,
|
||||
}
|
||||
draw.Draw(img, img.Bounds(), &image.Uniform{bgColor}, image.Point{}, draw.Src)
|
||||
|
||||
for i := 0; i < 50; i++ {
|
||||
x := r.Intn(width)
|
||||
y := r.Intn(height)
|
||||
noiseColor := color.RGBA{
|
||||
R: uint8(r.Intn(256)),
|
||||
G: uint8(r.Intn(256)),
|
||||
B: uint8(r.Intn(256)),
|
||||
A: 255,
|
||||
}
|
||||
img.Set(x, y, noiseColor)
|
||||
}
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
x1 := r.Intn(width)
|
||||
y1 := r.Intn(height)
|
||||
x2 := r.Intn(width)
|
||||
y2 := r.Intn(height)
|
||||
lineColor := color.RGBA{
|
||||
R: uint8(r.Intn(200)),
|
||||
G: uint8(r.Intn(200)),
|
||||
B: uint8(r.Intn(200)),
|
||||
A: 255,
|
||||
}
|
||||
drawLine(img, x1, y1, x2, y2, lineColor)
|
||||
}
|
||||
|
||||
charWidth := width / captchaLen
|
||||
for i, c := range code {
|
||||
x := i*charWidth + charWidth/4
|
||||
y := 8 + r.Intn(10)
|
||||
|
||||
textColor := color.RGBA{
|
||||
R: uint8(r.Intn(150)),
|
||||
G: uint8(r.Intn(150)),
|
||||
B: uint8(r.Intn(150)),
|
||||
A: 255,
|
||||
}
|
||||
drawChar(img, x, y, string(c), textColor)
|
||||
}
|
||||
|
||||
return img
|
||||
}
|
||||
|
||||
func drawLine(img *image.RGBA, x1, y1, x2, y2 int, c color.Color) {
|
||||
dx := abs(x2 - x1)
|
||||
dy := abs(y2 - y1)
|
||||
sx, sy := 1, 1
|
||||
if x1 >= x2 {
|
||||
sx = -1
|
||||
}
|
||||
if y1 >= y2 {
|
||||
sy = -1
|
||||
}
|
||||
err := dx - dy
|
||||
|
||||
for {
|
||||
img.Set(x1, y1, c)
|
||||
if x1 == x2 && y1 == y2 {
|
||||
break
|
||||
}
|
||||
e2 := err * 2
|
||||
if e2 > -dy {
|
||||
err -= dy
|
||||
x1 += sx
|
||||
}
|
||||
if e2 < dx {
|
||||
err += dx
|
||||
y1 += sy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func abs(x int) int {
|
||||
if x < 0 {
|
||||
return -x
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
func drawChar(img *image.RGBA, x, y int, char string, c color.Color) {
|
||||
patterns := map[string][]string{
|
||||
"A": {" ## ", " # # ", "######", "# #", "# #"},
|
||||
"B": {"##### ", "# #", "##### ", "# #", "##### "},
|
||||
"C": {" #####", "# ", "# ", "# ", " #####"},
|
||||
"D": {"##### ", "# #", "# #", "# #", "##### "},
|
||||
"E": {"######", "# ", "#### ", "# ", "######"},
|
||||
"F": {"######", "# ", "#### ", "# ", "# "},
|
||||
"G": {" #####", "# ", "# ###", "# #", " #####"},
|
||||
"H": {"# #", "# #", "######", "# #", "# #"},
|
||||
"J": {" ####", " # ", " # ", "# # ", " ### "},
|
||||
"K": {"# # ", "# # ", "### ", "# # ", "# # "},
|
||||
"L": {"# ", "# ", "# ", "# ", "######"},
|
||||
"M": {"# #", "## ##", "# ## #", "# #", "# #"},
|
||||
"N": {"# #", "## #", "# # #", "# # #", "# ##"},
|
||||
"P": {"##### ", "# #", "##### ", "# ", "# "},
|
||||
"Q": {" #####", "# #", "# # #", "# # ", " #### #"},
|
||||
"R": {"##### ", "# #", "##### ", "# # ", "# # "},
|
||||
"S": {" #####", "# ", " #####", " #", "##### "},
|
||||
"T": {"######", " ## ", " ## ", " ## ", " ## "},
|
||||
"U": {"# #", "# #", "# #", "# #", " #### "},
|
||||
"V": {"# #", "# #", " # # ", " # # ", " ## "},
|
||||
"W": {"# #", "# #", "# ## #", "## ##", "# #"},
|
||||
"X": {"# #", " # # ", " ## ", " # # ", "# #"},
|
||||
"Y": {"# #", " # # ", " ## ", " ## ", " ## "},
|
||||
"Z": {"######", " ## ", " ## ", " ## ", "######"},
|
||||
"2": {" #####", "# #", " ## ", " # ", "######"},
|
||||
"3": {" #####", " #", " ####", " #", " #####"},
|
||||
"4": {"# #", "# #", "######", " #", " #"},
|
||||
"5": {"######", "# ", "##### ", " #", "##### "},
|
||||
"6": {" #####", "# ", "##### ", "# #", " #####"},
|
||||
"7": {"######", " # ", " # ", " # ", " # "},
|
||||
"8": {" #####", "# #", " #####", "# #", " #####"},
|
||||
"9": {" #####", "# #", " ######", " #", " #####"},
|
||||
}
|
||||
|
||||
pattern, ok := patterns[char]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
for row, line := range pattern {
|
||||
for col, ch := range line {
|
||||
if ch == '#' {
|
||||
px := x + col*2
|
||||
py := y + row*2
|
||||
if px >= 0 && px < width-1 && py >= 0 && py < height-1 {
|
||||
img.Set(px, py, c)
|
||||
img.Set(px+1, py, c)
|
||||
img.Set(px, py+1, c)
|
||||
img.Set(px+1, py+1, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
104
app/internal/db/db.go
Normal file
104
app/internal/db/db.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"doudizhu-server/internal/models"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Host string
|
||||
Port int
|
||||
User string
|
||||
Password string
|
||||
Database string
|
||||
}
|
||||
|
||||
type DB struct {
|
||||
pool *pgxpool.Pool
|
||||
}
|
||||
|
||||
func New(cfg Config) (*DB, error) {
|
||||
connStr := fmt.Sprintf(
|
||||
"host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
|
||||
cfg.Host, cfg.Port, cfg.User, cfg.Password, cfg.Database,
|
||||
)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
pool, err := pgxpool.New(ctx, connStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to database: %w", err)
|
||||
}
|
||||
|
||||
if err := pool.Ping(ctx); err != nil {
|
||||
pool.Close()
|
||||
return nil, fmt.Errorf("failed to ping database: %w", err)
|
||||
}
|
||||
|
||||
return &DB{pool: pool}, nil
|
||||
}
|
||||
|
||||
func (d *DB) Close() {
|
||||
if d.pool != nil {
|
||||
d.pool.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DB) CreateUser(ctx context.Context, user *models.User) error {
|
||||
query := `
|
||||
INSERT INTO users (id, username, password, nickname, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $5)
|
||||
`
|
||||
_, err := d.pool.Exec(ctx, query, user.ID, user.Username, user.Password, user.Nickname, time.Now())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create user: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DB) GetUserByUsername(ctx context.Context, username string) (*models.User, error) {
|
||||
query := `
|
||||
SELECT id, username, password, nickname
|
||||
FROM users
|
||||
WHERE username = $1
|
||||
`
|
||||
user := &models.User{}
|
||||
err := d.pool.QueryRow(ctx, query, username).Scan(
|
||||
&user.ID, &user.Username, &user.Password, &user.Nickname,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get user: %w", err)
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (d *DB) GetUserByID(ctx context.Context, id string) (*models.User, error) {
|
||||
query := `
|
||||
SELECT id, username, password, nickname
|
||||
FROM users
|
||||
WHERE id = $1
|
||||
`
|
||||
user := &models.User{}
|
||||
err := d.pool.QueryRow(ctx, query, id).Scan(
|
||||
&user.ID, &user.Username, &user.Password, &user.Nickname,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get user: %w", err)
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (d *DB) UsernameExists(ctx context.Context, username string) (bool, error) {
|
||||
query := `SELECT EXISTS(SELECT 1 FROM users WHERE username = $1)`
|
||||
var exists bool
|
||||
err := d.pool.QueryRow(ctx, query, username).Scan(&exists)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to check username: %w", err)
|
||||
}
|
||||
return exists, nil
|
||||
}
|
||||
@@ -263,6 +263,10 @@ func NewGameManager(rdb *redis.Client) *GameManager {
|
||||
}
|
||||
|
||||
func (gm *GameManager) CreateRoom(playerName string, maxPlayers int) (*models.Room, *models.Player) {
|
||||
return gm.CreateRoomWithUser("", playerName, maxPlayers)
|
||||
}
|
||||
|
||||
func (gm *GameManager) CreateRoomWithUser(userID, playerName string, maxPlayers int) (*models.Room, *models.Player) {
|
||||
gm.mu.Lock()
|
||||
defer gm.mu.Unlock()
|
||||
|
||||
@@ -270,7 +274,7 @@ func (gm *GameManager) CreateRoom(playerName string, maxPlayers int) (*models.Ro
|
||||
roomID := generateID()
|
||||
playerID := generateID()
|
||||
|
||||
player := &models.Player{ID: playerID, Name: playerName, IsOnline: true}
|
||||
player := &models.Player{ID: playerID, UserID: userID, Name: playerName, IsOnline: true}
|
||||
room := &models.Room{
|
||||
ID: roomID,
|
||||
Players: []*models.Player{player},
|
||||
@@ -289,6 +293,10 @@ func (gm *GameManager) CreateRoom(playerName string, maxPlayers int) (*models.Ro
|
||||
}
|
||||
|
||||
func (gm *GameManager) JoinRoom(roomID, playerName string) (*models.Room, *models.Player, error) {
|
||||
return gm.JoinRoomWithUser(roomID, "", playerName)
|
||||
}
|
||||
|
||||
func (gm *GameManager) JoinRoomWithUser(roomID, userID, playerName string) (*models.Room, *models.Player, error) {
|
||||
gm.mu.Lock()
|
||||
defer gm.mu.Unlock()
|
||||
|
||||
@@ -305,7 +313,7 @@ func (gm *GameManager) JoinRoom(roomID, playerName string) (*models.Room, *model
|
||||
}
|
||||
|
||||
playerID := generateID()
|
||||
player := &models.Player{ID: playerID, Name: playerName, IsOnline: true}
|
||||
player := &models.Player{ID: playerID, UserID: userID, Name: playerName, IsOnline: true}
|
||||
room.Players = append(room.Players, player)
|
||||
|
||||
gm.saveRoom(ctx, room)
|
||||
@@ -378,6 +386,25 @@ func (gm *GameManager) MarkPlayerOnline(roomID, playerID string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (gm *GameManager) MarkPlayerOffline(roomID, playerID string) {
|
||||
gm.mu.Lock()
|
||||
defer gm.mu.Unlock()
|
||||
|
||||
ctx := context.Background()
|
||||
room, err := gm.loadRoom(ctx, roomID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, p := range room.Players {
|
||||
if p.ID == playerID {
|
||||
p.IsOnline = false
|
||||
gm.saveRoom(ctx, room)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (gm *GameManager) SetReady(roomID, playerID string, ready bool) error {
|
||||
gm.mu.Lock()
|
||||
defer gm.mu.Unlock()
|
||||
@@ -581,6 +608,40 @@ func (gm *GameManager) GetRoom(roomID string) (*models.Room, error) {
|
||||
return gm.loadRoom(context.Background(), roomID)
|
||||
}
|
||||
|
||||
func (gm *GameManager) GetPlayerName(roomID, playerID string) string {
|
||||
gm.mu.RLock()
|
||||
defer gm.mu.RUnlock()
|
||||
|
||||
room, err := gm.loadRoom(context.Background(), roomID)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
for _, p := range room.Players {
|
||||
if p.ID == playerID {
|
||||
return p.Name
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (gm *GameManager) GetPlayerIDByUserID(roomID, userID string) string {
|
||||
gm.mu.RLock()
|
||||
defer gm.mu.RUnlock()
|
||||
|
||||
room, err := gm.loadRoom(context.Background(), roomID)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
for _, p := range room.Players {
|
||||
if p.UserID == userID {
|
||||
return p.ID
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (gm *GameManager) GetRoomState(roomID, playerID string) *models.Room {
|
||||
gm.mu.RLock()
|
||||
defer gm.mu.RUnlock()
|
||||
|
||||
@@ -1,42 +1,243 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"doudizhu-server/internal/captcha"
|
||||
"doudizhu-server/internal/db"
|
||||
"doudizhu-server/internal/game"
|
||||
"doudizhu-server/internal/models"
|
||||
"doudizhu-server/internal/redis"
|
||||
"doudizhu-server/internal/ws"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
GameMgr *game.GameManager
|
||||
Hub *ws.Hub
|
||||
Rdb *redis.Client
|
||||
DB *db.DB
|
||||
Captcha *captcha.Manager
|
||||
UserTTL time.Duration
|
||||
}
|
||||
|
||||
func NewHandler(gameMgr *game.GameManager, hub *ws.Hub) *Handler {
|
||||
return &Handler{GameMgr: gameMgr, Hub: hub}
|
||||
func NewHandler(gameMgr *game.GameManager, hub *ws.Hub, rdb *redis.Client, database *db.DB, captchaMgr *captcha.Manager) *Handler {
|
||||
return &Handler{
|
||||
GameMgr: gameMgr,
|
||||
Hub: hub,
|
||||
Rdb: rdb,
|
||||
DB: database,
|
||||
Captcha: captchaMgr,
|
||||
UserTTL: 24 * time.Hour,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) GetCaptcha(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.Background()
|
||||
captchaID, imageBase64, err := h.Captcha.Generate(ctx)
|
||||
if err != nil {
|
||||
h.jsonError(w, http.StatusInternalServerError, "failed to generate captcha")
|
||||
return
|
||||
}
|
||||
h.jsonSuccess(w, models.CaptchaResponse{
|
||||
CaptchaID: captchaID,
|
||||
Image: imageBase64,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) Register(w http.ResponseWriter, r *http.Request) {
|
||||
var req models.RegisterRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
h.jsonError(w, http.StatusBadRequest, "invalid request")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Username == "" || req.Password == "" || req.Nickname == "" {
|
||||
h.jsonError(w, http.StatusBadRequest, "username, password and nickname required")
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.Username) < 3 || len(req.Username) > 20 {
|
||||
h.jsonError(w, http.StatusBadRequest, "username must be 3-20 characters")
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.Password) < 6 {
|
||||
h.jsonError(w, http.StatusBadRequest, "password must be at least 6 characters")
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
if !h.Captcha.Verify(ctx, req.CaptchaID, strings.ToUpper(req.Captcha)) {
|
||||
h.jsonError(w, http.StatusBadRequest, "invalid captcha")
|
||||
return
|
||||
}
|
||||
|
||||
exists, err := h.DB.UsernameExists(ctx, req.Username)
|
||||
if err != nil {
|
||||
h.jsonError(w, http.StatusInternalServerError, "failed to check username")
|
||||
return
|
||||
}
|
||||
if exists {
|
||||
h.jsonError(w, http.StatusBadRequest, "username already exists")
|
||||
return
|
||||
}
|
||||
|
||||
user := &models.User{
|
||||
ID: generateUserID(),
|
||||
Username: req.Username,
|
||||
Password: hashPassword(req.Password),
|
||||
Nickname: req.Nickname,
|
||||
}
|
||||
|
||||
if err := h.DB.CreateUser(ctx, user); err != nil {
|
||||
h.jsonError(w, http.StatusInternalServerError, "failed to create user")
|
||||
return
|
||||
}
|
||||
|
||||
token := generateToken()
|
||||
h.Rdb.Set(ctx, redis.SessionKey(token), user.ID, h.UserTTL)
|
||||
|
||||
h.jsonSuccess(w, map[string]interface{}{
|
||||
"token": token,
|
||||
"userId": user.ID,
|
||||
"username": user.Username,
|
||||
"nickname": user.Nickname,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) Login(w http.ResponseWriter, r *http.Request) {
|
||||
var req models.LoginRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
h.jsonError(w, http.StatusBadRequest, "invalid request")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Username == "" || req.Password == "" {
|
||||
h.jsonError(w, http.StatusBadRequest, "username and password required")
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
if !h.Captcha.Verify(ctx, req.CaptchaID, strings.ToUpper(req.Captcha)) {
|
||||
h.jsonError(w, http.StatusBadRequest, "invalid captcha")
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.DB.GetUserByUsername(ctx, req.Username)
|
||||
if err != nil {
|
||||
h.jsonError(w, http.StatusUnauthorized, "invalid username or password")
|
||||
return
|
||||
}
|
||||
|
||||
if user.Password != hashPassword(req.Password) {
|
||||
h.jsonError(w, http.StatusUnauthorized, "invalid username or password")
|
||||
return
|
||||
}
|
||||
|
||||
token := generateToken()
|
||||
h.Rdb.Set(ctx, redis.SessionKey(token), user.ID, h.UserTTL)
|
||||
|
||||
h.jsonSuccess(w, map[string]interface{}{
|
||||
"token": token,
|
||||
"userId": user.ID,
|
||||
"username": user.Username,
|
||||
"nickname": user.Nickname,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) ValidateToken(w http.ResponseWriter, r *http.Request) {
|
||||
token := r.Header.Get("Authorization")
|
||||
if token == "" {
|
||||
h.jsonError(w, http.StatusUnauthorized, "token required")
|
||||
return
|
||||
}
|
||||
|
||||
token = strings.TrimPrefix(token, "Bearer ")
|
||||
|
||||
ctx := context.Background()
|
||||
var userID string
|
||||
if err := h.Rdb.Get(ctx, redis.SessionKey(token), &userID); err != nil {
|
||||
h.jsonError(w, http.StatusUnauthorized, "invalid token")
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.DB.GetUserByID(ctx, userID)
|
||||
if err != nil {
|
||||
h.jsonError(w, http.StatusUnauthorized, "user not found")
|
||||
return
|
||||
}
|
||||
|
||||
h.jsonSuccess(w, map[string]interface{}{
|
||||
"userId": user.ID,
|
||||
"username": user.Username,
|
||||
"nickname": user.Nickname,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) Logout(w http.ResponseWriter, r *http.Request) {
|
||||
token := r.Header.Get("Authorization")
|
||||
if token == "" {
|
||||
h.jsonSuccess(w, nil)
|
||||
return
|
||||
}
|
||||
|
||||
token = strings.TrimPrefix(token, "Bearer ")
|
||||
h.Rdb.Delete(context.Background(), redis.SessionKey(token))
|
||||
h.jsonSuccess(w, nil)
|
||||
}
|
||||
|
||||
func (h *Handler) CreateRoom(w http.ResponseWriter, r *http.Request) {
|
||||
userID, err := h.authenticate(r)
|
||||
if err != nil {
|
||||
h.jsonError(w, http.StatusUnauthorized, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var req models.CreateRoomRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
h.jsonError(w, http.StatusBadRequest, "invalid request")
|
||||
return
|
||||
}
|
||||
if req.PlayerName == "" {
|
||||
h.jsonError(w, http.StatusBadRequest, "player name required")
|
||||
return
|
||||
}
|
||||
|
||||
if req.MaxPlayers < 2 || req.MaxPlayers > 10 {
|
||||
req.MaxPlayers = 4
|
||||
}
|
||||
|
||||
room, player := h.GameMgr.CreateRoom(req.PlayerName, req.MaxPlayers)
|
||||
ctx := context.Background()
|
||||
|
||||
var currentRoomID string
|
||||
h.Rdb.Get(ctx, redis.UserRoomKey(userID), ¤tRoomID)
|
||||
if currentRoomID != "" {
|
||||
room, _ := h.GameMgr.GetRoom(currentRoomID)
|
||||
if room != nil {
|
||||
h.jsonError(w, http.StatusBadRequest, "you are already in a room, please leave first")
|
||||
return
|
||||
}
|
||||
h.Rdb.Delete(ctx, redis.UserRoomKey(userID))
|
||||
}
|
||||
|
||||
room, player := h.GameMgr.CreateRoomWithUser(userID, req.PlayerName, req.MaxPlayers)
|
||||
|
||||
h.Rdb.Set(ctx, redis.UserRoomKey(userID), room.ID, redis.RoomTTL)
|
||||
|
||||
h.jsonSuccess(w, map[string]string{"roomId": room.ID, "playerId": player.ID})
|
||||
}
|
||||
|
||||
func (h *Handler) JoinRoom(w http.ResponseWriter, r *http.Request) {
|
||||
userID, err := h.authenticate(r)
|
||||
if err != nil {
|
||||
h.jsonError(w, http.StatusUnauthorized, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
roomID := strings.ToLower(extractRoomID(r.URL.Path))
|
||||
if roomID == "" {
|
||||
h.jsonError(w, http.StatusBadRequest, "room id required")
|
||||
@@ -53,18 +254,109 @@ func (h *Handler) JoinRoom(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
room, player, err := h.GameMgr.JoinRoom(roomID, req.PlayerName)
|
||||
ctx := context.Background()
|
||||
|
||||
var currentRoomID string
|
||||
h.Rdb.Get(ctx, redis.UserRoomKey(userID), ¤tRoomID)
|
||||
if currentRoomID != "" && currentRoomID != roomID {
|
||||
room, _ := h.GameMgr.GetRoom(currentRoomID)
|
||||
if room != nil {
|
||||
h.jsonError(w, http.StatusBadRequest, "you are already in another room, please leave first")
|
||||
return
|
||||
}
|
||||
h.Rdb.Delete(ctx, redis.UserRoomKey(userID))
|
||||
}
|
||||
|
||||
room, player, err := h.GameMgr.JoinRoomWithUser(roomID, userID, req.PlayerName)
|
||||
if err != nil {
|
||||
h.jsonError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.Rdb.Set(ctx, redis.UserRoomKey(userID), room.ID, redis.RoomTTL)
|
||||
|
||||
h.jsonSuccess(w, map[string]string{"roomId": room.ID, "playerId": player.ID})
|
||||
}
|
||||
|
||||
func (h *Handler) GetCurrentRoom(w http.ResponseWriter, r *http.Request) {
|
||||
userID, err := h.authenticate(r)
|
||||
if err != nil {
|
||||
h.jsonError(w, http.StatusUnauthorized, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
var roomID string
|
||||
if err := h.Rdb.Get(ctx, redis.UserRoomKey(userID), &roomID); err != nil {
|
||||
h.jsonSuccess(w, nil)
|
||||
return
|
||||
}
|
||||
|
||||
room, err := h.GameMgr.GetRoom(roomID)
|
||||
if err != nil {
|
||||
h.Rdb.Delete(ctx, redis.UserRoomKey(userID))
|
||||
h.jsonSuccess(w, nil)
|
||||
return
|
||||
}
|
||||
|
||||
playerID := h.GameMgr.GetPlayerIDByUserID(roomID, userID)
|
||||
if playerID == "" {
|
||||
h.Rdb.Delete(ctx, redis.UserRoomKey(userID))
|
||||
h.jsonSuccess(w, nil)
|
||||
return
|
||||
}
|
||||
|
||||
h.jsonSuccess(w, map[string]string{"roomId": room.ID, "playerId": playerID})
|
||||
}
|
||||
|
||||
func (h *Handler) LeaveRoom(w http.ResponseWriter, r *http.Request) {
|
||||
userID, err := h.authenticate(r)
|
||||
if err != nil {
|
||||
h.jsonError(w, http.StatusUnauthorized, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
var roomID string
|
||||
if err := h.Rdb.Get(ctx, redis.UserRoomKey(userID), &roomID); err != nil {
|
||||
h.jsonSuccess(w, nil)
|
||||
return
|
||||
}
|
||||
|
||||
playerID := h.GameMgr.GetPlayerIDByUserID(roomID, userID)
|
||||
|
||||
if err := h.GameMgr.LeaveRoom(roomID, playerID); err != nil {
|
||||
h.jsonError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.Rdb.Delete(ctx, redis.UserRoomKey(userID))
|
||||
h.Hub.BroadcastRoomLeft(roomID, playerID)
|
||||
|
||||
h.jsonSuccess(w, nil)
|
||||
}
|
||||
|
||||
func (h *Handler) WebSocket(w http.ResponseWriter, r *http.Request) {
|
||||
ws.ServeWs(h.Hub, w, r)
|
||||
}
|
||||
|
||||
func (h *Handler) authenticate(r *http.Request) (string, error) {
|
||||
token := r.Header.Get("Authorization")
|
||||
if token == "" {
|
||||
return "", errors.New("token required")
|
||||
}
|
||||
|
||||
token = strings.TrimPrefix(token, "Bearer ")
|
||||
|
||||
ctx := context.Background()
|
||||
var userID string
|
||||
if err := h.Rdb.Get(ctx, redis.SessionKey(token), &userID); err != nil {
|
||||
return "", errors.New("invalid token")
|
||||
}
|
||||
|
||||
return userID, nil
|
||||
}
|
||||
|
||||
func (h *Handler) jsonSuccess(w http.ResponseWriter, data interface{}) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(models.ApiResponse{
|
||||
@@ -88,9 +380,30 @@ func (h *Handler) jsonError(w http.ResponseWriter, status int, msg string) {
|
||||
func extractRoomID(path string) string {
|
||||
parts := strings.Split(strings.Trim(path, "/"), "/")
|
||||
for i, p := range parts {
|
||||
if p == "rooms" && i+1 < len(parts) && parts[i+1] != "join" {
|
||||
if p == "rooms" && i+1 < len(parts) && parts[i+1] != "join" && parts[i+1] != "leave" {
|
||||
return parts[i+1]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func generateUserID() string {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyz0123456789"
|
||||
b := make([]byte, 16)
|
||||
for i := range b {
|
||||
b[i] = charset[time.Now().UnixNano()%int64(len(charset))]
|
||||
time.Sleep(time.Nanosecond)
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func generateToken() string {
|
||||
data := time.Now().String() + generateUserID()
|
||||
hash := sha256.Sum256([]byte(data))
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
func hashPassword(password string) string {
|
||||
hash := sha256.Sum256([]byte(password + "doudizhu_salt"))
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ const (
|
||||
|
||||
type Player struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"userId,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Cards []Card `json:"cards,omitempty"`
|
||||
CardCount int `json:"cardCount"`
|
||||
@@ -101,3 +102,39 @@ type CreateRoomRequest struct {
|
||||
type JoinRoomRequest struct {
|
||||
PlayerName string `json:"playerName"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"-"`
|
||||
Nickname string `json:"nickname"`
|
||||
}
|
||||
|
||||
type RegisterRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Nickname string `json:"nickname"`
|
||||
Captcha string `json:"captcha"`
|
||||
CaptchaID string `json:"captchaId"`
|
||||
}
|
||||
|
||||
type LoginRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Captcha string `json:"captcha"`
|
||||
CaptchaID string `json:"captchaId"`
|
||||
}
|
||||
|
||||
type CaptchaResponse struct {
|
||||
CaptchaID string `json:"captchaId"`
|
||||
Image string `json:"image"`
|
||||
}
|
||||
|
||||
type LeaveRoomRequest struct {
|
||||
PlayerID string `json:"playerId"`
|
||||
}
|
||||
|
||||
type PlayerRoomInfo struct {
|
||||
UserID string `json:"userId"`
|
||||
RoomID string `json:"roomId"`
|
||||
}
|
||||
|
||||
@@ -250,3 +250,23 @@ func RoomTTLKey(roomID string) string {
|
||||
func PlayerTTLKey(playerID string) string {
|
||||
return "player:" + playerID + ":ttl"
|
||||
}
|
||||
|
||||
func CaptchaKey(captchaID string) string {
|
||||
return "captcha:" + captchaID
|
||||
}
|
||||
|
||||
func UserKey(username string) string {
|
||||
return "user:" + username
|
||||
}
|
||||
|
||||
func UserRoomKey(userID string) string {
|
||||
return "user:" + userID + ":room"
|
||||
}
|
||||
|
||||
func SessionKey(token string) string {
|
||||
return "session:" + token
|
||||
}
|
||||
|
||||
func UserIDToUsernameKey(userID string) string {
|
||||
return "user_id:" + userID + ":username"
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ func (h *Hub) Run() {
|
||||
}
|
||||
h.mu.Unlock()
|
||||
if c.RoomID != "" {
|
||||
h.GameMgr.LeaveRoomWithTTL(c.RoomID, c.ID)
|
||||
h.GameMgr.MarkPlayerOffline(c.RoomID, c.ID)
|
||||
h.broadcastRoomState(c.RoomID)
|
||||
}
|
||||
|
||||
@@ -201,7 +201,20 @@ func (c *Client) handleMessage(data []byte) {
|
||||
c.Hub.broadcastRoomState(c.RoomID)
|
||||
|
||||
case models.MsgTypeChat:
|
||||
c.Hub.broadcastToRoom(c.RoomID, msg)
|
||||
chatMsg, ok := msg.Data.(string)
|
||||
if !ok || chatMsg == "" {
|
||||
return
|
||||
}
|
||||
playerName := c.Hub.GameMgr.GetPlayerName(c.RoomID, c.ID)
|
||||
c.Hub.broadcastToRoom(c.RoomID, models.Message{
|
||||
Type: models.MsgTypeChat,
|
||||
Data: map[string]string{
|
||||
"playerId": c.ID,
|
||||
"playerName": playerName,
|
||||
"message": chatMsg,
|
||||
},
|
||||
Timestamp: time.Now().Unix(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -300,6 +313,33 @@ func (h *Hub) broadcastRoomState(roomID string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Hub) BroadcastRoomLeft(roomID, playerID string) {
|
||||
h.mu.RLock()
|
||||
clients := make([]*Client, 0)
|
||||
for _, c := range h.Clients {
|
||||
if c.RoomID == roomID {
|
||||
clients = append(clients, c)
|
||||
}
|
||||
}
|
||||
h.mu.RUnlock()
|
||||
|
||||
data, _ := json.Marshal(models.Message{
|
||||
Type: models.MsgTypeLeave,
|
||||
PlayerID: playerID,
|
||||
Timestamp: time.Now().Unix(),
|
||||
})
|
||||
|
||||
for _, c := range clients {
|
||||
select {
|
||||
case c.Send <- data:
|
||||
case <-c.done:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
h.broadcastRoomState(roomID)
|
||||
}
|
||||
|
||||
func ServeWs(hub *Hub, w http.ResponseWriter, r *http.Request) {
|
||||
roomID := strings.ToLower(r.URL.Query().Get("roomId"))
|
||||
playerID := r.URL.Query().Get("playerId")
|
||||
|
||||
Reference in New Issue
Block a user