Gateway:
- 7 routes /api/v1/dashboard/* with JWT auth (middleware.AuthMiddleware)
- statistic_controller.go: 7 methods, response format {code:200, data:resp}
- gateway/main.go: StatisticServiceClient wired
- gateway/config: StatisticServiceURL (DUBBO_STATISTIC_SERVICE_URL, default tri://127.0.0.1:20009)
pkg/statistic SDK:
- fire-and-forget TrackEvent + BatchTrackEvent
- Init(client) + Get() global singleton pattern
Business-side integration (7/7 event types):
- socialService.LikeAsset → asset.like
- galleryService.PlaceAsset → exhibition.start
- galleryService.RemoveFromSlot → exhibition.end (with duration_ms)
- taskService.OnExhibitionCompleted → exhibition.revenue
- assetService.CreateMintOrder → asset.mint
- assetService.logLevelChange → asset.level_up
- userService.UpdateCrystalBalance → crystal.change (wrapper fn added)
Cache warmup:
- main.go: 7 RPCs x 5 sample starIDs at startup (15s delay)
- prevents cold-start DB thundering herd
Existing service modifications:
- galleryService/exhibition_service.go
- taskService/revenue_service.go
- assetService/{mint_service,asset_level_service}.go
- userService/user_service.go
- socialService/asset_like_service.go
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
92 lines
2.3 KiB
Go
92 lines
2.3 KiB
Go
package statistic
|
||
|
||
import (
|
||
"context"
|
||
"sync"
|
||
"time"
|
||
|
||
dubboclient "dubbo.apache.org/dubbo-go/v3/client"
|
||
"github.com/google/uuid"
|
||
pb "github.com/topfans/backend/pkg/proto/event"
|
||
statisticPb "github.com/topfans/backend/pkg/proto/statistic"
|
||
)
|
||
|
||
var (
|
||
instance *Client
|
||
once sync.Once
|
||
)
|
||
|
||
// Client 业务侧统一 SDK(fire-and-forget 调用 statisticService)
|
||
type Client struct {
|
||
service statisticPb.StatisticService
|
||
}
|
||
|
||
// Init 用 Dubbo client 初始化 SDK(在业务服务 main.go 启动时调用一次)
|
||
func Init(dubboClient *dubboclient.Client) error {
|
||
var err error
|
||
once.Do(func() {
|
||
svc, e := statisticPb.NewStatisticService(dubboClient)
|
||
if e != nil {
|
||
err = e
|
||
return
|
||
}
|
||
instance = &Client{service: svc}
|
||
})
|
||
return err
|
||
}
|
||
|
||
// Get 返回全局 SDK 实例(Init 之后才能用)
|
||
func Get() *Client { return instance }
|
||
|
||
// TrackEvent fire-and-forget 上报单个事件
|
||
// - 自动填充 event_id(若为空)和 occurred_at
|
||
// - 不阻塞业务方(独立 goroutine + background context)
|
||
func (c *Client) TrackEvent(ctx context.Context, e *pb.Event) {
|
||
if c == nil || c.service == nil {
|
||
return
|
||
}
|
||
if e.EventId == "" {
|
||
e.EventId = uuid.New().String()
|
||
}
|
||
if e.OccurredAt == 0 {
|
||
e.OccurredAt = time.Now().UnixMilli()
|
||
}
|
||
bgCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||
go func() {
|
||
defer cancel()
|
||
_, _ = c.service.TrackEvent(bgCtx, e)
|
||
}()
|
||
}
|
||
|
||
// SetMockForTest 测试钩子:注入 mock 客户端
|
||
var mockForTest Capturer
|
||
|
||
// Capturer 测试用 capture 接口
|
||
type Capturer interface {
|
||
Capture(e *pb.Event)
|
||
}
|
||
|
||
// SetMockForTest 注入测试 mock
|
||
func SetMockForTest(c Capturer) { mockForTest = c }
|
||
|
||
// ResetMockForTest 重置 mock
|
||
func ResetMockForTest() { mockForTest = nil }
|
||
|
||
// TrackEventSync 同步版本(测试用)
|
||
func (c *Client) TrackEventSync(ctx context.Context, e *pb.Event) (*statisticPb.TrackEventResponse, error) {
|
||
if e.EventId == "" {
|
||
e.EventId = uuid.New().String()
|
||
}
|
||
if e.OccurredAt == 0 {
|
||
e.OccurredAt = time.Now().UnixMilli()
|
||
}
|
||
if mockForTest != nil {
|
||
mockForTest.Capture(e)
|
||
return &statisticPb.TrackEventResponse{Accepted: 1, Rejected: 0}, nil
|
||
}
|
||
if c == nil || c.service == nil {
|
||
return &statisticPb.TrackEventResponse{Accepted: 0, Rejected: 1}, nil
|
||
}
|
||
return c.service.TrackEvent(ctx, e)
|
||
}
|