refactor: rewrite backend in Go, replacing Python FastAPI
Port the FastAPI backend to Go 1.26, preserving all API functionality: - auth (register/login/guest/logout/profile/history) with JWT + bcrypt - scripts (list/detail) backed by embedded sample_data.json - games (create/join/start/chat/phase/clue/vote/state/replay) + WebSocket - rule-based NPC agent plus DeepSeek/Ollama AI backends - SQLite persistence via modernc.org/sqlite (pure Go, no cgo) Frontend and documents are unchanged.
This commit is contained in:
@@ -0,0 +1,263 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"mmgame/internal/config"
|
||||
"mmgame/internal/database"
|
||||
"mmgame/internal/models"
|
||||
"mmgame/internal/util"
|
||||
)
|
||||
|
||||
var errUnauthorized = errors.New("unauthorized")
|
||||
|
||||
func createToken(userID string) (string, error) {
|
||||
exp := time.Now().Add(time.Duration(config.Cfg.AccessTokenExpireMinutes) * time.Minute)
|
||||
claims := jwt.MapClaims{
|
||||
"sub": userID,
|
||||
"exp": exp.Unix(),
|
||||
"v": 0,
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString([]byte(config.Cfg.SecretKey))
|
||||
}
|
||||
|
||||
func currentUser(r *http.Request) (*models.User, error) {
|
||||
auth := r.Header.Get("Authorization")
|
||||
if !strings.HasPrefix(auth, "Bearer ") {
|
||||
return nil, errUnauthorized
|
||||
}
|
||||
tokenStr := strings.TrimPrefix(auth, "Bearer ")
|
||||
claims := jwt.MapClaims{}
|
||||
token, err := jwt.ParseWithClaims(tokenStr, claims, func(t *jwt.Token) (interface{}, error) {
|
||||
return []byte(config.Cfg.SecretKey), nil
|
||||
}, jwt.WithValidMethods([]string{config.Cfg.Algorithm}))
|
||||
if err != nil || !token.Valid {
|
||||
return nil, errUnauthorized
|
||||
}
|
||||
userID, _ := claims["sub"].(string)
|
||||
if userID == "" {
|
||||
return nil, errUnauthorized
|
||||
}
|
||||
row := database.DB.QueryRow(
|
||||
`SELECT id, email, nickname, avatar_url, password_hash, role, is_guest, game_count, token_version, created_at
|
||||
FROM users WHERE id = ?`, userID)
|
||||
var u models.User
|
||||
var email, nickname, avatarURL, passwordHash, createdAt sql.NullString
|
||||
if err := row.Scan(&u.ID, &email, &nickname, &avatarURL, &passwordHash, &u.Role,
|
||||
&u.IsGuest, &u.GameCount, &u.TokenVersion, &createdAt); err != nil {
|
||||
return nil, errUnauthorized
|
||||
}
|
||||
if email.Valid {
|
||||
u.Email = &email.String
|
||||
}
|
||||
if nickname.Valid {
|
||||
u.Nickname = &nickname.String
|
||||
}
|
||||
if avatarURL.Valid {
|
||||
u.AvatarURL = &avatarURL.String
|
||||
}
|
||||
if passwordHash.Valid {
|
||||
u.PasswordHash = &passwordHash.String
|
||||
}
|
||||
u.CreatedAt = util.ParseTime(createdAt.String)
|
||||
return &u, nil
|
||||
}
|
||||
|
||||
func Register(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
Nickname string `json:"nickname"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, 422, "请求格式错误")
|
||||
return
|
||||
}
|
||||
var existingID string
|
||||
err := database.DB.QueryRow(`SELECT id FROM users WHERE email = ?`, req.Email).Scan(&existingID)
|
||||
if err == nil {
|
||||
writeError(w, 400, "邮箱已注册")
|
||||
return
|
||||
}
|
||||
if err != sql.ErrNoRows {
|
||||
writeError(w, 500, "服务器错误")
|
||||
return
|
||||
}
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
writeError(w, 500, "服务器错误")
|
||||
return
|
||||
}
|
||||
userID := newID()
|
||||
_, err = database.DB.Exec(
|
||||
`INSERT INTO users (id, email, nickname, password_hash, role, is_guest, game_count, token_version, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, 0, 0, 0, ?)`,
|
||||
userID, req.Email, req.Nickname, string(hash), models.RolePlayer, util.NowStr())
|
||||
if err != nil {
|
||||
writeError(w, 500, "服务器错误")
|
||||
return
|
||||
}
|
||||
token, _ := createToken(userID)
|
||||
writeJSON(w, 200, map[string]interface{}{
|
||||
"token": token,
|
||||
"user_id": userID,
|
||||
"nickname": req.Nickname,
|
||||
"is_guest": false,
|
||||
})
|
||||
}
|
||||
|
||||
func Login(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, 422, "请求格式错误")
|
||||
return
|
||||
}
|
||||
var id, email, nickname, passwordHash string
|
||||
err := database.DB.QueryRow(
|
||||
`SELECT id, email, nickname, password_hash FROM users WHERE email = ?`, req.Email).
|
||||
Scan(&id, &email, &nickname, &passwordHash)
|
||||
if err == sql.ErrNoRows || passwordHash == "" {
|
||||
writeError(w, 401, "邮箱或密码错误")
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
writeError(w, 500, "服务器错误")
|
||||
return
|
||||
}
|
||||
if bcrypt.CompareHashAndPassword([]byte(passwordHash), []byte(req.Password)) != nil {
|
||||
writeError(w, 401, "邮箱或密码错误")
|
||||
return
|
||||
}
|
||||
token, _ := createToken(id)
|
||||
writeJSON(w, 200, map[string]interface{}{
|
||||
"token": token,
|
||||
"user_id": id,
|
||||
"nickname": nickname,
|
||||
"is_guest": false,
|
||||
})
|
||||
}
|
||||
|
||||
func GuestLogin(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Nickname string `json:"nickname"`
|
||||
}
|
||||
_ = json.NewDecoder(r.Body).Decode(&req)
|
||||
if req.Nickname == "" {
|
||||
req.Nickname = "游客"
|
||||
}
|
||||
userID := newID()
|
||||
_, err := database.DB.Exec(
|
||||
`INSERT INTO users (id, nickname, role, is_guest, game_count, token_version, created_at)
|
||||
VALUES (?, ?, ?, 1, 0, 0, ?)`,
|
||||
userID, req.Nickname, models.RolePlayer, util.NowStr())
|
||||
if err != nil {
|
||||
writeError(w, 500, "服务器错误")
|
||||
return
|
||||
}
|
||||
token, _ := createToken(userID)
|
||||
writeJSON(w, 200, map[string]interface{}{
|
||||
"token": token,
|
||||
"user_id": userID,
|
||||
"nickname": req.Nickname,
|
||||
"is_guest": true,
|
||||
})
|
||||
}
|
||||
|
||||
func Logout(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, 200, map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
func GetProfile(w http.ResponseWriter, r *http.Request) {
|
||||
user, err := currentUser(r)
|
||||
if err != nil {
|
||||
writeError(w, 401, "未登录")
|
||||
return
|
||||
}
|
||||
writeJSON(w, 200, map[string]interface{}{
|
||||
"user_id": user.ID,
|
||||
"email": nullStr(user.Email),
|
||||
"nickname": nullStr(user.Nickname),
|
||||
"avatar_url": nullStr(user.AvatarURL),
|
||||
"is_guest": user.IsGuest,
|
||||
"role": user.Role,
|
||||
"game_count": user.GameCount,
|
||||
"created_at": util.TimeStr(user.CreatedAt),
|
||||
})
|
||||
}
|
||||
|
||||
func UpdateProfile(w http.ResponseWriter, r *http.Request) {
|
||||
user, err := currentUser(r)
|
||||
if err != nil {
|
||||
writeError(w, 401, "未登录")
|
||||
return
|
||||
}
|
||||
var data map[string]interface{}
|
||||
_ = json.NewDecoder(r.Body).Decode(&data)
|
||||
|
||||
var nickname, avatarURL *string
|
||||
if v, ok := data["nickname"].(string); ok {
|
||||
nickname = &v
|
||||
}
|
||||
if v, ok := data["avatar_url"].(string); ok {
|
||||
avatarURL = &v
|
||||
}
|
||||
if nickname != nil {
|
||||
if _, err := database.DB.Exec(`UPDATE users SET nickname = ? WHERE id = ?`, *nickname, user.ID); err != nil {
|
||||
writeError(w, 500, "服务器错误")
|
||||
return
|
||||
}
|
||||
}
|
||||
if avatarURL != nil {
|
||||
if _, err := database.DB.Exec(`UPDATE users SET avatar_url = ? WHERE id = ?`, *avatarURL, user.ID); err != nil {
|
||||
writeError(w, 500, "服务器错误")
|
||||
return
|
||||
}
|
||||
}
|
||||
writeJSON(w, 200, map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
func GetHistory(w http.ResponseWriter, r *http.Request) {
|
||||
if _, err := currentUser(r); err != nil {
|
||||
writeError(w, 401, "未登录")
|
||||
return
|
||||
}
|
||||
rows, err := database.DB.Query(
|
||||
`SELECT id, script_id, status, phase, phase_index, started_at, completed_at, created_at
|
||||
FROM game_sessions ORDER BY created_at DESC LIMIT 50`)
|
||||
if err != nil {
|
||||
writeError(w, 500, "服务器错误")
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []map[string]interface{}
|
||||
for rows.Next() {
|
||||
var id, scriptID, status, phase, startedAt, completedAt, createdAt sql.NullString
|
||||
var phaseIndex int
|
||||
if err := rows.Scan(&id, &scriptID, &status, &phase, &phaseIndex, &startedAt, &completedAt, &createdAt); err != nil {
|
||||
continue
|
||||
}
|
||||
items = append(items, map[string]interface{}{
|
||||
"id": id.String,
|
||||
"script_id": scriptID.String,
|
||||
"status": status.String,
|
||||
"phase": phase.String,
|
||||
"phase_index": phaseIndex,
|
||||
"started_at": startedAt.String,
|
||||
"completed_at": completedAt.String,
|
||||
"created_at": createdAt.String,
|
||||
})
|
||||
}
|
||||
writeJSON(w, 200, items)
|
||||
}
|
||||
Reference in New Issue
Block a user