1405 lines
48 KiB
Go
1405 lines
48 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"cmr-backend/internal/apperr"
|
|
"cmr-backend/internal/platform/security"
|
|
"cmr-backend/internal/store/postgres"
|
|
)
|
|
|
|
type AdminProductionService struct {
|
|
store *postgres.Store
|
|
}
|
|
|
|
type AdminPlaceSummary struct {
|
|
ID string `json:"id"`
|
|
Code string `json:"code"`
|
|
Name string `json:"name"`
|
|
Region *string `json:"region,omitempty"`
|
|
CoverURL *string `json:"coverUrl,omitempty"`
|
|
Description *string `json:"description,omitempty"`
|
|
CenterPoint map[string]any `json:"centerPoint,omitempty"`
|
|
Status string `json:"status"`
|
|
}
|
|
|
|
type AdminPlaceDetail struct {
|
|
Place AdminPlaceSummary `json:"place"`
|
|
MapAssets []AdminMapAssetSummary `json:"mapAssets"`
|
|
}
|
|
|
|
type CreateAdminPlaceInput struct {
|
|
Code string `json:"code"`
|
|
Name string `json:"name"`
|
|
Region *string `json:"region,omitempty"`
|
|
CoverURL *string `json:"coverUrl,omitempty"`
|
|
Description *string `json:"description,omitempty"`
|
|
CenterPoint map[string]any `json:"centerPoint,omitempty"`
|
|
Status string `json:"status"`
|
|
}
|
|
|
|
type AdminMapAssetSummary struct {
|
|
ID string `json:"id"`
|
|
PlaceID string `json:"placeId"`
|
|
PlaceName *string `json:"placeName,omitempty"`
|
|
LegacyMapID *string `json:"legacyMapId,omitempty"`
|
|
Code string `json:"code"`
|
|
Name string `json:"name"`
|
|
MapType string `json:"mapType"`
|
|
CoverURL *string `json:"coverUrl,omitempty"`
|
|
Description *string `json:"description,omitempty"`
|
|
Status string `json:"status"`
|
|
CurrentTileRelease *AdminTileReleaseBrief `json:"currentTileRelease,omitempty"`
|
|
}
|
|
|
|
type AdminTileReleaseBrief struct {
|
|
ID string `json:"id"`
|
|
VersionCode string `json:"versionCode"`
|
|
Status string `json:"status"`
|
|
}
|
|
|
|
type AdminMapAssetDetail struct {
|
|
MapAsset AdminMapAssetSummary `json:"mapAsset"`
|
|
TileReleases []AdminTileReleaseView `json:"tileReleases"`
|
|
CourseSets []AdminCourseSetBrief `json:"courseSets"`
|
|
LinkedEvents []AdminMapLinkedEventBrief `json:"linkedEvents"`
|
|
}
|
|
|
|
type CreateAdminMapAssetInput struct {
|
|
Code string `json:"code"`
|
|
Name string `json:"name"`
|
|
MapType string `json:"mapType"`
|
|
LegacyMapID *string `json:"legacyMapId,omitempty"`
|
|
CoverURL *string `json:"coverUrl,omitempty"`
|
|
Description *string `json:"description,omitempty"`
|
|
Status string `json:"status"`
|
|
}
|
|
|
|
type UpdateAdminMapAssetInput struct {
|
|
Code string `json:"code"`
|
|
Name string `json:"name"`
|
|
MapType string `json:"mapType"`
|
|
CoverURL *string `json:"coverUrl,omitempty"`
|
|
Description *string `json:"description,omitempty"`
|
|
Status string `json:"status"`
|
|
}
|
|
|
|
type AdminMapLinkedEventBrief struct {
|
|
EventID string `json:"eventId"`
|
|
Title string `json:"title"`
|
|
Summary *string `json:"summary,omitempty"`
|
|
Status string `json:"status"`
|
|
IsDefaultExperience bool `json:"isDefaultExperience"`
|
|
ShowInEventList bool `json:"showInEventList"`
|
|
CurrentReleaseID *string `json:"currentReleaseId,omitempty"`
|
|
ConfigLabel *string `json:"configLabel,omitempty"`
|
|
RouteCode *string `json:"routeCode,omitempty"`
|
|
CurrentPresentationID *string `json:"currentPresentationId,omitempty"`
|
|
CurrentPresentation *string `json:"currentPresentation,omitempty"`
|
|
CurrentContentBundleID *string `json:"currentContentBundleId,omitempty"`
|
|
CurrentContentBundle *string `json:"currentContentBundle,omitempty"`
|
|
}
|
|
|
|
type AdminTileReleaseView struct {
|
|
ID string `json:"id"`
|
|
LegacyVersionID *string `json:"legacyVersionId,omitempty"`
|
|
VersionCode string `json:"versionCode"`
|
|
Status string `json:"status"`
|
|
TileBaseURL string `json:"tileBaseUrl"`
|
|
MetaURL string `json:"metaUrl"`
|
|
PublishedAssetRoot *string `json:"publishedAssetRoot,omitempty"`
|
|
Metadata map[string]any `json:"metadata,omitempty"`
|
|
PublishedAt *time.Time `json:"publishedAt,omitempty"`
|
|
}
|
|
|
|
type CreateAdminTileReleaseInput struct {
|
|
LegacyVersionID *string `json:"legacyVersionId,omitempty"`
|
|
VersionCode string `json:"versionCode"`
|
|
Status string `json:"status"`
|
|
TileBaseURL string `json:"tileBaseUrl"`
|
|
MetaURL string `json:"metaUrl"`
|
|
PublishedAssetRoot *string `json:"publishedAssetRoot,omitempty"`
|
|
Metadata map[string]any `json:"metadata,omitempty"`
|
|
SetAsCurrent bool `json:"setAsCurrent"`
|
|
}
|
|
|
|
type AdminCourseSourceSummary struct {
|
|
ID string `json:"id"`
|
|
LegacyVersionID *string `json:"legacyVersionId,omitempty"`
|
|
SourceType string `json:"sourceType"`
|
|
FileURL string `json:"fileUrl"`
|
|
Checksum *string `json:"checksum,omitempty"`
|
|
ParserVersion *string `json:"parserVersion,omitempty"`
|
|
ImportStatus string `json:"importStatus"`
|
|
Metadata map[string]any `json:"metadata,omitempty"`
|
|
ImportedAt time.Time `json:"importedAt"`
|
|
}
|
|
|
|
type CreateAdminCourseSourceInput struct {
|
|
LegacyPlayfieldID *string `json:"legacyPlayfieldId,omitempty"`
|
|
LegacyVersionID *string `json:"legacyVersionId,omitempty"`
|
|
SourceType string `json:"sourceType"`
|
|
FileURL string `json:"fileUrl"`
|
|
Checksum *string `json:"checksum,omitempty"`
|
|
ParserVersion *string `json:"parserVersion,omitempty"`
|
|
ImportStatus string `json:"importStatus"`
|
|
Metadata map[string]any `json:"metadata,omitempty"`
|
|
}
|
|
|
|
type AdminCourseSetBrief struct {
|
|
ID string `json:"id"`
|
|
Code string `json:"code"`
|
|
Mode string `json:"mode"`
|
|
Name string `json:"name"`
|
|
Description *string `json:"description,omitempty"`
|
|
Status string `json:"status"`
|
|
CurrentVariant *AdminCourseVariantBrief `json:"currentVariant,omitempty"`
|
|
}
|
|
|
|
type AdminCourseVariantBrief struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
RouteCode *string `json:"routeCode,omitempty"`
|
|
Status string `json:"status"`
|
|
}
|
|
|
|
type AdminCourseSetDetail struct {
|
|
CourseSet AdminCourseSetBrief `json:"courseSet"`
|
|
Variants []AdminCourseVariantView `json:"variants"`
|
|
}
|
|
|
|
type CreateAdminCourseSetInput struct {
|
|
Code string `json:"code"`
|
|
Mode string `json:"mode"`
|
|
Name string `json:"name"`
|
|
Description *string `json:"description,omitempty"`
|
|
Status string `json:"status"`
|
|
}
|
|
|
|
type AdminCourseVariantView struct {
|
|
ID string `json:"id"`
|
|
SourceID *string `json:"sourceId,omitempty"`
|
|
Name string `json:"name"`
|
|
RouteCode *string `json:"routeCode,omitempty"`
|
|
Mode string `json:"mode"`
|
|
ControlCount *int `json:"controlCount,omitempty"`
|
|
Difficulty *string `json:"difficulty,omitempty"`
|
|
Status string `json:"status"`
|
|
IsDefault bool `json:"isDefault"`
|
|
ConfigPatch map[string]any `json:"configPatch,omitempty"`
|
|
Metadata map[string]any `json:"metadata,omitempty"`
|
|
}
|
|
|
|
type CreateAdminCourseVariantInput struct {
|
|
SourceID *string `json:"sourceId,omitempty"`
|
|
Name string `json:"name"`
|
|
RouteCode *string `json:"routeCode,omitempty"`
|
|
Mode string `json:"mode"`
|
|
ControlCount *int `json:"controlCount,omitempty"`
|
|
Difficulty *string `json:"difficulty,omitempty"`
|
|
Status string `json:"status"`
|
|
IsDefault bool `json:"isDefault"`
|
|
ConfigPatch map[string]any `json:"configPatch,omitempty"`
|
|
Metadata map[string]any `json:"metadata,omitempty"`
|
|
}
|
|
|
|
type AdminRuntimeBindingSummary struct {
|
|
ID string `json:"id"`
|
|
EventID string `json:"eventId"`
|
|
PlaceID string `json:"placeId"`
|
|
MapAssetID string `json:"mapAssetId"`
|
|
TileReleaseID string `json:"tileReleaseId"`
|
|
CourseSetID string `json:"courseSetId"`
|
|
CourseVariantID string `json:"courseVariantId"`
|
|
Status string `json:"status"`
|
|
Notes *string `json:"notes,omitempty"`
|
|
}
|
|
|
|
type CreateAdminRuntimeBindingInput struct {
|
|
EventID string `json:"eventId"`
|
|
PlaceID string `json:"placeId"`
|
|
MapAssetID string `json:"mapAssetId"`
|
|
TileReleaseID string `json:"tileReleaseId"`
|
|
CourseSetID string `json:"courseSetId"`
|
|
CourseVariantID string `json:"courseVariantId"`
|
|
Status string `json:"status"`
|
|
Notes *string `json:"notes,omitempty"`
|
|
}
|
|
|
|
type ImportAdminTileReleaseInput struct {
|
|
PlaceCode string `json:"placeCode"`
|
|
PlaceName string `json:"placeName"`
|
|
PlaceRegion *string `json:"placeRegion,omitempty"`
|
|
PlaceCoverURL *string `json:"placeCoverUrl,omitempty"`
|
|
PlaceDescription *string `json:"placeDescription,omitempty"`
|
|
PlaceCenterPoint map[string]any `json:"placeCenterPoint,omitempty"`
|
|
MapAssetCode string `json:"mapAssetCode"`
|
|
MapAssetName string `json:"mapAssetName"`
|
|
MapType string `json:"mapType"`
|
|
MapCoverURL *string `json:"mapCoverUrl,omitempty"`
|
|
MapDescription *string `json:"mapDescription,omitempty"`
|
|
VersionCode string `json:"versionCode"`
|
|
Status string `json:"status"`
|
|
TileBaseURL string `json:"tileBaseUrl"`
|
|
MetaURL string `json:"metaUrl"`
|
|
PublishedAssetRoot *string `json:"publishedAssetRoot,omitempty"`
|
|
Metadata map[string]any `json:"metadata,omitempty"`
|
|
SetAsCurrent bool `json:"setAsCurrent"`
|
|
}
|
|
|
|
type ImportAdminTileReleaseResult struct {
|
|
Place AdminPlaceSummary `json:"place"`
|
|
MapAsset AdminMapAssetSummary `json:"mapAsset"`
|
|
TileRelease AdminTileReleaseView `json:"tileRelease"`
|
|
}
|
|
|
|
type ImportAdminCourseRouteInput struct {
|
|
Name string `json:"name"`
|
|
RouteCode string `json:"routeCode"`
|
|
FileURL string `json:"fileUrl"`
|
|
SourceType string `json:"sourceType"`
|
|
ControlCount *int `json:"controlCount,omitempty"`
|
|
Difficulty *string `json:"difficulty,omitempty"`
|
|
Status string `json:"status"`
|
|
Metadata map[string]any `json:"metadata,omitempty"`
|
|
}
|
|
|
|
type ImportAdminCourseSetBatchInput struct {
|
|
PlaceCode string `json:"placeCode"`
|
|
PlaceName string `json:"placeName"`
|
|
MapAssetCode string `json:"mapAssetCode"`
|
|
MapAssetName string `json:"mapAssetName"`
|
|
MapType string `json:"mapType"`
|
|
CourseSetCode string `json:"courseSetCode"`
|
|
CourseSetName string `json:"courseSetName"`
|
|
Mode string `json:"mode"`
|
|
Description *string `json:"description,omitempty"`
|
|
Status string `json:"status"`
|
|
DefaultRouteCode *string `json:"defaultRouteCode,omitempty"`
|
|
Routes []ImportAdminCourseRouteInput `json:"routes"`
|
|
}
|
|
|
|
type ImportAdminCourseSetBatchResult struct {
|
|
Place AdminPlaceSummary `json:"place"`
|
|
MapAsset AdminMapAssetSummary `json:"mapAsset"`
|
|
CourseSet AdminCourseSetBrief `json:"courseSet"`
|
|
Variants []AdminCourseVariantView `json:"variants"`
|
|
}
|
|
|
|
func NewAdminProductionService(store *postgres.Store) *AdminProductionService {
|
|
return &AdminProductionService{store: store}
|
|
}
|
|
|
|
func (s *AdminProductionService) ListPlaces(ctx context.Context, limit int) ([]AdminPlaceSummary, error) {
|
|
items, err := s.store.ListPlaces(ctx, limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result := make([]AdminPlaceSummary, 0, len(items))
|
|
for _, item := range items {
|
|
result = append(result, buildAdminPlaceSummary(item))
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (s *AdminProductionService) ListMapAssets(ctx context.Context, limit int) ([]AdminMapAssetSummary, error) {
|
|
items, err := s.store.ListMapAssets(ctx, limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result := make([]AdminMapAssetSummary, 0, len(items))
|
|
for _, item := range items {
|
|
summary, err := s.buildAdminMapAssetSummary(ctx, item)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result = append(result, summary)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (s *AdminProductionService) CreatePlace(ctx context.Context, input CreateAdminPlaceInput) (*AdminPlaceSummary, error) {
|
|
input.Code = strings.TrimSpace(input.Code)
|
|
input.Name = strings.TrimSpace(input.Name)
|
|
if input.Code == "" || input.Name == "" {
|
|
return nil, apperr.New(http.StatusBadRequest, "invalid_params", "code and name are required")
|
|
}
|
|
publicID, err := security.GeneratePublicID("place")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tx, err := s.store.Begin(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer tx.Rollback(ctx)
|
|
item, err := s.store.CreatePlace(ctx, tx, postgres.CreatePlaceParams{
|
|
PublicID: publicID,
|
|
Code: input.Code,
|
|
Name: input.Name,
|
|
Region: trimStringPtr(input.Region),
|
|
CoverURL: trimStringPtr(input.CoverURL),
|
|
Description: trimStringPtr(input.Description),
|
|
CenterPoint: input.CenterPoint,
|
|
Status: normalizeCatalogStatus(input.Status),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := tx.Commit(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
result := buildAdminPlaceSummary(*item)
|
|
return &result, nil
|
|
}
|
|
|
|
func (s *AdminProductionService) GetPlaceDetail(ctx context.Context, placePublicID string) (*AdminPlaceDetail, error) {
|
|
place, err := s.store.GetPlaceByPublicID(ctx, strings.TrimSpace(placePublicID))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if place == nil {
|
|
return nil, apperr.New(http.StatusNotFound, "place_not_found", "place not found")
|
|
}
|
|
mapAssets, err := s.store.ListMapAssetsByPlaceID(ctx, place.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result := &AdminPlaceDetail{
|
|
Place: buildAdminPlaceSummary(*place),
|
|
MapAssets: make([]AdminMapAssetSummary, 0, len(mapAssets)),
|
|
}
|
|
for _, item := range mapAssets {
|
|
summary, err := s.buildAdminMapAssetSummary(ctx, item)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result.MapAssets = append(result.MapAssets, summary)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (s *AdminProductionService) CreateMapAsset(ctx context.Context, placePublicID string, input CreateAdminMapAssetInput) (*AdminMapAssetSummary, error) {
|
|
place, err := s.store.GetPlaceByPublicID(ctx, strings.TrimSpace(placePublicID))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if place == nil {
|
|
return nil, apperr.New(http.StatusNotFound, "place_not_found", "place not found")
|
|
}
|
|
input.Code = strings.TrimSpace(input.Code)
|
|
input.Name = strings.TrimSpace(input.Name)
|
|
mapType := strings.TrimSpace(input.MapType)
|
|
if mapType == "" {
|
|
mapType = "standard"
|
|
}
|
|
if input.Code == "" || input.Name == "" {
|
|
return nil, apperr.New(http.StatusBadRequest, "invalid_params", "code and name are required")
|
|
}
|
|
|
|
var legacyMapID *string
|
|
if input.LegacyMapID != nil && strings.TrimSpace(*input.LegacyMapID) != "" {
|
|
legacyMap, err := s.store.GetResourceMapByPublicID(ctx, strings.TrimSpace(*input.LegacyMapID))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if legacyMap == nil {
|
|
return nil, apperr.New(http.StatusNotFound, "legacy_map_not_found", "legacy map not found")
|
|
}
|
|
legacyMapID = &legacyMap.ID
|
|
}
|
|
|
|
publicID, err := security.GeneratePublicID("mapasset")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tx, err := s.store.Begin(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer tx.Rollback(ctx)
|
|
item, err := s.store.CreateMapAsset(ctx, tx, postgres.CreateMapAssetParams{
|
|
PublicID: publicID,
|
|
PlaceID: place.ID,
|
|
LegacyMapID: legacyMapID,
|
|
Code: input.Code,
|
|
Name: input.Name,
|
|
MapType: mapType,
|
|
CoverURL: trimStringPtr(input.CoverURL),
|
|
Description: trimStringPtr(input.Description),
|
|
Status: normalizeCatalogStatus(input.Status),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := tx.Commit(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
result, err := s.buildAdminMapAssetSummary(ctx, *item)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &result, nil
|
|
}
|
|
|
|
func (s *AdminProductionService) GetMapAssetDetail(ctx context.Context, mapAssetPublicID string) (*AdminMapAssetDetail, error) {
|
|
item, err := s.store.GetMapAssetByPublicID(ctx, strings.TrimSpace(mapAssetPublicID))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if item == nil {
|
|
return nil, apperr.New(http.StatusNotFound, "map_asset_not_found", "map asset not found")
|
|
}
|
|
summary, err := s.buildAdminMapAssetSummary(ctx, *item)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tileReleases, err := s.store.ListTileReleasesByMapAssetID(ctx, item.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
courseSets, err := s.store.ListCourseSetsByMapAssetID(ctx, item.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
linkedEvents, err := s.store.ListMapAssetLinkedEvents(ctx, item.ID, 100)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result := &AdminMapAssetDetail{
|
|
MapAsset: summary,
|
|
TileReleases: make([]AdminTileReleaseView, 0, len(tileReleases)),
|
|
CourseSets: make([]AdminCourseSetBrief, 0, len(courseSets)),
|
|
LinkedEvents: make([]AdminMapLinkedEventBrief, 0, len(linkedEvents)),
|
|
}
|
|
for _, release := range tileReleases {
|
|
result.TileReleases = append(result.TileReleases, buildAdminTileReleaseView(release))
|
|
}
|
|
for _, courseSet := range courseSets {
|
|
brief, err := s.buildAdminCourseSetBrief(ctx, courseSet)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result.CourseSets = append(result.CourseSets, brief)
|
|
}
|
|
for _, linked := range linkedEvents {
|
|
result.LinkedEvents = append(result.LinkedEvents, buildAdminMapLinkedEventBrief(linked))
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (s *AdminProductionService) UpdateMapAsset(ctx context.Context, mapAssetPublicID string, input UpdateAdminMapAssetInput) (*AdminMapAssetSummary, error) {
|
|
item, err := s.store.GetMapAssetByPublicID(ctx, strings.TrimSpace(mapAssetPublicID))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if item == nil {
|
|
return nil, apperr.New(http.StatusNotFound, "map_asset_not_found", "map asset not found")
|
|
}
|
|
input.Code = strings.TrimSpace(input.Code)
|
|
input.Name = strings.TrimSpace(input.Name)
|
|
input.MapType = strings.TrimSpace(input.MapType)
|
|
if input.Code == "" || input.Name == "" {
|
|
return nil, apperr.New(http.StatusBadRequest, "invalid_params", "code and name are required")
|
|
}
|
|
if input.MapType == "" {
|
|
input.MapType = "standard"
|
|
}
|
|
|
|
tx, err := s.store.Begin(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer tx.Rollback(ctx)
|
|
updated, err := s.store.UpdateMapAsset(ctx, tx, postgres.UpdateMapAssetParams{
|
|
MapAssetID: item.ID,
|
|
Code: input.Code,
|
|
Name: input.Name,
|
|
MapType: input.MapType,
|
|
CoverURL: trimStringPtr(input.CoverURL),
|
|
Description: trimStringPtr(input.Description),
|
|
Status: normalizeCatalogStatus(input.Status),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := tx.Commit(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
refreshed, err := s.store.GetMapAssetByPublicID(ctx, updated.PublicID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if refreshed == nil {
|
|
return nil, apperr.New(http.StatusNotFound, "map_asset_not_found", "map asset not found")
|
|
}
|
|
result, err := s.buildAdminMapAssetSummary(ctx, *refreshed)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &result, nil
|
|
}
|
|
|
|
func (s *AdminProductionService) CreateTileRelease(ctx context.Context, mapAssetPublicID string, input CreateAdminTileReleaseInput) (*AdminTileReleaseView, error) {
|
|
mapAsset, err := s.store.GetMapAssetByPublicID(ctx, strings.TrimSpace(mapAssetPublicID))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if mapAsset == nil {
|
|
return nil, apperr.New(http.StatusNotFound, "map_asset_not_found", "map asset not found")
|
|
}
|
|
input.VersionCode = strings.TrimSpace(input.VersionCode)
|
|
input.TileBaseURL = strings.TrimSpace(input.TileBaseURL)
|
|
input.MetaURL = strings.TrimSpace(input.MetaURL)
|
|
if input.VersionCode == "" || input.TileBaseURL == "" || input.MetaURL == "" {
|
|
return nil, apperr.New(http.StatusBadRequest, "invalid_params", "versionCode, tileBaseUrl and metaUrl are required")
|
|
}
|
|
|
|
var legacyVersionID *string
|
|
if input.LegacyVersionID != nil && strings.TrimSpace(*input.LegacyVersionID) != "" {
|
|
if mapAsset.LegacyMapPublicID == nil || strings.TrimSpace(*mapAsset.LegacyMapPublicID) == "" {
|
|
return nil, apperr.New(http.StatusBadRequest, "legacy_map_missing", "map asset has no linked legacy map")
|
|
}
|
|
legacyVersion, err := s.store.GetResourceMapVersionByPublicID(ctx, *mapAsset.LegacyMapPublicID, strings.TrimSpace(*input.LegacyVersionID))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if legacyVersion == nil {
|
|
return nil, apperr.New(http.StatusNotFound, "legacy_tile_version_not_found", "legacy map version not found")
|
|
}
|
|
legacyVersionID = &legacyVersion.ID
|
|
}
|
|
|
|
publicID, err := security.GeneratePublicID("tile")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tx, err := s.store.Begin(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer tx.Rollback(ctx)
|
|
publishedAt := time.Now()
|
|
release, err := s.store.CreateTileRelease(ctx, tx, postgres.CreateTileReleaseParams{
|
|
PublicID: publicID,
|
|
MapAssetID: mapAsset.ID,
|
|
LegacyMapVersionID: legacyVersionID,
|
|
VersionCode: input.VersionCode,
|
|
Status: normalizeReleaseStatus(input.Status),
|
|
TileBaseURL: input.TileBaseURL,
|
|
MetaURL: input.MetaURL,
|
|
PublishedAssetRoot: trimStringPtr(input.PublishedAssetRoot),
|
|
MetadataJSON: input.Metadata,
|
|
PublishedAt: &publishedAt,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if input.SetAsCurrent {
|
|
if err := s.store.SetMapAssetCurrentTileRelease(ctx, tx, mapAsset.ID, release.ID); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if err := tx.Commit(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
view := buildAdminTileReleaseView(*release)
|
|
return &view, nil
|
|
}
|
|
|
|
func (s *AdminProductionService) ListCourseSources(ctx context.Context, limit int) ([]AdminCourseSourceSummary, error) {
|
|
items, err := s.store.ListCourseSources(ctx, limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result := make([]AdminCourseSourceSummary, 0, len(items))
|
|
for _, item := range items {
|
|
result = append(result, buildAdminCourseSourceSummary(item))
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (s *AdminProductionService) CreateCourseSource(ctx context.Context, input CreateAdminCourseSourceInput) (*AdminCourseSourceSummary, error) {
|
|
sourceType := strings.TrimSpace(input.SourceType)
|
|
fileURL := strings.TrimSpace(input.FileURL)
|
|
if sourceType == "" || fileURL == "" {
|
|
return nil, apperr.New(http.StatusBadRequest, "invalid_params", "sourceType and fileUrl are required")
|
|
}
|
|
|
|
var legacyPlayfieldVersionID *string
|
|
if input.LegacyPlayfieldID != nil && input.LegacyVersionID != nil && strings.TrimSpace(*input.LegacyPlayfieldID) != "" && strings.TrimSpace(*input.LegacyVersionID) != "" {
|
|
version, err := s.store.GetResourcePlayfieldVersionByPublicID(ctx, strings.TrimSpace(*input.LegacyPlayfieldID), strings.TrimSpace(*input.LegacyVersionID))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if version == nil {
|
|
return nil, apperr.New(http.StatusNotFound, "legacy_playfield_version_not_found", "legacy playfield version not found")
|
|
}
|
|
legacyPlayfieldVersionID = &version.ID
|
|
}
|
|
|
|
publicID, err := security.GeneratePublicID("csrc")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tx, err := s.store.Begin(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer tx.Rollback(ctx)
|
|
item, err := s.store.CreateCourseSource(ctx, tx, postgres.CreateCourseSourceParams{
|
|
PublicID: publicID,
|
|
LegacyPlayfieldVersionID: legacyPlayfieldVersionID,
|
|
SourceType: sourceType,
|
|
FileURL: fileURL,
|
|
Checksum: trimStringPtr(input.Checksum),
|
|
ParserVersion: trimStringPtr(input.ParserVersion),
|
|
ImportStatus: normalizeCourseSourceStatus(input.ImportStatus),
|
|
MetadataJSON: input.Metadata,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := tx.Commit(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
result := buildAdminCourseSourceSummary(*item)
|
|
return &result, nil
|
|
}
|
|
|
|
func (s *AdminProductionService) GetCourseSource(ctx context.Context, sourcePublicID string) (*AdminCourseSourceSummary, error) {
|
|
item, err := s.store.GetCourseSourceByPublicID(ctx, strings.TrimSpace(sourcePublicID))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if item == nil {
|
|
return nil, apperr.New(http.StatusNotFound, "course_source_not_found", "course source not found")
|
|
}
|
|
result := buildAdminCourseSourceSummary(*item)
|
|
return &result, nil
|
|
}
|
|
|
|
func (s *AdminProductionService) CreateCourseSet(ctx context.Context, mapAssetPublicID string, input CreateAdminCourseSetInput) (*AdminCourseSetBrief, error) {
|
|
mapAsset, err := s.store.GetMapAssetByPublicID(ctx, strings.TrimSpace(mapAssetPublicID))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if mapAsset == nil {
|
|
return nil, apperr.New(http.StatusNotFound, "map_asset_not_found", "map asset not found")
|
|
}
|
|
input.Code = strings.TrimSpace(input.Code)
|
|
input.Mode = strings.TrimSpace(input.Mode)
|
|
input.Name = strings.TrimSpace(input.Name)
|
|
if input.Code == "" || input.Mode == "" || input.Name == "" {
|
|
return nil, apperr.New(http.StatusBadRequest, "invalid_params", "code, mode and name are required")
|
|
}
|
|
publicID, err := security.GeneratePublicID("cset")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tx, err := s.store.Begin(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer tx.Rollback(ctx)
|
|
item, err := s.store.CreateCourseSet(ctx, tx, postgres.CreateCourseSetParams{
|
|
PublicID: publicID,
|
|
PlaceID: mapAsset.PlaceID,
|
|
MapAssetID: mapAsset.ID,
|
|
Code: input.Code,
|
|
Mode: input.Mode,
|
|
Name: input.Name,
|
|
Description: trimStringPtr(input.Description),
|
|
Status: normalizeCatalogStatus(input.Status),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := tx.Commit(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
brief, err := s.buildAdminCourseSetBrief(ctx, *item)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &brief, nil
|
|
}
|
|
|
|
func (s *AdminProductionService) GetCourseSetDetail(ctx context.Context, courseSetPublicID string) (*AdminCourseSetDetail, error) {
|
|
item, err := s.store.GetCourseSetByPublicID(ctx, strings.TrimSpace(courseSetPublicID))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if item == nil {
|
|
return nil, apperr.New(http.StatusNotFound, "course_set_not_found", "course set not found")
|
|
}
|
|
brief, err := s.buildAdminCourseSetBrief(ctx, *item)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
variants, err := s.store.ListCourseVariantsByCourseSetID(ctx, item.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result := &AdminCourseSetDetail{
|
|
CourseSet: brief,
|
|
Variants: make([]AdminCourseVariantView, 0, len(variants)),
|
|
}
|
|
for _, variant := range variants {
|
|
result.Variants = append(result.Variants, buildAdminCourseVariantView(variant))
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (s *AdminProductionService) CreateCourseVariant(ctx context.Context, courseSetPublicID string, input CreateAdminCourseVariantInput) (*AdminCourseVariantView, error) {
|
|
courseSet, err := s.store.GetCourseSetByPublicID(ctx, strings.TrimSpace(courseSetPublicID))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if courseSet == nil {
|
|
return nil, apperr.New(http.StatusNotFound, "course_set_not_found", "course set not found")
|
|
}
|
|
input.Name = strings.TrimSpace(input.Name)
|
|
input.Mode = strings.TrimSpace(input.Mode)
|
|
if input.Name == "" || input.Mode == "" {
|
|
return nil, apperr.New(http.StatusBadRequest, "invalid_params", "name and mode are required")
|
|
}
|
|
|
|
var sourceID *string
|
|
if input.SourceID != nil && strings.TrimSpace(*input.SourceID) != "" {
|
|
source, err := s.store.GetCourseSourceByPublicID(ctx, strings.TrimSpace(*input.SourceID))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if source == nil {
|
|
return nil, apperr.New(http.StatusNotFound, "course_source_not_found", "course source not found")
|
|
}
|
|
sourceID = &source.ID
|
|
}
|
|
|
|
publicID, err := security.GeneratePublicID("cvar")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tx, err := s.store.Begin(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer tx.Rollback(ctx)
|
|
item, err := s.store.CreateCourseVariant(ctx, tx, postgres.CreateCourseVariantParams{
|
|
PublicID: publicID,
|
|
CourseSetID: courseSet.ID,
|
|
SourceID: sourceID,
|
|
Name: input.Name,
|
|
RouteCode: trimStringPtr(input.RouteCode),
|
|
Mode: input.Mode,
|
|
ControlCount: input.ControlCount,
|
|
Difficulty: trimStringPtr(input.Difficulty),
|
|
Status: normalizeCatalogStatus(input.Status),
|
|
IsDefault: input.IsDefault,
|
|
ConfigPatch: input.ConfigPatch,
|
|
MetadataJSON: input.Metadata,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if input.IsDefault {
|
|
if err := s.store.SetCourseSetCurrentVariant(ctx, tx, courseSet.ID, item.ID); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if err := tx.Commit(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
view := buildAdminCourseVariantView(*item)
|
|
return &view, nil
|
|
}
|
|
|
|
func (s *AdminProductionService) ListRuntimeBindings(ctx context.Context, limit int) ([]AdminRuntimeBindingSummary, error) {
|
|
items, err := s.store.ListMapRuntimeBindings(ctx, limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result := make([]AdminRuntimeBindingSummary, 0, len(items))
|
|
for _, item := range items {
|
|
result = append(result, buildAdminRuntimeBindingSummary(item))
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (s *AdminProductionService) CreateRuntimeBinding(ctx context.Context, input CreateAdminRuntimeBindingInput) (*AdminRuntimeBindingSummary, error) {
|
|
eventRecord, err := s.store.GetAdminEventByPublicID(ctx, strings.TrimSpace(input.EventID))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if eventRecord == nil {
|
|
return nil, apperr.New(http.StatusNotFound, "event_not_found", "event not found")
|
|
}
|
|
place, err := s.store.GetPlaceByPublicID(ctx, strings.TrimSpace(input.PlaceID))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if place == nil {
|
|
return nil, apperr.New(http.StatusNotFound, "place_not_found", "place not found")
|
|
}
|
|
mapAsset, err := s.store.GetMapAssetByPublicID(ctx, strings.TrimSpace(input.MapAssetID))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if mapAsset == nil || mapAsset.PlaceID != place.ID {
|
|
return nil, apperr.New(http.StatusBadRequest, "map_asset_mismatch", "map asset does not belong to place")
|
|
}
|
|
tileRelease, err := s.store.GetTileReleaseByPublicID(ctx, strings.TrimSpace(input.TileReleaseID))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if tileRelease == nil || tileRelease.MapAssetID != mapAsset.ID {
|
|
return nil, apperr.New(http.StatusBadRequest, "tile_release_mismatch", "tile release does not belong to map asset")
|
|
}
|
|
courseSet, err := s.store.GetCourseSetByPublicID(ctx, strings.TrimSpace(input.CourseSetID))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if courseSet == nil || courseSet.PlaceID != place.ID || courseSet.MapAssetID != mapAsset.ID {
|
|
return nil, apperr.New(http.StatusBadRequest, "course_set_mismatch", "course set does not match place/map asset")
|
|
}
|
|
courseVariant, err := s.store.GetCourseVariantByPublicID(ctx, strings.TrimSpace(input.CourseVariantID))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if courseVariant == nil || courseVariant.CourseSetID != courseSet.ID {
|
|
return nil, apperr.New(http.StatusBadRequest, "course_variant_mismatch", "course variant does not belong to course set")
|
|
}
|
|
|
|
publicID, err := security.GeneratePublicID("rtbind")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tx, err := s.store.Begin(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer tx.Rollback(ctx)
|
|
item, err := s.store.CreateMapRuntimeBinding(ctx, tx, postgres.CreateMapRuntimeBindingParams{
|
|
PublicID: publicID,
|
|
EventID: eventRecord.ID,
|
|
PlaceID: place.ID,
|
|
MapAssetID: mapAsset.ID,
|
|
TileReleaseID: tileRelease.ID,
|
|
CourseSetID: courseSet.ID,
|
|
CourseVariantID: courseVariant.ID,
|
|
Status: normalizeRuntimeBindingStatus(input.Status),
|
|
Notes: trimStringPtr(input.Notes),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := tx.Commit(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
created, err := s.store.GetMapRuntimeBindingByPublicID(ctx, item.PublicID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if created == nil {
|
|
return nil, apperr.New(http.StatusNotFound, "runtime_binding_not_found", "runtime binding not found")
|
|
}
|
|
result := buildAdminRuntimeBindingSummary(*created)
|
|
return &result, nil
|
|
}
|
|
|
|
func (s *AdminProductionService) ImportTileRelease(ctx context.Context, input ImportAdminTileReleaseInput) (*ImportAdminTileReleaseResult, error) {
|
|
input.PlaceCode = strings.TrimSpace(input.PlaceCode)
|
|
input.PlaceName = strings.TrimSpace(input.PlaceName)
|
|
input.MapAssetCode = strings.TrimSpace(input.MapAssetCode)
|
|
input.MapAssetName = strings.TrimSpace(input.MapAssetName)
|
|
input.VersionCode = strings.TrimSpace(input.VersionCode)
|
|
input.TileBaseURL = strings.TrimSpace(input.TileBaseURL)
|
|
input.MetaURL = strings.TrimSpace(input.MetaURL)
|
|
if input.PlaceCode == "" || input.PlaceName == "" || input.MapAssetCode == "" || input.MapAssetName == "" || input.VersionCode == "" || input.TileBaseURL == "" || input.MetaURL == "" {
|
|
return nil, apperr.New(http.StatusBadRequest, "invalid_params", "placeCode, placeName, mapAssetCode, mapAssetName, versionCode, tileBaseUrl and metaUrl are required")
|
|
}
|
|
|
|
place, err := s.store.GetPlaceByCode(ctx, input.PlaceCode)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if place == nil {
|
|
created, err := s.CreatePlace(ctx, CreateAdminPlaceInput{
|
|
Code: input.PlaceCode,
|
|
Name: input.PlaceName,
|
|
Region: trimStringPtr(input.PlaceRegion),
|
|
CoverURL: trimStringPtr(input.PlaceCoverURL),
|
|
Description: trimStringPtr(input.PlaceDescription),
|
|
CenterPoint: input.PlaceCenterPoint,
|
|
Status: normalizeCatalogStatus(input.Status),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
place, err = s.store.GetPlaceByPublicID(ctx, created.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if place == nil {
|
|
return nil, apperr.New(http.StatusNotFound, "place_not_found", "place not found")
|
|
}
|
|
|
|
mapAsset, err := s.store.GetMapAssetByCode(ctx, input.MapAssetCode)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if mapAsset == nil {
|
|
created, err := s.CreateMapAsset(ctx, place.PublicID, CreateAdminMapAssetInput{
|
|
Code: input.MapAssetCode,
|
|
Name: input.MapAssetName,
|
|
MapType: strings.TrimSpace(input.MapType),
|
|
CoverURL: trimStringPtr(input.MapCoverURL),
|
|
Description: trimStringPtr(input.MapDescription),
|
|
Status: normalizeCatalogStatus(input.Status),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
mapAsset, err = s.store.GetMapAssetByPublicID(ctx, created.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if mapAsset == nil || mapAsset.PlaceID != place.ID {
|
|
return nil, apperr.New(http.StatusBadRequest, "map_asset_mismatch", "map asset does not belong to place")
|
|
}
|
|
|
|
release, err := s.store.GetTileReleaseByMapAssetIDAndVersionCode(ctx, mapAsset.ID, input.VersionCode)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if release == nil {
|
|
created, err := s.CreateTileRelease(ctx, mapAsset.PublicID, CreateAdminTileReleaseInput{
|
|
VersionCode: input.VersionCode,
|
|
Status: normalizeReleaseStatus(input.Status),
|
|
TileBaseURL: input.TileBaseURL,
|
|
MetaURL: input.MetaURL,
|
|
PublishedAssetRoot: trimStringPtr(input.PublishedAssetRoot),
|
|
Metadata: input.Metadata,
|
|
SetAsCurrent: input.SetAsCurrent,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
release, err = s.store.GetTileReleaseByPublicID(ctx, created.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else if input.SetAsCurrent {
|
|
tx, err := s.store.Begin(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer tx.Rollback(ctx)
|
|
if err := s.store.SetMapAssetCurrentTileRelease(ctx, tx, mapAsset.ID, release.ID); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := tx.Commit(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
mapAsset, err = s.store.GetMapAssetByPublicID(ctx, mapAsset.PublicID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if release == nil {
|
|
return nil, apperr.New(http.StatusNotFound, "tile_release_not_found", "tile release not found")
|
|
}
|
|
|
|
placeSummary := buildAdminPlaceSummary(*place)
|
|
mapSummary, err := s.buildAdminMapAssetSummary(ctx, *mapAsset)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &ImportAdminTileReleaseResult{
|
|
Place: placeSummary,
|
|
MapAsset: mapSummary,
|
|
TileRelease: buildAdminTileReleaseView(*release),
|
|
}, nil
|
|
}
|
|
|
|
func (s *AdminProductionService) ImportCourseSetKMLBatch(ctx context.Context, input ImportAdminCourseSetBatchInput) (*ImportAdminCourseSetBatchResult, error) {
|
|
input.PlaceCode = strings.TrimSpace(input.PlaceCode)
|
|
input.PlaceName = strings.TrimSpace(input.PlaceName)
|
|
input.MapAssetCode = strings.TrimSpace(input.MapAssetCode)
|
|
input.MapAssetName = strings.TrimSpace(input.MapAssetName)
|
|
input.CourseSetCode = strings.TrimSpace(input.CourseSetCode)
|
|
input.CourseSetName = strings.TrimSpace(input.CourseSetName)
|
|
input.Mode = strings.TrimSpace(input.Mode)
|
|
if input.PlaceCode == "" || input.PlaceName == "" || input.MapAssetCode == "" || input.MapAssetName == "" || input.CourseSetCode == "" || input.CourseSetName == "" || input.Mode == "" || len(input.Routes) == 0 {
|
|
return nil, apperr.New(http.StatusBadRequest, "invalid_params", "placeCode, placeName, mapAssetCode, mapAssetName, courseSetCode, courseSetName, mode and routes are required")
|
|
}
|
|
|
|
place, err := s.store.GetPlaceByCode(ctx, input.PlaceCode)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if place == nil {
|
|
created, err := s.CreatePlace(ctx, CreateAdminPlaceInput{
|
|
Code: input.PlaceCode,
|
|
Name: input.PlaceName,
|
|
Status: normalizeCatalogStatus(input.Status),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
place, err = s.store.GetPlaceByPublicID(ctx, created.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
mapAsset, err := s.store.GetMapAssetByCode(ctx, input.MapAssetCode)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if mapAsset == nil {
|
|
created, err := s.CreateMapAsset(ctx, place.PublicID, CreateAdminMapAssetInput{
|
|
Code: input.MapAssetCode,
|
|
Name: input.MapAssetName,
|
|
MapType: strings.TrimSpace(input.MapType),
|
|
Status: normalizeCatalogStatus(input.Status),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
mapAsset, err = s.store.GetMapAssetByPublicID(ctx, created.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if mapAsset == nil || mapAsset.PlaceID != place.ID {
|
|
return nil, apperr.New(http.StatusBadRequest, "map_asset_mismatch", "map asset does not belong to place")
|
|
}
|
|
|
|
courseSet, err := s.store.GetCourseSetByCode(ctx, input.CourseSetCode)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if courseSet == nil {
|
|
created, err := s.CreateCourseSet(ctx, mapAsset.PublicID, CreateAdminCourseSetInput{
|
|
Code: input.CourseSetCode,
|
|
Mode: input.Mode,
|
|
Name: input.CourseSetName,
|
|
Description: trimStringPtr(input.Description),
|
|
Status: normalizeCatalogStatus(input.Status),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
courseSet, err = s.store.GetCourseSetByPublicID(ctx, created.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if courseSet == nil || courseSet.PlaceID != place.ID || courseSet.MapAssetID != mapAsset.ID {
|
|
return nil, apperr.New(http.StatusBadRequest, "course_set_mismatch", "course set does not match place/map asset")
|
|
}
|
|
|
|
defaultRouteCode := ""
|
|
if input.DefaultRouteCode != nil {
|
|
defaultRouteCode = strings.TrimSpace(*input.DefaultRouteCode)
|
|
}
|
|
|
|
for _, route := range input.Routes {
|
|
route.Name = strings.TrimSpace(route.Name)
|
|
route.RouteCode = strings.TrimSpace(route.RouteCode)
|
|
route.FileURL = strings.TrimSpace(route.FileURL)
|
|
sourceType := strings.TrimSpace(route.SourceType)
|
|
if sourceType == "" {
|
|
sourceType = "kml"
|
|
}
|
|
if route.Name == "" || route.RouteCode == "" || route.FileURL == "" {
|
|
return nil, apperr.New(http.StatusBadRequest, "invalid_params", "route name, routeCode and fileUrl are required")
|
|
}
|
|
|
|
existing, err := s.store.GetCourseVariantByCourseSetIDAndRouteCode(ctx, courseSet.ID, route.RouteCode)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if existing != nil {
|
|
if defaultRouteCode != "" && route.RouteCode == defaultRouteCode {
|
|
tx, err := s.store.Begin(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer tx.Rollback(ctx)
|
|
if err := s.store.SetCourseSetCurrentVariant(ctx, tx, courseSet.ID, existing.ID); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := tx.Commit(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
|
|
source, err := s.CreateCourseSource(ctx, CreateAdminCourseSourceInput{
|
|
SourceType: sourceType,
|
|
FileURL: route.FileURL,
|
|
ImportStatus: "imported",
|
|
Metadata: route.Metadata,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
isDefault := defaultRouteCode != "" && route.RouteCode == defaultRouteCode
|
|
_, err = s.CreateCourseVariant(ctx, courseSet.PublicID, CreateAdminCourseVariantInput{
|
|
SourceID: &source.ID,
|
|
Name: route.Name,
|
|
RouteCode: &route.RouteCode,
|
|
Mode: input.Mode,
|
|
ControlCount: route.ControlCount,
|
|
Difficulty: trimStringPtr(route.Difficulty),
|
|
Status: normalizeCatalogStatus(route.Status),
|
|
IsDefault: isDefault,
|
|
Metadata: route.Metadata,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
courseSet, err = s.store.GetCourseSetByPublicID(ctx, courseSet.PublicID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
variants, err := s.store.ListCourseVariantsByCourseSetID(ctx, courseSet.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
views := make([]AdminCourseVariantView, 0, len(variants))
|
|
for _, variant := range variants {
|
|
views = append(views, buildAdminCourseVariantView(variant))
|
|
}
|
|
placeSummary := buildAdminPlaceSummary(*place)
|
|
mapSummary, err := s.buildAdminMapAssetSummary(ctx, *mapAsset)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
courseBrief, err := s.buildAdminCourseSetBrief(ctx, *courseSet)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &ImportAdminCourseSetBatchResult{
|
|
Place: placeSummary,
|
|
MapAsset: mapSummary,
|
|
CourseSet: courseBrief,
|
|
Variants: views,
|
|
}, nil
|
|
}
|
|
|
|
func (s *AdminProductionService) GetRuntimeBinding(ctx context.Context, runtimeBindingPublicID string) (*AdminRuntimeBindingSummary, error) {
|
|
item, err := s.store.GetMapRuntimeBindingByPublicID(ctx, strings.TrimSpace(runtimeBindingPublicID))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if item == nil {
|
|
return nil, apperr.New(http.StatusNotFound, "runtime_binding_not_found", "runtime binding not found")
|
|
}
|
|
result := buildAdminRuntimeBindingSummary(*item)
|
|
return &result, nil
|
|
}
|
|
|
|
func (s *AdminProductionService) buildAdminMapAssetSummary(ctx context.Context, item postgres.MapAsset) (AdminMapAssetSummary, error) {
|
|
result := AdminMapAssetSummary{
|
|
ID: item.PublicID,
|
|
PlaceID: item.PlaceID,
|
|
PlaceName: item.PlaceName,
|
|
LegacyMapID: item.LegacyMapPublicID,
|
|
Code: item.Code,
|
|
Name: item.Name,
|
|
MapType: item.MapType,
|
|
CoverURL: item.CoverURL,
|
|
Description: item.Description,
|
|
Status: item.Status,
|
|
}
|
|
if item.CurrentTileReleaseID != nil {
|
|
releases, err := s.store.ListTileReleasesByMapAssetID(ctx, item.ID)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
for _, release := range releases {
|
|
if release.ID == *item.CurrentTileReleaseID {
|
|
result.CurrentTileRelease = &AdminTileReleaseBrief{
|
|
ID: release.PublicID,
|
|
VersionCode: release.VersionCode,
|
|
Status: release.Status,
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func buildAdminMapLinkedEventBrief(item postgres.MapAssetLinkedEvent) AdminMapLinkedEventBrief {
|
|
return AdminMapLinkedEventBrief{
|
|
EventID: item.EventPublicID,
|
|
Title: item.DisplayName,
|
|
Summary: item.Summary,
|
|
Status: item.Status,
|
|
IsDefaultExperience: item.IsDefaultExperience,
|
|
ShowInEventList: item.ShowInEventList,
|
|
CurrentReleaseID: item.CurrentReleasePublicID,
|
|
ConfigLabel: item.ConfigLabel,
|
|
RouteCode: item.RouteCode,
|
|
CurrentPresentationID: item.CurrentPresentationID,
|
|
CurrentPresentation: item.CurrentPresentationName,
|
|
CurrentContentBundleID: item.CurrentContentBundleID,
|
|
CurrentContentBundle: item.CurrentContentBundleName,
|
|
}
|
|
}
|
|
|
|
func (s *AdminProductionService) buildAdminCourseSetBrief(ctx context.Context, item postgres.CourseSet) (AdminCourseSetBrief, error) {
|
|
result := AdminCourseSetBrief{
|
|
ID: item.PublicID,
|
|
Code: item.Code,
|
|
Mode: item.Mode,
|
|
Name: item.Name,
|
|
Description: item.Description,
|
|
Status: item.Status,
|
|
}
|
|
if item.CurrentVariantID != nil {
|
|
variants, err := s.store.ListCourseVariantsByCourseSetID(ctx, item.ID)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
for _, variant := range variants {
|
|
if variant.ID == *item.CurrentVariantID {
|
|
result.CurrentVariant = &AdminCourseVariantBrief{
|
|
ID: variant.PublicID,
|
|
Name: variant.Name,
|
|
RouteCode: variant.RouteCode,
|
|
Status: variant.Status,
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func buildAdminPlaceSummary(item postgres.Place) AdminPlaceSummary {
|
|
return AdminPlaceSummary{
|
|
ID: item.PublicID,
|
|
Code: item.Code,
|
|
Name: item.Name,
|
|
Region: item.Region,
|
|
CoverURL: item.CoverURL,
|
|
Description: item.Description,
|
|
CenterPoint: decodeJSONMap(item.CenterPoint),
|
|
Status: item.Status,
|
|
}
|
|
}
|
|
|
|
func buildAdminTileReleaseView(item postgres.TileRelease) AdminTileReleaseView {
|
|
return AdminTileReleaseView{
|
|
ID: item.PublicID,
|
|
LegacyVersionID: item.LegacyMapVersionPub,
|
|
VersionCode: item.VersionCode,
|
|
Status: item.Status,
|
|
TileBaseURL: item.TileBaseURL,
|
|
MetaURL: item.MetaURL,
|
|
PublishedAssetRoot: item.PublishedAssetRoot,
|
|
Metadata: decodeJSONMap(item.MetadataJSON),
|
|
PublishedAt: item.PublishedAt,
|
|
}
|
|
}
|
|
|
|
func buildAdminCourseSourceSummary(item postgres.CourseSource) AdminCourseSourceSummary {
|
|
return AdminCourseSourceSummary{
|
|
ID: item.PublicID,
|
|
LegacyVersionID: item.LegacyPlayfieldVersionPub,
|
|
SourceType: item.SourceType,
|
|
FileURL: item.FileURL,
|
|
Checksum: item.Checksum,
|
|
ParserVersion: item.ParserVersion,
|
|
ImportStatus: item.ImportStatus,
|
|
Metadata: decodeJSONMap(item.MetadataJSON),
|
|
ImportedAt: item.ImportedAt,
|
|
}
|
|
}
|
|
|
|
func buildAdminCourseVariantView(item postgres.CourseVariant) AdminCourseVariantView {
|
|
return AdminCourseVariantView{
|
|
ID: item.PublicID,
|
|
SourceID: item.SourcePublicID,
|
|
Name: item.Name,
|
|
RouteCode: item.RouteCode,
|
|
Mode: item.Mode,
|
|
ControlCount: item.ControlCount,
|
|
Difficulty: item.Difficulty,
|
|
Status: item.Status,
|
|
IsDefault: item.IsDefault,
|
|
ConfigPatch: decodeJSONMap(item.ConfigPatch),
|
|
Metadata: decodeJSONMap(item.MetadataJSON),
|
|
}
|
|
}
|
|
|
|
func buildAdminRuntimeBindingSummary(item postgres.MapRuntimeBinding) AdminRuntimeBindingSummary {
|
|
return AdminRuntimeBindingSummary{
|
|
ID: item.PublicID,
|
|
EventID: item.EventPublicID,
|
|
PlaceID: item.PlacePublicID,
|
|
MapAssetID: item.MapAssetPublicID,
|
|
TileReleaseID: item.TileReleasePublicID,
|
|
CourseSetID: item.CourseSetPublicID,
|
|
CourseVariantID: item.CourseVariantPublicID,
|
|
Status: item.Status,
|
|
Notes: item.Notes,
|
|
}
|
|
}
|
|
|
|
func normalizeCourseSourceStatus(value string) string {
|
|
switch strings.TrimSpace(value) {
|
|
case "draft":
|
|
return "draft"
|
|
case "parsed":
|
|
return "parsed"
|
|
case "failed":
|
|
return "failed"
|
|
case "archived":
|
|
return "archived"
|
|
default:
|
|
return "imported"
|
|
}
|
|
}
|
|
|
|
func normalizeRuntimeBindingStatus(value string) string {
|
|
switch strings.TrimSpace(value) {
|
|
case "active":
|
|
return "active"
|
|
case "disabled":
|
|
return "disabled"
|
|
case "archived":
|
|
return "archived"
|
|
default:
|
|
return "draft"
|
|
}
|
|
}
|
|
|
|
func normalizeReleaseStatus(value string) string {
|
|
switch strings.TrimSpace(value) {
|
|
case "active":
|
|
return "active"
|
|
case "published":
|
|
return "published"
|
|
case "retired":
|
|
return "retired"
|
|
case "archived":
|
|
return "archived"
|
|
default:
|
|
return "draft"
|
|
}
|
|
}
|