手动领取房间奖励

This commit is contained in:
hy001 2026-05-19 15:17:47 +08:00
parent 8c53a64416
commit 6195544950
13 changed files with 599 additions and 37 deletions

View File

@ -196,6 +196,11 @@ func (g *Gateways) MapRoomProfiles(ctx context.Context, roomIDs []int64) (map[in
return g.Room.MapRoomProfiles(ctx, roomIDs)
}
// MarkRoomTurnoverRewardCoinsSent 透传到房间流水奖励标记接口。
func (g *Gateways) MarkRoomTurnoverRewardCoinsSent(ctx context.Context, id string) (bool, error) {
return g.Room.MarkRoomTurnoverRewardCoinsSent(ctx, id)
}
// GetTeamPolicyRelease 透传到团队政策网关。
func (g *Gateways) GetTeamPolicyRelease(ctx context.Context, query TeamPolicyReleaseQuery) (TeamPolicyRelease, error) {
return g.Team.GetTeamPolicyRelease(ctx, query)
@ -445,6 +450,11 @@ func (g *RoomGateway) MapRoomProfiles(ctx context.Context, roomIDs []int64) (map
return g.client.MapRoomProfiles(ctx, roomIDs)
}
// MarkRoomTurnoverRewardCoinsSent 标记 Java 房间流水统计记录已经发奖。
func (g *RoomGateway) MarkRoomTurnoverRewardCoinsSent(ctx context.Context, id string) (bool, error) {
return g.client.MarkRoomTurnoverRewardCoinsSent(ctx, id)
}
// TeamGateway 负责团队政策相关接口。
type TeamGateway struct {
client *Client

View File

@ -1030,6 +1030,19 @@ func (c *Client) GetCurrentWeekRoomContribution(ctx context.Context, roomID int6
return resp.Body, nil
}
func (c *Client) MarkRoomTurnoverRewardCoinsSent(ctx context.Context, id string) (bool, error) {
id = strings.TrimSpace(id)
if id == "" {
return false, fmt.Errorf("empty room contribution id")
}
endpoint := c.cfg.Java.OtherBaseURL + "/room-contribution-activity-count/client/rewardCoinsSent?id=" + url.QueryEscape(id)
var resp resultResponse[bool]
if err := c.postJSON(ctx, endpoint, nil, nil, &resp); err != nil {
return false, err
}
return resp.Body, nil
}
func (c *Client) GetRoomProfileByUserID(ctx context.Context, userID int64) (RoomProfile, error) {
if userID <= 0 {
return RoomProfile{}, nil

View File

@ -4,12 +4,13 @@ import "time"
// RoomTurnoverRewardConfig 保存房间流水奖励的系统级配置。
type RoomTurnoverRewardConfig struct {
ID int64 `gorm:"column:id;primaryKey"`
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_room_turnover_reward_sys_origin"`
Enabled bool `gorm:"column:enabled;index:idx_room_turnover_reward_enabled"`
Timezone string `gorm:"column:timezone;size:64"`
CreateTime time.Time `gorm:"column:create_time"`
UpdateTime time.Time `gorm:"column:update_time"`
ID int64 `gorm:"column:id;primaryKey"`
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_room_turnover_reward_sys_origin"`
Enabled bool `gorm:"column:enabled;index:idx_room_turnover_reward_enabled"`
AutoRewardEnabled bool `gorm:"column:auto_reward_enabled"`
Timezone string `gorm:"column:timezone;size:64"`
CreateTime time.Time `gorm:"column:create_time"`
UpdateTime time.Time `gorm:"column:update_time"`
}
func (RoomTurnoverRewardConfig) TableName() string { return "room_turnover_reward_config" }
@ -48,6 +49,7 @@ type RoomTurnoverRewardRecord struct {
TurnoverThreshold int64 `gorm:"column:turnover_threshold"`
RewardGold int64 `gorm:"column:reward_gold"`
TrackID string `gorm:"column:track_id;size:128;uniqueIndex:uk_room_turnover_reward_track"`
SourceID string `gorm:"column:source_id;size:128"`
Status string `gorm:"column:status;size:32;index:idx_room_turnover_reward_record_status"`
RetryCount int `gorm:"column:retry_count"`
LastError string `gorm:"column:last_error;size:1024"`

View File

@ -1,6 +1,8 @@
package router
import (
"errors"
"io"
"net/http"
"strconv"
"strings"
@ -35,6 +37,32 @@ func registerRoomTurnoverRewardRoutes(engine *gin.Engine, javaClient authGateway
}
writeOK(c, resp)
})
appGroup.GET("/reward/claim/status", func(c *gin.Context) {
resp, err := service.GetClaimStatus(c.Request.Context(), mustAuthUser(c))
if err != nil {
writeError(c, err)
return
}
writeOK(c, resp)
})
appGroup.POST("/reward/claim", func(c *gin.Context) {
var req roomturnoverreward.RoomTurnoverRewardClaimRequest
if c.Request.ContentLength != 0 {
if err := c.ShouldBindJSON(&req); err != nil {
writeError(c, roomturnoverreward.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
return
}
} else if err := c.ShouldBindJSON(&req); err != nil && !errors.Is(err, io.EOF) {
writeError(c, roomturnoverreward.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
return
}
resp, err := service.ClaimReward(c.Request.Context(), mustAuthUser(c), req)
if err != nil {
writeError(c, err)
return
}
writeOK(c, resp)
})
group := engine.Group("/resident-activity/room-turnover-reward")
group.Use(consoleAuthMiddleware(javaClient))

View File

@ -0,0 +1,254 @@
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
}

View File

@ -0,0 +1,140 @@
package roomturnoverreward
import (
"context"
"testing"
"time"
"chatapp3-golang/internal/common"
"chatapp3-golang/internal/model"
)
func TestGetClaimStatusReturnsClaimableRecordWhenAutoRewardDisabled(t *testing.T) {
service, db := newTestService(t)
ctx := context.Background()
autoRewardEnabled := false
configResp, err := service.SaveConfig(ctx, SaveRoomTurnoverRewardConfigRequest{
SysOrigin: "LIKEI",
Enabled: true,
AutoRewardEnabled: &autoRewardEnabled,
Timezone: "Asia/Riyadh",
LevelConfigs: []RoomTurnoverRewardLevelInput{
{Level: 1, TurnoverThreshold: 100000, RewardGold: 2000, Enabled: true},
},
})
if err != nil {
t.Fatalf("SaveConfig() error = %v", err)
}
record := seedRoomTurnoverRewardRecord(t, db, configResp.ID, roomTurnoverRewardStatusPending)
resp, err := service.GetClaimStatus(ctx, common.AuthUser{UserID: record.OwnerUserID, SysOrigin: "LIKEI"})
if err != nil {
t.Fatalf("GetClaimStatus() error = %v", err)
}
if !resp.CanClaim || resp.Reason != "available" || resp.Record == nil || resp.Record.ID != record.ID {
t.Fatalf("claim status = %+v", resp)
}
}
func TestGetClaimStatusDisablesButtonWhenAutoRewardEnabled(t *testing.T) {
service, db := newTestService(t)
ctx := context.Background()
configResp, err := service.SaveConfig(ctx, SaveRoomTurnoverRewardConfigRequest{
SysOrigin: "LIKEI",
Enabled: true,
Timezone: "Asia/Riyadh",
LevelConfigs: []RoomTurnoverRewardLevelInput{
{Level: 1, TurnoverThreshold: 100000, RewardGold: 2000, Enabled: true},
},
})
if err != nil {
t.Fatalf("SaveConfig() error = %v", err)
}
record := seedRoomTurnoverRewardRecord(t, db, configResp.ID, roomTurnoverRewardStatusPending)
resp, err := service.GetClaimStatus(ctx, common.AuthUser{UserID: record.OwnerUserID, SysOrigin: "LIKEI"})
if err != nil {
t.Fatalf("GetClaimStatus() error = %v", err)
}
if resp.CanClaim || resp.Reason != "auto_reward_enabled" {
t.Fatalf("claim status = %+v", resp)
}
}
func TestClaimRewardDispatchesGoldAndMarksRecordSuccess(t *testing.T) {
service, db := newTestService(t)
gateway := &stubRoomTurnoverRewardJavaGateway{}
service.repo.Java = gateway
ctx := context.Background()
autoRewardEnabled := false
configResp, err := service.SaveConfig(ctx, SaveRoomTurnoverRewardConfigRequest{
SysOrigin: "LIKEI",
Enabled: true,
AutoRewardEnabled: &autoRewardEnabled,
Timezone: "Asia/Riyadh",
LevelConfigs: []RoomTurnoverRewardLevelInput{
{Level: 1, TurnoverThreshold: 100000, RewardGold: 2000, Enabled: true},
},
})
if err != nil {
t.Fatalf("SaveConfig() error = %v", err)
}
record := seedRoomTurnoverRewardRecord(t, db, configResp.ID, roomTurnoverRewardStatusPending)
resp, err := service.ClaimReward(ctx, common.AuthUser{UserID: record.OwnerUserID, SysOrigin: "LIKEI"}, RoomTurnoverRewardClaimRequest{
RecordID: flexibleInt64(record.ID),
})
if err != nil {
t.Fatalf("ClaimReward() error = %v", err)
}
if !resp.Claimed || resp.Record.Status != roomTurnoverRewardStatusSuccess || resp.Record.SentAt == "" {
t.Fatalf("claim response = %+v", resp)
}
if len(gateway.changed) != 1 {
t.Fatalf("wallet change count = %d, want 1", len(gateway.changed))
}
if got := gateway.changed[0]; got.EventID != record.TrackID || got.UserID != record.OwnerUserID || got.Amount.DollarAmount != record.RewardGold {
t.Fatalf("wallet command = %+v", got)
}
if len(gateway.markedIDs) != 1 || gateway.markedIDs[0] != record.SourceID {
t.Fatalf("marked IDs = %+v", gateway.markedIDs)
}
var saved model.RoomTurnoverRewardRecord
if err := db.Where("id = ?", record.ID).First(&saved).Error; err != nil {
t.Fatalf("load saved record: %v", err)
}
if saved.Status != roomTurnoverRewardStatusSuccess || saved.SentAt == nil {
t.Fatalf("saved record = %+v", saved)
}
}
func seedRoomTurnoverRewardRecord(t *testing.T, db roomTurnoverRewardDB, configID int64, status string) model.RoomTurnoverRewardRecord {
t.Helper()
now := time.Now()
record := model.RoomTurnoverRewardRecord{
ID: now.UnixNano(),
ConfigID: configID,
SysOrigin: "LIKEI",
CycleKey: "20260511",
PeriodStartAt: now.AddDate(0, 0, -7),
PeriodEndAt: now,
RoomID: 101,
RoomAccount: "R101",
RoomName: "Owner room",
OwnerUserID: 9001,
TurnoverAmount: 120000,
Level: 1,
TurnoverThreshold: 100000,
RewardGold: 2000,
TrackID: "room-turnover:LIKEI:20260511:101",
SourceID: "source-101",
Status: status,
CreateTime: now,
UpdateTime: now,
}
if err := db.WithContext(context.Background()).Create(&record).Error; err != nil {
t.Fatalf("seed reward record: %v", err)
}
return record
}

View File

@ -24,11 +24,12 @@ func (s *Service) GetConfig(ctx context.Context, sysOrigin string) (*RoomTurnove
First(&configRow).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return &RoomTurnoverRewardConfigResponse{
Configured: false,
SysOrigin: sysOrigin,
Enabled: false,
Timezone: s.defaultTimezone(),
LevelConfigs: defaultLevelPayloads(),
Configured: false,
SysOrigin: sysOrigin,
Enabled: false,
AutoRewardEnabled: true,
Timezone: s.defaultTimezone(),
LevelConfigs: defaultLevelPayloads(),
}, nil
}
if err != nil {
@ -40,13 +41,14 @@ func (s *Service) GetConfig(ctx context.Context, sysOrigin string) (*RoomTurnove
return nil, err
}
return &RoomTurnoverRewardConfigResponse{
Configured: true,
ID: configRow.ID,
SysOrigin: configRow.SysOrigin,
Enabled: configRow.Enabled,
Timezone: configRow.Timezone,
UpdateTime: formatDateTime(configRow.UpdateTime),
LevelConfigs: buildLevelPayloads(levels),
Configured: true,
ID: configRow.ID,
SysOrigin: configRow.SysOrigin,
Enabled: configRow.Enabled,
AutoRewardEnabled: configRow.AutoRewardEnabled,
Timezone: configRow.Timezone,
UpdateTime: formatDateTime(configRow.UpdateTime),
LevelConfigs: buildLevelPayloads(levels),
}, nil
}
@ -83,6 +85,7 @@ func (s *Service) SaveConfig(ctx context.Context, req SaveRoomTurnoverRewardConf
if err := s.repo.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
now := time.Now()
var configRow model.RoomTurnoverRewardConfig
creatingConfig := false
if requestID > 0 {
err := tx.Where("id = ?", requestID).First(&configRow).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
@ -102,10 +105,12 @@ func (s *Service) SaveConfig(ctx context.Context, req SaveRoomTurnoverRewardConf
return idErr
}
configRow = model.RoomTurnoverRewardConfig{
ID: configID,
SysOrigin: sysOrigin,
CreateTime: now,
ID: configID,
SysOrigin: sysOrigin,
AutoRewardEnabled: true,
CreateTime: now,
}
creatingConfig = true
} else if err != nil {
return err
}
@ -113,6 +118,7 @@ func (s *Service) SaveConfig(ctx context.Context, req SaveRoomTurnoverRewardConf
configRow.SysOrigin = sysOrigin
configRow.Enabled = req.Enabled
configRow.AutoRewardEnabled = resolveAutoRewardEnabled(req.AutoRewardEnabled, configRow.AutoRewardEnabled, creatingConfig)
configRow.Timezone = timezone
configRow.UpdateTime = now
if configRow.CreateTime.IsZero() {
@ -142,6 +148,16 @@ func (s *Service) SaveConfig(ctx context.Context, req SaveRoomTurnoverRewardConf
return s.GetConfig(ctx, sysOriginBySavedID(ctx, s.repo.DB, savedConfigID, sysOrigin))
}
func resolveAutoRewardEnabled(value *bool, current bool, creating bool) bool {
if value != nil {
return *value
}
if creating {
return true
}
return current
}
func (s *Service) normalizeLevelInputs(inputs []RoomTurnoverRewardLevelInput) ([]model.RoomTurnoverRewardLevel, error) {
if len(inputs) == 0 {
return nil, NewAppError(http.StatusBadRequest, "invalid_level_configs", "levelConfigs is required")

View File

@ -43,6 +43,9 @@ func TestGetConfigReturnsDefaultLevelsWhenMissing(t *testing.T) {
if resp.Configured || resp.Enabled {
t.Fatalf("GetConfig() configured/enabled = %v/%v, want false/false", resp.Configured, resp.Enabled)
}
if !resp.AutoRewardEnabled {
t.Fatalf("GetConfig() autoRewardEnabled = false, want true")
}
if resp.SysOrigin != "LIKEI" || resp.Timezone != "Asia/Riyadh" {
t.Fatalf("GetConfig() sys/timezone = %q/%q", resp.SysOrigin, resp.Timezone)
}
@ -103,4 +106,28 @@ func TestSaveConfigUpsertsBySysOrigin(t *testing.T) {
if got := second.LevelConfigs[0]; got.TurnoverThreshold != 200000 || got.RewardGold != 5000 {
t.Fatalf("upserted level = %+v", got)
}
if !second.AutoRewardEnabled {
t.Fatalf("autoRewardEnabled = false, want default true")
}
}
func TestSaveConfigCanDisableAutoReward(t *testing.T) {
service, _ := newTestService(t)
autoRewardEnabled := false
resp, err := service.SaveConfig(context.Background(), SaveRoomTurnoverRewardConfigRequest{
SysOrigin: "LIKEI",
Enabled: true,
AutoRewardEnabled: &autoRewardEnabled,
Timezone: "Asia/Riyadh",
LevelConfigs: []RoomTurnoverRewardLevelInput{
{Level: 1, TurnoverThreshold: 100000, RewardGold: 2000, Enabled: true},
},
})
if err != nil {
t.Fatalf("SaveConfig() error = %v", err)
}
if resp.AutoRewardEnabled {
t.Fatalf("autoRewardEnabled = true, want false")
}
}

View File

@ -10,6 +10,9 @@ import (
type stubRoomTurnoverRewardJavaGateway struct {
contributions []integration.RoomContributionActivityCount
current integration.RoomContributionActivityCount
changeErr error
changed []integration.GoldReceiptCommand
markedIDs []string
userProfile integration.UserProfile
userRoom integration.RoomProfile
profiles map[int64]integration.RoomProfile
@ -35,9 +38,19 @@ func (s stubRoomTurnoverRewardJavaGateway) MapRoomProfiles(_ context.Context, _
return s.profiles, nil
}
func (s *stubRoomTurnoverRewardJavaGateway) ChangeGoldBalance(_ context.Context, cmd integration.GoldReceiptCommand) error {
s.changed = append(s.changed, cmd)
return s.changeErr
}
func (s *stubRoomTurnoverRewardJavaGateway) MarkRoomTurnoverRewardCoinsSent(_ context.Context, id string) (bool, error) {
s.markedIDs = append(s.markedIDs, id)
return true, nil
}
func TestCurrentRoomRankingSortsAndMatchesHighestLevel(t *testing.T) {
service, _ := newTestService(t)
service.repo.Java = stubRoomTurnoverRewardJavaGateway{
service.repo.Java = &stubRoomTurnoverRewardJavaGateway{
contributions: []integration.RoomContributionActivityCount{
{RoomID: integration.Int64Value(101), ContributionValue: integration.AmountValue(120000)},
{RoomID: integration.Int64Value(102), ContributionValue: integration.AmountValue(800000)},

View File

@ -10,7 +10,7 @@ import (
func TestGetHomeReturnsUserRoomTurnoverAndExpectedReward(t *testing.T) {
service, _ := newTestService(t)
service.repo.Java = stubRoomTurnoverRewardJavaGateway{
service.repo.Java = &stubRoomTurnoverRewardJavaGateway{
userProfile: integration.UserProfile{
ID: integration.Int64Value(9001),
Account: "123456789",
@ -60,7 +60,7 @@ func TestGetHomeReturnsUserRoomTurnoverAndExpectedReward(t *testing.T) {
func TestGetHomeReturnsZeroRewardWhenNoRoom(t *testing.T) {
service, _ := newTestService(t)
service.repo.Java = stubRoomTurnoverRewardJavaGateway{
service.repo.Java = &stubRoomTurnoverRewardJavaGateway{
userProfile: integration.UserProfile{
ID: integration.Int64Value(9001),
UserNickname: "Owner",
@ -78,7 +78,7 @@ func TestGetHomeReturnsZeroRewardWhenNoRoom(t *testing.T) {
func TestGetHomeMatchesDefaultLevelsWhenConfigDisabled(t *testing.T) {
service, _ := newTestService(t)
service.repo.Java = stubRoomTurnoverRewardJavaGateway{
service.repo.Java = &stubRoomTurnoverRewardJavaGateway{
userRoom: integration.RoomProfile{
ID: integration.Int64Value(101),
UserID: integration.Int64Value(9001),

View File

@ -78,6 +78,7 @@ func toRecordView(row model.RoomTurnoverRewardRecord) RoomTurnoverRewardRecordVi
TurnoverThreshold: row.TurnoverThreshold,
RewardGold: row.RewardGold,
TrackID: row.TrackID,
SourceID: row.SourceID,
Status: row.Status,
RetryCount: row.RetryCount,
LastError: row.LastError,

View File

@ -20,6 +20,8 @@ const (
roomTurnoverRewardStatusSuccess = "SUCCESS"
roomTurnoverRewardStatusFailed = "FAILED"
roomTurnoverRewardGoldOrigin = "ROOM_CONTRIBUTION_REWARD"
defaultRoomTurnoverRewardTimezone = "Asia/Riyadh"
)
@ -79,22 +81,24 @@ type RoomTurnoverRewardLevelInput struct {
// RoomTurnoverRewardConfigResponse 是房间流水奖励配置读模型。
type RoomTurnoverRewardConfigResponse struct {
Configured bool `json:"configured"`
ID int64 `json:"id,string"`
SysOrigin string `json:"sysOrigin"`
Enabled bool `json:"enabled"`
Timezone string `json:"timezone"`
UpdateTime string `json:"updateTime"`
LevelConfigs []RoomTurnoverRewardLevelPayload `json:"levelConfigs"`
Configured bool `json:"configured"`
ID int64 `json:"id,string"`
SysOrigin string `json:"sysOrigin"`
Enabled bool `json:"enabled"`
AutoRewardEnabled bool `json:"autoRewardEnabled"`
Timezone string `json:"timezone"`
UpdateTime string `json:"updateTime"`
LevelConfigs []RoomTurnoverRewardLevelPayload `json:"levelConfigs"`
}
// SaveRoomTurnoverRewardConfigRequest 是房间流水奖励配置保存入参。
type SaveRoomTurnoverRewardConfigRequest struct {
ID flexibleInt64 `json:"id"`
SysOrigin string `json:"sysOrigin"`
Enabled bool `json:"enabled"`
Timezone string `json:"timezone"`
LevelConfigs []RoomTurnoverRewardLevelInput `json:"levelConfigs"`
ID flexibleInt64 `json:"id"`
SysOrigin string `json:"sysOrigin"`
Enabled bool `json:"enabled"`
AutoRewardEnabled *bool `json:"autoRewardEnabled"`
Timezone string `json:"timezone"`
LevelConfigs []RoomTurnoverRewardLevelInput `json:"levelConfigs"`
}
// RoomTurnoverRewardRecordView 是后台发奖记录列表项。
@ -117,6 +121,7 @@ type RoomTurnoverRewardRecordView struct {
TurnoverThreshold int64 `json:"turnoverThreshold"`
RewardGold int64 `json:"rewardGold"`
TrackID string `json:"trackId"`
SourceID string `json:"sourceId"`
Status string `json:"status"`
RetryCount int `json:"retryCount"`
LastError string `json:"lastError"`
@ -195,6 +200,26 @@ type RoomTurnoverRewardHomeResponse struct {
LevelConfigs []RoomTurnoverRewardLevelPayload `json:"levelConfigs"`
}
// RoomTurnoverRewardClaimStatusResponse 是 App 领取按钮判断接口的读模型。
type RoomTurnoverRewardClaimStatusResponse struct {
CanClaim bool `json:"canClaim"`
Reason string `json:"reason"`
SysOrigin string `json:"sysOrigin"`
AutoRewardEnabled bool `json:"autoRewardEnabled"`
Record *RoomTurnoverRewardRecordView `json:"record,omitempty"`
}
// RoomTurnoverRewardClaimRequest 是 App 手动领取奖励入参。
type RoomTurnoverRewardClaimRequest struct {
RecordID flexibleInt64 `json:"recordId"`
}
// RoomTurnoverRewardClaimResponse 是 App 手动领取奖励返回。
type RoomTurnoverRewardClaimResponse struct {
Claimed bool `json:"claimed"`
Record RoomTurnoverRewardRecordView `json:"record"`
}
type roomTurnoverRewardDB interface {
WithContext(ctx context.Context) *gorm.DB
}
@ -205,6 +230,8 @@ type roomTurnoverRewardJavaGateway interface {
GetCurrentWeekRoomContribution(ctx context.Context, roomID int64) (integration.RoomContributionActivityCount, error)
ListCurrentWeekRoomContribution(ctx context.Context, limit int) ([]integration.RoomContributionActivityCount, error)
MapRoomProfiles(ctx context.Context, roomIDs []int64) (map[int64]integration.RoomProfile, error)
ChangeGoldBalance(ctx context.Context, cmd integration.GoldReceiptCommand) error
MarkRoomTurnoverRewardCoinsSent(ctx context.Context, id string) (bool, error)
}
type roomTurnoverRewardPorts struct {

View File

@ -0,0 +1,31 @@
SET @current_schema := DATABASE();
SET @add_room_turnover_reward_auto_sql := IF(
NOT EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = @current_schema
AND table_name = 'room_turnover_reward_config'
AND column_name = 'auto_reward_enabled'
),
'ALTER TABLE `room_turnover_reward_config` ADD COLUMN `auto_reward_enabled` tinyint(1) NOT NULL DEFAULT 1 COMMENT ''是否由 cron 自动发放奖励'' AFTER `enabled`',
'SELECT 1'
);
PREPARE add_room_turnover_reward_auto_stmt FROM @add_room_turnover_reward_auto_sql;
EXECUTE add_room_turnover_reward_auto_stmt;
DEALLOCATE PREPARE add_room_turnover_reward_auto_stmt;
SET @add_room_turnover_reward_source_sql := IF(
NOT EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = @current_schema
AND table_name = 'room_turnover_reward_record'
AND column_name = 'source_id'
),
'ALTER TABLE `room_turnover_reward_record` ADD COLUMN `source_id` varchar(128) DEFAULT NULL COMMENT ''Java 房间流水统计记录ID'' AFTER `track_id`',
'SELECT 1'
);
PREPARE add_room_turnover_reward_source_stmt FROM @add_room_turnover_reward_source_sql;
EXECUTE add_room_turnover_reward_source_stmt;
DEALLOCATE PREPARE add_room_turnover_reward_source_stmt;