108 lines
2.8 KiB
Go
108 lines
2.8 KiB
Go
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 业务侧统一 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 {
|
||
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)
|
||
}
|