chore: refresh gramsrv public release
This commit is contained in:
parent
75cebe8dbf
commit
70b6820474
1274 changed files with 378751 additions and 59919 deletions
124
internal/app/stargifts/service.go
Normal file
124
internal/app/stargifts/service.go
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
// Package stargifts 实现 Star 礼物应用服务:礼物目录(从 seed 合成、懒加载缓存)+ peer 收到的
|
||||
// 礼物实例 CRUD。扣费/退款/服务消息投递由 rpc 层编排(复用 Stars 账本 + SendPrivateText),
|
||||
// 本层只管目录与持久化。
|
||||
package stargifts
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/store"
|
||||
)
|
||||
|
||||
// CatalogProvider 合成礼物目录(app/files 实现)。
|
||||
type CatalogProvider interface {
|
||||
BuildStarGiftCatalog(ctx context.Context) ([]domain.StarGift, error)
|
||||
}
|
||||
|
||||
// Service 是 Star 礼物应用服务。
|
||||
type Service struct {
|
||||
store store.StarGiftStore
|
||||
catalog CatalogProvider
|
||||
|
||||
mu sync.Mutex
|
||||
built bool
|
||||
gifts []domain.StarGift
|
||||
byID map[int64]domain.StarGift
|
||||
hash int
|
||||
}
|
||||
|
||||
// NewService 创建 Star 礼物服务。
|
||||
func NewService(st store.StarGiftStore, catalog CatalogProvider) *Service {
|
||||
return &Service{store: st, catalog: catalog}
|
||||
}
|
||||
|
||||
// ensureCatalog 懒加载并缓存目录(静态数据,构建一次)。
|
||||
func (s *Service) ensureCatalog(ctx context.Context) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.built {
|
||||
return nil
|
||||
}
|
||||
gifts, err := s.catalog.BuildStarGiftCatalog(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.gifts = gifts
|
||||
s.byID = make(map[int64]domain.StarGift, len(gifts))
|
||||
for _, g := range gifts {
|
||||
s.byID[g.ID] = g
|
||||
}
|
||||
s.hash = domain.StarGiftCatalogHash(gifts)
|
||||
s.built = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Catalog 返回礼物目录。
|
||||
func (s *Service) Catalog(ctx context.Context) ([]domain.StarGift, error) {
|
||||
if err := s.ensureCatalog(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
out := make([]domain.StarGift, len(s.gifts))
|
||||
copy(out, s.gifts)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// CatalogHash 返回目录 hash(getStarGifts NotModified 判定)。
|
||||
func (s *Service) CatalogHash(ctx context.Context) (int, error) {
|
||||
if err := s.ensureCatalog(ctx); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.hash, nil
|
||||
}
|
||||
|
||||
// GiftByID 返回目录中指定礼物,不存在返回 ok=false。
|
||||
func (s *Service) GiftByID(ctx context.Context, id int64) (domain.StarGift, bool, error) {
|
||||
if err := s.ensureCatalog(ctx); err != nil {
|
||||
return domain.StarGift{}, false, err
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
g, ok := s.byID[id]
|
||||
return g, ok, nil
|
||||
}
|
||||
|
||||
// RecordSavedGift 持久化一条收到的礼物实例,返回行 id。
|
||||
func (s *Service) RecordSavedGift(ctx context.Context, gift domain.SavedStarGift) (int64, error) {
|
||||
return s.store.Create(ctx, gift)
|
||||
}
|
||||
|
||||
// ListSaved 分页返回某 owner 收到的礼物。
|
||||
func (s *Service) ListSaved(ctx context.Context, owner domain.Peer, excludeUnsaved bool, offset string, limit int) (domain.SavedStarGiftPage, error) {
|
||||
if len(offset) > domain.MaxStarGiftsOffsetBytes {
|
||||
offset = ""
|
||||
}
|
||||
if limit <= 0 || limit > domain.MaxSavedStarGiftsLimit {
|
||||
limit = domain.MaxSavedStarGiftsLimit
|
||||
}
|
||||
return s.store.ListByOwner(ctx, owner, excludeUnsaved, offset, limit)
|
||||
}
|
||||
|
||||
// GetSaved 按协议引用取礼物实例。
|
||||
func (s *Service) GetSaved(ctx context.Context, ref domain.SavedStarGiftRef) (domain.SavedStarGift, bool, error) {
|
||||
return s.store.GetByRef(ctx, ref)
|
||||
}
|
||||
|
||||
// CountSaved 返回某 owner 展示在资料的礼物数(full.stargifts_count)。
|
||||
func (s *Service) CountSaved(ctx context.Context, owner domain.Peer) (int, error) {
|
||||
return s.store.CountByOwner(ctx, owner)
|
||||
}
|
||||
|
||||
// ToggleSaved 切换礼物在资料的展示(saveStarGift)。
|
||||
func (s *Service) ToggleSaved(ctx context.Context, ref domain.SavedStarGiftRef, unsaved bool) (bool, error) {
|
||||
return s.store.SetUnsaved(ctx, ref, unsaved)
|
||||
}
|
||||
|
||||
// Convert 把礼物标记为已转换(convertStarGift),返回该行供调用方据 ConvertStars 入账。
|
||||
func (s *Service) Convert(ctx context.Context, ref domain.SavedStarGiftRef) (domain.SavedStarGift, error) {
|
||||
return s.store.MarkConverted(ctx, ref)
|
||||
}
|
||||
150
internal/app/stargifts/service_test.go
Normal file
150
internal/app/stargifts/service_test.go
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
package stargifts
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/store/memory"
|
||||
)
|
||||
|
||||
type fakeCatalog struct {
|
||||
gifts []domain.StarGift
|
||||
calls int
|
||||
}
|
||||
|
||||
func (f *fakeCatalog) BuildStarGiftCatalog(_ context.Context) ([]domain.StarGift, error) {
|
||||
f.calls++
|
||||
return f.gifts, nil
|
||||
}
|
||||
|
||||
func newTestService(gifts []domain.StarGift) (*Service, *fakeCatalog) {
|
||||
cat := &fakeCatalog{gifts: gifts}
|
||||
return NewService(memory.NewStarGiftStore(), cat), cat
|
||||
}
|
||||
|
||||
func TestCatalogCachedAndHash(t *testing.T) {
|
||||
gifts := []domain.StarGift{
|
||||
{ID: 1, Stars: 15, ConvertStars: 15, Title: "Heart"},
|
||||
{ID: 2, Stars: 50, ConvertStars: 50, Title: "Cake"},
|
||||
}
|
||||
svc, cat := newTestService(gifts)
|
||||
ctx := context.Background()
|
||||
|
||||
got, err := svc.Catalog(ctx)
|
||||
if err != nil || len(got) != 2 {
|
||||
t.Fatalf("catalog = %d err %v, want 2", len(got), err)
|
||||
}
|
||||
// 再取一次不重新构建(缓存)。
|
||||
if _, err := svc.Catalog(ctx); err != nil {
|
||||
t.Fatalf("catalog#2: %v", err)
|
||||
}
|
||||
if cat.calls != 1 {
|
||||
t.Fatalf("BuildStarGiftCatalog called %d times, want 1 (cached)", cat.calls)
|
||||
}
|
||||
hash, err := svc.CatalogHash(ctx)
|
||||
if err != nil || hash != domain.StarGiftCatalogHash(gifts) {
|
||||
t.Fatalf("hash = %d err %v, want %d", hash, err, domain.StarGiftCatalogHash(gifts))
|
||||
}
|
||||
if g, ok, _ := svc.GiftByID(ctx, 2); !ok || g.Stars != 50 {
|
||||
t.Fatalf("GiftByID(2) = %+v ok %v, want Cake 50", g, ok)
|
||||
}
|
||||
if _, ok, _ := svc.GiftByID(ctx, 999); ok {
|
||||
t.Fatalf("GiftByID(999) found, want missing")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSavedGiftLifecycle(t *testing.T) {
|
||||
svc, _ := newTestService(nil)
|
||||
ctx := context.Background()
|
||||
owner := domain.Peer{Type: domain.PeerTypeUser, ID: 1001}
|
||||
|
||||
id, err := svc.RecordSavedGift(ctx, domain.SavedStarGift{
|
||||
Owner: owner, FromUserID: 2002, GiftID: 1, MsgID: 50, Date: 1700000000, ConvertStars: 15,
|
||||
})
|
||||
if err != nil || id == 0 {
|
||||
t.Fatalf("RecordSavedGift = %d err %v", id, err)
|
||||
}
|
||||
|
||||
page, err := svc.ListSaved(ctx, owner, false, "", 100)
|
||||
if err != nil || len(page.Gifts) != 1 || page.Count != 1 {
|
||||
t.Fatalf("list = %d count %d err %v, want 1/1", len(page.Gifts), page.Count, err)
|
||||
}
|
||||
if page.NextOffset != "" {
|
||||
t.Fatalf("single page next_offset = %q, want empty", page.NextOffset)
|
||||
}
|
||||
|
||||
// 隐藏(unsave=true)→ excludeUnsaved 列表为空。
|
||||
ref := domain.SavedStarGiftRef{Owner: owner, MsgID: 50}
|
||||
if ok, err := svc.ToggleSaved(ctx, ref, true); err != nil || !ok {
|
||||
t.Fatalf("ToggleSaved hide = %v err %v", ok, err)
|
||||
}
|
||||
hidden, _ := svc.ListSaved(ctx, owner, true, "", 100)
|
||||
if len(hidden.Gifts) != 0 {
|
||||
t.Fatalf("excludeUnsaved list = %d, want 0 after hide", len(hidden.Gifts))
|
||||
}
|
||||
// 不带 exclude 仍能看到。
|
||||
all, _ := svc.ListSaved(ctx, owner, false, "", 100)
|
||||
if len(all.Gifts) != 1 {
|
||||
t.Fatalf("full list = %d, want 1 (hidden still listed)", len(all.Gifts))
|
||||
}
|
||||
|
||||
// 转换回 Stars → 标记 converted,从列表消失。
|
||||
saved, err := svc.Convert(ctx, ref)
|
||||
if err != nil || saved.ConvertStars != 15 {
|
||||
t.Fatalf("Convert = %+v err %v, want ConvertStars 15", saved, err)
|
||||
}
|
||||
after, _ := svc.ListSaved(ctx, owner, false, "", 100)
|
||||
if len(after.Gifts) != 0 {
|
||||
t.Fatalf("list after convert = %d, want 0", len(after.Gifts))
|
||||
}
|
||||
// 重复转换被拒。
|
||||
if _, err := svc.Convert(ctx, ref); !errors.Is(err, domain.ErrStarGiftAlreadyConverted) {
|
||||
t.Fatalf("double convert err = %v, want ErrStarGiftAlreadyConverted", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelSavedGiftAllocatesSavedIDWithoutMessage(t *testing.T) {
|
||||
svc, _ := newTestService(nil)
|
||||
ctx := context.Background()
|
||||
owner := domain.Peer{Type: domain.PeerTypeChannel, ID: 2001}
|
||||
|
||||
savedID, err := svc.RecordSavedGift(ctx, domain.SavedStarGift{
|
||||
Owner: owner, FromUserID: 1001, GiftID: 1, MsgID: 0, SavedID: 0,
|
||||
Date: 1700000000, ConvertStars: 15,
|
||||
})
|
||||
if err != nil || savedID == 0 {
|
||||
t.Fatalf("RecordSavedGift(channel) = %d err %v, want allocated saved_id", savedID, err)
|
||||
}
|
||||
|
||||
gift, found, err := svc.GetSaved(ctx, domain.SavedStarGiftRef{Owner: owner, SavedID: savedID})
|
||||
if err != nil || !found {
|
||||
t.Fatalf("GetSaved(channel) found=%v err=%v, want hit", found, err)
|
||||
}
|
||||
if gift.MsgID != 0 || gift.SavedID != savedID {
|
||||
t.Fatalf("channel saved gift ids = msg_id %d saved_id %d, want 0/%d", gift.MsgID, gift.SavedID, savedID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSavedGiftPagination(t *testing.T) {
|
||||
svc, _ := newTestService(nil)
|
||||
ctx := context.Background()
|
||||
owner := domain.Peer{Type: domain.PeerTypeUser, ID: 1001}
|
||||
for i := 0; i < 5; i++ {
|
||||
if _, err := svc.RecordSavedGift(ctx, domain.SavedStarGift{
|
||||
Owner: owner, FromUserID: 2002, GiftID: 1, MsgID: 100 + i, Date: 1700000000 + i, ConvertStars: 15,
|
||||
}); err != nil {
|
||||
t.Fatalf("record#%d: %v", i, err)
|
||||
}
|
||||
}
|
||||
page1, _ := svc.ListSaved(ctx, owner, false, "", 2)
|
||||
if len(page1.Gifts) != 2 || page1.NextOffset == "" {
|
||||
t.Fatalf("page1 = %d next=%q, want 2 + next", len(page1.Gifts), page1.NextOffset)
|
||||
}
|
||||
page2, _ := svc.ListSaved(ctx, owner, false, page1.NextOffset, 2)
|
||||
page3, _ := svc.ListSaved(ctx, owner, false, page2.NextOffset, 2)
|
||||
if len(page3.Gifts) != 1 || page3.NextOffset != "" {
|
||||
t.Fatalf("page3 = %d next=%q, want 1 + empty (terminal)", len(page3.Gifts), page3.NextOffset)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue