topfans/backend/pkg/statistic/client.go
2026-07-03 14:57:14 +08:00

108 lines
2.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package statistic
import (
"context"
"sync"
"time"
dubboclient "dubbo.apache.org/dubbo-go/v3/client"
"github.com/google/uuid"
"go.uber.org/zap"
"github.com/topfans/backend/pkg/logger"
pb "github.com/topfans/backend/pkg/proto/event"
statisticPb "github.com/topfans/backend/pkg/proto/statistic"
)
var (
instance *Client
once sync.Once
)
// Client 业务侧统一 SDKfire-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 {
logger.Logger.Warn("TrackEvent: SDK not initialized, event dropped",
zap.String("event_type", e.EventType),
zap.Int64("user_id", e.UserId),
zap.Int64("star_id", e.StarId))
return
}
if e.EventId == "" {
e.EventId = uuid.New().String()
}
if e.OccurredAt == 0 {
e.OccurredAt = time.Now().UnixMilli()
}
eventType := e.EventType
eventID := e.EventId
bgCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
go func() {
defer cancel()
if _, err := c.service.TrackEvent(bgCtx, e); err != nil {
logger.Logger.Error("TrackEvent: gRPC call failed, event lost",
zap.String("event_id", eventID),
zap.String("event_type", eventType),
zap.Int64("user_id", e.UserId),
zap.Int64("star_id", e.StarId),
zap.Error(err))
}
}()
}
// 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)
}