package service import ( "context" "encoding/json" "fmt" "time" "gitea.com/texpixel/document_ai/internal/model/analytics" "gitea.com/texpixel/document_ai/internal/storage/dao" "gitea.com/texpixel/document_ai/pkg/log" "gorm.io/datatypes" "gorm.io/gorm" ) type AnalyticsService struct { db *gorm.DB eventDao *dao.AnalyticsEventDao } func NewAnalyticsService() *AnalyticsService { return &AnalyticsService{ eventDao: dao.NewAnalyticsEventDao(), } } // TrackEvent 记录单个事件 func (s *AnalyticsService) TrackEvent(ctx context.Context, req *analytics.TrackEventRequest) error { // 将 map 转换为 JSON propertiesJSON, err := json.Marshal(req.Properties) if err != nil { log.Error(ctx, "marshal properties failed", "error", err) return fmt.Errorf("invalid properties format") } deviceInfoJSON, err := json.Marshal(req.DeviceInfo) if err != nil { log.Error(ctx, "marshal device_info failed", "error", err) return fmt.Errorf("invalid device_info format") } metaDataJSON, err := json.Marshal(req.MetaData) if err != nil { log.Error(ctx, "marshal meta_data failed", "error", err) return fmt.Errorf("invalid meta_data format") } event := &dao.AnalyticsEvent{ UserID: req.UserID, EventName: req.EventName, Properties: datatypes.JSON(propertiesJSON), DeviceInfo: datatypes.JSON(deviceInfoJSON), MetaData: datatypes.JSON(metaDataJSON), CreatedAt: time.Now(), } if err := s.eventDao.Create(s.db, event); err != nil { log.Error(ctx, "create analytics event failed", "error", err) return fmt.Errorf("failed to track event") } log.Info(ctx, "event tracked successfully", "event_id", event.ID, "user_id", req.UserID, "event_name", req.EventName) return nil } // BatchTrackEvents 批量记录事件 func (s *AnalyticsService) BatchTrackEvents(ctx context.Context, req *analytics.BatchTrackEventRequest) error { events := make([]*dao.AnalyticsEvent, 0, len(req.Events)) for _, eventReq := range req.Events { propertiesJSON, err := json.Marshal(eventReq.Properties) if err != nil { log.Error(ctx, "marshal properties failed", "error", err) continue } deviceInfoJSON, err := json.Marshal(eventReq.DeviceInfo) if err != nil { log.Error(ctx, "marshal device_info failed", "error", err) continue } metaDataJSON, err := json.Marshal(eventReq.MetaData) if err != nil { log.Error(ctx, "marshal meta_data failed", "error", err) continue } event := &dao.AnalyticsEvent{ UserID: eventReq.UserID, EventName: eventReq.EventName, Properties: datatypes.JSON(propertiesJSON), DeviceInfo: datatypes.JSON(deviceInfoJSON), MetaData: datatypes.JSON(metaDataJSON), CreatedAt: time.Now(), } events = append(events, event) } if len(events) == 0 { return fmt.Errorf("no valid events to track") } if err := s.eventDao.BatchCreate(s.db, events); err != nil { log.Error(ctx, "batch create analytics events failed", "error", err) return fmt.Errorf("failed to batch track events") } log.Info(ctx, "batch events tracked successfully", "count", len(events)) return nil } // QueryEvents 查询事件 func (s *AnalyticsService) QueryEvents(ctx context.Context, req *analytics.QueryEventsRequest) (*analytics.EventListResponse, error) { var events []*dao.AnalyticsEvent var total int64 var err error // 根据不同条件查询 if req.UserID != nil && req.EventName != "" { // 查询用户的指定事件 events, total, err = s.eventDao.GetUserEventsByName(s.db, *req.UserID, req.EventName, req.Page, req.PageSize) } else if req.UserID != nil { // 查询用户的所有事件 events, total, err = s.eventDao.GetUserEvents(s.db, *req.UserID, req.Page, req.PageSize) } else if req.EventName != "" { // 查询指定事件 events, total, err = s.eventDao.GetEventsByName(s.db, req.EventName, req.Page, req.PageSize) } else if req.StartTime != nil && req.EndTime != nil { // 查询时间范围内的事件 events, total, err = s.eventDao.GetEventsByTimeRange(s.db, *req.StartTime, *req.EndTime, req.Page, req.PageSize) } else { return nil, fmt.Errorf("invalid query parameters") } if err != nil { log.Error(ctx, "query events failed", "error", err) return nil, fmt.Errorf("failed to query events") } // 转换为响应格式 eventResponses := make([]*analytics.EventResponse, 0, len(events)) for _, event := range events { var properties, deviceInfo, metaData map[string]interface{} if len(event.Properties) > 0 { json.Unmarshal(event.Properties, &properties) } if len(event.DeviceInfo) > 0 { json.Unmarshal(event.DeviceInfo, &deviceInfo) } if len(event.MetaData) > 0 { json.Unmarshal(event.MetaData, &metaData) } eventResponses = append(eventResponses, &analytics.EventResponse{ ID: event.ID, UserID: event.UserID, EventName: event.EventName, Properties: properties, DeviceInfo: deviceInfo, MetaData: metaData, CreatedAt: event.CreatedAt, }) } return &analytics.EventListResponse{ Events: eventResponses, Total: total, Page: req.Page, Size: req.PageSize, }, nil } // GetEventStats 获取事件统计 func (s *AnalyticsService) GetEventStats(ctx context.Context, req *analytics.EventStatsRequest) (*analytics.EventStatsListResponse, error) { results, err := s.eventDao.GetEventStats(s.db, req.StartTime, req.EndTime) if err != nil { log.Error(ctx, "get event stats failed", "error", err) return nil, fmt.Errorf("failed to get event stats") } stats := make([]*analytics.EventStatsResponse, 0, len(results)) for _, result := range results { stats = append(stats, &analytics.EventStatsResponse{ EventName: result["event_name"].(string), Count: result["count"].(int64), UniqueUsers: result["unique_users"].(int64), }) } return &analytics.EventStatsListResponse{ Stats: stats, StartTime: req.StartTime, EndTime: req.EndTime, }, nil } // CountUserEvents 统计用户事件数量 func (s *AnalyticsService) CountUserEvents(ctx context.Context, userID int64) (int64, error) { count, err := s.eventDao.CountUserEvents(s.db, userID) if err != nil { log.Error(ctx, "count user events failed", "error", err, "user_id", userID) return 0, fmt.Errorf("failed to count user events") } return count, nil } // CountEventsByName 统计指定事件的数量 func (s *AnalyticsService) CountEventsByName(ctx context.Context, eventName string) (int64, error) { count, err := s.eventDao.CountEventsByName(s.db, eventName) if err != nil { log.Error(ctx, "count events by name failed", "error", err, "event_name", eventName) return 0, fmt.Errorf("failed to count events") } return count, nil } // CleanOldEvents 清理旧数据(可以定时执行) func (s *AnalyticsService) CleanOldEvents(ctx context.Context, retentionDays int) error { beforeTime := time.Now().AddDate(0, 0, -retentionDays) if err := s.eventDao.DeleteOldEvents(s.db, beforeTime); err != nil { log.Error(ctx, "clean old events failed", "error", err, "before_time", beforeTime) return fmt.Errorf("failed to clean old events") } log.Info(ctx, "old events cleaned successfully", "retention_days", retentionDays) return nil }