2026-05-10 02:43:07 +08:00

278 lines
8.5 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 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
}