feat: sync official star gift lifecycle tooling

Sync telesrv 4c0e2d9 (feat: complete official star gift lifecycle and admin tooling).

Public adjustments: skipped private docs/deploy-nginx/README source changes, kept iamxvbaba/td public dependency, replaced local/private sample IP and orange seed label.
This commit is contained in:
A 2026-07-19 03:11:35 +08:00
parent f2c2fd0236
commit 14bf7d1e20
92 changed files with 14768 additions and 727 deletions

View file

@ -4,6 +4,7 @@ import (
"context"
"crypto/subtle"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
@ -15,6 +16,7 @@ import (
"telesrv/internal/admin"
"telesrv/internal/domain"
"telesrv/internal/officialgifts"
)
type Config struct {
@ -32,6 +34,9 @@ type Service interface {
DeletePrivateMessages(ctx context.Context, req admin.DeletePrivateMessagesRequest) (admin.CommandResult, error)
DeletePrivateHistory(ctx context.Context, req admin.DeletePrivateHistoryRequest) (admin.CommandResult, error)
ImportStarGift(ctx context.Context, req admin.ImportStarGiftRequest) (admin.CommandResult, error)
ImportOfficialStarGift(ctx context.Context, req admin.ImportOfficialStarGiftRequest) (admin.CommandResult, error)
OfficialStarGifts(ctx context.Context) ([]officialgifts.GiftSummary, error)
OfficialStarGiftAnimation(ctx context.Context, sourceGiftID string) ([]byte, bool, error)
PublishStarGiftCollectibles(ctx context.Context, req admin.PublishStarGiftCollectiblesRequest) (admin.CommandResult, error)
SetStarGiftEnabled(ctx context.Context, req admin.SetStarGiftEnabledRequest) (admin.CommandResult, error)
SetStarGiftSortOrder(ctx context.Context, req admin.SetStarGiftSortOrderRequest) (admin.CommandResult, error)
@ -95,6 +100,9 @@ func (s *Server) routes() http.Handler {
mux.HandleFunc("POST /v1/messages/delete", s.authenticated(s.handleDeleteMessages))
mux.HandleFunc("POST /v1/messages/delete-history", s.authenticated(s.handleDeleteHistory))
mux.HandleFunc("POST /v1/gifts/import", s.authenticated(s.handleImportStarGift))
mux.HandleFunc("GET /v1/official-gifts", s.authenticated(s.handleOfficialStarGifts))
mux.HandleFunc("GET /v1/official-gifts/{id}/animation", s.authenticated(s.handleOfficialStarGiftAnimation))
mux.HandleFunc("POST /v1/official-gifts/import", s.authenticated(s.handleImportOfficialStarGift))
mux.HandleFunc("POST /v1/gifts/{id}/collectibles/publish", s.authenticated(s.handlePublishStarGiftCollectibles))
mux.HandleFunc("POST /v1/gifts/set-enabled", s.authenticated(s.handleSetStarGiftEnabled))
mux.HandleFunc("POST /v1/gifts/set-sort-order", s.authenticated(s.handleSetStarGiftSortOrder))
@ -221,6 +229,60 @@ func (s *Server) handleImportStarGift(w http.ResponseWriter, r *http.Request) {
writeCommandResult(w, result, err)
}
func (s *Server) handleOfficialStarGifts(w http.ResponseWriter, r *http.Request) {
items, err := s.svc.OfficialStarGifts(r.Context())
if err != nil {
status := http.StatusInternalServerError
if errors.Is(err, officialgifts.ErrUnavailable) {
status = http.StatusServiceUnavailable
}
writeError(w, status, err.Error())
return
}
result := make([]map[string]any, 0, len(items))
for _, item := range items {
result = append(result, officialStarGiftListItem(item))
}
writeJSON(w, http.StatusOK, map[string]any{"gifts": result})
}
func officialStarGiftListItem(item officialgifts.GiftSummary) map[string]any {
return map[string]any{
"source_gift_id": strconv.FormatInt(item.ID, 10), "title": item.Title,
"stars": strconv.FormatInt(item.Stars, 10), "convert_stars": strconv.FormatInt(item.ConvertStars, 10),
"upgrade_stars": strconv.FormatInt(item.UpgradeStars, 10),
"availability_total": item.AvailabilityTotal, "limited": item.Limited, "sold_out": item.SoldOut,
"model_count": item.ModelCount, "pattern_count": item.PatternCount, "backdrop_count": item.BackdropCount,
"crafted_model_count": item.CraftedModelCount, "can_upgrade": item.CanUpgrade(), "can_craft": item.CanCraft(),
"document_id": strconv.FormatInt(item.DocumentID, 10), "animation_validated": item.AnimationValidated,
}
}
func (s *Server) handleOfficialStarGiftAnimation(w http.ResponseWriter, r *http.Request) {
raw, found, err := s.svc.OfficialStarGiftAnimation(r.Context(), r.PathValue("id"))
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
if !found {
writeError(w, http.StatusNotFound, "official gift animation not found")
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("Cache-Control", "private, max-age=60")
w.WriteHeader(http.StatusOK)
_, _ = w.Write(raw)
}
func (s *Server) handleImportOfficialStarGift(w http.ResponseWriter, r *http.Request) {
var req admin.ImportOfficialStarGiftRequest
if !decodeJSON(w, r, &req) {
return
}
result, err := s.svc.ImportOfficialStarGift(r.Context(), req)
writeCommandResult(w, result, err)
}
func (s *Server) handlePublishStarGiftCollectibles(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
giftID, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
@ -338,7 +400,7 @@ func (s *Server) handleStarGiftCollectibles(w http.ResponseWriter, r *http.Reque
return
}
if !found {
writeJSON(w, http.StatusOK, map[string]any{"found": false, "gift_id": giftID})
writeJSON(w, http.StatusOK, map[string]any{"found": false, "gift_id": strconv.FormatInt(giftID, 10)})
return
}
writeJSON(w, http.StatusOK, collectiblePreviewResponse(preview))
@ -347,8 +409,10 @@ func (s *Server) handleStarGiftCollectibles(w http.ResponseWriter, r *http.Reque
func collectiblePreviewResponse(preview domain.StarGiftUpgradePreview) map[string]any {
attribute := func(value domain.StarGiftCollectibleAttribute) map[string]any {
result := map[string]any{
"id": value.ID, "name": value.Name, "rarity_permille": value.RarityPermille,
"sort_order": value.SortOrder, "kind": value.Kind,
"id": strconv.FormatInt(value.ID, 10), "name": value.Name, "rarity_kind": value.RarityKind,
"rarity_permille": value.RarityPermille, "crafted": value.Crafted,
"official_document_id": strconv.FormatInt(value.OfficialDocumentID, 10),
"sort_order": value.SortOrder, "kind": value.Kind,
}
if value.Animation != nil {
result["source_name"] = value.Animation.SourceName
@ -371,8 +435,9 @@ func collectiblePreviewResponse(preview domain.StarGiftUpgradePreview) map[strin
return result
}
return map[string]any{
"found": true, "gift_id": preview.GiftID, "revision": preview.Revision, "upgrade_stars": preview.UpgradeStars,
"supply_total": preview.SupplyTotal, "issued": preview.Issued,
"found": true, "gift_id": strconv.FormatInt(preview.GiftID, 10), "revision": preview.Revision,
"upgrade_stars": strconv.FormatInt(preview.UpgradeStars, 10),
"supply_total": preview.SupplyTotal, "issued": preview.Issued,
"slug_prefix": preview.SlugPrefix,
"models": mapAttributes(preview.Models), "patterns": mapAttributes(preview.Patterns),
"backdrops": mapAttributes(preview.Backdrops),