Files
MMGame/backend/internal/handlers/helpers.go
T
gmh01 7614e79d12 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.
2026-08-03 11:39:53 +08:00

219 lines
5.6 KiB
Go

package handlers
import (
"crypto/rand"
"database/sql"
"encoding/json"
"fmt"
"net/http"
"mmgame/internal/database"
"mmgame/internal/models"
"mmgame/internal/scripts"
"mmgame/internal/util"
)
func newID() string {
b := make([]byte, 16)
_, _ = rand.Read(b)
b[6] = (b[6] & 0x0f) | 0x40
b[8] = (b[8] & 0x3f) | 0x80
return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16])
}
func writeJSON(w http.ResponseWriter, status int, v interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(v)
}
func writeError(w http.ResponseWriter, status int, detail string) {
writeJSON(w, status, map[string]string{"detail": detail})
}
func nullStr(s *string) string {
if s == nil {
return ""
}
return *s
}
func Health(w http.ResponseWriter, r *http.Request) {
writeJSON(w, 200, map[string]string{"status": "ok"})
}
func parseConfig(configStr string) map[string]interface{} {
if configStr == "" {
return map[string]interface{}{}
}
var m map[string]interface{}
if err := json.Unmarshal([]byte(configStr), &m); err != nil {
return map[string]interface{}{}
}
return m
}
func scriptFromConfig(configStr string) *scripts.Script {
cfg := parseConfig(configStr)
raw, ok := cfg["script"]
if !ok || raw == nil {
return nil
}
b, err := json.Marshal(raw)
if err != nil {
return nil
}
var s scripts.Script
if err := json.Unmarshal(b, &s); err != nil {
return nil
}
return &s
}
func getGame(gameID string) (*models.GameSession, error) {
row := database.DB.QueryRow(
`SELECT id, script_id, status, phase, phase_index, started_at, completed_at, config, created_at
FROM game_sessions WHERE id = ?`, gameID)
var g models.GameSession
var startedAt, completedAt, configStr, createdAt sql.NullString
err := row.Scan(&g.ID, &g.ScriptID, &g.Status, &g.Phase, &g.PhaseIndex,
&startedAt, &completedAt, &configStr, &createdAt)
if err != nil {
return nil, err
}
if startedAt.Valid {
t := util.ParseTime(startedAt.String)
g.StartedAt = &t
}
if completedAt.Valid {
t := util.ParseTime(completedAt.String)
g.CompletedAt = &t
}
g.Config = configStr.String
g.CreatedAt = util.ParseTime(createdAt.String)
return &g, nil
}
func queryPlayers(gameID string) []*models.SessionPlayer {
rows, err := database.DB.Query(
`SELECT id, session_id, user_id, role_id, role_name, is_human, is_ready, avatar_url, status, joined_at
FROM session_players WHERE session_id = ?`, gameID)
if err != nil {
return nil
}
defer rows.Close()
var list []*models.SessionPlayer
for rows.Next() {
p := &models.SessionPlayer{}
var userID, avatarURL, joinedAt sql.NullString
if err := rows.Scan(&p.ID, &p.SessionID, &userID, &p.RoleID, &p.RoleName,
&p.IsHuman, &p.IsReady, &avatarURL, &p.Status, &joinedAt); err != nil {
continue
}
if userID.Valid {
p.UserID = &userID.String
}
if avatarURL.Valid {
p.AvatarURL = &avatarURL.String
}
p.JoinedAt = util.ParseTime(joinedAt.String)
list = append(list, p)
}
return list
}
func queryMessages(gameID string) []*models.ChatMessage {
rows, err := database.DB.Query(
`SELECT id, session_id, sender_role_id, sender_name, message_type, content, target_role_id, clue_id, phase_index, created_at
FROM chat_messages WHERE session_id = ? ORDER BY created_at`, gameID)
if err != nil {
return nil
}
defer rows.Close()
var list []*models.ChatMessage
for rows.Next() {
m := &models.ChatMessage{}
var targetRoleID, clueID, createdAt sql.NullString
if err := rows.Scan(&m.ID, &m.SessionID, &m.SenderRoleID, &m.SenderName, &m.MessageType,
&m.Content, &targetRoleID, &clueID, &m.PhaseIndex, &createdAt); err != nil {
continue
}
if targetRoleID.Valid {
m.TargetRoleID = &targetRoleID.String
}
if clueID.Valid {
m.ClueID = &clueID.String
}
m.CreatedAt = util.ParseTime(createdAt.String)
list = append(list, m)
}
return list
}
func queryClues(gameID string) []*models.ClueState {
rows, err := database.DB.Query(
`SELECT id, session_id, clue_id, status, revealed_by, released_at, created_at
FROM clues_state WHERE session_id = ?`, gameID)
if err != nil {
return nil
}
defer rows.Close()
var list []*models.ClueState
for rows.Next() {
c := &models.ClueState{}
var revealedBy, releasedAt, createdAt sql.NullString
if err := rows.Scan(&c.ID, &c.SessionID, &c.ClueID, &c.Status, &revealedBy, &releasedAt, &createdAt); err != nil {
continue
}
if revealedBy.Valid {
c.RevealedBy = &revealedBy.String
}
if releasedAt.Valid {
t := util.ParseTime(releasedAt.String)
c.ReleasedAt = &t
}
c.CreatedAt = util.ParseTime(createdAt.String)
list = append(list, c)
}
return list
}
func queryVotes(gameID string) []*models.Vote {
rows, err := database.DB.Query(
`SELECT id, session_id, round, voter_id, target_id, reason, created_at
FROM votes WHERE session_id = ?`, gameID)
if err != nil {
return nil
}
defer rows.Close()
var list []*models.Vote
for rows.Next() {
v := &models.Vote{}
var reason, createdAt sql.NullString
if err := rows.Scan(&v.ID, &v.SessionID, &v.Round, &v.VoterID, &v.TargetID, &reason, &createdAt); err != nil {
continue
}
if reason.Valid {
v.Reason = &reason.String
}
v.CreatedAt = util.ParseTime(createdAt.String)
list = append(list, v)
}
return list
}
func messageDict(m *models.ChatMessage) map[string]interface{} {
d := map[string]interface{}{
"id": m.ID,
"sender_role_id": m.SenderRoleID,
"sender_name": m.SenderName,
"message_type": m.MessageType,
"content": m.Content,
"target_role_id": m.TargetRoleID,
"clue_id": m.ClueID,
"phase_index": m.PhaseIndex,
"created_at": util.TimeStr(m.CreatedAt),
}
return d
}