Bugfixes. Optimizations/refactor. Add redis for player-cache. Add docker files. Replace sqlite dep. Single-Calc for existing players. Game-Metrics in JSON.

This commit is contained in:
MaxJa4
2024-01-21 00:49:20 +01:00
parent 069d76520e
commit 16d782fbe8
41 changed files with 1154 additions and 203 deletions

View File

@@ -3,37 +3,29 @@ package controllers
import (
"InfantrySkillCalculator/models"
"InfantrySkillCalculator/utils"
"context"
"errors"
"fmt"
"github.com/gin-gonic/gin"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"internal/cache"
"log"
"net/http"
"time"
)
type AddCacheInput struct {
PlayerID uint `json:"player_id" binding:"required"`
CacheDate time.Time `json:"date" binding:"required"`
Score float32 `json:"score" gorm:"default:-1.0"`
Game string `json:"game" binding:"required"`
PlayerID uint `json:"player_id" binding:"required"`
Score float32 `json:"score" gorm:"default:-1.0"`
GameTag string `json:"game_tag" binding:"required"`
}
type UpdateCacheInput struct {
CacheDate time.Time `json:"date" binding:"required"`
Score float32 `json:"score" gorm:"default:-1.0"`
Score float32 `json:"score" gorm:"default:-1.0"`
}
var ctx = context.Background()
// GetCacheByPlayerID GET /cache/:player_id?game_id=TAG
// GetCacheByPlayerID GET /cache/:player_id?game_tag=TAG
func GetCacheByPlayerID(c *gin.Context) {
playerId := utils.StringToUint(c.Param("player_id"))
gameId := utils.StringToUint(c.Request.URL.Query().Get("game_id"))
gameTag := c.Request.URL.Query().Get("game_tag")
val, err := GetCacheByPlayerIDGorm(playerId, gameId)
val, err := cache.GetScore(playerId, gameTag)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"})
} else {
@@ -41,19 +33,6 @@ func GetCacheByPlayerID(c *gin.Context) {
}
}
func GetCacheByPlayerIDGorm(playerId uint, gameId uint) (float32, error) {
key := fmt.Sprintf("player:%d:game:%d", playerId, gameId)
val, err := models.Cache.Get(ctx, key).Result()
if errors.Is(err, redis.Nil) {
return -1.0, nil // cache miss
} else if err != nil {
return -1.0, err // cache error
} else {
return utils.StringToFloat(val), nil // cache hit
}
}
// AddCache POST /cache
func AddCache(c *gin.Context) {
var input AddCacheInput
@@ -63,69 +42,84 @@ func AddCache(c *gin.Context) {
}
var game models.Game
if err := FindGameByTag(&game, input.Game).Error; err != nil {
if err := FindGameByTag(&game, input.GameTag).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Game not found!"})
return
}
cache := models.PlayerCache{CacheDate: input.CacheDate, PlayerID: input.PlayerID, Score: input.Score, Game: input.Game}
err := cache.SetScore(input.PlayerID, input.GameTag, input.Score)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Cache update failed! Error: " + err.Error()})
return
}
c.JSON(http.StatusOK, cache)
c.JSON(http.StatusOK, nil)
}
// UpdateCacheByPlayerID PATCH /cache/:id?game=TAG
func UpdateCacheByPlayerID(c *gin.Context) {
var cache models.PlayerCache
if err := FindCacheGin(&cache, c).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"})
return
playerID := utils.StringToUint(c.Param("id"))
gameTag := c.Request.URL.Query().Get("game")
score := utils.StringToFloat(c.PostForm("score"))
err := cache.SetScore(playerID, gameTag, score)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Cache update failed! Error: " + err.Error()})
} else {
c.JSON(http.StatusOK, nil)
}
var input UpdateCacheInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
models.DB.Model(&cache).Updates(map[string]interface{}{
"CacheDate": input.CacheDate,
"Score": input.Score,
})
c.JSON(http.StatusOK, cache)
}
// DeleteCacheByPlayerID DELETE /cache/:id?game=TAG
func DeleteCacheByPlayerID(c *gin.Context) {
var cache models.PlayerCache
if err := FindCacheGin(&cache, c).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"})
return
playerID := utils.StringToUint(c.Param("id"))
gameTag := c.Request.URL.Query().Get("game")
if err := cache.DeleteScore(playerID, gameTag); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
} else {
c.JSON(http.StatusOK, nil)
}
models.DB.Delete(&cache)
c.JSON(http.StatusOK, true)
}
// DeleteAllCaches DELETE /cache
func DeleteAllCaches(c *gin.Context) {
var caches []models.PlayerCache
if err := cache.PurgeCache(); err != nil {
c.String(http.StatusBadRequest, err.Error())
} else {
c.String(http.StatusOK, "Purged all caches!")
}
}
// GetScoreByPlayerID GET /score/:player_id?game_tag=TAG
func GetScoreByPlayerID(c *gin.Context) {
var player models.Player
var gameTag = c.Request.URL.Query().Get("game_tag")
var playerId = utils.StringToUint(c.Param("player_id"))
if err := models.DB.
Session(&gorm.Session{AllowGlobalUpdate: true}).
Clauses(clause.Returning{}).
Delete(&caches).Error; err != nil {
c.String(http.StatusBadRequest, "Purge failed! Error: "+err.Error())
Model(&models.Player{}).
Where("id = ?", playerId).
First(&player).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Player not found!"})
return
}
c.String(http.StatusOK, "Purged "+utils.UintToString(uint(len(caches)))+" caches!")
}
score, err := cache.GetScore(player.ID, gameTag)
if err != nil || score == -1 {
score = utils.CalcPlayerScore(player.Name, gameTag)
if score == score && score != -1 { // not NaN
if err := cache.SetScore(player.ID, gameTag, score); err != nil {
log.Fatal(err)
}
}
}
func FindCacheGin(out interface{}, c *gin.Context) *gorm.DB {
return FindCache(out, utils.StringToUint(c.Param("id")), c.Request.URL.Query().Get("game"))
}
func FindCache(out interface{}, id uint, game string) *gorm.DB {
return models.DB.Where("player_id = ?", id).Where("game = ?", game).First(out)
if score != score || score == -1 { // NaN
c.String(http.StatusOK, "<i class=\"bi bi-person-x-fill me-2 text-danger fs-5\" style=\"margin-left: 0.69rem;\"></i>")
} else if score != -1 {
c.String(http.StatusOK, fmt.Sprintf("%.2f", score))
} else {
c.JSON(http.StatusBadRequest, "Invalid request!")
}
}

View File

@@ -0,0 +1,32 @@
package controllers
import (
"InfantrySkillCalculator/models"
"InfantrySkillCalculator/utils"
"encoding/json"
"io"
"log"
"os"
)
func LoadMetrics() {
f, err := os.Open("./config/metrics.json")
if err != nil {
log.Fatal("Failed to open metrics.json: ", err)
}
defer func(f *os.File) {
_ = f.Close()
}(f)
data, err := io.ReadAll(f)
if err != nil {
log.Fatal("Failed to read metrics.json: ", err)
}
var metrics models.GameMetrics
if err := json.Unmarshal(data, &metrics); err != nil {
log.Fatal("Failed to deserialize metrics.json: ", err)
}
utils.GameMetrics = metrics
}

View File

@@ -7,6 +7,7 @@ import (
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"internal/cache"
"log"
"net/http"
"os"
@@ -37,24 +38,36 @@ func GetPlayersByClanHTML(c *gin.Context) {
if err := models.DB.
Where("clan_id = ?", utils.StringToUint(clanId)).
Find(&players).Error; err != nil {
c.String(http.StatusBadRequest, "")
log.Fatal(err)
return
}
file, err := os.ReadFile("./templates/player_list_item.html")
if err != nil {
c.String(http.StatusBadRequest, "")
log.Fatal(err)
return
}
playerItem := string(file)
game, err := GetActiveGame(c)
if err != nil {
c.String(http.StatusBadRequest, "")
log.Fatal(err)
return
}
var htmlOptions string
for _, player := range players {
var score string
if val, err := GetCacheByPlayerIDGorm(player.ID, 0); err != nil {
score = "----"
if val, err := cache.GetScore(player.ID, game.Tag); err != nil || val == -1.0 {
score = "<i class=\"bi bi-dash me-2\" style=\"margin-left:0.7rem;\"></i>"
} else {
score = utils.FloatToString(val)
score = fmt.Sprintf("%.2f", val)
}
htmlOptions += fmt.Sprintf(playerItem, player.Name, score, player.ID, player.ID)
htmlOptions += fmt.Sprintf(playerItem, player.Name, score, utils.UintToString(player.ID)+"?game_tag="+game.Tag, player.ID, player.ID)
}
c.Header("Content-Type", "text/html")

View File

@@ -2,9 +2,10 @@ package controllers
import (
"InfantrySkillCalculator/models"
"InfantrySkillCalculator/utils"
"errors"
"github.com/gin-gonic/gin"
"net/http"
"session"
)
type UpdateSettingsInput struct {
@@ -18,9 +19,11 @@ type UpdateSettingsInput struct {
func GetSettings(c *gin.Context) {
var settings models.UserSettings
session, _ := utils.Store.Get(c.Request, utils.LoginSessionName)
var username string
username, _ = session.Values["username"].(string)
username, ok := session.GetUsername(c)
if !ok {
c.JSON(http.StatusBadRequest, gin.H{"error": "Not logged in!"})
return
}
if err := models.DB.Where("username = ?", username).First(&settings).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "No settings available!"})
@@ -37,13 +40,39 @@ func GetSettings(c *gin.Context) {
c.JSON(http.StatusOK, sanitizedSettings)
}
func GetActiveGame(c *gin.Context) (models.Game, error) {
var settings models.UserSettings
var game models.Game
username, ok := session.GetUsername(c)
if !ok {
err := errors.New("not logged in")
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return models.Game{}, err
}
if err := models.DB.Where("username = ?", username).First(&settings).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "No settings available!"})
return models.Game{}, err
}
if err := models.DB.Where("id = ?", settings.ActiveGameID).First(&game).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "No active game available!"})
return models.Game{}, err
}
return game, nil
}
// UpdateSettings PATCH /settings
func UpdateSettings(c *gin.Context) {
var settings models.UserSettings
session, _ := utils.Store.Get(c.Request, utils.LoginSessionName)
var username string
username, _ = session.Values["username"].(string)
username, ok := session.GetUsername(c)
if !ok {
c.JSON(http.StatusBadRequest, gin.H{"error": "Not logged in!"})
return
}
if err := models.DB.Where("username = ?", username).First(&settings).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "No settings available!"})