add load applications

This commit is contained in:
2026-08-30 21:17:32 +07:00
parent 3b15fb3a2b
commit c0b5ceb65f
28 changed files with 1536 additions and 139 deletions
@@ -1,6 +0,0 @@
package game_service
type Application struct {
Name string
Image string
}
@@ -1,21 +1,64 @@
package game_service
import (
"strings"
"evening_detective_server/internal/modules/file_storage"
"evening_detective_server/internal/modules/storytelling"
"evening_detective_server/internal/repos"
)
func mapApplications(o []*repos.Application) []*storytelling.Application {
// mapApplications — улики команды для ответа организатору (GetFullGame):
// имена файлов префиксуются доменом идемпотентно, file_type деривируется
// из расширения при пустом значении (легаси-строки таблицы).
func mapApplications(o []*repos.Application, domain string) []*storytelling.Application {
res := make([]*storytelling.Application, 0, len(o))
for _, item := range o {
res = append(res, mapApplication(item))
res = append(res, mapApplication(item, domain))
}
return res
}
func mapApplication(o *repos.Application) *storytelling.Application {
return &storytelling.Application{
func mapApplication(o *repos.Application, domain string) *storytelling.Application {
image := prefixDomain(o.Image, domain)
app := &storytelling.Application{
Name: o.Name,
Image: o.Image,
Image: image,
}
app.FileType = o.FileType
if app.FileType == "" {
app.FileType = file_storage.FileType(image)
}
return app
}
// newApplication — улика для сохранения команде (AddTeamAction): имена
// приходят из истории уже с доменным префиксом (mapStory), в БД хранятся
// относительные пути — префикс снимается идемпотентно; file_type
// деривируется при пустом значении. Name не меняется (очистка — в
// вызывающем коде, cleaner'ом).
func newApplication(app *storytelling.Application, domain string) *repos.Application {
image := strings.TrimPrefix(app.Image, domain)
fileType := app.FileType
if fileType == "" {
fileType = file_storage.FileType(image)
}
return &repos.Application{
Name: app.Name,
Image: image,
FileType: fileType,
}
}
// prefixDomain — идемпотентная префиксация ссылки доменом хранилища
// (пустые ссылки и внешние http/https URL не трогаются).
func prefixDomain(ref, domain string) string {
if ref == "" || strings.HasPrefix(ref, domain) {
return ref
}
low := strings.ToLower(ref)
if strings.HasPrefix(low, "http://") || strings.HasPrefix(low, "https://") {
return ref
}
return domain + ref
}
@@ -0,0 +1,117 @@
package game_service
import (
"testing"
"evening_detective_server/internal/modules/storytelling"
"evening_detective_server/internal/repos"
)
const testDomain = "http://storage.test/api/files/"
func TestNewApplication(t *testing.T) {
cases := []struct {
name string
app *storytelling.Application
image string // ожидаемое хранимое имя (без домена)
ftype string // ожидаемый file_type
}{
{
name: "доменный префикс снимается, file_type сохраняется",
app: &storytelling.Application{
Name: "Улика",
Image: testDomain + "clue.pdf",
FileType: "pdf",
},
image: "clue.pdf",
ftype: "pdf",
},
{
name: "относительное имя не трогается",
app: &storytelling.Application{
Name: "Улика",
Image: "clue.png",
FileType: "image",
},
image: "clue.png",
ftype: "image",
},
{
name: "пустой file_type деривируется из расширения",
app: &storytelling.Application{
Name: "Улика",
Image: testDomain + "clue.ogg",
},
image: "clue.ogg",
ftype: "audio",
},
{
name: "внешний URL снимается как префикс только при совпадении домена",
app: &storytelling.Application{
Name: "Улика",
Image: "https://example.com/clue.pdf",
},
image: "https://example.com/clue.pdf",
ftype: "pdf",
},
}
for _, c := range cases {
got := newApplication(c.app, testDomain)
if got.Name != c.app.Name {
t.Errorf("%s: Name = %q, want %q", c.name, got.Name, c.app.Name)
}
if got.Image != c.image {
t.Errorf("%s: Image = %q, want %q", c.name, got.Image, c.image)
}
if got.FileType != c.ftype {
t.Errorf("%s: FileType = %q, want %q", c.name, got.FileType, c.ftype)
}
}
}
func TestMapApplications(t *testing.T) {
o := []*repos.Application{
{Name: "Улика", Image: "clue.pdf", FileType: ""},
{Name: "Фото", Image: "https://external.example.com/pic.jpg", FileType: "image"},
}
got := mapApplications(o, testDomain)
if len(got) != 2 {
t.Fatalf("len = %d, want 2", len(got))
}
// Относительное имя префиксуется доменом, file_type деривируется.
if got[0].Image != testDomain+"clue.pdf" {
t.Errorf("Image[0] = %q, want %q", got[0].Image, testDomain+"clue.pdf")
}
if got[0].FileType != "pdf" {
t.Errorf("FileType[0] = %q, want pdf", got[0].FileType)
}
// Внешний URL не префиксуется повторно.
if got[1].Image != "https://external.example.com/pic.jpg" {
t.Errorf("Image[1] = %q, want внешний URL без изменений", got[1].Image)
}
if got[1].FileType != "image" {
t.Errorf("FileType[1] = %q, want image", got[1].FileType)
}
}
func TestPrefixDomain(t *testing.T) {
cases := []struct {
ref string
domain string
want string
}{
{"clue.pdf", testDomain, testDomain + "clue.pdf"},
{testDomain + "clue.pdf", testDomain, testDomain + "clue.pdf"},
{"https://example.com/clue.pdf", testDomain, "https://example.com/clue.pdf"},
{"http://example.com/clue.pdf", testDomain, "http://example.com/clue.pdf"},
{"", testDomain, ""},
}
for _, c := range cases {
if got := prefixDomain(c.ref, c.domain); got != c.want {
t.Errorf("prefixDomain(%q) = %q, want %q", c.ref, got, c.want)
}
}
}
+4 -3
View File
@@ -114,7 +114,7 @@ func (s *GameService) GetFullGame(
for _, team := range res.Teams {
team.ActionsCount = actionsCountByTeamIDs[team.ID]
team.Applications = mapApplications(needApplicationsByTeamIDs[team.ID])
team.Applications = mapApplications(needApplicationsByTeamIDs[team.ID], s.domain)
}
return res, nil
@@ -263,8 +263,9 @@ func (s *GameService) AddTeamAction(ctx context.Context, teamId int, password st
lastPlace := teamStory.Places[len(teamStory.Places)-1]
for _, application := range lastPlace.Applications {
_, err := s.applicationsRepo.AddApplication(ctx, teamId, s.cleaner.ClearText(application.Name), application.Image)
if err != nil {
app := newApplication(application, s.domain)
app.Name = s.cleaner.ClearText(app.Name)
if _, err := s.applicationsRepo.AddApplication(ctx, teamId, app.Name, app.Image, app.FileType); err != nil {
return err
}
}