package taskcenter import ( "context" "encoding/json" "errors" "fmt" "net/http" "strings" "time" "chatapp3-golang/internal/model" "chatapp3-golang/internal/utils" "gorm.io/gorm" "gorm.io/gorm/clause" ) // ReportSimpleEvent 供 Go 内部业务模块上报任务进度事件。 // 上报方只需要传标准 int64,内部再转换成兼容 HTTP 入参的 EventRequest。 func (s *Service) ReportSimpleEvent(ctx context.Context, sysOrigin, eventID, eventType string, userID, deltaValue int64, payload map[string]any) error { return s.ReportSimpleEventAt(ctx, sysOrigin, eventID, eventType, userID, deltaValue, time.Now(), payload) } // ReportSimpleEventAt 和 ReportSimpleEvent 相同,但允许 MQ 等异步事件传入真实发生时间。 func (s *Service) ReportSimpleEventAt(ctx context.Context, sysOrigin, eventID, eventType string, userID, deltaValue int64, occurredAt time.Time, payload map[string]any) error { _, err := s.ProcessEvent(ctx, EventRequest{ SysOrigin: sysOrigin, EventID: eventID, EventType: eventType, UserID: flexibleInt64(userID), DeltaValue: deltaValue, OccurredAt: occurredAt.Format(time.RFC3339Nano), Payload: payload, }) if err != nil { return fmt.Errorf("report task event %s failed: %w", eventID, err) } return nil } // ReceiveEvent 接收后端业务事件并更新命中的任务进度。 func (s *Service) ReceiveEvent(ctx context.Context, req EventRequest) (*EventResponse, error) { return s.ProcessEvent(ctx, req) } // ProcessEvent 统一处理 HTTP 和 MQ 上报的任务事件。 func (s *Service) ProcessEvent(ctx context.Context, req EventRequest) (*EventResponse, error) { sysOrigin := s.normalizeSysOrigin(req.SysOrigin) eventID := strings.TrimSpace(req.EventID) if eventID == "" { return nil, NewAppError(http.StatusBadRequest, "invalid_event_id", "eventId is required") } eventType := strings.ToUpper(strings.TrimSpace(req.EventType)) if eventType == "" { return nil, NewAppError(http.StatusBadRequest, "invalid_event_type", "eventType is required") } if !isAllowedConditionType(eventType) { return &EventResponse{Processed: false, Updated: 0, Reason: "unsupported_condition_type"}, nil } userID := req.UserID.Int64() if userID <= 0 { return nil, NewAppError(http.StatusBadRequest, "invalid_user_id", "userId is required") } if req.DeltaValue <= 0 { return nil, NewAppError(http.StatusBadRequest, "invalid_delta_value", "deltaValue must be greater than 0") } occurredAt := parseOccurredAt(req.OccurredAt, time.Now()) receivedAt := time.Now() rawJSON := buildArchiveRawJSON(req, sysOrigin, eventID, eventType, userID, req.DeltaValue, occurredAt, receivedAt, "", "") bundle, err := s.loadEnabledBundle(ctx, sysOrigin) if err != nil { return nil, err } matchedTasks := make([]model.TaskCenterTaskConfig, 0) if bundle != nil { for _, task := range bundle.Tasks { if strings.EqualFold(task.ConditionType, eventType) { matchedTasks = append(matchedTasks, task) } } } resultReason := "" if bundle == nil { resultReason = "task_center_disabled" } else if len(matchedTasks) == 0 { resultReason = "no_matched_task" } updated := 0 duplicate := false if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { inserted, err := insertTaskEventDedupTx(tx, sysOrigin, eventID, eventType, userID) if err != nil { return err } if !inserted { duplicate = true return nil } if err := createTaskEventArchiveOutboxTx(tx, sysOrigin, eventID, eventType, userID, occurredAt, rawJSON); err != nil { return err } if resultReason != "" { return updateTaskEventDedupTx(tx, sysOrigin, eventID, DedupStatusIgnored, resultReason) } for _, task := range matchedTasks { cycleKey := currentCycleKey(task.TaskCategory, bundle.Location, occurredAt) progress, err := s.loadOrCreateProgressTx(tx, sysOrigin, userID, task, cycleKey) if err != nil { return err } if progress.RewardCollected { continue } progress.CompletedValue += req.DeltaValue if progress.CompletedValue >= progress.TargetValue { progress.Status = TaskStatusCompleted } else { progress.Status = TaskStatusInProgress } progress.UpdateTime = time.Now() if err := tx.Save(&progress).Error; err != nil { return err } updated++ } if updated == 0 { resultReason = "no_updated_task" return updateTaskEventDedupTx(tx, sysOrigin, eventID, DedupStatusIgnored, resultReason) } return updateTaskEventDedupTx(tx, sysOrigin, eventID, DedupStatusSuccess, "") }); err != nil { return nil, err } if duplicate { return &EventResponse{Processed: false, Updated: 0, Reason: "duplicate_event"}, nil } return &EventResponse{Processed: updated > 0, Updated: updated, Reason: resultReason}, nil } func insertTaskEventDedupTx(tx *gorm.DB, sysOrigin, eventID, eventType string, userID int64) (bool, error) { id, err := utils.NextID() if err != nil { return false, err } now := time.Now() row := model.TaskCenterEventDedup{ ID: id, SysOrigin: sysOrigin, EventID: eventID, EventType: eventType, UserID: userID, Status: DedupStatusProcessing, CreateTime: now, UpdateTime: now, } result := tx.Clauses(clause.OnConflict{ Columns: []clause.Column{{Name: "sys_origin"}, {Name: "event_id"}}, DoNothing: true, }).Create(&row) if result.Error != nil { return false, result.Error } return result.RowsAffected > 0, nil } func updateTaskEventDedupTx(tx *gorm.DB, sysOrigin, eventID, status, failureReason string) error { return tx.Model(&model.TaskCenterEventDedup{}). Where("sys_origin = ? AND event_id = ?", sysOrigin, eventID). Updates(map[string]any{ "status": status, "failure_reason": truncateTaskCenterFailure(failureReason), "update_time": time.Now(), }).Error } func createTaskEventArchiveOutboxTx(tx *gorm.DB, sysOrigin, eventID, eventType string, userID int64, occurredAt time.Time, rawJSON string) error { id, err := utils.NextID() if err != nil { return err } now := time.Now() row := model.TaskCenterEventArchiveOutbox{ ID: id, SysOrigin: sysOrigin, EventID: eventID, EventType: eventType, UserID: userID, OccurredAt: occurredAt, RawJSON: rawJSON, Status: ArchiveStatusPending, CreateTime: now, UpdateTime: now, } return tx.Clauses(clause.OnConflict{ Columns: []clause.Column{{Name: "sys_origin"}, {Name: "event_id"}}, DoNothing: true, }).Create(&row).Error } func buildArchiveRawJSON(req EventRequest, sysOrigin, eventID, eventType string, userID, deltaValue int64, occurredAt, receivedAt time.Time, processResult, processReason string) string { row := map[string]any{ "schema_version": 1, "sys_origin": sysOrigin, "event_id": eventID, "event_type": eventType, "user_id": userID, "delta_value": deltaValue, "occurred_at": occurredAt.UTC().Format(time.RFC3339Nano), "received_at": receivedAt.UTC().Format(time.RFC3339Nano), "payload": req.Payload, } if processResult != "" { row["process_result"] = processResult } if processReason != "" { row["process_reason"] = processReason } bytes, err := json.Marshal(row) if err != nil { return "{}" } return string(bytes) } func truncateTaskCenterFailure(value string) string { value = strings.TrimSpace(value) if len(value) <= 512 { return value } return value[:512] } func (s *Service) loadOrCreateProgressTx(tx *gorm.DB, sysOrigin string, userID int64, task model.TaskCenterTaskConfig, cycleKey string) (model.TaskCenterUserProgress, error) { var progress model.TaskCenterUserProgress err := tx.Where("sys_origin = ? AND user_id = ? AND task_id = ? AND cycle_key = ?", sysOrigin, userID, task.ID, cycleKey). First(&progress).Error if errors.Is(err, gorm.ErrRecordNotFound) { id, idErr := utils.NextID() if idErr != nil { return model.TaskCenterUserProgress{}, idErr } now := time.Now() progress = model.TaskCenterUserProgress{ ID: id, SysOrigin: sysOrigin, UserID: userID, TaskID: task.ID, TaskCode: task.TaskCode, TaskCategory: task.TaskCategory, CycleKey: cycleKey, ConditionType: task.ConditionType, CompletedValue: 0, TargetValue: task.TargetValue, Status: TaskStatusInProgress, CreateTime: now, UpdateTime: now, } return progress, nil } if err != nil { return model.TaskCenterUserProgress{}, err } if progress.TargetValue != task.TargetValue || progress.ConditionType != task.ConditionType { progress.TargetValue = task.TargetValue progress.ConditionType = task.ConditionType } return progress, nil }