feat: implement battle replay module with database migrations, repository, and CRUD service endpoints
All checks were successful
Build and Release / release (push) Successful in 1m32s
All checks were successful
Build and Release / release (push) Successful in 1m32s
This commit is contained in:
72
internal/controllers/battleReplayController.go
Normal file
72
internal/controllers/battleReplayController.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"history-api/internal/dtos/response"
|
||||
"history-api/internal/services"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
type BattleReplayController struct {
|
||||
service services.BattleReplayService
|
||||
}
|
||||
|
||||
func NewBattleReplayController(svc services.BattleReplayService) *BattleReplayController {
|
||||
return &BattleReplayController{service: svc}
|
||||
}
|
||||
|
||||
// GetBattleReplayById handles fetching a single battle replay by ID.
|
||||
// @Summary Get battle replay by ID
|
||||
// @Description Get detailed information about a specific battle replay
|
||||
// @Tags BattleReplays
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Battle Replay ID"
|
||||
// @Success 200 {object} response.CommonResponse
|
||||
// @Failure 404 {object} response.CommonResponse
|
||||
// @Router /battle-replays/{id} [get]
|
||||
func (h *BattleReplayController) GetBattleReplayById(c fiber.Ctx) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
id := c.Params("id")
|
||||
res, err := h.service.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return c.Status(err.Code).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: err.Message,
|
||||
})
|
||||
}
|
||||
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
|
||||
Status: true,
|
||||
Data: res,
|
||||
})
|
||||
}
|
||||
|
||||
// GetBattleReplaysByGeometryId handles fetching battle replays by geometry ID.
|
||||
// @Summary Get battle replays by geometry ID
|
||||
// @Description Get all battle replays associated with a specific geometry
|
||||
// @Tags BattleReplays
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param geometryId path string true "Geometry ID"
|
||||
// @Success 200 {object} response.CommonResponse
|
||||
// @Failure 400 {object} response.CommonResponse
|
||||
// @Router /battle-replays/geometry/{geometryId} [get]
|
||||
func (h *BattleReplayController) GetBattleReplaysByGeometryId(c fiber.Ctx) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
geometryID := c.Params("geometryId")
|
||||
res, err := h.service.GetByGeometryID(ctx, geometryID)
|
||||
if err != nil {
|
||||
return c.Status(err.Code).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: err.Message,
|
||||
})
|
||||
}
|
||||
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
|
||||
Status: true,
|
||||
Data: res,
|
||||
})
|
||||
}
|
||||
@@ -9,6 +9,14 @@ type CommitSnapshot struct {
|
||||
Wikis []*WikiSnapshot `json:"wikis,omitempty" validate:"omitempty,dive"`
|
||||
GeometryEntity []*GeometryEntitySnapshot `json:"geometry_entity,omitempty" validate:"omitempty,dive"`
|
||||
EntityWiki []*EntityWikiLinkSnapshot `json:"entity_wiki,omitempty" validate:"omitempty,dive"`
|
||||
Replays []*BattleReplaySnapshot `json:"replays,omitempty" validate:"omitempty,dive"`
|
||||
}
|
||||
|
||||
type BattleReplaySnapshot struct {
|
||||
ID string `json:"id" validate:"required,uuidv7"`
|
||||
GeometryID string `json:"geometry_id" validate:"required,uuidv7"`
|
||||
TargetGeometryIDs []string `json:"target_geometry_ids,omitempty" validate:"omitempty,dive,uuidv7"`
|
||||
Detail json.RawMessage `json:"detail,omitempty"`
|
||||
}
|
||||
|
||||
type FeatureCollection struct {
|
||||
|
||||
17
internal/dtos/response/battle_replay.go
Normal file
17
internal/dtos/response/battle_replay.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package response
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
type BattleReplayResponse struct {
|
||||
ID string `json:"id"`
|
||||
GeometryID string `json:"geometry_id"`
|
||||
ProjectID string `json:"project_id"`
|
||||
TargetGeometryIDs json.RawMessage `json:"target_geometry_ids,omitempty"`
|
||||
Detail json.RawMessage `json:"detail,omitempty"`
|
||||
IsDeleted bool `json:"is_deleted,omitempty"`
|
||||
CreatedAt *time.Time `json:"created_at,omitempty"`
|
||||
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
||||
}
|
||||
272
internal/gen/sqlc/battle_replay.sql.go
Normal file
272
internal/gen/sqlc/battle_replay.sql.go
Normal file
@@ -0,0 +1,272 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
// source: battle_replay.sql
|
||||
|
||||
package sqlc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const createBattleReplay = `-- name: CreateBattleReplay :one
|
||||
INSERT INTO battle_replays (
|
||||
id, geometry_id, project_id, target_geometry_ids, detail
|
||||
) VALUES (
|
||||
COALESCE($5::uuid, uuidv7()), $1, $2, $3, $4
|
||||
)
|
||||
RETURNING id, geometry_id, project_id, target_geometry_ids, detail, is_deleted, created_at, updated_at
|
||||
`
|
||||
|
||||
type CreateBattleReplayParams struct {
|
||||
GeometryID pgtype.UUID `json:"geometry_id"`
|
||||
ProjectID pgtype.UUID `json:"project_id"`
|
||||
TargetGeometryIds json.RawMessage `json:"target_geometry_ids"`
|
||||
Detail json.RawMessage `json:"detail"`
|
||||
ID pgtype.UUID `json:"id"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateBattleReplay(ctx context.Context, arg CreateBattleReplayParams) (BattleReplay, error) {
|
||||
row := q.db.QueryRow(ctx, createBattleReplay,
|
||||
arg.GeometryID,
|
||||
arg.ProjectID,
|
||||
arg.TargetGeometryIds,
|
||||
arg.Detail,
|
||||
arg.ID,
|
||||
)
|
||||
var i BattleReplay
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.GeometryID,
|
||||
&i.ProjectID,
|
||||
&i.TargetGeometryIds,
|
||||
&i.Detail,
|
||||
&i.IsDeleted,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const deleteBattleReplay = `-- name: DeleteBattleReplay :exec
|
||||
UPDATE battle_replays
|
||||
SET is_deleted = true
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteBattleReplay(ctx context.Context, id pgtype.UUID) error {
|
||||
_, err := q.db.Exec(ctx, deleteBattleReplay, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const deleteBattleReplaysByIDs = `-- name: DeleteBattleReplaysByIDs :exec
|
||||
UPDATE battle_replays
|
||||
SET is_deleted = true
|
||||
WHERE id = ANY($1::uuid[])
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteBattleReplaysByIDs(ctx context.Context, dollar_1 []pgtype.UUID) error {
|
||||
_, err := q.db.Exec(ctx, deleteBattleReplaysByIDs, dollar_1)
|
||||
return err
|
||||
}
|
||||
|
||||
const getBattleReplayById = `-- name: GetBattleReplayById :one
|
||||
SELECT id, geometry_id, project_id, target_geometry_ids, detail, is_deleted, created_at, updated_at
|
||||
FROM battle_replays
|
||||
WHERE id = $1 AND is_deleted = false
|
||||
`
|
||||
|
||||
func (q *Queries) GetBattleReplayById(ctx context.Context, id pgtype.UUID) (BattleReplay, error) {
|
||||
row := q.db.QueryRow(ctx, getBattleReplayById, id)
|
||||
var i BattleReplay
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.GeometryID,
|
||||
&i.ProjectID,
|
||||
&i.TargetGeometryIds,
|
||||
&i.Detail,
|
||||
&i.IsDeleted,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getBattleReplaysByGeometryIDs = `-- name: GetBattleReplaysByGeometryIDs :many
|
||||
SELECT id, geometry_id, project_id, target_geometry_ids, detail, is_deleted, created_at, updated_at
|
||||
FROM battle_replays
|
||||
WHERE geometry_id = ANY($1::uuid[]) AND is_deleted = false
|
||||
`
|
||||
|
||||
func (q *Queries) GetBattleReplaysByGeometryIDs(ctx context.Context, dollar_1 []pgtype.UUID) ([]BattleReplay, error) {
|
||||
rows, err := q.db.Query(ctx, getBattleReplaysByGeometryIDs, dollar_1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []BattleReplay{}
|
||||
for rows.Next() {
|
||||
var i BattleReplay
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.GeometryID,
|
||||
&i.ProjectID,
|
||||
&i.TargetGeometryIds,
|
||||
&i.Detail,
|
||||
&i.IsDeleted,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getBattleReplaysByGeometryId = `-- name: GetBattleReplaysByGeometryId :many
|
||||
SELECT id, geometry_id, project_id, target_geometry_ids, detail, is_deleted, created_at, updated_at
|
||||
FROM battle_replays
|
||||
WHERE geometry_id = $1 AND is_deleted = false
|
||||
`
|
||||
|
||||
func (q *Queries) GetBattleReplaysByGeometryId(ctx context.Context, geometryID pgtype.UUID) ([]BattleReplay, error) {
|
||||
rows, err := q.db.Query(ctx, getBattleReplaysByGeometryId, geometryID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []BattleReplay{}
|
||||
for rows.Next() {
|
||||
var i BattleReplay
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.GeometryID,
|
||||
&i.ProjectID,
|
||||
&i.TargetGeometryIds,
|
||||
&i.Detail,
|
||||
&i.IsDeleted,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getBattleReplaysByIDs = `-- name: GetBattleReplaysByIDs :many
|
||||
SELECT id, geometry_id, project_id, target_geometry_ids, detail, is_deleted, created_at, updated_at FROM battle_replays WHERE id = ANY($1::uuid[]) AND is_deleted = false
|
||||
`
|
||||
|
||||
func (q *Queries) GetBattleReplaysByIDs(ctx context.Context, dollar_1 []pgtype.UUID) ([]BattleReplay, error) {
|
||||
rows, err := q.db.Query(ctx, getBattleReplaysByIDs, dollar_1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []BattleReplay{}
|
||||
for rows.Next() {
|
||||
var i BattleReplay
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.GeometryID,
|
||||
&i.ProjectID,
|
||||
&i.TargetGeometryIds,
|
||||
&i.Detail,
|
||||
&i.IsDeleted,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getBattleReplaysByProjectId = `-- name: GetBattleReplaysByProjectId :many
|
||||
SELECT id, geometry_id, project_id, target_geometry_ids, detail, is_deleted, created_at, updated_at
|
||||
FROM battle_replays
|
||||
WHERE project_id = $1 AND is_deleted = false
|
||||
`
|
||||
|
||||
func (q *Queries) GetBattleReplaysByProjectId(ctx context.Context, projectID pgtype.UUID) ([]BattleReplay, error) {
|
||||
rows, err := q.db.Query(ctx, getBattleReplaysByProjectId, projectID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []BattleReplay{}
|
||||
for rows.Next() {
|
||||
var i BattleReplay
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.GeometryID,
|
||||
&i.ProjectID,
|
||||
&i.TargetGeometryIds,
|
||||
&i.Detail,
|
||||
&i.IsDeleted,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const updateBattleReplay = `-- name: UpdateBattleReplay :one
|
||||
UPDATE battle_replays
|
||||
SET
|
||||
geometry_id = COALESCE($1, geometry_id),
|
||||
target_geometry_ids = COALESCE($2, target_geometry_ids),
|
||||
detail = COALESCE($3, detail)
|
||||
WHERE id = $4 AND is_deleted = false
|
||||
RETURNING id, geometry_id, project_id, target_geometry_ids, detail, is_deleted, created_at, updated_at
|
||||
`
|
||||
|
||||
type UpdateBattleReplayParams struct {
|
||||
GeometryID pgtype.UUID `json:"geometry_id"`
|
||||
TargetGeometryIds []byte `json:"target_geometry_ids"`
|
||||
Detail []byte `json:"detail"`
|
||||
ID pgtype.UUID `json:"id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateBattleReplay(ctx context.Context, arg UpdateBattleReplayParams) (BattleReplay, error) {
|
||||
row := q.db.QueryRow(ctx, updateBattleReplay,
|
||||
arg.GeometryID,
|
||||
arg.TargetGeometryIds,
|
||||
arg.Detail,
|
||||
arg.ID,
|
||||
)
|
||||
var i BattleReplay
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.GeometryID,
|
||||
&i.ProjectID,
|
||||
&i.TargetGeometryIds,
|
||||
&i.Detail,
|
||||
&i.IsDeleted,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -11,6 +11,17 @@ import (
|
||||
"github.com/pgvector/pgvector-go"
|
||||
)
|
||||
|
||||
type BattleReplay struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
GeometryID pgtype.UUID `json:"geometry_id"`
|
||||
ProjectID pgtype.UUID `json:"project_id"`
|
||||
TargetGeometryIds json.RawMessage `json:"target_geometry_ids"`
|
||||
Detail json.RawMessage `json:"detail"`
|
||||
IsDeleted bool `json:"is_deleted"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
type ChatbotHistory struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
UserID pgtype.UUID `json:"user_id"`
|
||||
|
||||
48
internal/models/battle_replay.go
Normal file
48
internal/models/battle_replay.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"history-api/internal/dtos/response"
|
||||
"time"
|
||||
)
|
||||
|
||||
type BattleReplayEntity struct {
|
||||
ID string `json:"id"`
|
||||
GeometryID string `json:"geometry_id"`
|
||||
ProjectID string `json:"project_id"`
|
||||
TargetGeometryIDs json.RawMessage `json:"target_geometry_ids"`
|
||||
Detail json.RawMessage `json:"detail"`
|
||||
IsDeleted bool `json:"is_deleted"`
|
||||
CreatedAt *time.Time `json:"created_at"`
|
||||
UpdatedAt *time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (b *BattleReplayEntity) ToResponse() *response.BattleReplayResponse {
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
return &response.BattleReplayResponse{
|
||||
ID: b.ID,
|
||||
GeometryID: b.GeometryID,
|
||||
ProjectID: b.ProjectID,
|
||||
TargetGeometryIDs: b.TargetGeometryIDs,
|
||||
Detail: b.Detail,
|
||||
IsDeleted: b.IsDeleted,
|
||||
CreatedAt: b.CreatedAt,
|
||||
UpdatedAt: b.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func BattleReplaysEntityToResponse(bs []*BattleReplayEntity) []*response.BattleReplayResponse {
|
||||
out := make([]*response.BattleReplayResponse, 0)
|
||||
if bs == nil {
|
||||
return out
|
||||
}
|
||||
for _, b := range bs {
|
||||
if b == nil {
|
||||
continue
|
||||
}
|
||||
out = append(out, b.ToResponse())
|
||||
}
|
||||
return out
|
||||
}
|
||||
290
internal/repositories/battleReplayRepository.go
Normal file
290
internal/repositories/battleReplayRepository.go
Normal file
@@ -0,0 +1,290 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
|
||||
"history-api/internal/gen/sqlc"
|
||||
"history-api/internal/models"
|
||||
"history-api/pkg/cache"
|
||||
"history-api/pkg/constants"
|
||||
"history-api/pkg/convert"
|
||||
)
|
||||
|
||||
type BattleReplayRepository interface {
|
||||
GetByID(ctx context.Context, id pgtype.UUID) (*models.BattleReplayEntity, error)
|
||||
GetByIDs(ctx context.Context, ids []string) ([]*models.BattleReplayEntity, error)
|
||||
GetByGeometryID(ctx context.Context, geometryID pgtype.UUID) ([]*models.BattleReplayEntity, error)
|
||||
GetByGeometryIDs(ctx context.Context, geometryIDs []string) ([]*models.BattleReplayEntity, error)
|
||||
Create(ctx context.Context, params sqlc.CreateBattleReplayParams) (*models.BattleReplayEntity, error)
|
||||
Update(ctx context.Context, params sqlc.UpdateBattleReplayParams) (*models.BattleReplayEntity, error)
|
||||
Delete(ctx context.Context, id pgtype.UUID) error
|
||||
DeleteByIDs(ctx context.Context, ids []pgtype.UUID) error
|
||||
GetByProjectID(ctx context.Context, projectID pgtype.UUID) ([]*models.BattleReplayEntity, error)
|
||||
WithTx(tx pgx.Tx) BattleReplayRepository
|
||||
}
|
||||
|
||||
type battleReplayRepository struct {
|
||||
q *sqlc.Queries
|
||||
c cache.Cache
|
||||
}
|
||||
|
||||
func NewBattleReplayRepository(db sqlc.DBTX, c cache.Cache) BattleReplayRepository {
|
||||
return &battleReplayRepository{
|
||||
q: sqlc.New(db),
|
||||
c: c,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *battleReplayRepository) WithTx(tx pgx.Tx) BattleReplayRepository {
|
||||
return &battleReplayRepository{
|
||||
q: r.q.WithTx(tx),
|
||||
c: r.c,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *battleReplayRepository) rowToEntity(row sqlc.BattleReplay) *models.BattleReplayEntity {
|
||||
return &models.BattleReplayEntity{
|
||||
ID: convert.UUIDToString(row.ID),
|
||||
GeometryID: convert.UUIDToString(row.GeometryID),
|
||||
ProjectID: convert.UUIDToString(row.ProjectID),
|
||||
TargetGeometryIDs: row.TargetGeometryIds,
|
||||
Detail: row.Detail,
|
||||
IsDeleted: row.IsDeleted,
|
||||
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *battleReplayRepository) getByIDsWithFallback(ctx context.Context, ids []string) ([]*models.BattleReplayEntity, error) {
|
||||
if len(ids) == 0 {
|
||||
return []*models.BattleReplayEntity{}, nil
|
||||
}
|
||||
keys := make([]string, len(ids))
|
||||
for i, id := range ids {
|
||||
keys[i] = fmt.Sprintf("battle_replay:id:%s", id)
|
||||
}
|
||||
raws := r.c.MGet(ctx, keys...)
|
||||
|
||||
var items []*models.BattleReplayEntity
|
||||
missingToCache := make(map[string]any)
|
||||
|
||||
var missingPgIds []pgtype.UUID
|
||||
for i, b := range raws {
|
||||
if len(b) == 0 {
|
||||
pgId := pgtype.UUID{}
|
||||
err := pgId.Scan(ids[i])
|
||||
if err == nil {
|
||||
missingPgIds = append(missingPgIds, pgId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dbMap := make(map[string]*models.BattleReplayEntity)
|
||||
if len(missingPgIds) > 0 {
|
||||
dbRows, err := r.q.GetBattleReplaysByIDs(ctx, missingPgIds)
|
||||
if err == nil {
|
||||
for _, row := range dbRows {
|
||||
item := r.rowToEntity(row)
|
||||
dbMap[item.ID] = item
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i, b := range raws {
|
||||
if len(b) > 0 {
|
||||
var u models.BattleReplayEntity
|
||||
if err := json.Unmarshal(b, &u); err == nil {
|
||||
items = append(items, &u)
|
||||
}
|
||||
} else {
|
||||
if item, ok := dbMap[ids[i]]; ok {
|
||||
items = append(items, item)
|
||||
missingToCache[keys[i]] = item
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(missingToCache) > 0 {
|
||||
_ = r.c.MSet(ctx, missingToCache, constants.NormalCacheDuration)
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (r *battleReplayRepository) GetByIDs(ctx context.Context, ids []string) ([]*models.BattleReplayEntity, error) {
|
||||
return r.getByIDsWithFallback(ctx, ids)
|
||||
}
|
||||
|
||||
func (r *battleReplayRepository) GetByID(ctx context.Context, id pgtype.UUID) (*models.BattleReplayEntity, error) {
|
||||
cacheId := fmt.Sprintf("battle_replay:id:%s", convert.UUIDToString(id))
|
||||
var item models.BattleReplayEntity
|
||||
err := r.c.Get(ctx, cacheId, &item)
|
||||
if err == nil {
|
||||
_ = r.c.Set(ctx, cacheId, item, constants.NormalCacheDuration)
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
row, err := r.q.GetBattleReplayById(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
entity := r.rowToEntity(row)
|
||||
_ = r.c.Set(ctx, cacheId, entity, constants.NormalCacheDuration)
|
||||
|
||||
return entity, nil
|
||||
}
|
||||
|
||||
func (r *battleReplayRepository) GetByGeometryID(ctx context.Context, geometryID pgtype.UUID) ([]*models.BattleReplayEntity, error) {
|
||||
cacheKey := fmt.Sprintf("battle_replay:geometry:%s", convert.UUIDToString(geometryID))
|
||||
var cachedIDs []string
|
||||
if err := r.c.Get(ctx, cacheKey, &cachedIDs); err == nil && len(cachedIDs) > 0 {
|
||||
return r.getByIDsWithFallback(ctx, cachedIDs)
|
||||
}
|
||||
|
||||
rows, err := r.q.GetBattleReplaysByGeometryId(ctx, geometryID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var items []*models.BattleReplayEntity
|
||||
var ids []string
|
||||
itemToCache := make(map[string]any)
|
||||
|
||||
for _, row := range rows {
|
||||
item := r.rowToEntity(row)
|
||||
ids = append(ids, item.ID)
|
||||
items = append(items, item)
|
||||
itemToCache[fmt.Sprintf("battle_replay:id:%s", item.ID)] = item
|
||||
}
|
||||
|
||||
if len(itemToCache) > 0 {
|
||||
_ = r.c.MSet(ctx, itemToCache, constants.NormalCacheDuration)
|
||||
}
|
||||
if len(ids) > 0 {
|
||||
_ = r.c.Set(ctx, cacheKey, ids, constants.ListCacheDuration)
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (r *battleReplayRepository) GetByGeometryIDs(ctx context.Context, geometryIDs []string) ([]*models.BattleReplayEntity, error) {
|
||||
if len(geometryIDs) == 0 {
|
||||
return []*models.BattleReplayEntity{}, nil
|
||||
}
|
||||
|
||||
var pgIds []pgtype.UUID
|
||||
for _, id := range geometryIDs {
|
||||
pgId := pgtype.UUID{}
|
||||
if err := pgId.Scan(id); err == nil {
|
||||
pgIds = append(pgIds, pgId)
|
||||
}
|
||||
}
|
||||
|
||||
rows, err := r.q.GetBattleReplaysByGeometryIDs(ctx, pgIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var items []*models.BattleReplayEntity
|
||||
itemToCache := make(map[string]any)
|
||||
|
||||
for _, row := range rows {
|
||||
item := r.rowToEntity(row)
|
||||
items = append(items, item)
|
||||
itemToCache[fmt.Sprintf("battle_replay:id:%s", item.ID)] = item
|
||||
}
|
||||
|
||||
if len(itemToCache) > 0 {
|
||||
_ = r.c.MSet(ctx, itemToCache, constants.NormalCacheDuration)
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (r *battleReplayRepository) GetByProjectID(ctx context.Context, projectID pgtype.UUID) ([]*models.BattleReplayEntity, error) {
|
||||
cacheKey := fmt.Sprintf("battle_replay:project:%s", convert.UUIDToString(projectID))
|
||||
var cachedIDs []string
|
||||
if err := r.c.Get(ctx, cacheKey, &cachedIDs); err == nil && len(cachedIDs) > 0 {
|
||||
return r.getByIDsWithFallback(ctx, cachedIDs)
|
||||
}
|
||||
|
||||
rows, err := r.q.GetBattleReplaysByProjectId(ctx, projectID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var items []*models.BattleReplayEntity
|
||||
var ids []string
|
||||
itemToCache := make(map[string]any)
|
||||
|
||||
for _, row := range rows {
|
||||
item := r.rowToEntity(row)
|
||||
ids = append(ids, item.ID)
|
||||
items = append(items, item)
|
||||
itemToCache[fmt.Sprintf("battle_replay:id:%s", item.ID)] = item
|
||||
}
|
||||
|
||||
if len(itemToCache) > 0 {
|
||||
_ = r.c.MSet(ctx, itemToCache, constants.NormalCacheDuration)
|
||||
}
|
||||
if len(ids) > 0 {
|
||||
_ = r.c.Set(ctx, cacheKey, ids, constants.ListCacheDuration)
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (r *battleReplayRepository) Create(ctx context.Context, params sqlc.CreateBattleReplayParams) (*models.BattleReplayEntity, error) {
|
||||
row, err := r.q.CreateBattleReplay(ctx, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
entity := r.rowToEntity(row)
|
||||
|
||||
_ = r.c.Del(ctx, fmt.Sprintf("battle_replay:project:%s", entity.ProjectID))
|
||||
_ = r.c.Del(ctx, fmt.Sprintf("battle_replay:geometry:%s", entity.GeometryID))
|
||||
|
||||
return entity, nil
|
||||
}
|
||||
|
||||
func (r *battleReplayRepository) Update(ctx context.Context, params sqlc.UpdateBattleReplayParams) (*models.BattleReplayEntity, error) {
|
||||
row, err := r.q.UpdateBattleReplay(ctx, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
entity := r.rowToEntity(row)
|
||||
_ = r.c.Del(ctx, fmt.Sprintf("battle_replay:id:%s", entity.ID))
|
||||
return entity, nil
|
||||
}
|
||||
|
||||
func (r *battleReplayRepository) Delete(ctx context.Context, id pgtype.UUID) error {
|
||||
err := r.q.DeleteBattleReplay(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = r.c.Del(ctx, fmt.Sprintf("battle_replay:id:%s", convert.UUIDToString(id)))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *battleReplayRepository) DeleteByIDs(ctx context.Context, ids []pgtype.UUID) error {
|
||||
err := r.q.DeleteBattleReplaysByIDs(ctx, ids)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(ids) > 0 {
|
||||
keys := make([]string, len(ids))
|
||||
for i, id := range ids {
|
||||
keys[i] = fmt.Sprintf("battle_replay:id:%s", convert.UUIDToString(id))
|
||||
}
|
||||
_ = r.c.Del(ctx, keys...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
13
internal/routes/battleReplayRoute.go
Normal file
13
internal/routes/battleReplayRoute.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"history-api/internal/controllers"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
func BattleReplayRoutes(router fiber.Router, battleReplayController *controllers.BattleReplayController) {
|
||||
br := router.Group("/battle-replays")
|
||||
br.Get("/geometry/:geometryId", battleReplayController.GetBattleReplaysByGeometryId)
|
||||
br.Get("/:id", battleReplayController.GetBattleReplayById)
|
||||
}
|
||||
54
internal/services/battleReplayService.go
Normal file
54
internal/services/battleReplayService.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"history-api/internal/dtos/response"
|
||||
"history-api/internal/models"
|
||||
"history-api/internal/repositories"
|
||||
"history-api/pkg/convert"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
type BattleReplayService interface {
|
||||
GetByID(ctx context.Context, id string) (*response.BattleReplayResponse, *fiber.Error)
|
||||
GetByGeometryID(ctx context.Context, geometryID string) ([]*response.BattleReplayResponse, *fiber.Error)
|
||||
}
|
||||
|
||||
type battleReplayService struct {
|
||||
battleReplayRepo repositories.BattleReplayRepository
|
||||
}
|
||||
|
||||
func NewBattleReplayService(battleReplayRepo repositories.BattleReplayRepository) BattleReplayService {
|
||||
return &battleReplayService{
|
||||
battleReplayRepo: battleReplayRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *battleReplayService) GetByID(ctx context.Context, id string) (*response.BattleReplayResponse, *fiber.Error) {
|
||||
replayUUID, err := convert.StringToUUID(id)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid battle replay ID format")
|
||||
}
|
||||
|
||||
replay, err := s.battleReplayRepo.GetByID(ctx, replayUUID)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Battle replay not found")
|
||||
}
|
||||
|
||||
return replay.ToResponse(), nil
|
||||
}
|
||||
|
||||
func (s *battleReplayService) GetByGeometryID(ctx context.Context, geometryID string) ([]*response.BattleReplayResponse, *fiber.Error) {
|
||||
geomUUID, err := convert.StringToUUID(geometryID)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid geometry ID format")
|
||||
}
|
||||
|
||||
replays, err := s.battleReplayRepo.GetByGeometryID(ctx, geomUUID)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get battle replays")
|
||||
}
|
||||
|
||||
return models.BattleReplaysEntityToResponse(replays), nil
|
||||
}
|
||||
@@ -32,17 +32,18 @@ type SubmissionService interface {
|
||||
}
|
||||
|
||||
type submissionService struct {
|
||||
submissionRepo repositories.SubmissionRepository
|
||||
projectRepo repositories.ProjectRepository
|
||||
commitRepo repositories.CommitRepository
|
||||
userRepo repositories.UserRepository
|
||||
wikiRepo repositories.WikiRepository
|
||||
geometryRepo repositories.GeometryRepository
|
||||
entityRepo repositories.EntityRepository
|
||||
ragRepo repositories.RagRepository
|
||||
ragUtils *ai.RagUtils
|
||||
db *pgxpool.Pool
|
||||
c cache.Cache
|
||||
submissionRepo repositories.SubmissionRepository
|
||||
projectRepo repositories.ProjectRepository
|
||||
commitRepo repositories.CommitRepository
|
||||
userRepo repositories.UserRepository
|
||||
wikiRepo repositories.WikiRepository
|
||||
geometryRepo repositories.GeometryRepository
|
||||
entityRepo repositories.EntityRepository
|
||||
battleReplayRepo repositories.BattleReplayRepository
|
||||
ragRepo repositories.RagRepository
|
||||
ragUtils *ai.RagUtils
|
||||
db *pgxpool.Pool
|
||||
c cache.Cache
|
||||
}
|
||||
|
||||
func NewSubmissionService(
|
||||
@@ -53,23 +54,25 @@ func NewSubmissionService(
|
||||
wikiRepo repositories.WikiRepository,
|
||||
geometryRepo repositories.GeometryRepository,
|
||||
entityRepo repositories.EntityRepository,
|
||||
battleReplayRepo repositories.BattleReplayRepository,
|
||||
ragRepo repositories.RagRepository,
|
||||
ragUtils *ai.RagUtils,
|
||||
db *pgxpool.Pool,
|
||||
c cache.Cache,
|
||||
) SubmissionService {
|
||||
return &submissionService{
|
||||
submissionRepo: submissionRepo,
|
||||
projectRepo: projectRepo,
|
||||
commitRepo: commitRepo,
|
||||
userRepo: userRepo,
|
||||
wikiRepo: wikiRepo,
|
||||
geometryRepo: geometryRepo,
|
||||
entityRepo: entityRepo,
|
||||
ragRepo: ragRepo,
|
||||
ragUtils: ragUtils,
|
||||
db: db,
|
||||
c: c,
|
||||
submissionRepo: submissionRepo,
|
||||
projectRepo: projectRepo,
|
||||
commitRepo: commitRepo,
|
||||
userRepo: userRepo,
|
||||
wikiRepo: wikiRepo,
|
||||
geometryRepo: geometryRepo,
|
||||
entityRepo: entityRepo,
|
||||
battleReplayRepo: battleReplayRepo,
|
||||
ragRepo: ragRepo,
|
||||
ragUtils: ragUtils,
|
||||
db: db,
|
||||
c: c,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,6 +190,7 @@ func (s *submissionService) UpdateSubmissionStatus(ctx context.Context, reviewer
|
||||
entityRepo := s.entityRepo.WithTx(tx)
|
||||
geometryRepo := s.geometryRepo.WithTx(tx)
|
||||
wikiRepo := s.wikiRepo.WithTx(tx)
|
||||
battleReplayRepo := s.battleReplayRepo.WithTx(tx)
|
||||
|
||||
submissionUUID, err := convert.StringToUUID(submissionID)
|
||||
if err != nil {
|
||||
@@ -229,6 +233,7 @@ func (s *submissionService) UpdateSubmissionStatus(ctx context.Context, reviewer
|
||||
listDeleteEntities := make([]pgtype.UUID, 0)
|
||||
listDeleteWikis := make([]pgtype.UUID, 0)
|
||||
listDeleteGeometries := make([]pgtype.UUID, 0)
|
||||
listDeleteBattleReplays := make([]pgtype.UUID, 0)
|
||||
var snapshotData request.CommitSnapshot
|
||||
err = json.Unmarshal(commit.SnapshotJson, &snapshotData)
|
||||
if err != nil {
|
||||
@@ -242,17 +247,22 @@ func (s *submissionService) UpdateSubmissionStatus(ctx context.Context, reviewer
|
||||
}
|
||||
currentEntity, err := s.entityRepo.GetByProjectID(ctx, projectUUID)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Entity not found")
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Entity not found: "+err.Error())
|
||||
}
|
||||
|
||||
currentGeometry, err := s.geometryRepo.GetByProjectID(ctx, projectUUID)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Geometry not found")
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Geometry not found: "+err.Error())
|
||||
}
|
||||
|
||||
currentWiki, err := s.wikiRepo.GetByProjectID(ctx, projectUUID)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Wiki not found")
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Wiki not found: "+err.Error())
|
||||
}
|
||||
|
||||
currentBattleReplay, err := s.battleReplayRepo.GetByProjectID(ctx, projectUUID)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Battle replay not found: "+err.Error())
|
||||
}
|
||||
|
||||
persistItemIDs := make(map[string]struct{})
|
||||
@@ -265,6 +275,9 @@ func (s *submissionService) UpdateSubmissionStatus(ctx context.Context, reviewer
|
||||
for _, item := range snapshotData.Wikis {
|
||||
persistItemIDs[item.ID] = struct{}{}
|
||||
}
|
||||
for _, item := range snapshotData.Replays {
|
||||
persistItemIDs[item.ID] = struct{}{}
|
||||
}
|
||||
|
||||
persistCurrentItemIDs := make(map[string]struct{})
|
||||
for _, item := range currentEntity {
|
||||
@@ -276,6 +289,9 @@ func (s *submissionService) UpdateSubmissionStatus(ctx context.Context, reviewer
|
||||
for _, item := range currentWiki {
|
||||
persistCurrentItemIDs[item.ID] = struct{}{}
|
||||
}
|
||||
for _, item := range currentBattleReplay {
|
||||
persistCurrentItemIDs[item.ID] = struct{}{}
|
||||
}
|
||||
|
||||
for _, e := range currentEntity {
|
||||
if _, ok := persistItemIDs[e.ID]; !ok {
|
||||
@@ -310,6 +326,17 @@ func (s *submissionService) UpdateSubmissionStatus(ctx context.Context, reviewer
|
||||
}
|
||||
}
|
||||
|
||||
for _, br := range currentBattleReplay {
|
||||
if _, ok := persistItemIDs[br.ID]; !ok {
|
||||
itemUUID, err := convert.StringToUUID(br.ID)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Invalid battle replay ID")
|
||||
}
|
||||
listDeleteBattleReplays = append(listDeleteBattleReplays, itemUUID)
|
||||
delete(persistCurrentItemIDs, br.ID)
|
||||
}
|
||||
}
|
||||
|
||||
if len(listDeleteEntities) > 0 {
|
||||
if err = entityRepo.DeleteByIDs(ctx, listDeleteEntities); err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to delete entities")
|
||||
@@ -328,6 +355,12 @@ func (s *submissionService) UpdateSubmissionStatus(ctx context.Context, reviewer
|
||||
}
|
||||
}
|
||||
|
||||
if len(listDeleteBattleReplays) > 0 {
|
||||
if err = battleReplayRepo.DeleteByIDs(ctx, listDeleteBattleReplays); err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to delete battle replays")
|
||||
}
|
||||
}
|
||||
|
||||
refEntityIDs := []string{}
|
||||
for _, e := range snapshotData.Entities {
|
||||
if e.Source == "ref" {
|
||||
@@ -570,6 +603,46 @@ func (s *submissionService) UpdateSubmissionStatus(ctx context.Context, reviewer
|
||||
}
|
||||
snapshotData.Wikis = newWikis
|
||||
|
||||
for _, replay := range snapshotData.Replays {
|
||||
replayUUID, err := convert.StringToUUID(replay.ID)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Invalid battle replay ID")
|
||||
}
|
||||
|
||||
geomUUID, err := convert.StringToUUID(replay.GeometryID)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Invalid geometry ID in battle replay")
|
||||
}
|
||||
|
||||
targetIDs, err := json.Marshal(replay.TargetGeometryIDs)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to marshal target geometry IDs")
|
||||
}
|
||||
|
||||
if _, ok := persistCurrentItemIDs[replay.ID]; ok {
|
||||
_, err := battleReplayRepo.Update(ctx, sqlc.UpdateBattleReplayParams{
|
||||
ID: replayUUID,
|
||||
GeometryID: geomUUID,
|
||||
TargetGeometryIds: targetIDs,
|
||||
Detail: replay.Detail,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update battle replay: "+err.Error())
|
||||
}
|
||||
} else {
|
||||
_, err := battleReplayRepo.Create(ctx, sqlc.CreateBattleReplayParams{
|
||||
ID: replayUUID,
|
||||
GeometryID: geomUUID,
|
||||
ProjectID: projectUUID,
|
||||
TargetGeometryIds: targetIDs,
|
||||
Detail: replay.Detail,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create battle replay: "+err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = geometryRepo.DeleteEntityGeometriesByProjectID(ctx, projectUUID)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to delete geometry entity: "+err.Error())
|
||||
|
||||
Reference in New Issue
Block a user