手动领取房间奖励
This commit is contained in:
parent
8c53a64416
commit
6195544950
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"`
|
||||
|
||||
@ -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))
|
||||
|
||||
254
internal/service/roomturnoverreward/claim.go
Normal file
254
internal/service/roomturnoverreward/claim.go
Normal 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
|
||||
}
|
||||
140
internal/service/roomturnoverreward/claim_test.go
Normal file
140
internal/service/roomturnoverreward/claim_test.go
Normal 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
|
||||
}
|
||||
@ -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")
|
||||
|
||||
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)},
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 {
|
||||
|
||||
31
migrations/044_room_turnover_reward_manual_claim.sql
Normal file
31
migrations/044_room_turnover_reward_manual_claim.sql
Normal 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;
|
||||
Loading…
x
Reference in New Issue
Block a user