2026-05-19 15:17:47 +08:00

255 lines
7.8 KiB
Go

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
}