package roomturnoverreward import ( "context" "errors" "fmt" "log" "net/http" "strings" "time" "chatapp3-golang/internal/common" "chatapp3-golang/internal/integration" "chatapp3-golang/internal/model" "gorm.io/gorm" "gorm.io/gorm/clause" ) // GetClaimStatus 返回当前用户是否存在可手动领取的房间流水奖励。 func (s *Service) GetClaimStatus(ctx context.Context, user common.AuthUser) (*RoomTurnoverRewardClaimStatusResponse, error) { if user.UserID <= 0 { return nil, NewAppError(http.StatusUnauthorized, "invalid_user", "user is required") } sysOrigin := s.normalizeSysOrigin(user.SysOrigin) resp := &RoomTurnoverRewardClaimStatusResponse{ CanClaim: false, Reason: "no_claimable_reward", SysOrigin: sysOrigin, AutoRewardEnabled: true, } configRow, ok, err := s.loadConfigRow(ctx, sysOrigin) if err != nil { return nil, err } if !ok { resp.Reason = "config_not_found" return resp, nil } resp.AutoRewardEnabled = configRow.AutoRewardEnabled if configRow.AutoRewardEnabled { resp.Reason = "auto_reward_enabled" return resp, nil } record, ok, err := s.findClaimableRewardRecord(ctx, sysOrigin, user.UserID, 0) if err != nil { return nil, err } if !ok { return resp, nil } view := toRecordView(record) resp.CanClaim = true resp.Reason = "available" resp.Record = &view return resp, nil } // ClaimReward 手动领取一条已结算但未发放的房间流水奖励。 func (s *Service) ClaimReward(ctx context.Context, user common.AuthUser, req RoomTurnoverRewardClaimRequest) (*RoomTurnoverRewardClaimResponse, error) { if s.repo.Java == nil { return nil, NewAppError(http.StatusServiceUnavailable, "java_gateway_unavailable", "java gateway is unavailable") } if user.UserID <= 0 { return nil, NewAppError(http.StatusUnauthorized, "invalid_user", "user is required") } sysOrigin := s.normalizeSysOrigin(user.SysOrigin) configRow, ok, err := s.loadConfigRow(ctx, sysOrigin) if err != nil { return nil, err } if !ok { return nil, NewAppError(http.StatusNotFound, "room_turnover_reward_config_not_found", "config not found") } if configRow.AutoRewardEnabled { return nil, NewAppError(http.StatusConflict, "manual_claim_disabled", "manual claim is disabled when auto reward is enabled") } recordID := req.RecordID.Int64() var saved model.RoomTurnoverRewardRecord claimed := false var dispatchErr error err = s.repo.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { record, found, findErr := s.lockRewardRecord(ctx, tx, sysOrigin, user.UserID, recordID) if findErr != nil { return findErr } if !found { return NewAppError(http.StatusNotFound, "no_claimable_reward", "no claimable reward") } if record.Status == roomTurnoverRewardStatusSuccess { saved = record return nil } if record.Status != roomTurnoverRewardStatusPending && record.Status != roomTurnoverRewardStatusFailed { return NewAppError(http.StatusConflict, "reward_not_claimable", "reward is not claimable") } dispatchErr = s.dispatchRewardRecord(ctx, &record) now := time.Now() if dispatchErr != nil { retryCount := record.RetryCount + 1 updates := map[string]any{ "status": roomTurnoverRewardStatusFailed, "retry_count": retryCount, "last_error": truncateString(dispatchErr.Error(), 1024), "update_time": now, } if err := tx.Model(&model.RoomTurnoverRewardRecord{}). Where("id = ?", record.ID). Updates(updates).Error; err != nil { return err } record.Status = roomTurnoverRewardStatusFailed record.RetryCount = retryCount record.LastError = truncateString(dispatchErr.Error(), 1024) record.UpdateTime = now saved = record return nil } retryCount := record.RetryCount if record.Status == roomTurnoverRewardStatusFailed { retryCount++ } if err := tx.Model(&model.RoomTurnoverRewardRecord{}). Where("id = ?", record.ID). Updates(map[string]any{ "status": roomTurnoverRewardStatusSuccess, "retry_count": retryCount, "last_error": "", "sent_at": now, "update_time": now, }).Error; err != nil { return err } record.Status = roomTurnoverRewardStatusSuccess record.RetryCount = retryCount record.LastError = "" record.SentAt = &now record.UpdateTime = now saved = record claimed = true return nil }) if err != nil { return nil, err } if dispatchErr != nil { return nil, NewAppError(http.StatusBadGateway, "room_turnover_reward_claim_failed", dispatchErr.Error()) } if claimed { s.markSourceSent(ctx, saved) } return &RoomTurnoverRewardClaimResponse{ Claimed: claimed, Record: toRecordView(saved), }, nil } func (s *Service) dispatchRewardRecord(ctx context.Context, record *model.RoomTurnoverRewardRecord) error { if record == nil { return nil } return s.repo.Java.ChangeGoldBalance(ctx, integration.GoldReceiptCommand{ ReceiptType: "INCOME", UserID: record.OwnerUserID, SysOrigin: record.SysOrigin, EventID: record.TrackID, Origin: roomTurnoverRewardGoldOrigin, Remark: fmt.Sprintf("room turnover reward %s room %d", record.CycleKey, record.RoomID), Amount: integration.NewPennyAmountPayloadFromDollar(record.RewardGold), CloseDelayAsset: false, OpUserType: "APP", }) } func (s *Service) markSourceSent(ctx context.Context, record model.RoomTurnoverRewardRecord) { sourceID := strings.TrimSpace(record.SourceID) if sourceID == "" { return } marked, err := s.repo.Java.MarkRoomTurnoverRewardCoinsSent(ctx, sourceID) if err != nil { log.Printf("mark room turnover reward source failed after manual claim. recordId=%d sourceId=%s err=%v", record.ID, sourceID, err) return } if !marked { log.Printf("room turnover reward source was not modified after manual claim. recordId=%d sourceId=%s", record.ID, sourceID) } } func (s *Service) loadConfigRow(ctx context.Context, sysOrigin string) (model.RoomTurnoverRewardConfig, bool, error) { var row model.RoomTurnoverRewardConfig err := s.repo.DB.WithContext(ctx). Where("sys_origin = ?", s.normalizeSysOrigin(sysOrigin)). First(&row).Error if errors.Is(err, gorm.ErrRecordNotFound) { return model.RoomTurnoverRewardConfig{}, false, nil } if err != nil { return model.RoomTurnoverRewardConfig{}, false, err } return row, true, nil } func (s *Service) findClaimableRewardRecord(ctx context.Context, sysOrigin string, userID int64, recordID int64) (model.RoomTurnoverRewardRecord, bool, error) { query := s.repo.DB.WithContext(ctx). Where("sys_origin = ? AND owner_user_id = ?", s.normalizeSysOrigin(sysOrigin), userID). Where("status IN ?", []string{roomTurnoverRewardStatusPending, roomTurnoverRewardStatusFailed}) if recordID > 0 { query = query.Where("id = ?", recordID) } var record model.RoomTurnoverRewardRecord err := query. Order("period_end_at desc"). Order("id desc"). First(&record).Error if errors.Is(err, gorm.ErrRecordNotFound) { return model.RoomTurnoverRewardRecord{}, false, nil } if err != nil { return model.RoomTurnoverRewardRecord{}, false, err } return record, true, nil } func (s *Service) lockRewardRecord(ctx context.Context, tx *gorm.DB, sysOrigin string, userID int64, recordID int64) (model.RoomTurnoverRewardRecord, bool, error) { query := tx.WithContext(ctx). Clauses(clause.Locking{Strength: "UPDATE"}). Where("sys_origin = ? AND owner_user_id = ?", s.normalizeSysOrigin(sysOrigin), userID) if recordID > 0 { query = query.Where("id = ?", recordID) } else { query = query.Where("status IN ?", []string{roomTurnoverRewardStatusPending, roomTurnoverRewardStatusFailed}) } var record model.RoomTurnoverRewardRecord err := query. Order("period_end_at desc"). Order("id desc"). First(&record).Error if errors.Is(err, gorm.ErrRecordNotFound) { return model.RoomTurnoverRewardRecord{}, false, nil } if err != nil { return model.RoomTurnoverRewardRecord{}, false, err } return record, true, nil }